From ee6e6ba2daeae00b2a2d2ded4a93cf26f74dfb62 Mon Sep 17 00:00:00 2001 From: sohalt Date: Tue, 14 Nov 2023 21:02:51 +0100 Subject: [PATCH 01/51] wip: dispatch-tree --- src/babashka/cli.cljc | 74 ++++++++++++++++++++++++++++--------- test/babashka/cli_test.cljc | 28 ++++++++++++++ 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 3493364..9cd2ced 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -564,6 +564,57 @@ (when (= prefix a) suffix))) +(defn keyword-map [m] + (select-keys m (filter keyword? (keys m)))) + +(defn table->tree [table] + (reduce (fn [tree {:as cfg :keys [cmds]}] + (assoc-in tree cmds (dissoc cfg :cmds))) + {} table)) + +(defn deep-merge [a b] + (reduce (fn [acc k] (update acc k (fn [v] + (if (map? v) + (deep-merge v (b k)) + (b k))))) + a (keys b))) + +(defn dispatch-tree' [tree args opts] + (loop [cmds [] all-opts {} args args cmd-info tree] + (let [parse-opts (deep-merge opts (keyword-map cmd-info)) + {:keys [args opts]} (if (or (some-> (first args) + (str/starts-with? "-")) + (contains? parse-opts :args->opts)) + (parse-args args parse-opts) + {:args args + :opts {}}) + [arg & rest] args] + (if-let [subcmd-info (get cmd-info arg)] + (recur (conj cmds arg) (merge all-opts opts) rest subcmd-info) + (if (:fn cmd-info) + {:cmd-info cmd-info + :dispatch cmds + :opts (merge all-opts opts) + :args args} + (if arg + {:error :no-match + :wrong-input arg + :available-commands (filter string? (keys cmd-info))} + {:error :input-exhausted + :available-commands (filter string? (keys cmd-info))})))))) + +(defn dispatch-tree [tree args opts] + (let [{:as res :keys [cmd-info error wrong-input available-commands]} + (dispatch-tree' tree args opts)] + (case error + :no-match (do (println "No matching command:" wrong-input) + (println "Available commands:") + (println (str/join "\n" available-commands))) + :input-exhausted (do (println "No matching command") + (println "Available commands:") + (println (str/join "\n" available-commands))) + nil ((:fn cmd-info) (dissoc res :cmd-info))))) + (defn dispatch "Subcommand dispatcher. @@ -591,21 +642,8 @@ Each entry in the table may have additional `parse-args` options. Examples: see [README.md](README.md#subcommands)." - ([table args] (dispatch table args nil)) - ([table args opts] - (let [{:keys [cmds args] :as m} (parse-cmds args opts)] - (reduce (fn [_ {dispatch :cmds - f :fn - :as sub-opts}] - (when-let [suffix (split dispatch cmds)] - (let [rest-cmds (some-> suffix seq vec) - args (concat rest-cmds args) - {:keys [opts args cmds]} (parse-args args (merge-opts opts sub-opts)) - args (concat cmds args)] - (reduced (f (assoc m - :args args - ;; deprecated name: will be removed in the future! - :rest-cmds args - :opts opts - :dispatch dispatch)))))) - nil table)))) + ([table-or-tree args] + (dispatch table-or-tree args {})) + ([table-or-tree args opts] + (let [tree (cond-> table-or-tree (vector? table-or-tree) table->tree)] + (dispatch-tree tree args opts)))) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index fc6f298..4180d10 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -283,6 +283,34 @@ :precision 100}} (cli/dispatch table ["dep" "search" "cheshire" "100"]))))) +(deftest table->tree-test + (is (= {"foo" {"bar" {:spec {:baz {:coerce :boolean}}, + :fn identity, + "baz" {:spec {:quux {:coerce :keyword}}, + :fn identity}}}} + (cli/table->tree [{:cmds ["foo" "bar"] + :spec {:baz {:coerce :boolean}} + :fn identity} + {:cmds ["foo" "bar" "baz"] + :spec {:quux {:coerce :keyword}} + :fn identity}])))) + +(deftest dispatch-tree-test + (let [table [{:cmds ["foo" "bar"] + :spec {:baz {:coerce :boolean}} + :fn identity} + {:cmds ["foo" "bar" "baz"] + :spec {:quux {:coerce :keyword}} + :fn identity}]] + (is (= "No matching command\nAvailable commands:\nbar\n" + (with-out-str (cli/dispatch table ["foo" "--baz" "quux"])))) + (is (= "No matching command: baz\nAvailable commands:\nbar\n" + (with-out-str (cli/dispatch table ["foo" "baz" "--baz" "quux"])))) + (is (= {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} + (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) + (is (= {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} + (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"]))))) + (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts ["--query" ":a" ":b" ":c"] From 731ea76036c8e92f1540da17983fa906c660efaf Mon Sep 17 00:00:00 2001 From: sohalt Date: Wed, 15 Nov 2023 12:19:17 +0100 Subject: [PATCH 02/51] wip --- src/babashka/cli.cljc | 20 ++++++++++++-------- test/babashka/cli_test.cljc | 2 ++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 9cd2ced..57e250e 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -564,27 +564,31 @@ (when (= prefix a) suffix))) -(defn keyword-map [m] - (select-keys m (filter keyword? (keys m)))) - (defn table->tree [table] (reduce (fn [tree {:as cfg :keys [cmds]}] (assoc-in tree cmds (dissoc cfg :cmds))) {} table)) -(defn deep-merge [a b] +(defn- deep-merge [a b] (reduce (fn [acc k] (update acc k (fn [v] (if (map? v) (deep-merge v (b k)) (b k))))) a (keys b))) +(defn- has-parse-opts? [m] + (some #{:spec :coerce :require :restrict :validate :args->opts :exec-args} (keys m))) + +(defn- is-option? [s] + (some-> s (str/starts-with? "-"))) + (defn dispatch-tree' [tree args opts] (loop [cmds [] all-opts {} args args cmd-info tree] - (let [parse-opts (deep-merge opts (keyword-map cmd-info)) - {:keys [args opts]} (if (or (some-> (first args) - (str/starts-with? "-")) - (contains? parse-opts :args->opts)) + (let [kwm (select-keys cmd-info (filter keyword? (keys cmd-info))) + should-parse-args? (or (has-parse-opts? kwm) + (is-option? (first args))) + parse-opts (deep-merge opts kwm) + {:keys [args opts]} (if should-parse-args? (parse-args args parse-opts) {:args args :opts {}}) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 4180d10..e5d94fa 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -302,6 +302,8 @@ {:cmds ["foo" "bar" "baz"] :spec {:quux {:coerce :keyword}} :fn identity}]] + (is (= "No matching command\nAvailable commands:\nfoo\n" + (with-out-str (cli/dispatch table [])))) (is (= "No matching command\nAvailable commands:\nbar\n" (with-out-str (cli/dispatch table ["foo" "--baz" "quux"])))) (is (= "No matching command: baz\nAvailable commands:\nbar\n" From 7cb4c47213fe8c79fd94280cf362e2dbd62aff5e Mon Sep 17 00:00:00 2001 From: sohalt Date: Wed, 15 Nov 2023 20:59:54 +0100 Subject: [PATCH 03/51] 2-arity versions of dispatch-tree(') --- src/babashka/cli.cljc | 78 +++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 57e250e..4981aa3 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -582,42 +582,48 @@ (defn- is-option? [s] (some-> s (str/starts-with? "-"))) -(defn dispatch-tree' [tree args opts] - (loop [cmds [] all-opts {} args args cmd-info tree] - (let [kwm (select-keys cmd-info (filter keyword? (keys cmd-info))) - should-parse-args? (or (has-parse-opts? kwm) - (is-option? (first args))) - parse-opts (deep-merge opts kwm) - {:keys [args opts]} (if should-parse-args? - (parse-args args parse-opts) - {:args args - :opts {}}) - [arg & rest] args] - (if-let [subcmd-info (get cmd-info arg)] - (recur (conj cmds arg) (merge all-opts opts) rest subcmd-info) - (if (:fn cmd-info) - {:cmd-info cmd-info - :dispatch cmds - :opts (merge all-opts opts) - :args args} - (if arg - {:error :no-match - :wrong-input arg - :available-commands (filter string? (keys cmd-info))} - {:error :input-exhausted - :available-commands (filter string? (keys cmd-info))})))))) - -(defn dispatch-tree [tree args opts] - (let [{:as res :keys [cmd-info error wrong-input available-commands]} - (dispatch-tree' tree args opts)] - (case error - :no-match (do (println "No matching command:" wrong-input) - (println "Available commands:") - (println (str/join "\n" available-commands))) - :input-exhausted (do (println "No matching command") - (println "Available commands:") - (println (str/join "\n" available-commands))) - nil ((:fn cmd-info) (dissoc res :cmd-info))))) +(defn dispatch-tree' + ([tree args] + (dispatch-tree' tree args nil)) + ([tree args opts] + (loop [cmds [] all-opts {} args args cmd-info tree] + (let [kwm (select-keys cmd-info (filter keyword? (keys cmd-info))) + should-parse-args? (or (has-parse-opts? kwm) + (is-option? (first args))) + parse-opts (deep-merge opts kwm) + {:keys [args opts]} (if should-parse-args? + (parse-args args parse-opts) + {:args args + :opts {}}) + [arg & rest] args] + (if-let [subcmd-info (get cmd-info arg)] + (recur (conj cmds arg) (merge all-opts opts) rest subcmd-info) + (if (:fn cmd-info) + {:cmd-info cmd-info + :dispatch cmds + :opts (merge all-opts opts) + :args args} + (if arg + {:error :no-match + :wrong-input arg + :available-commands (filter string? (keys cmd-info))} + {:error :input-exhausted + :available-commands (filter string? (keys cmd-info))}))))))) + +(defn dispatch-tree + ([tree args] + (dispatch-tree tree args nil)) + ([tree args opts] + (let [{:as res :keys [cmd-info error wrong-input available-commands]} + (dispatch-tree' tree args opts)] + (case error + :no-match (do (println "No matching command:" wrong-input) + (println "Available commands:") + (println (str/join "\n" available-commands))) + :input-exhausted (do (println "No matching command") + (println "Available commands:") + (println (str/join "\n" available-commands))) + nil ((:fn cmd-info) (dissoc res :cmd-info)))))) (defn dispatch "Subcommand dispatcher. From 07c87be57ee1f8a3323829d4b0259b0c681145a8 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 18 Dec 2023 17:31:10 +0100 Subject: [PATCH 04/51] wip --- .../borkdude/deflet/borkdude/deflet.clj_kondo | 26 +++++++++++++++++++ .clj-kondo/borkdude/deflet/config.edn | 3 +++ .dir-locals.el | 2 +- deps.edn | 3 ++- test/babashka/cli_test.cljc | 11 +++++++- 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 .clj-kondo/borkdude/deflet/borkdude/deflet.clj_kondo create mode 100644 .clj-kondo/borkdude/deflet/config.edn diff --git a/.clj-kondo/borkdude/deflet/borkdude/deflet.clj_kondo b/.clj-kondo/borkdude/deflet/borkdude/deflet.clj_kondo new file mode 100644 index 0000000..caff7e9 --- /dev/null +++ b/.clj-kondo/borkdude/deflet/borkdude/deflet.clj_kondo @@ -0,0 +1,26 @@ +(ns borkdude.deflet + (:require [clj-kondo.hooks-api :as hooks-api])) + +(defn deflet* [children] + (let [f (first children) + r (next children)] + (if (and (hooks-api/list-node? f) + (#{'def 'defp} (hooks-api/sexpr (first (:children f))))) + (let [def-children (:children f)] + (with-meta (hooks-api/list-node + [(hooks-api/coerce 'clojure.core/let) + (hooks-api/vector-node [(second def-children) + (nth def-children 2)]) + (deflet* r)]) + (meta f))) + (if-not r (or f (hooks-api/coerce nil)) + (with-meta + (hooks-api/list-node (list (hooks-api/coerce 'do) + f + (deflet* r))) + (meta f)))))) + +(defn deflet [{:keys [node]}] + (let [children (:children node) + new-node (deflet* children)] + {:node new-node})) diff --git a/.clj-kondo/borkdude/deflet/config.edn b/.clj-kondo/borkdude/deflet/config.edn new file mode 100644 index 0000000..2bd4b3e --- /dev/null +++ b/.clj-kondo/borkdude/deflet/config.edn @@ -0,0 +1,3 @@ +{:lint-as {borkdude.deflet/defp clojure.core/def} + :hooks {:analyze-call {borkdude.deflet/deflet borkdude.deflet/deflet + borkdude.deflet/defletp borkdude.deflet/deflet}}} diff --git a/.dir-locals.el b/.dir-locals.el index e6ec9ad..1787d04 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,2 +1,2 @@ ((clojure-mode - (cider-clojure-cli-aliases . ":repl"))) + (cider-clojure-cli-aliases . ":repl:test"))) diff --git a/deps.edn b/deps.edn index 72bce9e..9ced22e 100644 --- a/deps.edn +++ b/deps.edn @@ -7,7 +7,8 @@ {:extra-paths ["test"] :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"} - babashka/fs {:mvn/version "0.1.11"}} + babashka/fs {:mvn/version "0.1.11"} + io.github.borkdude/deflet {:mvn/version "0.1.0"}} :exec-args {:cmd "bb test"} :main-opts ["-m" "babashka.cli.exec"] :exec-fn babashka.test-runner/test #_cognitect.test-runner.api/test} diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index e5d94fa..5ce67af 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -3,6 +3,7 @@ [babashka.cli :as cli] [clojure.string :as str] [clojure.test :refer [deftest is testing]] + [borkdude.deflet :as d] #?(:clj [clojure.edn :as edn] :cljs [cljs.reader :as edn]))) @@ -311,7 +312,15 @@ (is (= {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) (is (= {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} - (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"]))))) + (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) + + (d/deflet + (def tree {"foo" {:fn identity + "bar" {"baz" {:fn identity}}}}) + (is (cli/dispatch-tree tree ["foo"]))) + (let [tree {"foo" {:fn identity + "bar" {"baz" {:fn identity}}}}] + (cli/dispatch-tree tree ["foo"]))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From 9f4d0f81258c8e58e90889131b7c94029cca2a45 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 18 Dec 2023 17:32:06 +0100 Subject: [PATCH 05/51] assert --- test/babashka/cli_test.cljc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 5ce67af..a63f370 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -317,10 +317,9 @@ (d/deflet (def tree {"foo" {:fn identity "bar" {"baz" {:fn identity}}}}) - (is (cli/dispatch-tree tree ["foo"]))) - (let [tree {"foo" {:fn identity - "bar" {"baz" {:fn identity}}}}] - (cli/dispatch-tree tree ["foo"]))) + (is (submap? + {:dispatch ["foo"]} + (cli/dispatch-tree tree ["foo"]))))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From a13e714523349ec609921848980eb0720bb936a8 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 18 Dec 2023 20:32:35 +0100 Subject: [PATCH 06/51] test case --- test/babashka/cli_test.cljc | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index a63f370..8643e0b 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -315,11 +315,16 @@ (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) (d/deflet - (def tree {"foo" {:fn identity - "bar" {"baz" {:fn identity}}}}) + (def tree {"foo" {"bar" {:fn identity + "baz" {:fn identity}} + :spec {:bar {:coerce :keyword}} + :fn identity}}) + ;; TODO: change :dispatch to :cmds? (is (submap? - {:dispatch ["foo"]} - (cli/dispatch-tree tree ["foo"]))))) + {:dispatch ["foo" "bar"] + :opts {:bar :bar}} + (cli/dispatch-tree tree ["foo" "--bar" "bar" "bar"]))) + )) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From 5d5f594f8f50b94862a41606be2f72c584bbd046 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 18 Dec 2023 20:44:47 +0100 Subject: [PATCH 07/51] fix windows --- test/babashka/cli_test.cljc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 8643e0b..5a95b3b 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -303,12 +303,12 @@ {:cmds ["foo" "bar" "baz"] :spec {:quux {:coerce :keyword}} :fn identity}]] - (is (= "No matching command\nAvailable commands:\nfoo\n" - (with-out-str (cli/dispatch table [])))) - (is (= "No matching command\nAvailable commands:\nbar\n" - (with-out-str (cli/dispatch table ["foo" "--baz" "quux"])))) - (is (= "No matching command: baz\nAvailable commands:\nbar\n" - (with-out-str (cli/dispatch table ["foo" "baz" "--baz" "quux"])))) + (is (= (str/split-lines "No matching command\nAvailable commands:\nfoo\n") + (str/split-lines (with-out-str (cli/dispatch table []))))) + (is (= (str/split-lines "No matching command\nAvailable commands:\nbar\n") + (str/split-lines (with-out-str (cli/dispatch table ["foo" "--baz" "quux"]))))) + (is (= (str/split-lines "No matching command: baz\nAvailable commands:\nbar\n") + (str/split-lines (with-out-str (cli/dispatch table ["foo" "baz" "--baz" "quux"]))))) (is (= {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) (is (= {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} From 1aa536f7f5baca2d587e4a21dc76d680c4a1cec4 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 18 Dec 2023 20:46:54 +0100 Subject: [PATCH 08/51] Add test deps to cljs --- bb.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bb.edn b/bb.edn index e249f90..802be8f 100644 --- a/bb.edn +++ b/bb.edn @@ -19,7 +19,7 @@ :task (apply clojure "-M:test" *command-line-args*)} cljs-test {:doc "Run CLJS tests" - :task (apply clojure "-M:cljs-test" *command-line-args*)} + :task (apply clojure "-M:test:cljs-test" *command-line-args*)} quickdoc {:doc "Invoke quickdoc" :requires ([quickdoc.api :as api]) From c2e2e2b7a529a6038924dccf3c144b6bb272e351 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 18 Dec 2023 21:28:22 +0100 Subject: [PATCH 09/51] wip --- src/babashka/cli.cljc | 19 ++++++++++++------- test/babashka/cli_test.cljc | 7 +++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 4981aa3..345bc95 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -582,7 +582,7 @@ (defn- is-option? [s] (some-> s (str/starts-with? "-"))) -(defn dispatch-tree' +(defn- dispatch-tree' ([tree args] (dispatch-tree' tree args nil)) ([tree args opts] @@ -617,12 +617,17 @@ (let [{:as res :keys [cmd-info error wrong-input available-commands]} (dispatch-tree' tree args opts)] (case error - :no-match (do (println "No matching command:" wrong-input) - (println "Available commands:") - (println (str/join "\n" available-commands))) - :input-exhausted (do (println "No matching command") - (println "Available commands:") - (println (str/join "\n" available-commands))) + ;; TODO: decide to print or return this via :error-fn or so? + ;; Either way, we need to print to stderr + (:no-match :input-exhausted) + (let [println (fn [& args] + #?(:cljs (apply *print-err-fn* args) + :clj (binding [*out* *err*] + (apply println args))))] + (println (str "No matching command" (when wrong-input + (str ": " wrong-input)))) + (println "Available commands:") + (println (str/join "\n" available-commands))) nil ((:fn cmd-info) (dissoc res :cmd-info)))))) (defn dispatch diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 5a95b3b..d1d4d14 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -304,8 +304,11 @@ :spec {:quux {:coerce :keyword}} :fn identity}]] (is (= (str/split-lines "No matching command\nAvailable commands:\nfoo\n") - (str/split-lines (with-out-str (cli/dispatch table []))))) - (is (= (str/split-lines "No matching command\nAvailable commands:\nbar\n") + (str/split-lines (with-out-str + (binding #?(:clj [*err* *out*] + :cljs [*print-fn* *print-err-fn*]) + (cli/dispatch table [])))))) + #_#_(is (= (str/split-lines "No matching command\nAvailable commands:\nbar\n") (str/split-lines (with-out-str (cli/dispatch table ["foo" "--baz" "quux"]))))) (is (= (str/split-lines "No matching command: baz\nAvailable commands:\nbar\n") (str/split-lines (with-out-str (cli/dispatch table ["foo" "baz" "--baz" "quux"]))))) From 40989aaa4a98ae2da94c1f252c971b0cafb5f904 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 18 Dec 2023 21:30:16 +0100 Subject: [PATCH 10/51] wip --- test/babashka/cli_test.cljc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index d1d4d14..58f414f 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -306,7 +306,8 @@ (is (= (str/split-lines "No matching command\nAvailable commands:\nfoo\n") (str/split-lines (with-out-str (binding #?(:clj [*err* *out*] - :cljs [*print-fn* *print-err-fn*]) + :cljs [*print-err-fn* *print-fn* + *print-newline* true]) (cli/dispatch table [])))))) #_#_(is (= (str/split-lines "No matching command\nAvailable commands:\nbar\n") (str/split-lines (with-out-str (cli/dispatch table ["foo" "--baz" "quux"]))))) From 344aefc32f226ac16e0ff1a9ff21b39f51060308 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 19 Dec 2023 14:55:40 +0100 Subject: [PATCH 11/51] Fix CLJS --- src/babashka/cli.cljc | 4 +++- test/babashka/cli_test.cljc | 15 ++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 345bc95..51b1d16 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -621,7 +621,9 @@ ;; Either way, we need to print to stderr (:no-match :input-exhausted) (let [println (fn [& args] - #?(:cljs (apply *print-err-fn* args) + #?(:cljs (doseq [a args] + (*print-err-fn* a) + (*print-err-fn* "\n")) :clj (binding [*out* *err*] (apply println args))))] (println (str "No matching command" (when wrong-input diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 58f414f..76a0db2 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -297,12 +297,13 @@ :fn identity}])))) (deftest dispatch-tree-test - (let [table [{:cmds ["foo" "bar"] - :spec {:baz {:coerce :boolean}} - :fn identity} - {:cmds ["foo" "bar" "baz"] - :spec {:quux {:coerce :keyword}} - :fn identity}]] + (d/deflet + (def table [{:cmds ["foo" "bar"] + :spec {:baz {:coerce :boolean}} + :fn identity} + {:cmds ["foo" "bar" "baz"] + :spec {:quux {:coerce :keyword}} + :fn identity}]) (is (= (str/split-lines "No matching command\nAvailable commands:\nfoo\n") (str/split-lines (with-out-str (binding #?(:clj [*err* *out*] @@ -310,7 +311,7 @@ *print-newline* true]) (cli/dispatch table [])))))) #_#_(is (= (str/split-lines "No matching command\nAvailable commands:\nbar\n") - (str/split-lines (with-out-str (cli/dispatch table ["foo" "--baz" "quux"]))))) + (str/split-lines (with-out-str (cli/dispatch table ["foo" "--baz" "quux"]))))) (is (= (str/split-lines "No matching command: baz\nAvailable commands:\nbar\n") (str/split-lines (with-out-str (cli/dispatch table ["foo" "baz" "--baz" "quux"]))))) (is (= {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} From c49bd07e967d46e8a2345198a9eaf824717b12fd Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 19 Dec 2023 17:05:15 +0100 Subject: [PATCH 12/51] wip --- src/babashka/cli.cljc | 36 ++++++++++++++++++++++-------------- test/babashka/cli_test.cljc | 36 ++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 51b1d16..5573bb9 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -564,7 +564,7 @@ (when (= prefix a) suffix))) -(defn table->tree [table] +(defn- table->tree [table] (reduce (fn [tree {:as cfg :keys [cmds]}] (assoc-in tree cmds (dissoc cfg :cmds))) {} table)) @@ -615,21 +615,29 @@ (dispatch-tree tree args nil)) ([tree args opts] (let [{:as res :keys [cmd-info error wrong-input available-commands]} - (dispatch-tree' tree args opts)] + (dispatch-tree' tree args opts) + error-fn* (or (:error-fn opts) + (fn [{:keys [msg] :as data}] + (throw (ex-info msg data)))) + error-fn (fn [data] + (-> {:tree tree :type :org.babashka/cli + :wrong-input wrong-input :all-commands available-commands} + (merge data) + error-fn*))] (case error - ;; TODO: decide to print or return this via :error-fn or so? - ;; Either way, we need to print to stderr (:no-match :input-exhausted) - (let [println (fn [& args] - #?(:cljs (doseq [a args] - (*print-err-fn* a) - (*print-err-fn* "\n")) - :clj (binding [*out* *err*] - (apply println args))))] - (println (str "No matching command" (when wrong-input - (str ": " wrong-input)))) - (println "Available commands:") - (println (str/join "\n" available-commands))) + (error-fn {:cause error}) + (when-let [f (:error-fn opts)] + (let [println (fn [& args] + #?(:cljs (doseq [a args] + (*print-err-fn* a) + (*print-err-fn* "\n")) + :clj (binding [*out* *err*] + (apply println args))))] + (println (str "No matching command" (when wrong-input + (str ": " wrong-input)))) + (println "Available commands:") + (println (str/join "\n" available-commands)))) nil ((:fn cmd-info) (dissoc res :cmd-info)))))) (defn dispatch diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 76a0db2..ede07f8 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -289,12 +289,12 @@ :fn identity, "baz" {:spec {:quux {:coerce :keyword}}, :fn identity}}}} - (cli/table->tree [{:cmds ["foo" "bar"] - :spec {:baz {:coerce :boolean}} - :fn identity} - {:cmds ["foo" "bar" "baz"] - :spec {:quux {:coerce :keyword}} - :fn identity}])))) + (#'cli/table->tree [{:cmds ["foo" "bar"] + :spec {:baz {:coerce :boolean}} + :fn identity} + {:cmds ["foo" "bar" "baz"] + :spec {:quux {:coerce :keyword}} + :fn identity}])))) (deftest dispatch-tree-test (d/deflet @@ -304,16 +304,13 @@ {:cmds ["foo" "bar" "baz"] :spec {:quux {:coerce :keyword}} :fn identity}]) - (is (= (str/split-lines "No matching command\nAvailable commands:\nfoo\n") - (str/split-lines (with-out-str - (binding #?(:clj [*err* *out*] - :cljs [*print-err-fn* *print-fn* - *print-newline* true]) - (cli/dispatch table [])))))) - #_#_(is (= (str/split-lines "No matching command\nAvailable commands:\nbar\n") - (str/split-lines (with-out-str (cli/dispatch table ["foo" "--baz" "quux"]))))) - (is (= (str/split-lines "No matching command: baz\nAvailable commands:\nbar\n") - (str/split-lines (with-out-str (cli/dispatch table ["foo" "baz" "--baz" "quux"]))))) + (def tree (#'cli/table->tree table)) + (is (submap? {:tree tree + :type :org.babashka/cli + :cause :input-exhausted + :all-commands ["foo"]} + (try (cli/dispatch table []) + (catch Exception e (ex-data e))))) (is (= {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) (is (= {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} @@ -324,12 +321,11 @@ "baz" {:fn identity}} :spec {:bar {:coerce :keyword}} :fn identity}}) - ;; TODO: change :dispatch to :cmds? (is (submap? {:dispatch ["foo" "bar"] - :opts {:bar :bar}} - (cli/dispatch-tree tree ["foo" "--bar" "bar" "bar"]))) - )) + :opts {:bar :bar} + :args ["arg1"]} + (cli/dispatch-tree tree ["foo" "--bar" "bar" "bar" "arg1"]))))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From a7cc39b2626e6b690e8c2818be2af50d7f2a9f7d Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 19 Dec 2023 17:09:09 +0100 Subject: [PATCH 13/51] wip --- src/babashka/cli.cljc | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 5573bb9..929c0c4 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -627,17 +627,6 @@ (case error (:no-match :input-exhausted) (error-fn {:cause error}) - (when-let [f (:error-fn opts)] - (let [println (fn [& args] - #?(:cljs (doseq [a args] - (*print-err-fn* a) - (*print-err-fn* "\n")) - :clj (binding [*out* *err*] - (apply println args))))] - (println (str "No matching command" (when wrong-input - (str ": " wrong-input)))) - (println "Available commands:") - (println (str/join "\n" available-commands)))) nil ((:fn cmd-info) (dissoc res :cmd-info)))))) (defn dispatch From b61622a7fa1827c8d3f9addc3c6f886dd1bdd5bb Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 20 Dec 2023 11:11:10 +0100 Subject: [PATCH 14/51] Add todo --- test/babashka/cli_test.cljc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index ede07f8..1806b25 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -316,16 +316,18 @@ (is (= {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) + ;; TODO: parse global option first (d/deflet (def tree {"foo" {"bar" {:fn identity "baz" {:fn identity}} - :spec {:bar {:coerce :keyword}} + :spec {:bar {:coerce :keyword} + :global {:coerce :boolean}} :fn identity}}) (is (submap? {:dispatch ["foo" "bar"] :opts {:bar :bar} :args ["arg1"]} - (cli/dispatch-tree tree ["foo" "--bar" "bar" "bar" "arg1"]))))) + (cli/dispatch-tree tree ["--global" "foo" "--bar" "bar" "bar" "arg1"]))))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From 0393d5629e38a0801eef82a924dcc40fe879abf6 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 21 Dec 2023 16:19:40 +0100 Subject: [PATCH 15/51] wip --- test/babashka/cli_test.cljc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 1806b25..4e07dd3 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -316,16 +316,18 @@ (is (= {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) - ;; TODO: parse global option first (d/deflet - (def tree {"foo" {"bar" {:fn identity + (def tree {:spec {:global {:coerce :boolean}} + "foo" {"bar" {:fn identity "baz" {:fn identity}} :spec {:bar {:coerce :keyword} - :global {:coerce :boolean}} + } :fn identity}}) + ;;=> {:args ["bar" "baz"], :opts {:global true}} (is (submap? {:dispatch ["foo" "bar"] - :opts {:bar :bar} + :opts {:bar :bar + :global true} :args ["arg1"]} (cli/dispatch-tree tree ["--global" "foo" "--bar" "bar" "bar" "arg1"]))))) From db4f8462832f85db985ab7ba304e120acf281f1a Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 21 Dec 2023 17:37:16 +0100 Subject: [PATCH 16/51] yooo --- src/babashka/cli.cljc | 10 +++++++--- test/babashka/cli_test.cljc | 30 ++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 929c0c4..e606505 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -580,7 +580,9 @@ (some #{:spec :coerce :require :restrict :validate :args->opts :exec-args} (keys m))) (defn- is-option? [s] - (some-> s (str/starts-with? "-"))) + (and s + (or (str/starts-with? s "-") + (str/starts-with? s ":")))) (defn- dispatch-tree' ([tree args] @@ -597,11 +599,13 @@ :opts {}}) [arg & rest] args] (if-let [subcmd-info (get cmd-info arg)] - (recur (conj cmds arg) (merge all-opts opts) rest subcmd-info) + (recur (conj cmds arg) (-> (merge all-opts opts) + (assoc-in (cons ::opts-by-cmd cmds) opts)) rest subcmd-info) (if (:fn cmd-info) {:cmd-info cmd-info :dispatch cmds - :opts (merge all-opts opts) + :opts (dissoc (merge all-opts opts) ::opts-by-cmd) + :opts-tree (::opts-by-cmd all-opts) :args args} (if arg {:error :no-match diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 4e07dd3..0e73ecc 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -311,10 +311,10 @@ :all-commands ["foo"]} (try (cli/dispatch table []) (catch Exception e (ex-data e))))) - (is (= {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} - (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) - (is (= {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} - (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) + (is (submap? {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} + (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) + (is (submap? {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} + (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) (d/deflet (def tree {:spec {:global {:coerce :boolean}} @@ -323,13 +323,31 @@ :spec {:bar {:coerce :keyword} } :fn identity}}) - ;;=> {:args ["bar" "baz"], :opts {:global true}} (is (submap? {:dispatch ["foo" "bar"] :opts {:bar :bar :global true} :args ["arg1"]} - (cli/dispatch-tree tree ["--global" "foo" "--bar" "bar" "bar" "arg1"]))))) + (cli/dispatch-tree tree ["--global" "foo" "--bar" "bar" "bar" "arg1"])))) + + (testing "distinguish options at every level" + (d/deflet + (def spec {:foo {:coerce :keyword}}) + (def tree {:spec spec + "foo" {"bar" {"baz" {:spec spec + :fn identity} + :fn identity + :spec spec} + :spec spec + :fn identity}}) + (is (submap? + {:dispatch ["foo" "bar"], + :opts {:foo :dude3}, + :opts-tree {:foo :dude1, "foo" {:foo :dude2}}, + :args ["bar" "arg1"]} + (cli/dispatch-tree + tree + ["--foo" "dude1" "foo" "--foo" "dude2" "bar" "--foo" "dude3" "bar" "arg1"])))))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From c9a0e1939340345a7f285f778792643b6a92437c Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 21 Dec 2023 18:47:56 +0100 Subject: [PATCH 17/51] faaaaaail --- src/babashka/cli.cljc | 2 +- test/babashka/cli_test.cljc | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index e606505..6aa1bb1 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -566,7 +566,7 @@ (defn- table->tree [table] (reduce (fn [tree {:as cfg :keys [cmds]}] - (assoc-in tree cmds (dissoc cfg :cmds))) + (assoc-in tree (interleave (repeat :cmd) cmds) (dissoc cfg :cmds))) {} table)) (defn- deep-merge [a b] diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 0e73ecc..747af94 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -316,7 +316,7 @@ (is (submap? {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) - (d/deflet + #_#_(d/deflet (def tree {:spec {:global {:coerce :boolean}} "foo" {"bar" {:fn identity "baz" {:fn identity}} @@ -347,7 +347,17 @@ :args ["bar" "arg1"]} (cli/dispatch-tree tree - ["--foo" "dude1" "foo" "--foo" "dude2" "bar" "--foo" "dude3" "bar" "arg1"])))))) + ["--foo" "dude1" "foo" "--foo" "dude2" "bar" "--foo" "dude3" "bar" "arg1"]))) + + (def tree {:spec spec + :cmd "foo" + :sub {:cmd "bar" + :sub {:cmd "bar" + :sub {:cmd "baz" + :fn identity + :spec spec}} + :spec spec + :fn identity}})))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From b4cad6d99ada4a9df681efd62111bf808ed77081 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 16:22:56 +0100 Subject: [PATCH 18/51] dude --- src/babashka/cli.cljc | 30 +++++++++++++++++++++++------- test/babashka/cli_test.cljc | 35 +++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 6aa1bb1..b25ed21 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -588,8 +588,10 @@ ([tree args] (dispatch-tree' tree args nil)) ([tree args opts] + (def t tree) (loop [cmds [] all-opts {} args args cmd-info tree] - (let [kwm (select-keys cmd-info (filter keyword? (keys cmd-info))) + (let [;; cmd-info (:cmd cmd-info) + kwm cmd-info #_(select-keys cmd-info (filter keyword? (keys cmd-info))) should-parse-args? (or (has-parse-opts? kwm) (is-option? (first args))) parse-opts (deep-merge opts kwm) @@ -597,14 +599,16 @@ (parse-args args parse-opts) {:args args :opts {}}) - [arg & rest] args] - (if-let [subcmd-info (get cmd-info arg)] - (recur (conj cmds arg) (-> (merge all-opts opts) - (assoc-in (cons ::opts-by-cmd cmds) opts)) rest subcmd-info) + [arg & rest] args + all-opts (-> (merge all-opts opts) + (assoc-in (cons ::opts-by-cmd cmds) opts))] + (prn :all-opts all-opts :opts opts) + (if-let [subcmd-info (get (:cmd cmd-info) arg)] + (recur (conj cmds arg) all-opts rest subcmd-info) (if (:fn cmd-info) {:cmd-info cmd-info :dispatch cmds - :opts (dissoc (merge all-opts opts) ::opts-by-cmd) + :opts (dissoc all-opts ::opts-by-cmd) :opts-tree (::opts-by-cmd all-opts) :args args} (if arg @@ -614,7 +618,19 @@ {:error :input-exhausted :available-commands (filter string? (keys cmd-info))}))))))) -(defn dispatch-tree +(comment + (dispatch [{:cmds ["foo"] :fn identity} + {:cmds [] :fn identity}] + []) + + (dispatch [{:cmds ["foo"] :fn identity}] ["foo"]) + (dispatch [{:cmds ["foo" "bar"] + :spec {:foo {:coerce :keyword}} + :fn identity}] ["foo" "bar" "--foo" "dude"]) + (dispatch [{:cmds ["foo" "bar" "baz"] :fn identity}] ["foo" "bar" "baz"]) + ) + +(defn- dispatch-tree ([tree args] (dispatch-tree tree args nil)) ([tree args opts] diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 747af94..4ccb14b 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -285,16 +285,28 @@ (cli/dispatch table ["dep" "search" "cheshire" "100"]))))) (deftest table->tree-test - (is (= {"foo" {"bar" {:spec {:baz {:coerce :boolean}}, - :fn identity, - "baz" {:spec {:quux {:coerce :keyword}}, - :fn identity}}}} - (#'cli/table->tree [{:cmds ["foo" "bar"] - :spec {:baz {:coerce :boolean}} - :fn identity} - {:cmds ["foo" "bar" "baz"] - :spec {:quux {:coerce :keyword}} - :fn identity}])))) + (testing "internal represenation" + (is (= {:cmd + {"foo" + {:cmd + {"bar" + {:spec {:baz {:coerce :boolean}}, + :fn identity + :cmd + {"baz" + {:spec {:quux {:coerce :keyword}}, + :fn identity}}}}}}} + (#'cli/table->tree [{:cmds ["foo" "bar"] + :spec {:baz {:coerce :boolean}} + :fn identity} + {:cmds ["foo" "bar" "baz"] + :spec {:quux {:coerce :keyword}} + :fn identity}]))))) + +;; TODO, test +#_(dispatch [{:cmds ["foo" "bar"] + :spec {:foo {:coerce :keyword}} + :fn identity}] ["foo" "bar" "--foo" "dude"]) (deftest dispatch-tree-test (d/deflet @@ -304,8 +316,7 @@ {:cmds ["foo" "bar" "baz"] :spec {:quux {:coerce :keyword}} :fn identity}]) - (def tree (#'cli/table->tree table)) - (is (submap? {:tree tree + (is (submap? {#_#_:tree tree :type :org.babashka/cli :cause :input-exhausted :all-commands ["foo"]} From 9d6578fed4706fd9553e3b70b392c95340a429fd Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 16:56:03 +0100 Subject: [PATCH 19/51] dude --- src/babashka/cli.cljc | 16 ++++++++++++---- test/babashka/cli_test.cljc | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index b25ed21..cc158d9 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -566,9 +566,17 @@ (defn- table->tree [table] (reduce (fn [tree {:as cfg :keys [cmds]}] - (assoc-in tree (interleave (repeat :cmd) cmds) (dissoc cfg :cmds))) + (let [ks (interleave (repeat :cmd) cmds)] + (if (seq ks) + (assoc-in tree ks (dissoc cfg :cmds)) + ;; catch-all + (merge tree (dissoc cfg :cmds))))) {} table)) +(comment + (table->tree [{:cmds [] :fn identity}]) + ) + (defn- deep-merge [a b] (reduce (fn [acc k] (update acc k (fn [v] (if (map? v) @@ -588,7 +596,7 @@ ([tree args] (dispatch-tree' tree args nil)) ([tree args opts] - (def t tree) + #_(def t tree) (loop [cmds [] all-opts {} args args cmd-info tree] (let [;; cmd-info (:cmd cmd-info) kwm cmd-info #_(select-keys cmd-info (filter keyword? (keys cmd-info))) @@ -614,9 +622,9 @@ (if arg {:error :no-match :wrong-input arg - :available-commands (filter string? (keys cmd-info))} + :available-commands (keys (:cmd cmd-info))} {:error :input-exhausted - :available-commands (filter string? (keys cmd-info))}))))))) + :available-commands (keys (:cmd cmd-info))}))))))) (comment (dispatch [{:cmds ["foo"] :fn identity} diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 4ccb14b..f76dd61 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -304,6 +304,7 @@ :fn identity}]))))) ;; TODO, test +;; TODO: add [] test #_(dispatch [{:cmds ["foo" "bar"] :spec {:foo {:coerce :keyword}} :fn identity}] ["foo" "bar" "--foo" "dude"]) From 8d4a12cf51cea5546630da07faf0a4136a11440f Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 16:59:36 +0100 Subject: [PATCH 20/51] rid prn --- src/babashka/cli.cljc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index cc158d9..bf16b9a 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -610,7 +610,6 @@ [arg & rest] args all-opts (-> (merge all-opts opts) (assoc-in (cons ::opts-by-cmd cmds) opts))] - (prn :all-opts all-opts :opts opts) (if-let [subcmd-info (get (:cmd cmd-info) arg)] (recur (conj cmds arg) all-opts rest subcmd-info) (if (:fn cmd-info) From 3f0bb0fa4c2f7ecf0d1bdcc63f4eaa84da084f0b Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 17:09:11 +0100 Subject: [PATCH 21/51] dude --- src/babashka/cli.cljc | 12 ------------ test/babashka/cli_test.cljc | 27 +++++++++++++++++++-------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index bf16b9a..05a8806 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -625,18 +625,6 @@ {:error :input-exhausted :available-commands (keys (:cmd cmd-info))}))))))) -(comment - (dispatch [{:cmds ["foo"] :fn identity} - {:cmds [] :fn identity}] - []) - - (dispatch [{:cmds ["foo"] :fn identity}] ["foo"]) - (dispatch [{:cmds ["foo" "bar"] - :spec {:foo {:coerce :keyword}} - :fn identity}] ["foo" "bar" "--foo" "dude"]) - (dispatch [{:cmds ["foo" "bar" "baz"] :fn identity}] ["foo" "bar" "baz"]) - ) - (defn- dispatch-tree ([tree args] (dispatch-tree tree args nil)) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index f76dd61..2848d25 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -309,6 +309,18 @@ :spec {:foo {:coerce :keyword}} :fn identity}] ["foo" "bar" "--foo" "dude"]) +#_(comment + (dispatch [{:cmds ["foo"] :fn identity} + {:cmds [] :fn identity}] + []) + + (dispatch [{:cmds ["foo"] :fn identity}] ["foo"]) + (dispatch [{:cmds ["foo" "bar"] + :spec {:foo {:coerce :keyword}} + :fn identity}] ["foo" "bar" "--foo" "dude"]) + (dispatch [{:cmds ["foo" "bar" "baz"] :fn identity}] ["foo" "bar" "baz"]) + ) + (deftest dispatch-tree-test (d/deflet (def table [{:cmds ["foo" "bar"] @@ -328,19 +340,18 @@ (is (submap? {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) - #_#_(d/deflet - (def tree {:spec {:global {:coerce :boolean}} - "foo" {"bar" {:fn identity - "baz" {:fn identity}} - :spec {:bar {:coerce :keyword} - } - :fn identity}}) + (d/deflet + (def table [{:cmds [] :spec {:global {:coerce :boolean}}} + {:cmds ["foo"] :spec {:bar {:coerce :keyword}}} + {:cmds ["foo" "bar"] + :spec {:bar {:coerce :keyword}} + :fn identity}]) (is (submap? {:dispatch ["foo" "bar"] :opts {:bar :bar :global true} :args ["arg1"]} - (cli/dispatch-tree tree ["--global" "foo" "--bar" "bar" "bar" "arg1"])))) + (cli/dispatch table ["--global" "foo" "--bar" "bar" "bar" "arg1"])))) (testing "distinguish options at every level" (d/deflet From a44ef3dcf33fed8c1f507f8bb1160476a42b4519 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 17:14:02 +0100 Subject: [PATCH 22/51] dude --- test/babashka/cli_test.cljc | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 2848d25..13521e0 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -356,31 +356,24 @@ (testing "distinguish options at every level" (d/deflet (def spec {:foo {:coerce :keyword}}) - (def tree {:spec spec - "foo" {"bar" {"baz" {:spec spec - :fn identity} - :fn identity - :spec spec} - :spec spec - :fn identity}}) + (def table [{:spec spec} + {:cmds ["foo"] + :spec spec + :fn identity} + {:cmds ["foo" "bar"] + :fn identity + :spec spec} + {:cmds ["foo" "bar" "baz"] + :spec spec + :fn identity}]) (is (submap? {:dispatch ["foo" "bar"], :opts {:foo :dude3}, :opts-tree {:foo :dude1, "foo" {:foo :dude2}}, :args ["bar" "arg1"]} - (cli/dispatch-tree - tree - ["--foo" "dude1" "foo" "--foo" "dude2" "bar" "--foo" "dude3" "bar" "arg1"]))) - - (def tree {:spec spec - :cmd "foo" - :sub {:cmd "bar" - :sub {:cmd "bar" - :sub {:cmd "baz" - :fn identity - :spec spec}} - :spec spec - :fn identity}})))) + (cli/dispatch + table + ["--foo" "dude1" "foo" "--foo" "dude2" "bar" "--foo" "dude3" "bar" "arg1"])))))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From a2efd94547f04f27ff387df80855546ac4c04ca0 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 17:54:42 +0100 Subject: [PATCH 23/51] yolo --- src/babashka/cli.cljc | 7 ++++--- test/babashka/cli_test.cljc | 23 ++++------------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 05a8806..48f56e5 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -609,14 +609,15 @@ :opts {}}) [arg & rest] args all-opts (-> (merge all-opts opts) - (assoc-in (cons ::opts-by-cmd cmds) opts))] + (update ::opts-by-cmds (fnil conj []) {:cmds cmds + :opts opts}))] (if-let [subcmd-info (get (:cmd cmd-info) arg)] (recur (conj cmds arg) all-opts rest subcmd-info) (if (:fn cmd-info) {:cmd-info cmd-info :dispatch cmds - :opts (dissoc all-opts ::opts-by-cmd) - :opts-tree (::opts-by-cmd all-opts) + :opts (dissoc all-opts ::opts-by-cmds) + :opts-by-cmds (::opts-by-cmds all-opts) :args args} (if arg {:error :no-match diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 13521e0..1e1b9e6 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -303,24 +303,6 @@ :spec {:quux {:coerce :keyword}} :fn identity}]))))) -;; TODO, test -;; TODO: add [] test -#_(dispatch [{:cmds ["foo" "bar"] - :spec {:foo {:coerce :keyword}} - :fn identity}] ["foo" "bar" "--foo" "dude"]) - -#_(comment - (dispatch [{:cmds ["foo"] :fn identity} - {:cmds [] :fn identity}] - []) - - (dispatch [{:cmds ["foo"] :fn identity}] ["foo"]) - (dispatch [{:cmds ["foo" "bar"] - :spec {:foo {:coerce :keyword}} - :fn identity}] ["foo" "bar" "--foo" "dude"]) - (dispatch [{:cmds ["foo" "bar" "baz"] :fn identity}] ["foo" "bar" "baz"]) - ) - (deftest dispatch-tree-test (d/deflet (def table [{:cmds ["foo" "bar"] @@ -369,7 +351,10 @@ (is (submap? {:dispatch ["foo" "bar"], :opts {:foo :dude3}, - :opts-tree {:foo :dude1, "foo" {:foo :dude2}}, + :opts-by-cmds + [{:cmds [], :opts {:foo :dude1}} + {:cmds ["foo"], :opts {:foo :dude2}} + {:cmds ["foo" "bar"], :opts {:foo :dude3}}], :args ["bar" "arg1"]} (cli/dispatch table From 6cf4bab5418cac4cd039cbba63e95e9e0992564f Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 17:54:58 +0100 Subject: [PATCH 24/51] yo --- API.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/API.md b/API.md index 833719b..0d18af1 100644 --- a/API.md +++ b/API.md @@ -53,8 +53,8 @@ Coerce string `s` using `f`. Does not coerce when `s` is not a string. ## `dispatch` ``` clojure -(dispatch table args) -(dispatch table args opts) +(dispatch table-or-tree args) +(dispatch table-or-tree args opts) ``` @@ -84,7 +84,7 @@ Subcommand dispatcher. Each entry in the table may have additional [`parse-args`](#parse-args) options. Examples: see [README.md](README.md#subcommands). -
[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L567-L611) +
[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L648-L679) ## `format-opts` ``` clojure From 4005e567bb0cb88b13f35aefa91ed96f9747266e Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 18:45:07 +0100 Subject: [PATCH 25/51] opts --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index ac12c32..8ceef69 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,35 @@ Additional `parse-arg` options may be passed in each table entry: {:cmds [] :fn help}]) ``` +### Shared options + +Since cli 0.8.54, babashka.cli supports parsing shared options in between and before the subcommands. + +E.g.: + +``` clojure +(def global-spec {:foo {:coerce :keyword}}) +(def sub1-spec {:bar {:coerce :keyword}}) +(def sub2-spec {:baz {:coerce :keyword}}) + +(def table + [{:cmds [] :spec global-spec} + {:cmds ["sub1"] :fn identity :spec sub1-spec} + {:cmds ["sub1" "sub2"] :fn identity :spec sub2-spec}]) + +(cli/dispatch table ["--foo" "a" "sub1" "--bar" "b" "sub2" "--baz" "c" "arg"]) + +;;=> + +{:dispatch ["sub1" "sub2"], + :opts {:foo :a, :bar :b, :baz :c}, + :opts-by-cmds + [{:cmds [], :opts {:foo :a}} + {:cmds ["sub1"], :opts {:bar :b}} + {:cmds ["sub1" "sub2"], :opts {:baz :c}}], + :args ["arg"]} +``` + ## Babashka tasks For documentation on babashka tasks, go From 054b72cd00797c1de82efc4647c6c99d9958ce46 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 18:45:20 +0100 Subject: [PATCH 26/51] wip --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c2732e9..e7c1e92 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ target .clj-kondo/rewrite-clj .clj-kondo/funcool cljs-test-runner-out +src/scratch.clj From 6c1340c4d6b518715d1fd571b5e0fa690dbf71d0 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 18:48:17 +0100 Subject: [PATCH 27/51] wip --- API.md | 23 +++++++++++++++++++++-- src/babashka/cli.cljc | 4 ++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index 0d18af1..96e42db 100644 --- a/API.md +++ b/API.md @@ -19,6 +19,11 @@ - [`babashka.cli.exec`](#babashkacliexec) - [`-main`](#-main) - Main entrypoint for command line usage. - [`main`](#main) +- [`scratch`](#scratch) + - [`global-spec`](#global-spec) + - [`sub1-spec`](#sub1-spec) + - [`sub2-spec`](#sub2-spec) + - [`table`](#table) # babashka.cli @@ -53,14 +58,14 @@ Coerce string `s` using `f`. Does not coerce when `s` is not a string. ## `dispatch` ``` clojure -(dispatch table-or-tree args) +(dispatch table args) (dispatch table-or-tree args opts) ``` Subcommand dispatcher. - Dispatches on first matching command entry in `table`. A match is + Dispatches on first matching command entry in [`table`](#table). A match is determines by whether `:cmds`, a vector of strings, is a subsequence (matching from the start) of the invoked commands. @@ -251,3 +256,17 @@ Main entrypoint for command line usage. ``` [source](https://github.com/babashka/cli/blob/main/src/babashka/cli/exec.clj#L83-L86) +# scratch + + + + + +## `global-spec` +[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L4-L4) +## `sub1-spec` +[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L5-L5) +## `sub2-spec` +[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L6-L6) +## `table` +[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L8-L11) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 48f56e5..d01537e 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -672,8 +672,8 @@ Each entry in the table may have additional `parse-args` options. Examples: see [README.md](README.md#subcommands)." - ([table-or-tree args] - (dispatch table-or-tree args {})) + ([table args] + (dispatch table args {})) ([table-or-tree args opts] (let [tree (cond-> table-or-tree (vector? table-or-tree) table->tree)] (dispatch-tree tree args opts)))) From e67208681537ee36140bf55157dc159b7315309b Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 18:49:23 +0100 Subject: [PATCH 28/51] nodoc --- API.md | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/API.md b/API.md index 96e42db..42afa18 100644 --- a/API.md +++ b/API.md @@ -19,11 +19,6 @@ - [`babashka.cli.exec`](#babashkacliexec) - [`-main`](#-main) - Main entrypoint for command line usage. - [`main`](#main) -- [`scratch`](#scratch) - - [`global-spec`](#global-spec) - - [`sub1-spec`](#sub1-spec) - - [`sub2-spec`](#sub2-spec) - - [`table`](#table) # babashka.cli @@ -65,7 +60,7 @@ Coerce string `s` using `f`. Does not coerce when `s` is not a string. Subcommand dispatcher. - Dispatches on first matching command entry in [`table`](#table). A match is + Dispatches on first matching command entry in `table`. A match is determines by whether `:cmds`, a vector of strings, is a subsequence (matching from the start) of the invoked commands. @@ -256,17 +251,3 @@ Main entrypoint for command line usage. ``` [source](https://github.com/babashka/cli/blob/main/src/babashka/cli/exec.clj#L83-L86) -# scratch - - - - - -## `global-spec` -[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L4-L4) -## `sub1-spec` -[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L5-L5) -## `sub2-spec` -[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L6-L6) -## `table` -[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L8-L11) From d7cda2b0d1ce54876f7137fa3e265404f83eb999 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 18:50:08 +0100 Subject: [PATCH 29/51] wip --- API.md | 2 +- src/babashka/cli.cljc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/API.md b/API.md index 42afa18..8288339 100644 --- a/API.md +++ b/API.md @@ -54,7 +54,7 @@ Coerce string `s` using `f`. Does not coerce when `s` is not a string. ``` clojure (dispatch table args) -(dispatch table-or-tree args opts) +(dispatch table args opts) ``` diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index d01537e..4828bea 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -674,6 +674,6 @@ Examples: see [README.md](README.md#subcommands)." ([table args] (dispatch table args {})) - ([table-or-tree args opts] - (let [tree (cond-> table-or-tree (vector? table-or-tree) table->tree)] + ([table args opts] + (let [tree (cond-> table (vector? table) table->tree)] (dispatch-tree tree args opts)))) From dc89558e290b4e0be472950b372a3ecfbdb5ea3f Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 20:27:14 +0100 Subject: [PATCH 30/51] yeah --- src/babashka/cli.cljc | 6 ++++-- test/babashka/cli_test.cljc | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 4828bea..c547f0d 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -617,7 +617,8 @@ {:cmd-info cmd-info :dispatch cmds :opts (dissoc all-opts ::opts-by-cmds) - :opts-by-cmds (::opts-by-cmds all-opts) + ;; NOTE: won't expose this just yet, wait for more feedback, structure may not be optimal + ;; :opts-by-cmds (::opts-by-cmds all-opts) :args args} (if arg {:error :no-match @@ -636,7 +637,8 @@ (fn [{:keys [msg] :as data}] (throw (ex-info msg data)))) error-fn (fn [data] - (-> {:tree tree :type :org.babashka/cli + (-> {;; :tree tree + :type :org.babashka/cli :wrong-input wrong-input :all-commands available-commands} (merge data) error-fn*))] diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 1e1b9e6..d66da49 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -311,8 +311,7 @@ {:cmds ["foo" "bar" "baz"] :spec {:quux {:coerce :keyword}} :fn identity}]) - (is (submap? {#_#_:tree tree - :type :org.babashka/cli + (is (submap? {:type :org.babashka/cli :cause :input-exhausted :all-commands ["foo"]} (try (cli/dispatch table []) @@ -351,7 +350,7 @@ (is (submap? {:dispatch ["foo" "bar"], :opts {:foo :dude3}, - :opts-by-cmds + #_#_:opts-by-cmds [{:cmds [], :opts {:foo :dude1}} {:cmds ["foo"], :opts {:foo :dude2}} {:cmds ["foo" "bar"], :opts {:foo :dude3}}], From 94cb514a2c4f9905ba8e0981890532495b4b5e7b Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 20:28:07 +0100 Subject: [PATCH 31/51] yah --- API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API.md b/API.md index 8288339..ec1e240 100644 --- a/API.md +++ b/API.md @@ -84,7 +84,7 @@ Subcommand dispatcher. Each entry in the table may have additional [`parse-args`](#parse-args) options. Examples: see [README.md](README.md#subcommands). -
[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L648-L679) +
[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L650-L681) ## `format-opts` ``` clojure From 758eaa337cec6a7d938d141a0e47498862c60da4 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 20:38:07 +0100 Subject: [PATCH 32/51] wip --- src/babashka/cli.cljc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index c547f0d..b7755f1 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -650,9 +650,8 @@ (defn dispatch "Subcommand dispatcher. - Dispatches on first matching command entry in `table`. A match is - determines by whether `:cmds`, a vector of strings, is a subsequence - (matching from the start) of the invoked commands. + Dispatches on longest matching command entry in `table` by matching + subcommands to the `:cmds` vector and invoking the correspondig `:fn`. Table is in the form: @@ -669,7 +668,8 @@ * `:args` - concatenation of unparsed commands and args * `:rest-cmds`: DEPRECATED, this will be removed in a future version - This function does not throw. Use an empty `:cmds` vector to always match. + This function does not throw. + Use an empty `:cmds` vector to always match or to provide global options. Each entry in the table may have additional `parse-args` options. @@ -677,5 +677,5 @@ ([table args] (dispatch table args {})) ([table args opts] - (let [tree (cond-> table (vector? table) table->tree)] + (let [tree (-> table table->tree)] (dispatch-tree tree args opts)))) From 282425b86ee2367a1cb7aeac61ded87af519ed3c Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 20:39:52 +0100 Subject: [PATCH 33/51] wip --- src/babashka/cli.cljc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index b7755f1..25855e5 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -668,12 +668,13 @@ * `:args` - concatenation of unparsed commands and args * `:rest-cmds`: DEPRECATED, this will be removed in a future version - This function does not throw. Use an empty `:cmds` vector to always match or to provide global options. + Provide an `:error-fn` to deal with non-matches. + Each entry in the table may have additional `parse-args` options. - Examples: see [README.md](README.md#subcommands)." + For more information and examples, see [README.md](README.md#subcommands)." ([table args] (dispatch table args {})) ([table args opts] From 92c4263f8f63e2cb677bbc3b3a4a68a3199ff3e8 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 20:41:18 +0100 Subject: [PATCH 34/51] dude --- API.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/API.md b/API.md index ec1e240..a11da0c 100644 --- a/API.md +++ b/API.md @@ -60,9 +60,8 @@ Coerce string `s` using `f`. Does not coerce when `s` is not a string. Subcommand dispatcher. - Dispatches on first matching command entry in `table`. A match is - determines by whether `:cmds`, a vector of strings, is a subsequence - (matching from the start) of the invoked commands. + Dispatches on longest matching command entry in `table` by matching + subcommands to the `:cmds` vector and invoking the correspondig `:fn`. Table is in the form: @@ -79,12 +78,14 @@ Subcommand dispatcher. * `:args` - concatenation of unparsed commands and args * `:rest-cmds`: DEPRECATED, this will be removed in a future version - This function does not throw. Use an empty `:cmds` vector to always match. + Use an empty `:cmds` vector to always match or to provide global options. + + Provide an `:error-fn` to deal with non-matches. Each entry in the table may have additional [`parse-args`](#parse-args) options. - Examples: see [README.md](README.md#subcommands). -
[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L650-L681) + For more information and examples, see [README.md](README.md#subcommands). +
[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L650-L682) ## `format-opts` ``` clojure From a9f8f811cdff4ee01c6f3f7c2066a660eb7cf518 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 20:59:25 +0100 Subject: [PATCH 35/51] verify option parsing --- test/babashka/cli_test.cljc | 109 +++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index d66da49..ef5b228 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -282,57 +282,39 @@ {:dispatch ["dep" "search"] :opts {:search-term "cheshire" :precision 100}} - (cli/dispatch table ["dep" "search" "cheshire" "100"]))))) + (cli/dispatch table ["dep" "search" "cheshire" "100"])))) -(deftest table->tree-test - (testing "internal represenation" - (is (= {:cmd - {"foo" - {:cmd - {"bar" - {:spec {:baz {:coerce :boolean}}, - :fn identity - :cmd - {"baz" - {:spec {:quux {:coerce :keyword}}, - :fn identity}}}}}}} - (#'cli/table->tree [{:cmds ["foo" "bar"] - :spec {:baz {:coerce :boolean}} - :fn identity} - {:cmds ["foo" "bar" "baz"] - :spec {:quux {:coerce :keyword}} - :fn identity}]))))) - -(deftest dispatch-tree-test - (d/deflet - (def table [{:cmds ["foo" "bar"] - :spec {:baz {:coerce :boolean}} - :fn identity} - {:cmds ["foo" "bar" "baz"] - :spec {:quux {:coerce :keyword}} - :fn identity}]) - (is (submap? {:type :org.babashka/cli - :cause :input-exhausted - :all-commands ["foo"]} - (try (cli/dispatch table []) - (catch Exception e (ex-data e))))) - (is (submap? {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} - (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) - (is (submap? {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} - (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"])))) + (testing "options of super commands" + (d/deflet + (def table [{:cmds ["foo" "bar"] + :spec {:baz {:coerce :boolean}} + :fn identity} + {:cmds ["foo" "bar" "baz"] + :spec {:quux {:coerce :keyword}} + :fn identity}]) + (is (submap? {:type :org.babashka/cli + :cause :input-exhausted + :all-commands ["foo"]} + (try (cli/dispatch table []) + (catch Exception e (ex-data e))))) + (is (submap? {:dispatch ["foo" "bar"], :opts {:baz true}, :args ["quux"]} + (cli/dispatch table ["foo" "bar" "--baz" "quux"]))) + (is (submap? {:dispatch ["foo" "bar" "baz"] , :opts {:baz true :quux :xyzzy}, :args nil} + (cli/dispatch table ["foo" "bar" "--baz" "baz" "--quux" "xyzzy"]))))) - (d/deflet - (def table [{:cmds [] :spec {:global {:coerce :boolean}}} - {:cmds ["foo"] :spec {:bar {:coerce :keyword}}} - {:cmds ["foo" "bar"] - :spec {:bar {:coerce :keyword}} - :fn identity}]) - (is (submap? - {:dispatch ["foo" "bar"] - :opts {:bar :bar - :global true} - :args ["arg1"]} - (cli/dispatch table ["--global" "foo" "--bar" "bar" "bar" "arg1"])))) + (testing "with global opts and conflicting options names" + (d/deflet + (def table [{:cmds [] :spec {:global {:coerce :boolean}}} + {:cmds ["foo"] :spec {:bar {:coerce :keyword}}} + {:cmds ["foo" "bar"] + :spec {:bar {:coerce :keyword}} + :fn identity}]) + (is (submap? + {:dispatch ["foo" "bar"] + :opts {:bar :bar + :global true} + :args ["arg1"]} + (cli/dispatch table ["--global" "foo" "--bar" "bar" "bar" "arg1"]))))) (testing "distinguish options at every level" (d/deflet @@ -357,7 +339,34 @@ :args ["bar" "arg1"]} (cli/dispatch table - ["--foo" "dude1" "foo" "--foo" "dude2" "bar" "--foo" "dude3" "bar" "arg1"])))))) + ["--foo" "dude1" "foo" "--foo" "dude2" "bar" "--foo" "dude3" "bar" "arg1"]))))) + + (testing "with colon options" + (d/deflet + (def table [{:cmds ["foo"] :fn identity}]) + (is (= "my-file.edn" (-> (cli/dispatch + table + ["foo" ":deps-file" "my-file.edn"]) + :opts :deps-file)))))) + +(deftest table->tree-test + (testing "internal represenation" + (is (= {:cmd + {"foo" + {:cmd + {"bar" + {:spec {:baz {:coerce :boolean}}, + :fn identity + :cmd + {"baz" + {:spec {:quux {:coerce :keyword}}, + :fn identity}}}}}}} + (#'cli/table->tree [{:cmds ["foo" "bar"] + :spec {:baz {:coerce :boolean}} + :fn identity} + {:cmds ["foo" "bar" "baz"] + :spec {:quux {:coerce :keyword}} + :fn identity}]))))) (deftest no-keyword-opts-test (is (= {:query [:a :b :c]} (cli/parse-opts From 47198a4f20195c62a8f6e47c5f4cc7ac8a485dd9 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 21:36:20 +0100 Subject: [PATCH 36/51] dude --- src/babashka/cli.cljc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 25855e5..16b3004 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -647,6 +647,8 @@ (error-fn {:cause error}) nil ((:fn cmd-info) (dissoc res :cmd-info)))))) +(prn :yooooo) + (defn dispatch "Subcommand dispatcher. From 68156389b2dc49b97856059dae49af874ee5bb2d Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Tue, 2 Jan 2024 22:39:28 +0100 Subject: [PATCH 37/51] edge case, todo add test --- src/babashka/cli.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 16b3004..c8fd3b9 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -604,7 +604,7 @@ (is-option? (first args))) parse-opts (deep-merge opts kwm) {:keys [args opts]} (if should-parse-args? - (parse-args args parse-opts) + (parse-args args (update parse-opts :exec-args merge all-opts)) {:args args :opts {}}) [arg & rest] args From a01d4b5087e3f7d9f7d0d60819b91d2a5f3bef74 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 3 Jan 2024 16:49:55 +0100 Subject: [PATCH 38/51] failing test --- test/babashka/cli_test.cljc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index ef5b228..d9475b4 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -347,7 +347,17 @@ (is (= "my-file.edn" (-> (cli/dispatch table ["foo" ":deps-file" "my-file.edn"]) - :opts :deps-file)))))) + :opts :deps-file))))) + + (testing "choose most specific" + (d/deflet + (def table [{:cmds ["foo" "bar"] :fn identity} + {:cmds ["foo" "baz"] :fn identity} + {:cmds ["foo"] :fn identity}]) + (is (= ["foo" "bar"] (-> (cli/dispatch + table + ["foo" "bar" "baz" "--dude" "1"]) + :dispatch)))))) (deftest table->tree-test (testing "internal represenation" From 1d52de2e269f8c61cb15a6e6377fd6bcf08a8ff3 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 3 Jan 2024 17:19:04 +0100 Subject: [PATCH 39/51] fixiefix --- src/babashka/cli.cljc | 5 +---- test/babashka/cli_test.cljc | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index c8fd3b9..1debb34 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -568,7 +568,7 @@ (reduce (fn [tree {:as cfg :keys [cmds]}] (let [ks (interleave (repeat :cmd) cmds)] (if (seq ks) - (assoc-in tree ks (dissoc cfg :cmds)) + (update-in tree ks merge (dissoc cfg :cmds)) ;; catch-all (merge tree (dissoc cfg :cmds))))) {} table)) @@ -596,7 +596,6 @@ ([tree args] (dispatch-tree' tree args nil)) ([tree args opts] - #_(def t tree) (loop [cmds [] all-opts {} args args cmd-info tree] (let [;; cmd-info (:cmd cmd-info) kwm cmd-info #_(select-keys cmd-info (filter keyword? (keys cmd-info))) @@ -647,8 +646,6 @@ (error-fn {:cause error}) nil ((:fn cmd-info) (dissoc res :cmd-info)))))) -(prn :yooooo) - (defn dispatch "Subcommand dispatcher. diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index d9475b4..7d61183 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -355,8 +355,8 @@ {:cmds ["foo" "baz"] :fn identity} {:cmds ["foo"] :fn identity}]) (is (= ["foo" "bar"] (-> (cli/dispatch - table - ["foo" "bar" "baz" "--dude" "1"]) + table + ["foo" "bar" "baz" "--dude" "1"]) :dispatch)))))) (deftest table->tree-test From e8565474a356c7c2568d9a0a31e0dfeb51257e26 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 3 Jan 2024 17:50:18 +0100 Subject: [PATCH 40/51] failing test --- test/babashka/cli_test.cljc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 7d61183..ed2f823 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -357,7 +357,16 @@ (is (= ["foo" "bar"] (-> (cli/dispatch table ["foo" "bar" "baz" "--dude" "1"]) - :dispatch)))))) + :dispatch))))) + + (testing "spec can be overriden" + (d/deflet + (def table [{:cmds ["foo" "bar"] :fn identity :spec {:version {:coerce :string}}} + {:cmds ["foo"] :fn identity :spec {:version {:coerce :boolean}}}]) + (is (= "2010" (-> (cli/dispatch + table + ["foo" "bar" "--version" "2010"]) + :opts :version)))))) (deftest table->tree-test (testing "internal represenation" From dd03f3a4757ca9cecdb1680d9b1e5b952feaee04 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 3 Jan 2024 18:05:28 +0100 Subject: [PATCH 41/51] wip --- src/babashka/cli.cljc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 1debb34..193a4ca 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -601,7 +601,9 @@ kwm cmd-info #_(select-keys cmd-info (filter keyword? (keys cmd-info))) should-parse-args? (or (has-parse-opts? kwm) (is-option? (first args))) + _ (prn :opts opts :kwm kwm) parse-opts (deep-merge opts kwm) + _ ((requiring-resolve 'clojure.pprint/pprint) parse-opts) {:keys [args opts]} (if should-parse-args? (parse-args args (update parse-opts :exec-args merge all-opts)) {:args args @@ -610,6 +612,7 @@ all-opts (-> (merge all-opts opts) (update ::opts-by-cmds (fnil conj []) {:cmds cmds :opts opts}))] + (prn :arg arg :all-opts all-opts) (if-let [subcmd-info (get (:cmd cmd-info) arg)] (recur (conj cmds arg) all-opts rest subcmd-info) (if (:fn cmd-info) From 12f38e539af3be4b65e0e9fbe1133dfe4a626fe9 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 3 Jan 2024 20:51:23 +0100 Subject: [PATCH 42/51] delay parsing opts in subcommand parsing --- src/babashka/cli.cljc | 229 ++++++++++++++++++++++-------------------- 1 file changed, 120 insertions(+), 109 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 193a4ca..97c91c9 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -330,116 +330,124 @@ [cmds args] (if (not= new-args args) [nil (concat new-args args)] [cmds args]) + ;; _ (prn :cmds cmds :args args) + opts* opts [opts last-opt added] - (loop [acc {} - current-opt nil - added nil - mode (when no-keyword-opts :hyphens) - args (seq args) - a->o a->o] - (if-not args - [acc current-opt added] - (let [raw-arg (first args) - opt? (keyword? raw-arg)] - (if opt? - (recur (process-previous acc current-opt added nil) - raw-arg added mode (next args) - a->o) - (let [implicit-true? (true? raw-arg) - arg (str raw-arg) - collect-fn (coerce-collect-fn collect current-opt (get coerce-opts current-opt)) - coerce-opt (get coerce-opts current-opt) - {:keys [hyphen-opt - composite-opt - kwd-opt - mode fst-colon]} (parse-key arg mode current-opt coerce-opt added)] - (if (or hyphen-opt - kwd-opt) - (let [long-opt? (str/starts-with? arg "--") - the-end? (and long-opt? (= "--" arg))] - (if the-end? - (let [nargs (next args)] - [(cond-> acc - nargs (vary-meta assoc-in [:org.babashka/cli :args] (vec nargs))) - current-opt added]) - (let [kname (if long-opt? - (subs arg 2) - (str/replace arg #"^(:|-|)" "")) - [kname arg-val] (if long-opt? - (str/split kname #"=") - [kname]) - raw-k (keyword kname) - k (get aliases raw-k raw-k)] - (if arg-val - (recur (process-previous acc current-opt added collect-fn) - k nil mode (cons arg-val (rest args)) a->o) - (let [next-args (next args) - next-arg (first next-args) - m (parse-key next-arg mode current-opt coerce-opt added)] - (if (or (:hyphen-opt m) - (empty? next-args)) - ;; implicit true - (if composite-opt - (let [chars (name k) - args (mapcat (fn [char] - [(str "-" char) true]) - chars) - next-args (concat args next-args)] - (recur acc - nil nil mode next-args - a->o)) - (let [negative? (when-not (contains? known-keys k) - (str/starts-with? (str k) ":no-")) - k (if negative? - (keyword (str/replace (str k) ":no-" "")) - k) - next-args (cons (not negative?) #_"true" next-args)] - (recur (process-previous acc current-opt added collect-fn) - k added mode next-args - a->o))) - (recur (process-previous acc current-opt added collect-fn) - k added mode next-args - a->o))))))) - (let [the-end? (or - (and (= :boolean coerce-opt) - (not= arg "true") - (not= arg "false")) - (and (= added current-opt) - (not collect-fn)))] - (if the-end? - (let [{new-args :args - a->o :args->opts} - (if args - (if a->o - (args->opts args a->o) + (if (and (::no-opts-after-args opts) + (seq cmds)) + (do + ;; (prn :result-to-dispatch cmds args :> (into (vec cmds) args)) + [(vary-meta {} assoc-in [:org.babashka/cli :args] (into (vec cmds) args)) nil nil]) + (loop [acc {} + current-opt nil + added nil + mode (when no-keyword-opts :hyphens) + args (seq args) + a->o a->o] + ;; (prn :acc acc :current-opt current-opt :added added :args args) + (if-not args + [acc current-opt added] + (let [raw-arg (first args) + opt? (keyword? raw-arg)] + (if opt? + (recur (process-previous acc current-opt added nil) + raw-arg added mode (next args) + a->o) + (let [implicit-true? (true? raw-arg) + arg (str raw-arg) + collect-fn (coerce-collect-fn collect current-opt (get coerce-opts current-opt)) + coerce-opt (get coerce-opts current-opt) + {:keys [hyphen-opt + composite-opt + kwd-opt + mode fst-colon]} (parse-key arg mode current-opt coerce-opt added)] + (if (or hyphen-opt + kwd-opt) + (let [long-opt? (str/starts-with? arg "--") + the-end? (and long-opt? (= "--" arg))] + (if the-end? + (let [nargs (next args)] + [(cond-> acc + nargs (vary-meta assoc-in [:org.babashka/cli :args] (vec nargs))) + current-opt added]) + (let [kname (if long-opt? + (subs arg 2) + (str/replace arg #"^(:|-|)" "")) + [kname arg-val] (if long-opt? + (str/split kname #"=") + [kname]) + raw-k (keyword kname) + k (get aliases raw-k raw-k)] + (if arg-val + (recur (process-previous acc current-opt added collect-fn) + k nil mode (cons arg-val (rest args)) a->o) + (let [next-args (next args) + next-arg (first next-args) + m (parse-key next-arg mode current-opt coerce-opt added)] + (if (or (:hyphen-opt m) + (empty? next-args)) + ;; implicit true + (if composite-opt + (let [chars (name k) + args (mapcat (fn [char] + [(str "-" char) true]) + chars) + next-args (concat args next-args)] + (recur acc + nil nil mode next-args + a->o)) + (let [negative? (when-not (contains? known-keys k) + (str/starts-with? (str k) ":no-")) + k (if negative? + (keyword (str/replace (str k) ":no-" "")) + k) + next-args (cons (not negative?) #_"true" next-args)] + (recur (process-previous acc current-opt added collect-fn) + k added mode next-args + a->o))) + (recur (process-previous acc current-opt added collect-fn) + k added mode next-args + a->o))))))) + (let [the-end? (or + (and (= :boolean coerce-opt) + (not= arg "true") + (not= arg "false")) + (and (= added current-opt) + (not collect-fn)))] + (if the-end? + (let [{new-args :args + a->o :args->opts} + (if args + (if a->o + (args->opts args a->o) + {:args args}) {:args args}) - {:args args}) - new-args? (not= args new-args)] - (if new-args? - (recur acc current-opt added mode new-args a->o) - [(vary-meta acc assoc-in [:org.babashka/cli :args] (vec args)) current-opt added])) - (let [opt (when-not (and (= :keywords mode) - fst-colon) - current-opt)] - (recur (try - (add-val acc current-opt collect-fn (coerce-coerce-fn coerce-opt) arg implicit-true?) - (catch #?(:clj ExceptionInfo :cljs :default) e - (error-fn {:cause :coerce - :msg #?(:clj (.getMessage e) - :cljs (ex-message e)) - :option current-opt - :value arg}) - ;; Since we've encountered an error, don't add this opt - acc)) - opt - opt - mode - (next args) - a->o)))))))))) + new-args? (not= args new-args)] + (if new-args? + (recur acc current-opt added mode new-args a->o) + [(vary-meta acc assoc-in [:org.babashka/cli :args] (vec args)) current-opt added])) + (let [opt (when-not (and (= :keywords mode) + fst-colon) + current-opt)] + (recur (try + (add-val acc current-opt collect-fn (coerce-coerce-fn coerce-opt) arg implicit-true?) + (catch #?(:clj ExceptionInfo :cljs :default) e + (error-fn {:cause :coerce + :msg #?(:clj (.getMessage e) + :cljs (ex-message e)) + :option current-opt + :value arg}) + ;; Since we've encountered an error, don't add this opt + acc)) + opt + opt + mode + (next args) + a->o))))))))))) collect-fn (coerce-collect-fn collect last-opt (get coerce-opts last-opt)) opts (-> (process-previous opts last-opt added collect-fn) (cond-> - (seq cmds) + (and (seq cmds) (not (::no-opts-after-args opts*))) (vary-meta update-in [:org.babashka/cli :args] (fn [args] (into (vec cmds) args))))) @@ -601,18 +609,21 @@ kwm cmd-info #_(select-keys cmd-info (filter keyword? (keys cmd-info))) should-parse-args? (or (has-parse-opts? kwm) (is-option? (first args))) - _ (prn :opts opts :kwm kwm) + ;; _ (prn :opts opts :kwm kwm) parse-opts (deep-merge opts kwm) - _ ((requiring-resolve 'clojure.pprint/pprint) parse-opts) + ;; _ ((requiring-resolve 'clojure.pprint/pprint) parse-opts) + ;; _ (prn :dispatch-args args) {:keys [args opts]} (if should-parse-args? - (parse-args args (update parse-opts :exec-args merge all-opts)) + (parse-args args (assoc (update parse-opts :exec-args merge all-opts) + ::no-opts-after-args true)) {:args args :opts {}}) + ;; _ (prn :dispatch-args-post args) [arg & rest] args all-opts (-> (merge all-opts opts) (update ::opts-by-cmds (fnil conj []) {:cmds cmds :opts opts}))] - (prn :arg arg :all-opts all-opts) + ;; (prn :arg arg :all-opts all-opts) (if-let [subcmd-info (get (:cmd cmd-info) arg)] (recur (conj cmds arg) all-opts rest subcmd-info) (if (:fn cmd-info) From aff64749717b296594d7e01450b74945ed1e5e14 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 3 Jan 2024 20:53:23 +0100 Subject: [PATCH 43/51] yeah --- test/babashka/cli_test.cljc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index ed2f823..1272fe4 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -363,6 +363,10 @@ (d/deflet (def table [{:cmds ["foo" "bar"] :fn identity :spec {:version {:coerce :string}}} {:cmds ["foo"] :fn identity :spec {:version {:coerce :boolean}}}]) + (is (submap? {:opts {:version true}, :args ["2010"]} + (cli/dispatch + table + ["foo" "--version" "2010"]))) (is (= "2010" (-> (cli/dispatch table ["foo" "bar" "--version" "2010"]) From 915120e889789be5add67cae8dafac99d3f8477f Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 3 Jan 2024 20:58:02 +0100 Subject: [PATCH 44/51] dude --- test/babashka/cli_test.cljc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 1272fe4..885fe24 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -362,7 +362,8 @@ (testing "spec can be overriden" (d/deflet (def table [{:cmds ["foo" "bar"] :fn identity :spec {:version {:coerce :string}}} - {:cmds ["foo"] :fn identity :spec {:version {:coerce :boolean}}}]) + {:cmds ["foo"] :fn identity :spec {:version {:coerce :boolean} + :dude {:coerce :boolean}}}]) (is (submap? {:opts {:version true}, :args ["2010"]} (cli/dispatch table @@ -370,7 +371,12 @@ (is (= "2010" (-> (cli/dispatch table ["foo" "bar" "--version" "2010"]) - :opts :version)))))) + :opts :version))) + (is (= {:dude true :version "2010"} + (-> (cli/dispatch + table + ["foo" "--dude" "bar" "--version" "2010"]) + :opts)))))) (deftest table->tree-test (testing "internal represenation" From 5d3f43e211f14862eabe1cb564a677b89ec80cf2 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 4 Jan 2024 11:38:08 +0100 Subject: [PATCH 45/51] dude --- test/babashka/cli_test.cljc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 885fe24..06f3eaa 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -361,7 +361,8 @@ (testing "spec can be overriden" (d/deflet - (def table [{:cmds ["foo" "bar"] :fn identity :spec {:version {:coerce :string}}} + (def table [{:cmds ["foo" "bar"] :fn identity :spec {:version {:coerce :string} + }} {:cmds ["foo"] :fn identity :spec {:version {:coerce :boolean} :dude {:coerce :boolean}}}]) (is (submap? {:opts {:version true}, :args ["2010"]} @@ -376,7 +377,16 @@ (-> (cli/dispatch table ["foo" "--dude" "bar" "--version" "2010"]) - :opts)))))) + :opts))) + (testing "specific spec replaces less specific spec (no merge)" + (is (= {:dude "some-value"} + (-> (cli/dispatch + table + ["foo" "bar" "--dude" "some-value"]) + :opts)))))) + ;; TODO: args->opts conflinct with subcommand + ;; TODO: more specific specs should not be merged, but overwritten + ) (deftest table->tree-test (testing "internal represenation" From a6726864eaaf4e47fe13ebf7a0b71c9f51f1f5ed Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 4 Jan 2024 12:24:53 +0100 Subject: [PATCH 46/51] prioritize subcommands --- src/babashka/cli.cljc | 41 ++++++++++++++++++++----------------- test/babashka/cli_test.cljc | 17 +++++++++++---- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 97c91c9..3d4c2ac 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -215,20 +215,22 @@ :args args}))) (defn- args->opts - [args args->opts] - (let [[new-args args->opts] - (if args->opts - (if (seq args) - (let [arg-count (count args) - cnt (min arg-count - (bounded-count arg-count args->opts))] - [(concat (interleave args->opts args) - (drop cnt args)) - (drop cnt args->opts)]) - [args args->opts]) - [args args->opts])] - {:args new-args - :args->opts args->opts})) + ([args args->opts-map] (args->opts args args->opts-map #{})) + ([args args->opts-map ignored-args] + (let [[new-args args->opts] + (if args->opts-map + (if (and (seq args) + (not (contains? ignored-args (first args)))) + (let [arg-count (count args) + cnt (min arg-count + (bounded-count arg-count args->opts-map))] + [(concat (interleave args->opts-map args) + (drop cnt args)) + (drop cnt args->opts-map)]) + [args args->opts-map]) + [args args->opts-map])] + {:args new-args + :args->opts args->opts}))) (defn- parse-key [arg mode current-opt coerce-opt added] (let [fst-char (first-char arg) @@ -324,7 +326,7 @@ (if-let [a->o (or (:args->opts opts) ;; DEPRECATED: (:cmds-opts opts))] - (args->opts cmds a->o) + (args->opts cmds a->o (::dispatch-tree-ignored-args opts)) {:args->opts nil :args args}) [cmds args] (if (not= new-args args) @@ -333,7 +335,7 @@ ;; _ (prn :cmds cmds :args args) opts* opts [opts last-opt added] - (if (and (::no-opts-after-args opts) + (if (and (::dispatch-tree opts) (seq cmds)) (do ;; (prn :result-to-dispatch cmds args :> (into (vec cmds) args)) @@ -419,7 +421,7 @@ a->o :args->opts} (if args (if a->o - (args->opts args a->o) + (args->opts args a->o (::dispatch-tree-ignored-args opts)) {:args args}) {:args args}) new-args? (not= args new-args)] @@ -447,7 +449,7 @@ collect-fn (coerce-collect-fn collect last-opt (get coerce-opts last-opt)) opts (-> (process-previous opts last-opt added collect-fn) (cond-> - (and (seq cmds) (not (::no-opts-after-args opts*))) + (and (seq cmds) (not (::dispatch-tree opts*))) (vary-meta update-in [:org.babashka/cli :args] (fn [args] (into (vec cmds) args))))) @@ -615,7 +617,8 @@ ;; _ (prn :dispatch-args args) {:keys [args opts]} (if should-parse-args? (parse-args args (assoc (update parse-opts :exec-args merge all-opts) - ::no-opts-after-args true)) + ::dispatch-tree true + ::dispatch-tree-ignored-args (set (keys (:cmd cmd-info))))) {:args args :opts {}}) ;; _ (prn :dispatch-args-post args) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 06f3eaa..0cbd7f2 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -383,10 +383,19 @@ (-> (cli/dispatch table ["foo" "bar" "--dude" "some-value"]) - :opts)))))) - ;; TODO: args->opts conflinct with subcommand - ;; TODO: more specific specs should not be merged, but overwritten - ) + :opts)))) + + (def table [{:cmds ["foo" "bar"] :fn identity :spec {:version {:coerce :string} + }} + {:cmds ["foo"] :fn identity :spec {:version {:coerce :boolean} + :dude {:coerce :boolean}} + :args->opts [:some-option]}]) + (testing "subcommand wins from args->opts" + (is (= {:version "2000"} + (-> (cli/dispatch + table + ["foo" "bar" "--version" "2000"]) + :opts))))))) (deftest table->tree-test (testing "internal represenation" From 988ae4803bb4449ddbcd095460f94c8c4fe7f3e2 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 4 Jan 2024 12:25:46 +0100 Subject: [PATCH 47/51] yeah --- test/babashka/cli_test.cljc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 0cbd7f2..2b285e1 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -391,11 +391,10 @@ :dude {:coerce :boolean}} :args->opts [:some-option]}]) (testing "subcommand wins from args->opts" - (is (= {:version "2000"} + (is (= {:dispatch ["foo" "bar"], :opts {:version "2000"}, :args ["some-arg"]} (-> (cli/dispatch table - ["foo" "bar" "--version" "2000"]) - :opts))))))) + ["foo" "bar" "--version" "2000" "some-arg"])))))))) (deftest table->tree-test (testing "internal represenation" From 5c4833353334ab1775086b9cc4eef8706b70e673 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 4 Jan 2024 12:27:04 +0100 Subject: [PATCH 48/51] dude --- test/babashka/cli_test.cljc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 2b285e1..54957f8 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -385,11 +385,12 @@ ["foo" "bar" "--dude" "some-value"]) :opts)))) - (def table [{:cmds ["foo" "bar"] :fn identity :spec {:version {:coerce :string} - }} - {:cmds ["foo"] :fn identity :spec {:version {:coerce :boolean} - :dude {:coerce :boolean}} - :args->opts [:some-option]}]) + (def table [{:cmds ["foo"] :fn identity + :spec {:version {:coerce :boolean}} + :args->opts [:some-option]} + {:cmds ["foo" "bar"] + :fn identity + :spec {:version {:coerce :string}}}]) (testing "subcommand wins from args->opts" (is (= {:dispatch ["foo" "bar"], :opts {:version "2000"}, :args ["some-arg"]} (-> (cli/dispatch From d6f1a6caa4219efeb66a7ffcdeb591176a863805 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 4 Jan 2024 20:29:31 +0100 Subject: [PATCH 49/51] doc --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 8ceef69..833d735 100644 --- a/README.md +++ b/README.md @@ -510,10 +510,6 @@ E.g.: {:dispatch ["sub1" "sub2"], :opts {:foo :a, :bar :b, :baz :c}, - :opts-by-cmds - [{:cmds [], :opts {:foo :a}} - {:cmds ["sub1"], :opts {:bar :b}} - {:cmds ["sub1" "sub2"], :opts {:baz :c}}], :args ["arg"]} ``` From 3c772be118e3b95e96f350a150342323c1786ee3 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 4 Jan 2024 20:39:18 +0100 Subject: [PATCH 50/51] doc --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 833d735..769c752 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,25 @@ E.g.: :args ["arg"]} ``` +Note that specs are not merged, such that: + +``` clojure +(cli/dispatch table ["sub1" "--foo" "bar"]) +``` + +returns `{:dispatch ["sub1"], :opts {:foo "bar"}}` (`"bar"` is not coerced as a keyword). + +Note that it is possible to use `:args->opts` but subcommands are always prioritized over arguments: + +``` clojure +(def table + [{:cmds ["sub1"] :fn identity :spec sub1-spec :args->opts [:some-opt]} + {:cmds ["sub1" "sub2"] :fn identity :spec sub2-spec}]) + +(cli/dispatch table ["sub1" "dude"]) ;;=> {:dispatch ["sub1"], :opts {:some-opt "dude"}} +(cli/dispatch table ["sub1" "sub2"]) ;;=> {:dispatch ["sub1" "sub2"], :opts {}} +``` + ## Babashka tasks For documentation on babashka tasks, go From 20953bffc25de7a2e52b408ef3871ead134c444a Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 4 Jan 2024 20:43:17 +0100 Subject: [PATCH 51/51] yeah --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 769c752..07470e6 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,8 @@ Additional `parse-arg` options may be passed in each table entry: {:cmds [] :fn help}]) ``` +Since cli 0.8.54 the order of `:cmds` in the table doesn't matter. + ### Shared options Since cli 0.8.54, babashka.cli supports parsing shared options in between and before the subcommands.