Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 23 additions & 12 deletions src/babashka/cli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions test/babashka/cli_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"}}}]]
Expand Down