diff --git a/CHANGELOG.md b/CHANGELOG.md index 94410cd..92023b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ For breaking changes, check [here](#breaking-changes). [Babashka CLI](https://github.com/babashka/cli): turn Clojure functions into CLIs! +## Unreleased + +- A required `:inherit` option may be supplied after its command, not only before it + ## 0.12.80 - Add ordered `:enum` values for validation, help and completion diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 312ff09..30dc2ae 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -532,19 +532,26 @@ :opts m} flag (assoc :flag flag))))))) (when require - (doseq [k require] - (when-not (find m k) - (let [flag (get opt->flag k) - arg (when (contains? positional k) (arg-label spec-map k))] - (error-fn (cond-> {:cause :require - :msg (if arg - (str "Required argument: " arg) - (str "Required option: " (flag-for k))) - :require require - :option k - :opts m} - arg (assoc :arg arg) - flag (assoc :flag flag))))))) + ;; in a dispatch tree, a leading command word after this level's options + ;; routes to a child, where an `:inherit` option can still be supplied + ;; (`group sub --opt v`). Defer its requirement to that child; the terminal + ;; level, which does not descend, enforces it. + (let [descending? (contains? (::dispatch-tree-ignored-args opts) + (first (-> (meta m) :org.babashka/cli :args)))] + (doseq [k require] + (when (and (not (find m k)) + (not (and descending? (:inherit (get spec-map k))))) + (let [flag (get opt->flag k) + arg (when (contains? positional k) (arg-label spec-map k))] + (error-fn (cond-> {:cause :require + :msg (if arg + (str "Required argument: " arg) + (str "Required option: " (flag-for k))) + :require require + :option k + :opts m} + arg (assoc :arg arg) + flag (assoc :flag flag)))))))) (when validate (doseq [[k vf] validate] (let [f (or (and diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 76cb7c4..7188e99 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -1638,6 +1638,43 @@ (is (thrown-with-msg? #?(:cljd Object :default Exception) #"Unknown option: --registry" (cli/dispatch table ["deps" "outdated" "--registry" "X"] {:restrict true}))))) + (testing "a non-:inherit parent option's spec does not reach the child after the command" + (let [table [{:cmds ["deps"] :spec {:registry {:coerce :keyword + :validate #{:a :b}}}} + {:cmds ["deps" "outdated"] :fn identity}]] + (testing "parent :coerce is not applied (value stays a raw string)" + (is (= {:registry "X"} + (:opts (cli/dispatch table ["deps" "outdated" "--registry" "X"]))))) + (testing "parent :validate is not applied (an invalid value is not rejected)" + (is (= {:registry "zzz"} + (:opts (cli/dispatch table ["deps" "outdated" "--registry" "zzz"]))))) + (testing "the same option before the command IS parsed by the parent spec" + (is (= {:registry :a} + (:opts (cli/dispatch table ["deps" "--registry" "a" "outdated"])))) + (is (thrown-with-msg? + #?(:cljd Object :default Exception) #"Invalid value for option --registry" + (cli/dispatch table ["deps" "--registry" "zzz" "outdated"])))))) + (testing "a required :inherit option may be supplied on either side of the command" + (let [table [{:cmds ["deps"] :spec {:registry {:require true :inherit true}}} + {:cmds ["deps" "outdated"] :fn identity}]] + (testing "before the command" + (is (= {:registry "X"} (:opts (cli/dispatch table ["deps" "--registry" "X" "outdated"]))))) + (testing "after the command (require deferred to where it is supplied)" + (is (= {:registry "X"} (:opts (cli/dispatch table ["deps" "outdated" "--registry" "X"]))))) + (testing "still required when supplied nowhere" + (is (thrown-with-msg? + #?(:cljd Object :default Exception) #"Required option: --registry" + (cli/dispatch table ["deps" "outdated"])))) + (testing "required at the group itself (no command)" + (is (thrown-with-msg? + #?(:cljd Object :default Exception) #"Required option: --registry" + (cli/dispatch table ["deps"]))))) + (testing "a required option WITHOUT :inherit is still parent-only (errors after the command)" + (let [table [{:cmds ["deps"] :spec {:registry {:require true}}} + {:cmds ["deps" "outdated"] :fn identity}]] + (is (thrown-with-msg? + #?(:cljd Object :default Exception) #"Required option: --registry" + (cli/dispatch table ["deps" "outdated" "--registry" "X"])))))) (testing "dispatch-level :inherit makes options inherit without per-option marking" (let [table [{:cmds ["deps"] :spec {:registry {} :token {}}} {:cmds ["deps" "outdated"] :fn identity :spec {:format {}}}]]