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
41 changes: 26 additions & 15 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Terminates the process after `dispatch`'s `:help` option prints an *error*
Must exit or throw.

Default: `System/exit` (JVM), `js/process.exit` (Node), `throw` (browser).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1930-L1958">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2035-L2063">Source</a></sub></p>

## <a name="babashka.cli/apply-defaults">`apply-defaults`</a>
``` clojure
Expand All @@ -76,7 +76,7 @@ Fills missing keys in `m` from defaults. Existing keys in `m` win.
Supported options:
* `:exec-args` - map of defaults. Not subject to `:restrict`.
* `:spec` - spec; `:default` entries become defaults via `spec->opts`.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L589-L605">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L589-L609">Source</a></sub></p>

## <a name="babashka.cli/auto-coerce">`auto-coerce`</a>
``` clojure
Expand Down Expand Up @@ -136,7 +136,7 @@ The default `:max-width-fn` for [`format-table`](#babashka.cli/format-table)/[`f
width or nil: node `process.stdout.columns`, else `$COLUMNS`, else a JLine
provider probe (clj, when JLine is on the classpath, e.g. babashka), else nil
(the caller then falls back to 80).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L913-L931">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L967-L985">Source</a></sub></p>

## <a name="babashka.cli/dispatch">`dispatch`</a>
``` clojure
Expand Down Expand Up @@ -176,6 +176,14 @@ Command dispatcher.
what order (like `:order` does for options). A table keeps its entry order
automatically.

`:cmd-aliases` declares alternative names for a command: a collection of
strings, symbols or keywords. On a table entry it aliases the entry's last
command, e.g. `{:cmds ["dep" "add"] :fn add :cmd-aliases ["a"]}` makes
`prog dep a` dispatch like `prog dep add`. In the tree format it sits on the
command's node. `:dispatch` always carries the canonical name, the command
index and completion offer only canonical names, and the command's own help
page lists its aliases.

When a match is found, `:fn` called with the return value of
[`parse-args`](#babashka.cli/parse-args) applied to `args` enhanced with:

Expand Down Expand Up @@ -218,11 +226,11 @@ Command dispatcher.
Each entry in the table may have additional [`parse-args`](#babashka.cli/parse-args) options.

For more information and examples, see [README.md](README.md#commands).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2270-L2367">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2386-L2491">Source</a></sub></p>

## <a name="babashka.cli/format-command-error">`format-command-error`</a>
``` clojure
(format-command-error {:keys [cause dispatch wrong-input msg prog inherit tree]})
(format-command-error {:keys [cause dispatch wrong-input msg prog inherit tree], :as data})
```
Function.

Expand All @@ -242,11 +250,11 @@ Render a terse, helpful message (a string) for a dispatch error.
this, then calls [`*exit-fn*`](#babashka.cli/*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`](#babashka.cli/format-command-help).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1978-L2022">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2084-L2128">Source</a></sub></p>

## <a name="babashka.cli/format-command-help">`format-command-help`</a>
``` clojure
(format-command-help {:keys [table cmds prog inherit], :or {cmds []}})
(format-command-help {:keys [table cmds prog inherit spec], :or {cmds []}})
```
Function.

Expand Down Expand Up @@ -280,6 +288,9 @@ Render conventional `--help` text (a string) for the command at path `cmds`
* `: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 @@ -288,7 +299,7 @@ Render conventional `--help` text (a string) for the command at path `cmds`
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:`.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1886-L1928">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1988-L2033">Source</a></sub></p>

## <a name="babashka.cli/format-opts">`format-opts`</a>
``` clojure
Expand All @@ -299,7 +310,7 @@ Function.
Formats options into an options usage help string.

See [Printing options](/README.md#printing-options).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1109-L1120">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1163-L1174">Source</a></sub></p>

## <a name="babashka.cli/format-table">`format-table`</a>
``` clojure
Expand All @@ -312,7 +323,7 @@ Function.

Formats `rows` into a table (string).
See [Printing options](/README.md#printing-options).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L984-L1006">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1038-L1060">Source</a></sub></p>

## <a name="babashka.cli/merge-opts">`merge-opts`</a>
``` clojure
Expand All @@ -331,7 +342,7 @@ Function.

Converts options to a table of rows.
See [Printing options](/README.md#printing-options).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1046-L1066">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1100-L1120">Source</a></sub></p>

## <a name="babashka.cli/parse-args">`parse-args`</a>
``` clojure
Expand All @@ -345,7 +356,7 @@ Same as [`parse-opts`](#babashka.cli/parse-opts) with return data reshaped.
Returns a map with:
* `:opts` parsed opts
* `:args` remaining unparsed `args`
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L854-L864">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L908-L918">Source</a></sub></p>

## <a name="babashka.cli/parse-cmds">`parse-cmds`</a>
``` clojure
Expand Down Expand Up @@ -406,7 +417,7 @@ Returns a map of options parsed from command line arguments `args`, a seq of str
;; => throws 'Unknown option --qux' exception b/c there is no :qux key in the spec
```
See also: [`parse-args`](#babashka.cli/parse-args)
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L808-L852">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L859-L903">Source</a></sub></p>

## <a name="babashka.cli/parse-opts*">`parse-opts*`</a>
``` clojure
Expand All @@ -428,7 +439,7 @@ Parses CLI `args` into a raw opts map. Returns string values unchanged
`:collect`, `:no-keyword-opts`, `:repeated-opts`, `:args->opts`, `:spec`.
A `:spec`'s `:coerce`/`:collect`/`:alias` entries steer parsing (e.g. boolean
disambiguation) like they do in `parse-opts` - values still come back raw.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L610-L806">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L614-L857">Source</a></sub></p>

## <a name="babashka.cli/spec->opts">`spec->opts`</a>
``` clojure
Expand Down Expand Up @@ -458,7 +469,7 @@ Converts a `dispatch` table into a tree. Each `:cmds` becomes a path of
```

A tree passed in is normalized and returned, so the function is idempotent.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1376-L1398">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1470-L1492">Source</a></sub></p>

## <a name="babashka.cli/validate-opts">`validate-opts`</a>
``` clojure
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ For breaking changes, check [here](#breaking-changes).

## Unreleased

- [#219](https://github.com/babashka/cli/issues/219): `:cmd-aliases` on a table entry or tree node gives a command one or more alternative names. An alias dispatches like the command. The `:dispatch` value contains the canonical name. The command index and completion output contain canonical names. The command's `--help` output lists its aliases, as in npm and gh.
- A short option that declares a non-boolean `:coerce` takes the rest of its token as its value, like getopt: `-J-Dfoo=bar` binds `"-Dfoo=bar"`, `-p80` binds `80`. One leading `=` is stripped, like `--foo=bar`. Flag letters may precede the valued option in a cluster: with `:b` a flag and `:a` valued, `-ba x` parses as `-b -a x`
- [#216](https://github.com/babashka/cli/issues/216): in a cluster of flags, where no letter takes a value, an interior hyphen is an error instead of silently ending option parsing. With an `:error-fn` the remaining letters still parse
- Fix: a value bound with `--foo=val` may start with a hyphen: `--foo=-bar` binds `"-bar"` instead of reporting a missing value
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,18 @@ of printed output, but are still callable on the command line.
"cache" {...}}}
```

Use `:cmd-aliases` to give a command one or more alternative names:

``` clojure
[{:cmds ["new"] :fn new :cmd-aliases ["n"]}]
```

`prog n` now dispatches like `prog new`. The `:dispatch` value contains the
canonical name. The command index and completion output contain canonical
names. The command's help page lists its aliases (`Aliases: n`), as in npm and
gh. An alias that collides with a command name, or with an alias of a sibling
command, is an error.

### Help

> For a guided walkthrough of automatic help and shell completions, see this
Expand Down
64 changes: 59 additions & 5 deletions src/babashka/cli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1264,7 +1264,11 @@
(str " " (str/join " " labels))))
:else "")))

(declare cmd-name)

(defn- help-commands-table [node]
;; canonical names only: aliases show on the command's own help page,
;; as npm and gh do, so a long alias list cannot crowd the index
(mapv (fn [[cmd subnode]]
[(str cmd) (or (help-first-line (:doc subnode)) "")])
(cmd-children node)))
Expand Down Expand Up @@ -1292,11 +1296,15 @@
inherited (apply dissoc inherited (keys spec-map))
desc (help-description (:doc node))
cmds (help-commands-table node)
aliases (map cmd-name (:cmd-aliases node))
sections
(cond-> [(help-usage-line prog node (or (visible-spec? opt-spec) (visible-spec? inherited)))]
desc
(conj desc)

(seq aliases)
(conj (str "Aliases: " (str/join ", " aliases)))

(seq cmds)
(conj (str "Commands:\n" (format-table {:rows cmds :indent 2 :divider " "})))

Expand Down Expand Up @@ -1387,6 +1395,37 @@
(:cmd-order node) (update :cmd-order #(mapv cmd-name %)))
node))

(defn- aliases-of
"Returns the `:cmd-aliases` names of child command `child-name` as strings.
If `:cmd-aliases` is not a collection, the function throws an error."
[child-name child]
(when-let [a (:cmd-aliases child)]
(if (coll? a)
(map cmd-name a)
(throw (ex-info (str ":cmd-aliases of command " child-name
" takes a collection of names, got " (pr-str a))
{:cmd-aliases a :command child-name})))))

(defn- cmd-aliases
"Returns a map from each alias to its canonical child name.
Throws an error when an alias matches a command name or another child alias."
[node]
(reduce (fn [acc [alias child-name]]
(cond
(contains? (:cmd node) alias)
(throw (ex-info (str "Alias " alias " of command " child-name
" collides with command " alias)
{:alias alias :command child-name}))
(contains? acc alias)
(throw (ex-info (str "Alias " alias " is claimed by commands "
(get acc alias) " and " child-name)
{:alias alias :commands [(get acc alias) child-name]}))
:else (assoc acc alias child-name)))
{}
(for [[child-name child] (:cmd node)
alias (aliases-of child-name child)]
[alias child-name])))

(defn- normalize-node
"Normalize tree `node`, recursively. Rejects table-entry `:cmds` on a node,
dedupes an explicit `:cmd-order` and
Expand Down Expand Up @@ -1626,14 +1665,16 @@
(loop [node tree, ropts (resolve-node {} tree), inherited {},
toks (seq tokens), level [], eoo? false]
(let [head (first toks)
[opts _ known] ropts]
[opts _ known] ropts
;; head resolved through this level's aliases, computed once
child (when-not eoo?
(get-in node [:cmd (get (cmd-aliases node) head head)]))]
(cond
(nil? head) [ropts node level eoo?]
;; literal `--`: everything after is positional
(= "--" head) (recur node ropts inherited (next toks) (conj level head) true)
(and (not eoo?) (get-in node [:cmd head]))
(let [inherited (merge inherited (inherited-entries (:spec node) inherit-opt))
child (get-in node [:cmd head])]
child
(let [inherited (merge inherited (inherited-entries (:spec node) inherit-opt))]
(recur child (resolve-node inherited child) inherited (next toks) [] false))
(and (not eoo?) (gnu-option? head))
;; flags consume one token, other options also their value
Expand Down Expand Up @@ -2157,16 +2198,21 @@ $env.config.completions.external.completer = {|spans|
(if error-stash
(swap! error-stash conj fire)
(fire)))))
aliases (cmd-aliases cmd-info)
{:keys [args opts]} (if should-parse-args?
(parse-args args (assoc parse-opts
::dispatch-tree true
;; shared options parsed at parent levels: seeded as
;; values and exempt from this level's :restrict
::dispatch-inherited user-opts
::dispatch-tree-ignored-args (set (keys (:cmd cmd-info)))))
::dispatch-tree-ignored-args (into (set (keys (:cmd cmd-info)))
(keys aliases))))
{:args args
:opts {}})
[arg & rest] args
;; an alias resolves to its canonical command name, so
;; `:dispatch` and help always carry the canonical path
arg (get aliases arg arg)
user-opts (merge user-opts (user-supplied opts))
opts (vary-meta opts dissoc ::defaulted)
all-opts (-> (merge all-opts opts)
Expand Down Expand Up @@ -2368,6 +2414,14 @@ $env.config.completions.external.completer = {|spans|
what order (like `:order` does for options). A table keeps its entry order
automatically.

`:cmd-aliases` declares alternative names for a command: a collection of
strings, symbols or keywords. On a table entry it aliases the entry's last
command, e.g. `{:cmds [\"dep\" \"add\"] :fn add :cmd-aliases [\"a\"]}` makes
`prog dep a` dispatch like `prog dep add`. In the tree format it sits on the
command's node. `:dispatch` always carries the canonical name, the command
index and completion offer only canonical names, and the command's own help
page lists its aliases.

When a match is found, `:fn` called with the return value of
[[parse-args]] applied to `args` enhanced with:

Expand Down
9 changes: 9 additions & 0 deletions test/babashka/cli/completion_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@
(is (= (mapv #(str "cmd" %) (range 10))
(complete table [""]))))))

(deftest cmd-aliases-completion-test
(let [table [{:cmds ["new"] :fn identity :cmd-aliases ["n"]}
{:cmds ["dep" "add"] :fn identity :cmd-aliases ["a"]}
{:cmds ["dep" "add" "git"] :fn identity}]]
(testing "aliases are not offered as candidates"
(is (= ["new" "dep"] (complete table [""]))))
(testing "completion descends through an alias"
(is (= ["git"] (complete table ["dep" "a" ""]))))))

(deftest value-completion-test
(testing ":complete as a static coll of strings"
(let [o {:spec {:env {:coerce :string :complete ["dev" "prod"]}}}]
Expand Down
Loading