diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6abe7..77168ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ For breaking changes, check [here](#breaking-changes). ## Unreleased +- Help: show the dispatch-level `:spec` options under `Inherited options:`. The parser always accepted these options, but help did not show them +- Help: `format-command-help` accepts `:spec`, the dispatch-level spec, so a standalone call shows the same options as `dispatch` - Completion: do not offer `:positional` keys as options, they are rejected when typed as a flag ## 0.12.85 diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 333f482..cbd0664 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1857,9 +1857,13 @@ $env.config.completions.external.completer = {|spans| ancestors) and the `:parents` pointers (ancestors with non-inherited options that must precede the command). + `global-spec` is the dispatch-level `:spec`: options accepted at every level, + which have no ancestor node to be listed under. Ancestors win over it, and the + node's own spec wins over both. + Specs are mapified here for set reasoning (a standalone `format-command-help` spec may be a vec-of-pairs); display order is handled by `render-help`." - [tree cmds prog inherit] + [tree cmds prog inherit global-spec] (let [node-at (fn [path] (get-in tree (interleave (repeat :cmd) path))) prog-at (fn [path] (str/join " " (cons prog path))) ;; options available at the target level itself (e.g. an injected --help, @@ -1872,7 +1876,7 @@ $env.config.completions.external.completer = {|spans| spec (->spec-map (:spec (node-at pre))) inh (inherited-entries spec inherit)]] {:pre pre :inh inh :own (apply dissoc spec (keys inh))}) - inherited (reduce merge {} (map :inh ancestors)) + inherited (reduce merge (->spec-map global-spec) (map :inh ancestors)) ;; ancestors with non-inherited options that aren't also available here ;; (those must be given before the command) parents (for [{:keys [pre own]} ancestors @@ -1916,6 +1920,9 @@ $env.config.completions.external.completer = {|spans| * `:inherit` - only needed when you pass a dispatch-level `:inherit` to `dispatch`; pass the same value so `Inherited options:` matches. Per-option `:inherit true` is detected automatically. + * `:spec` - the dispatch-level spec, when you pass one to `dispatch`: + options accepted at every level, listed under + `Inherited options:` since no ancestor node declares them. Options are listed in the entry's `:order` when it has one, else in spec order (a vec-of-pairs `:spec` keeps its order; a map follows key order, unreliable @@ -1924,9 +1931,9 @@ $env.config.completions.external.completer = {|spans| This is the renderer the `:help` option uses; call it from a custom `:help-fn` to render the standard help and then add your own output. An entry may carry `:no-doc true` to be omitted from `Commands:`." - [{:keys [table cmds prog inherit] :or {cmds []}}] + [{:keys [table cmds prog inherit spec] :or {cmds []}}] (let [tree (table->tree table) - ctx (command-help-context tree (vec cmds) prog inherit)] + ctx (command-help-context tree (vec cmds) prog inherit spec)] (render-help (:node ctx) ctx))) (defn ^:dynamic *exit-fn* @@ -1965,8 +1972,9 @@ $env.config.completions.external.completer = {|spans| caller does not exit (it returns like a normal `:fn`, so the process ends with status 0). Reads the command tree, `:prog` and `:inherit` from the data dispatch threads in; renders via [[format-command-help]]." - [{:keys [tree dispatch prog inherit]}] - (println (format-command-help {:table tree :cmds (or dispatch []) :prog prog :inherit inherit}))) + [{:keys [tree dispatch prog inherit] :as data}] + (println (format-command-help {:table tree :cmds (or dispatch []) :prog prog + :inherit inherit :spec (::global-spec data)}))) (defn- dispatch-error-msg "The terse one-line `:msg` for a command-level dispatch error, or nil for an @@ -1994,10 +2002,10 @@ $env.config.completions.external.completer = {|spans| this, then calls [[*exit-fn*]]). Call it from a custom `:error-fn` to keep the standard message and add your own output. `--help`/`-h` is not an error - it goes to the `:help-fn`, rendered by [[format-command-help]]." - [{:keys [cause dispatch wrong-input msg prog inherit tree]}] + [{:keys [cause dispatch wrong-input msg prog inherit tree] :as data}] (let [tree (table->tree tree) path (or dispatch []) - ctx-at (fn [p] (command-help-context tree (vec p) prog inherit)) + ctx-at (fn [p] (command-help-context tree (vec p) prog inherit (::global-spec data))) hint (str "Run \"" (str/join " " (cons prog path)) " --help\" for more information.") usage (fn [p] @@ -2032,12 +2040,15 @@ $env.config.completions.external.completer = {|spans| (eprintln (format-command-error data))) (defn- thread-dispatch-context - "Add the dispatch-level `:prog` and `:inherit` (when set) to error/help `data`, - so an `:error-fn` / `:help-fn` can render without being handed them." - [data {:keys [prog inherit]}] + "Add the dispatch-level `:prog`, `:inherit` and `:spec` (when set) to + error/help `data`, so an `:error-fn` / `:help-fn` can render without being + handed them. The spec goes under a namespaced key: option-error data already + carries the parser's own `:spec`." + [data {:keys [prog inherit spec]}] (cond-> data prog (assoc :prog prog) - inherit (assoc :inherit inherit))) + inherit (assoc :inherit inherit) + spec (assoc ::global-spec spec))) ;; command names to suggest in errors: skip `:no-doc`, same as help and ;; completion hide them diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 05df5a8..f71f4ee 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -682,6 +682,18 @@ (when-not (::exit (ex-data e)) (throw e))))))] {:out out :exit @exit})) +(deftest dispatch-global-spec-test + (let [tree {:spec {:bar {:coerce :long}} :restrict true :fn identity} + global {:spec {:foo {:coerce :long :desc "global foo"}}}] + (testing "a dispatch-level :spec option parses though the node does not declare it" + (is (submap? {:opts {:foo 1}} (cli/dispatch tree ["--foo" "1"] global)))) + (testing "the node's own options still parse" + (is (submap? {:opts {:bar 2}} (cli/dispatch tree ["--bar" "2"] global)))) + (testing ":restrict still rejects an option neither declares" + (is (thrown-with-msg? + #?(:cljd Object :default Exception) #"Unknown option: --nope" + (cli/dispatch tree ["--nope" "1"] global)))))) + (deftest dispatch-tree-input-test ;; dispatch accepts a tree (the table->tree shape) directly (let [tree {:doc "tool" @@ -1148,6 +1160,19 @@ (is (= (str "Usage: p sub [options]\n\n" "Options:\n --x local x") (cli/format-command-help {:table t :cmds ["sub"] :prog "p"}))))) + (testing "the dispatch-level :spec is listed, having no ancestor to carry it" + (is (= (str "Usage: p [options]\n\n" + "Options:\n --bar own\n\n" + "Inherited options:\n --foo global foo") + (cli/format-command-help {:table {:spec {:bar {:desc "own"}}} + :prog "p" + :spec {:foo {:desc "global foo"}}}))) + (testing "the command's own spec wins over it" + (is (= (str "Usage: p [options]\n\n" + "Options:\n --bar own") + (cli/format-command-help {:table {:spec {:bar {:desc "own"}}} + :prog "p" + :spec {:bar {:desc "global bar"}}}))))) (testing ":args->opts renders labeled positionals in the usage line" (let [t [{:cmds ["copy"] :fn identity :doc "Copy" :args->opts [:src :dest] :spec {:force {:desc "Force"}}}]]