@babashka/cli 0.12.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.html +1229 -0
- package/README.md +1837 -0
- package/index.mjs +70 -0
- package/js/babashka/cli/internal.mjs +36 -0
- package/js/babashka/cli.mjs +2797 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,1837 @@
|
|
|
1
|
+
# babashka.cli
|
|
2
|
+
|
|
3
|
+
[](https://clojars.org/org.babashka/cli)
|
|
4
|
+
[](https://book.babashka.org#badges)
|
|
5
|
+
|
|
6
|
+
Turn Clojure functions into Command Line Interfaces! This library can be used from:
|
|
7
|
+
- [Babashka](https://book.babashka.org/) - included as a built-in library
|
|
8
|
+
- [Clojure on the JVM](https://www.clojure.org/guides/install_clojure) - we support Clojure 1.10.3 and above on Java 11 and above
|
|
9
|
+
- [ClojureScript](https://clojurescript.org) - we test against the current release
|
|
10
|
+
- [Squint](https://github.com/squint-cljs/squint) - we test againt the current release
|
|
11
|
+
- [ClojureDart](https://github.com/Tensegritics/ClojureDart) - we test against the current release
|
|
12
|
+
|
|
13
|
+
## [API](API.md)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
For Clojure, ClojureScript, and ClojureDart include a `:deps` entry in your `deps.edn` file:
|
|
18
|
+
|
|
19
|
+
``` clojure
|
|
20
|
+
org.babashka/cli {:mvn/version "<latest-version>"}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For babashka, no changes are needed; `org.babashka/cli` is a babashka built-in library.
|
|
24
|
+
|
|
25
|
+
For JavaScript, install from NPM:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm install @babashka/cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
See [JavaScript](#javascript) for how to use this library in JS.
|
|
32
|
+
|
|
33
|
+
## Intro
|
|
34
|
+
|
|
35
|
+
Turn a Clojure function into a CLI that takes Unix-style command line arguments. E.g.:
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
$ program command --verbose --long-opt1 v1 -o v2 arg
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Where:
|
|
42
|
+
- `program` is your executable program or script (other libraries might call this "command")
|
|
43
|
+
- `command` is a single or multi-word command for your program (other libraries might call this "subcommand")
|
|
44
|
+
- `--verbose` is a boolean option
|
|
45
|
+
- `--long-opt1 v1` is an option
|
|
46
|
+
- `-o v2` is a short option
|
|
47
|
+
- `arg` is a positional argument
|
|
48
|
+
|
|
49
|
+
See [Terminology](#terminology) for precise definitions.
|
|
50
|
+
|
|
51
|
+
The main ideas:
|
|
52
|
+
|
|
53
|
+
- Put as little effort as possible into turning a Clojure function into a CLI,
|
|
54
|
+
similar to `-X` exec style invocations. For lazy people like me! If you are not
|
|
55
|
+
familiar with `clj -X`, read the docs
|
|
56
|
+
[here](https://clojure.org/reference/clojure_cli#use_fn).
|
|
57
|
+
- But with a better user experience by not having to use quotes on the command line as a
|
|
58
|
+
result of having to pass EDN directly: `--dir foo` instead of `:dir '"foo"'` (or
|
|
59
|
+
who knows how to write the latter in Windows' `cmd.exe` or Powershell?).
|
|
60
|
+
- By default, employ an open world assumption: passing extra arguments does not break, and arguments
|
|
61
|
+
can be reused in multiple contexts.
|
|
62
|
+
- But also support incremental restrictions and validations as a way to polish a CLI for production use.
|
|
63
|
+
|
|
64
|
+
See [clojure CLI](#clojure-cli) for how to turn your `-X` exec functions into CLIs.
|
|
65
|
+
|
|
66
|
+
## Terminology
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
$ program command --verbose --long-opt1 v1 -o v2 arg
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Term | Meaning |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `program` | The executable program, typically launched in a terminal |
|
|
75
|
+
| `command` | A single or multi-word command for the program. Other libraries often call this a subcommand |
|
|
76
|
+
| `option` | A named option, written `--opt val` or short alias `-o val` |
|
|
77
|
+
| `flag` | The literal `--opt` part of `--opt val`, `-o` in `-o v2`, or `:opt` _as the user passed it_. We use the word flag both for boolean and non-boolean options. |
|
|
78
|
+
| `alias` | A single character alias for the option name |
|
|
79
|
+
| `argument` | A positional argument |
|
|
80
|
+
|
|
81
|
+
> [!NOTE]
|
|
82
|
+
> To explain terminology choices, a concrete example:
|
|
83
|
+
> ```shell
|
|
84
|
+
> git remote show
|
|
85
|
+
> ```
|
|
86
|
+
> Some CLI libraries call `git` the `command`, and `remote show` the `subcommand`.
|
|
87
|
+
> These same libraries typically use the term `command` when describing `remote show` in usage help.
|
|
88
|
+
> To keep terminology consistent for CLI users and CLI developers, we avoid `subcommand` entirely.
|
|
89
|
+
> We use `program` for `git` and `command` for `remote show`.
|
|
90
|
+
|
|
91
|
+
## Projects using babashka CLI
|
|
92
|
+
|
|
93
|
+
- [jet](https://github.com/borkdude/jet)
|
|
94
|
+
- [http-server](https://github.com/babashka/http-server)
|
|
95
|
+
- [neil](https://github.com/babashka/neil)
|
|
96
|
+
- [quickdoc](https://github.com/borkdude/quickdoc#clojure-cli)
|
|
97
|
+
- [clj-new](https://github.com/seancorfield/clj-new#babashka-cli)
|
|
98
|
+
- [deps-new](https://github.com/seancorfield/deps-new#babashka-cli)
|
|
99
|
+
|
|
100
|
+
## TOC
|
|
101
|
+
|
|
102
|
+
- [Simple example](#simple-example)
|
|
103
|
+
- [Options](#options)
|
|
104
|
+
- [Arguments](#arguments)
|
|
105
|
+
- [Commands](#commands)
|
|
106
|
+
- [Completions](#completions)
|
|
107
|
+
- [Adding Production Polish](#adding-production-polish)
|
|
108
|
+
- [Babashka tasks](#babashka-tasks)
|
|
109
|
+
- [Clojure CLI](#clojure-cli)
|
|
110
|
+
- [Leiningen](#leiningen)
|
|
111
|
+
|
|
112
|
+
## Simple example
|
|
113
|
+
|
|
114
|
+
Here is an example babashka script to get you started. We'll write a small
|
|
115
|
+
stand-in for `git`. Save it to `mygit.clj`.
|
|
116
|
+
|
|
117
|
+
```clojure
|
|
118
|
+
#!/usr/bin/env bb
|
|
119
|
+
(require '[babashka.cli :as cli]
|
|
120
|
+
'[babashka.fs :as fs])
|
|
121
|
+
|
|
122
|
+
(defn dir-exists? [path]
|
|
123
|
+
(fs/directory? path))
|
|
124
|
+
|
|
125
|
+
(def spec
|
|
126
|
+
{:depth {:coerce :long
|
|
127
|
+
:alias :d ; adds -d alias for --depth
|
|
128
|
+
:desc "Number of commits to fetch"
|
|
129
|
+
:validate pos? ; tests if supplied --depth > 0
|
|
130
|
+
:require true} ; --depth,-d is required
|
|
131
|
+
:dir {:alias :C ; like git's own -C
|
|
132
|
+
:desc "Run as if git was started in <dir>"
|
|
133
|
+
:validate ; tests if --dir exists,
|
|
134
|
+
{:pred dir-exists? ; with a custom error message
|
|
135
|
+
:ex-msg (fn [{:keys [value]}]
|
|
136
|
+
(str "Directory does not exist: " value))}}
|
|
137
|
+
:bare {:coerce :boolean ; defines a boolean flag
|
|
138
|
+
:desc "Create a bare repository"}})
|
|
139
|
+
|
|
140
|
+
(defn run [{:keys [opts]}]
|
|
141
|
+
(println "Here are your cli args!:" opts))
|
|
142
|
+
|
|
143
|
+
(defn -main [& args]
|
|
144
|
+
(cli/dispatch {:fn run :spec spec} args {:prog "mygit" :help true}))
|
|
145
|
+
|
|
146
|
+
(apply -main *command-line-args*)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The `:help true` option supplied to `dispatch` wires up automatic `--help`/`-h` support and terse error messages (as opposed to thrown exceptions) for you.
|
|
150
|
+
|
|
151
|
+
Let's request usage help:
|
|
152
|
+
```
|
|
153
|
+
$ bb mygit.clj --help
|
|
154
|
+
Usage: mygit [options]
|
|
155
|
+
|
|
156
|
+
Options:
|
|
157
|
+
-d, --depth Number of commits to fetch (required)
|
|
158
|
+
-C, --dir Run as if git was started in <dir>
|
|
159
|
+
--bare Create a bare repository
|
|
160
|
+
-h, --help Show this help
|
|
161
|
+
```
|
|
162
|
+
See [Commands > Help](#help) to customize help.
|
|
163
|
+
|
|
164
|
+
Let's try running with some options:
|
|
165
|
+
```
|
|
166
|
+
$ bb mygit.clj --depth 1 --dir my_dir --bare
|
|
167
|
+
Error: Directory does not exist: my_dir
|
|
168
|
+
|
|
169
|
+
Usage: mygit [options]
|
|
170
|
+
|
|
171
|
+
Run "mygit --help" for more information.
|
|
172
|
+
```
|
|
173
|
+
The directory was validated with `dir-exists?`. Because the directory does not exist, you see the custom error message produced by the `:ex-msg` function.
|
|
174
|
+
|
|
175
|
+
Let's create `my_dir`, then try again:
|
|
176
|
+
```
|
|
177
|
+
$ mkdir my_dir
|
|
178
|
+
$ bb mygit.clj --depth 1 --dir my_dir --bare
|
|
179
|
+
Here are your cli args!: {:depth 1, :dir my_dir, :bare true}
|
|
180
|
+
```
|
|
181
|
+
All validations passed, and the `run` function was invoked.
|
|
182
|
+
|
|
183
|
+
The `:depth` option includes `:require true`, let's see what happens when we don't include it on the command line:
|
|
184
|
+
```
|
|
185
|
+
$ bb mygit.clj
|
|
186
|
+
Error: Required option: --depth
|
|
187
|
+
|
|
188
|
+
Usage: mygit [options]
|
|
189
|
+
|
|
190
|
+
Run "mygit --help" for more information.
|
|
191
|
+
```
|
|
192
|
+
We, appropriately, get a terse error message and an exit status of 1.
|
|
193
|
+
|
|
194
|
+
### Adding commands
|
|
195
|
+
|
|
196
|
+
To add commands to this CLI, we need to specify a command structure. We'll just give an example here. See [Commands](#commands) for more info.
|
|
197
|
+
|
|
198
|
+
Alter `mygit.clj`:
|
|
199
|
+
``` clojure
|
|
200
|
+
;; renamed from `run`
|
|
201
|
+
(defn clone [{:keys [opts]}]
|
|
202
|
+
(println "Here are your cli args!:" opts))
|
|
203
|
+
|
|
204
|
+
;; new
|
|
205
|
+
(defn version [_]
|
|
206
|
+
(println "mygit 1.0"))
|
|
207
|
+
|
|
208
|
+
;; new
|
|
209
|
+
(def tree
|
|
210
|
+
{:cmd {"clone" {:fn clone :doc "Clone a repository" :spec spec}
|
|
211
|
+
"version" {:fn version :doc "Print version"}}})
|
|
212
|
+
|
|
213
|
+
;; updated to use `tree` command structure
|
|
214
|
+
(defn -main [& args]
|
|
215
|
+
(cli/dispatch tree args {:prog "mygit" :help true}))
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`--help` now lists the available commands:
|
|
219
|
+
```
|
|
220
|
+
$ bb mygit.clj --help
|
|
221
|
+
Usage: mygit [options] <command>
|
|
222
|
+
|
|
223
|
+
Commands:
|
|
224
|
+
clone Clone a repository
|
|
225
|
+
version Print version
|
|
226
|
+
|
|
227
|
+
Options:
|
|
228
|
+
-h, --help Show this help
|
|
229
|
+
|
|
230
|
+
Run "mygit <command> --help" for more information on a command.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The `clone` command calls the `clone` function:
|
|
234
|
+
```
|
|
235
|
+
$ bb mygit.clj clone --depth 1 --bare
|
|
236
|
+
Here are your cli args!: {:depth 1, :bare true}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The `version` command calls the `version` function:
|
|
240
|
+
```
|
|
241
|
+
$ bb mygit.clj version
|
|
242
|
+
mygit 1.0
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
See [Commands](#commands) for shared options, inheritance, and help customization.
|
|
246
|
+
|
|
247
|
+
## Options
|
|
248
|
+
|
|
249
|
+
If you'd like to parse options yourself (instead of using [`dispatch`](/API.md#dispatch)),
|
|
250
|
+
use either lower-level [`parse-opts`](/API.md#parse-opts) or [`parse-args`](/API.md#parse-args). We will
|
|
251
|
+
use these parse functions in this section to demonstrate how options parsing works.
|
|
252
|
+
|
|
253
|
+
On the command line, a named option is written as `--opt val` or short-form [alias](#aliases) `-o val`.
|
|
254
|
+
|
|
255
|
+
Options are configured with a [spec](#spec) (short for "options specification", not
|
|
256
|
+
`clojure.spec`): a map keyed by option name, each value a map of `:coerce`,
|
|
257
|
+
`:alias`, `:validate`, `:require`, `:desc`, etc., passed under `:spec`:
|
|
258
|
+
|
|
259
|
+
``` clojure
|
|
260
|
+
{:spec {:port {:coerce :long :alias :p}}}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A terser shape is also supported, where each key is lifted to the top level and
|
|
264
|
+
keyed by option name: `{:coerce {:port :long} :alias {:p :port}}`. It is handy
|
|
265
|
+
for quick scripts and partial parsing, but only a spec can carry `:desc`/`:ref`,
|
|
266
|
+
so generated help and option printing need a spec. The two are otherwise
|
|
267
|
+
equivalent. The examples below use the spec shape.
|
|
268
|
+
|
|
269
|
+
Examples:
|
|
270
|
+
|
|
271
|
+
Parse `{:port 1339}` from command line arguments:
|
|
272
|
+
|
|
273
|
+
``` clojure
|
|
274
|
+
(require '[babashka.cli :as cli])
|
|
275
|
+
|
|
276
|
+
(cli/parse-opts ["--port" "1339"] {:spec {:port {:coerce :long}}})
|
|
277
|
+
;;=> {:port 1339}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Use an alias (short option):
|
|
281
|
+
|
|
282
|
+
``` clojure
|
|
283
|
+
(cli/parse-opts ["-p" "1339"] {:spec {:port {:coerce :long :alias :p}}})
|
|
284
|
+
;; {:port 1339}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Coerce values into a collection:
|
|
288
|
+
|
|
289
|
+
``` clojure
|
|
290
|
+
(cli/parse-opts ["--paths" "src" "--paths" "test"] {:spec {:paths {:coerce []}}})
|
|
291
|
+
;;=> {:paths ["src" "test"]}
|
|
292
|
+
|
|
293
|
+
(cli/parse-opts ["--paths" "src" "test"] {:spec {:paths {:coerce []}}})
|
|
294
|
+
;;=> {:paths ["src" "test"]}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Transforming into a collection of a certain type:
|
|
298
|
+
|
|
299
|
+
``` clojure
|
|
300
|
+
(cli/parse-opts ["--foo" "bar" "--foo" "baz"] {:spec {:foo {:coerce [:keyword]}}})
|
|
301
|
+
;; => {:foo [:bar :baz]}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
In addition to the built-in coercion keywords, `:coerce` accepts any function (called
|
|
305
|
+
with the option's value as a string):
|
|
306
|
+
|
|
307
|
+
``` clojure
|
|
308
|
+
(cli/parse-opts ["--letter" "alpha"] {:spec {:letter {:coerce (fn [s] (subs s 0 1))}}})
|
|
309
|
+
;;=> {:letter "a"}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Boolean flags are assumed by default, like so:
|
|
313
|
+
|
|
314
|
+
``` clojure
|
|
315
|
+
(cli/parse-opts ["--verbose"])
|
|
316
|
+
;;=> {:verbose true}
|
|
317
|
+
|
|
318
|
+
(cli/parse-opts ["-v" "-v" "-v"] {:spec {:verbose {:alias :v :coerce []}}})
|
|
319
|
+
;;=> {:verbose [true true true]}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
But you can explicitly specify `:boolean` coercion (and will sometimes need to, see [Arguments](#arguments)):
|
|
323
|
+
|
|
324
|
+
``` clojure
|
|
325
|
+
(cli/parse-opts ["--verbose"] {:spec {:verbose {:coerce :boolean}}})
|
|
326
|
+
;;=> {:verbose true}
|
|
327
|
+
|
|
328
|
+
(cli/parse-opts ["-v" "-v" "-v"] {:spec {:verbose {:alias :v :coerce [:boolean]}}})
|
|
329
|
+
;;=> {:verbose [true true true]}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Long options also support the syntax `--foo=bar`:
|
|
333
|
+
|
|
334
|
+
``` clojure
|
|
335
|
+
(cli/parse-opts ["--foo=bar"])
|
|
336
|
+
;;=> {:foo "bar"}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Flags may be combined into a single short option:
|
|
340
|
+
|
|
341
|
+
``` clojure
|
|
342
|
+
(cli/parse-opts ["-abc"])
|
|
343
|
+
;;=> {:a true :b true :c true}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Long options that start with `--no-` are parsed as negative flags:
|
|
347
|
+
|
|
348
|
+
``` clojure
|
|
349
|
+
(cli/parse-opts ["--no-colors"])
|
|
350
|
+
;;=> {:colors false}
|
|
351
|
+
```
|
|
352
|
+
This works for any option. For a boolean option where the negation is meaningful,
|
|
353
|
+
set `:negatable true` in its spec to advertise it in help as `--[no-]colors`.
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
Babashka CLI also accepts a `:`-prefixed form, `:opt val`, to
|
|
357
|
+
match the Clojure CLI `-X` invocation style. The two forms cannot be mixed in a single
|
|
358
|
+
invocation. Use `--`/`-` or `:`, not both. If you prefer to only allow only `--`/`-` style options, specify `:no-keyword-opts true`:
|
|
359
|
+
|
|
360
|
+
```clojure
|
|
361
|
+
(cli/parse-args [":foo" "bar"])
|
|
362
|
+
;; => {:opts {:foo "bar"}}
|
|
363
|
+
|
|
364
|
+
(cli/parse-args [":foo" "bar"] {:no-keyword-opts true})
|
|
365
|
+
;; => {:args [":foo" "bar"], :opts {}}
|
|
366
|
+
|
|
367
|
+
(cli/parse-args ["--foo" "bar" ":no" "mixing"])
|
|
368
|
+
;; => {:args [":no" "mixing"], :opts {:foo "bar"}}
|
|
369
|
+
```
|
|
370
|
+
Notice how unrecognized options are considered [Arguments](#arguments).
|
|
371
|
+
|
|
372
|
+
### Spec
|
|
373
|
+
|
|
374
|
+
A spec (short for options specification, not `clojure.spec`) is a map keyed by option
|
|
375
|
+
name; each value configures one option.
|
|
376
|
+
Alongside the parsing keys (`:coerce`, `:alias`, `:validate`, ...), it carries
|
|
377
|
+
`:desc`, `:ref`, and `:default-desc` used when printing options (see [Printing options](#printing-options)). For
|
|
378
|
+
example:
|
|
379
|
+
|
|
380
|
+
``` clojure
|
|
381
|
+
(def spec {:from {:ref "<format>"
|
|
382
|
+
:desc "The input format. <format> can be edn, json or transit."
|
|
383
|
+
:coerce :keyword
|
|
384
|
+
:alias :i
|
|
385
|
+
:default-desc "edn"
|
|
386
|
+
:default :edn}
|
|
387
|
+
:to {:ref "<format>"
|
|
388
|
+
:desc "The output format. <format> can be edn, json or transit."
|
|
389
|
+
:coerce :keyword
|
|
390
|
+
:alias :o
|
|
391
|
+
:default-desc "json"
|
|
392
|
+
:default :json}
|
|
393
|
+
:pretty {:desc "Pretty-print output."
|
|
394
|
+
:alias :p}
|
|
395
|
+
:paths {:desc "Paths of files to transform."
|
|
396
|
+
:coerce []
|
|
397
|
+
:default ["src" "test"]
|
|
398
|
+
:default-desc "src test"}})
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
You can pass the spec to `parse-opts` under the `:spec` key: `(parse-opts args {:spec spec})`, or when using
|
|
402
|
+
`dispatch`, in the entry for each command.
|
|
403
|
+
An explanation of each key:
|
|
404
|
+
|
|
405
|
+
- `:ref`: a name that describes the option value, which is typically used as a reference in the description (`:desc`)
|
|
406
|
+
- `:desc`: a description of the option.
|
|
407
|
+
- `:coerce`: coerce a string value to a type. Built-in keywords: `:boolean`
|
|
408
|
+
(`:bool`), `:int` (`:long`), `:double`, `:number`, `:symbol`, `:keyword`,
|
|
409
|
+
`:string`, `:edn`, `:auto`. A collection collects repeated values: `[]`
|
|
410
|
+
(vector), `#{}` (set) or `()` (list); put a coercion keyword inside the collection to
|
|
411
|
+
coerce each element (e.g., `[:keyword]`, `#{:int}`). A function is also
|
|
412
|
+
accepted: it is called with the option value as a string and returns the coerced value.
|
|
413
|
+
- `:alias`: an alternative short name; a synonym for the option name.
|
|
414
|
+
- `:default`: default value.
|
|
415
|
+
- `:default-desc`: a string representation of the default value.
|
|
416
|
+
- `:require`: `true` make this opt required.
|
|
417
|
+
- `:validate`: a function used to validate the value of this option (as described
|
|
418
|
+
in the [Validate](#validate) section).
|
|
419
|
+
- `:collect`: collect repeated values into a collection (`[]` vector, `#{}` set
|
|
420
|
+
or `()` list), or a function `(fn [coll arg-value] ...)` for custom collection
|
|
421
|
+
- `:negatable`: `true` shows a boolean option as `--[no-]name` in help (the `--no-name` form parses regardless)
|
|
422
|
+
|
|
423
|
+
### Custom collection handling
|
|
424
|
+
|
|
425
|
+
For those rare cases when you need it, you can use a `:collect` function for custom collection.
|
|
426
|
+
Here's an example of parsing out `,` separated multi-arg-values:
|
|
427
|
+
|
|
428
|
+
``` clojure
|
|
429
|
+
(cli/parse-opts ["--foo" "a,b" "--foo=c,d,e" "--foo" "f"]
|
|
430
|
+
{:spec {:foo {:collect (fn [coll arg-value]
|
|
431
|
+
(into (or coll [])
|
|
432
|
+
(str/split arg-value #",")))}}})
|
|
433
|
+
;; => {:foo ["a" "b" "c" "d" "e" "f"]}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Auto-coercion
|
|
437
|
+
|
|
438
|
+
Babashka CLI auto-coerces values that have no explicit coercion
|
|
439
|
+
with [`auto-coerce`](/API.md#auto-coerce):
|
|
440
|
+
It automatically tries to convert booleans, numbers, and keywords.
|
|
441
|
+
|
|
442
|
+
``` clojure
|
|
443
|
+
(cli/parse-opts ["--num" "1339" "--kw" ":foo" "--bool" "false" "--str" "bar"])
|
|
444
|
+
;; => {:num 1339, :kw :foo, :bool false, :str "bar"}
|
|
445
|
+
|
|
446
|
+
;; the actual types...:
|
|
447
|
+
(->> (cli/parse-opts ["--num" "1339" "--kw" ":foo" "--bool" "false" "--str" "bar"])
|
|
448
|
+
(reduce-kv (fn [m k v]
|
|
449
|
+
(assoc m k [v (type v)]))
|
|
450
|
+
{}))
|
|
451
|
+
;; => {:num [1339 java.lang.Long],
|
|
452
|
+
;; :kw [:foo clojure.lang.Keyword],
|
|
453
|
+
;; :bool [false java.lang.Boolean],
|
|
454
|
+
;; :str ["bar" java.lang.String]}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Aliases
|
|
458
|
+
|
|
459
|
+
An `:alias` specifies a synonym short option name for the option name.
|
|
460
|
+
|
|
461
|
+
Babashka CLI distinguishes aliases with characters in common, so a way to implement the common `-v`/`-vv` Unix pattern is:
|
|
462
|
+
``` clojure
|
|
463
|
+
(def spec {:verbose {:alias :v
|
|
464
|
+
:desc "Enable verbose output."}
|
|
465
|
+
:very-verbose {:alias :vv
|
|
466
|
+
:desc "Enable very verbose output."}})
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
You get:
|
|
470
|
+
|
|
471
|
+
```clojure
|
|
472
|
+
(cli/parse-opts ["-v"] {:spec spec})
|
|
473
|
+
;;=> {:verbose true}
|
|
474
|
+
|
|
475
|
+
(cli/parse-opts ["-vv"] {:spec spec})
|
|
476
|
+
;;=> {:very-verbose true}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Another way would be to collect the flags in a vector with `:coerce` (and base verbosity on the size of that vector):
|
|
480
|
+
|
|
481
|
+
``` clojure
|
|
482
|
+
(def spec {:verbose {:alias :v
|
|
483
|
+
:desc "Enable verbose output."
|
|
484
|
+
:coerce []}})
|
|
485
|
+
|
|
486
|
+
user=> (cli/parse-opts ["-vvv"] {:spec spec})
|
|
487
|
+
{:verbose [true true true]}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Arguments
|
|
491
|
+
|
|
492
|
+
To parse positional arguments, you can use `parse-args` and/or the `:args->opts`
|
|
493
|
+
option. E.g., to parse arguments for the `git push` command:
|
|
494
|
+
|
|
495
|
+
``` clojure
|
|
496
|
+
(cli/parse-args ["--force" "ssh://foo"] {:spec {:force {:coerce :boolean}}})
|
|
497
|
+
;;=> {:args ["ssh://foo"], :opts {:force true}}
|
|
498
|
+
|
|
499
|
+
(cli/parse-args ["ssh://foo" "--force"] {:spec {:force {:coerce :boolean}}})
|
|
500
|
+
;;=> {:args ["ssh://foo"], :opts {:force true}}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Note that babashka CLI can only disambiguate correctly between values for
|
|
504
|
+
options and trailing arguments with enough `:coerce` information
|
|
505
|
+
available. Without the `:coerce :boolean` info, we get:
|
|
506
|
+
|
|
507
|
+
``` clojure
|
|
508
|
+
(cli/parse-args ["--force" "ssh://foo"])
|
|
509
|
+
{:opts {:force "ssh://foo"}}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
In case of ambiguity `--` may also be used to communicate the boundary between
|
|
513
|
+
options and arguments:
|
|
514
|
+
|
|
515
|
+
``` clojure
|
|
516
|
+
(cli/parse-args ["--paths" "src" "test" "--" "ssh://foo"] {:spec {:paths {:coerce []}}})
|
|
517
|
+
{:args ["ssh://foo"], :opts {:paths ["src" "test"]}}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### :args->opts
|
|
521
|
+
|
|
522
|
+
To fold positional arguments into the parsed options, you can use `:args->opts`:
|
|
523
|
+
|
|
524
|
+
``` clojure
|
|
525
|
+
(def cli-opts {:spec {:force {:coerce :boolean}} :args->opts [:url]})
|
|
526
|
+
|
|
527
|
+
(cli/parse-opts ["--force" "ssh://foo"] cli-opts)
|
|
528
|
+
;;=> {:force true, :url "ssh://foo"}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
``` clojure
|
|
532
|
+
(cli/parse-opts ["ssh://foo" "--force"] cli-opts)
|
|
533
|
+
;;=> {:url "ssh://foo", :force true}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
If you want to fold a variable number of arguments, you can coerce them into a vector
|
|
537
|
+
and specify the variable number of arguments with `repeat`:
|
|
538
|
+
|
|
539
|
+
``` clojure
|
|
540
|
+
(def cli-opts {:spec {:bar {:coerce []}} :args->opts (cons :foo (repeat :bar))})
|
|
541
|
+
(cli/parse-opts ["arg1" "arg2" "arg3" "arg4"] cli-opts)
|
|
542
|
+
;;=> {:foo "arg1", :bar ["arg2" "arg3" "arg4"]}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Options may be interspersed with the positional arguments:
|
|
546
|
+
|
|
547
|
+
``` clojure
|
|
548
|
+
(def cli-opts {:spec {:foo {:coerce :keyword}
|
|
549
|
+
:bar {:coerce []}
|
|
550
|
+
:force {:coerce :boolean}}
|
|
551
|
+
:args->opts (cons :foo (repeat :bar))})
|
|
552
|
+
|
|
553
|
+
(cli/parse-opts ["arg1" "arg2" "--force" "arg3"] cli-opts)
|
|
554
|
+
;; => {:foo :arg1, :bar ["arg2" "arg3"], :force true}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
This also holds for a command leaf in [dispatch](#commands): a command with
|
|
558
|
+
variadic `:args->opts` parses options before, among, or after its positional
|
|
559
|
+
arguments. Without `:args->opts`, `dispatch` stops at the first positional argument
|
|
560
|
+
(to route commands), so trailing options would not be parsed.
|
|
561
|
+
|
|
562
|
+
## Commands
|
|
563
|
+
|
|
564
|
+
Babashka CLI handles commands with [dispatch](/API.md#dispatch).
|
|
565
|
+
|
|
566
|
+
### Single-word Commands
|
|
567
|
+
|
|
568
|
+
Say we want a CLI with a `copy` command, a `delete` command, and an undocumented `debug` command:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
$ example copy <file> --dry-run
|
|
572
|
+
$ example delete <file> --recursive --depth 3
|
|
573
|
+
$ example debug
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Commands can be specified in two ways: as a tree or a table.
|
|
577
|
+
The difference is more apparent for [Multi-word Commands](#multi-word-commands).
|
|
578
|
+
We'll use the tree structure for this example, save it to `try_cmds.clj`.
|
|
579
|
+
|
|
580
|
+
```clojure
|
|
581
|
+
(ns try-cmds
|
|
582
|
+
(:require [babashka.cli :as cli]))
|
|
583
|
+
|
|
584
|
+
(defn copy [{:keys [opts]}]
|
|
585
|
+
(prn :copy opts))
|
|
586
|
+
|
|
587
|
+
(defn delete [{:keys [opts]}]
|
|
588
|
+
(prn :delete opts))
|
|
589
|
+
|
|
590
|
+
(def tree
|
|
591
|
+
{:cmd {"copy" {:fn copy :doc "Copy a file\nMore details here" :args->opts [:file]
|
|
592
|
+
:spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}}
|
|
593
|
+
"delete" {:fn delete :doc "Delete a file" :args->opts [:file]
|
|
594
|
+
:spec {:recursive {:coerce :boolean :desc "Recurse"}
|
|
595
|
+
:depth {:coerce :long :desc "Max depth"}}}
|
|
596
|
+
"debug" {:fn prn :doc "Dump internal state"}}
|
|
597
|
+
;; specify which commands to show and in what order (we exclude hidden debug command)
|
|
598
|
+
:cmd-order ["copy" "delete"]})
|
|
599
|
+
|
|
600
|
+
(defn -main [& args]
|
|
601
|
+
(cli/dispatch tree args {:prog "try-cmds" :help true}))
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The same command structure expressed as a table is:
|
|
605
|
+
|
|
606
|
+
```clojure
|
|
607
|
+
(def table
|
|
608
|
+
[{:cmds ["copy"] :fn copy :doc "Copy a file\nMore details here" :args->opts [:file]
|
|
609
|
+
:spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}}
|
|
610
|
+
{:cmds ["delete"] :fn delete :doc "Delete a file" :args->opts [:file]
|
|
611
|
+
:spec {:recursive {:coerce :boolean :desc "Recurse"}
|
|
612
|
+
:depth {:coerce :long :desc "Max depth"}}}
|
|
613
|
+
;; hide debug command from usage help and completions with :no-doc
|
|
614
|
+
{:cmds ["debug"] :fn prn :doc "Dump internal state" :no-doc true}])
|
|
615
|
+
|
|
616
|
+
(defn -main [& args]
|
|
617
|
+
(cli/dispatch table args {:prog "try-cmds" :help true}))
|
|
618
|
+
```
|
|
619
|
+
The order of the entries in the table does not matter when matching commands, but it is used for `--help`.
|
|
620
|
+
|
|
621
|
+
Regardless of tree or table format, each command entry accepts any [parse-args](#options) option (`:spec`,
|
|
622
|
+
`:args->opts`, `:alias`, `:restrict`, ...).
|
|
623
|
+
|
|
624
|
+
> [!NOTE]
|
|
625
|
+
> If you want to try `try_cmds.clj` from your terminal:
|
|
626
|
+
>
|
|
627
|
+
> - For babashka, create `bb.edn` in the same dir:
|
|
628
|
+
> ```clojure
|
|
629
|
+
> {:paths ["."]}
|
|
630
|
+
> ```
|
|
631
|
+
> Then run with `bb -m try-cmds ...` (Our examples below use babashka).
|
|
632
|
+
>
|
|
633
|
+
> - For Clojure, create a `deps.edn` in the same dir:
|
|
634
|
+
> ```clojure
|
|
635
|
+
> {:paths ["."] :deps {org.babashka/cli {:mvn/version "<latest-version>"}}}
|
|
636
|
+
> ```
|
|
637
|
+
> Then run with `clojure -M -m try-cmds ...`
|
|
638
|
+
|
|
639
|
+
`dispatch` matches the given command line args against specified commands and calls the matching entry's `:fn` with the parsed result.
|
|
640
|
+
`:help true` wires up `--help`/`-h` and prints terse errors instead of throwing exceptions (see [Help](#help)):
|
|
641
|
+
|
|
642
|
+
```
|
|
643
|
+
$ bb -m try-cmds --help
|
|
644
|
+
Usage: try-cmds [options] <command>
|
|
645
|
+
|
|
646
|
+
Commands:
|
|
647
|
+
copy Copy a file
|
|
648
|
+
delete Delete a file
|
|
649
|
+
|
|
650
|
+
Options:
|
|
651
|
+
-h, --help Show this help
|
|
652
|
+
|
|
653
|
+
Run "try-cmds <command> --help" for more information on a command.
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
The `Commands:` descriptions come from each command entry's `:doc` key.
|
|
657
|
+
The first line of `:doc` is used as a summary for the command.
|
|
658
|
+
|
|
659
|
+
The `debug` command is absent in the help because it is absent from `:cmd-order` (it was suppressed in table format via `:no-doc true`). This also hides `debug` from [completions](#completions).
|
|
660
|
+
|
|
661
|
+
> [!TIP]
|
|
662
|
+
> Like `:cmd-order`, options can be hidden with `:order`.
|
|
663
|
+
> And `:no-doc true` can also be used on an option to hide it.
|
|
664
|
+
> Hiding works well for deprecated or internal commands and options.
|
|
665
|
+
|
|
666
|
+
The full text of `:doc` is shown as the description on the command's `--help` output, between the usage line and `Options:`:
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
$ bb -m try-cmds copy --help
|
|
670
|
+
Usage: try-cmds copy [options] <file>
|
|
671
|
+
|
|
672
|
+
Copy a file
|
|
673
|
+
More details here
|
|
674
|
+
|
|
675
|
+
Options:
|
|
676
|
+
--dry-run Do a dry run
|
|
677
|
+
-h, --help Show this help
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
Running `bb -m try-cmds copy the-file --dry-run` calls `copy`, which prints:
|
|
681
|
+
|
|
682
|
+
``` clojure
|
|
683
|
+
:copy {:file "the-file", :dry-run true}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
The `copy` command entry `:fn` is called with a map of the parsed result:
|
|
687
|
+
|
|
688
|
+
- `:opts`: the parsed options (`{:file "the-file" :dry-run true}`; `:file` comes
|
|
689
|
+
from `:args->opts`)
|
|
690
|
+
- `:dispatch`: the matched command path `["copy"]` from the given `dispatch` command structure.
|
|
691
|
+
- `:args`: any leftover positional args (`nil` here)
|
|
692
|
+
|
|
693
|
+
An unknown or missing command prints a terse message and exits the process with a status of 1:
|
|
694
|
+
|
|
695
|
+
```
|
|
696
|
+
$ bb -m try-cmds bogus
|
|
697
|
+
Unknown command: bogus
|
|
698
|
+
|
|
699
|
+
Commands:
|
|
700
|
+
copy Copy a file
|
|
701
|
+
delete Delete a file
|
|
702
|
+
|
|
703
|
+
Run "try-cmds --help" for more information.
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Multi-word Commands
|
|
707
|
+
|
|
708
|
+
Sometimes a command can be made up of multiple words.
|
|
709
|
+
Think of `git`, for example. We have `git remote`, `git remote add ...`, `git remote delete ...`, etc.
|
|
710
|
+
|
|
711
|
+
The command hierarchy is:
|
|
712
|
+
- `remote` (level 1) is a parent of both:
|
|
713
|
+
- `remote add` (level 2)
|
|
714
|
+
- `remote delete` (level 2)
|
|
715
|
+
|
|
716
|
+
A `dispatch` tree might be expressed as:
|
|
717
|
+
```clojure
|
|
718
|
+
{:cmd {"remote"
|
|
719
|
+
{:fn remotes-list :doc "show list of remotes"
|
|
720
|
+
:cmd {"add" {:fn remote-add :doc "add a new remote"}
|
|
721
|
+
"delete" {:fn remote-delete :doc "delete a remote"}}
|
|
722
|
+
:cmd-order ["add" "delete"]}}}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
The same commands, expressed as a `dispatch` table:
|
|
726
|
+
```clojure
|
|
727
|
+
[{:cmds ["remote"] :fn remotes-list :doc "show list of remotes"}
|
|
728
|
+
{:cmds ["remote" "add"] :fn remote-add :doc "add a new remote"}
|
|
729
|
+
{:cmds ["remote" "delete"] :fn remote-delete :doc "delete a remote"}]
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Multi-word commands are matched from the command line in the specified order, and the longest matching entry's `:fn` is called.
|
|
733
|
+
So `git remote add` would result in a call to the `remote-add` `:fn` and not the `remotes-list` `:fn`.
|
|
734
|
+
|
|
735
|
+
A command line can have options before and between commands and command hierarchy levels.
|
|
736
|
+
|
|
737
|
+
Root-level options are specified in the root spec.
|
|
738
|
+
A contrived example to illustrate:
|
|
739
|
+
|
|
740
|
+
``` clojure
|
|
741
|
+
(def root-spec {:foo {:coerce #(str "global-" %)}})
|
|
742
|
+
(def sub1-spec {:bar {:coerce #(str "sub1-" %)}})
|
|
743
|
+
(def sub2-spec {:baz {:coerce #(str "sub2-" %)}})
|
|
744
|
+
|
|
745
|
+
(def tree
|
|
746
|
+
{:spec root-spec
|
|
747
|
+
:cmd {"sub1" {:fn identity :spec sub1-spec
|
|
748
|
+
:cmd {"sub2" {:fn identity :spec sub2-spec}}}}})
|
|
749
|
+
|
|
750
|
+
(cli/dispatch tree ["--foo" "a" "sub1" "--bar" "b" "sub2" "--baz" "c" "arg"])
|
|
751
|
+
;; => {:dispatch ["sub1" "sub2"],
|
|
752
|
+
;; :opts {:foo "global-a", :bar "sub1-b", :baz "sub2-c"},
|
|
753
|
+
;; :args ["arg"]}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
<details>
|
|
757
|
+
<summary>For reference, the equivalent command table structure:</summary>
|
|
758
|
+
|
|
759
|
+
```clojure
|
|
760
|
+
(def table
|
|
761
|
+
[{:cmds [] :spec root-spec} ;; root spec specified with `:cmds []`
|
|
762
|
+
{:cmds ["sub1"] :fn identity :spec sub1-spec}
|
|
763
|
+
{:cmds ["sub1" "sub2"] :fn identity :spec sub2-spec}])
|
|
764
|
+
```
|
|
765
|
+
</details>
|
|
766
|
+
|
|
767
|
+
Specs are not merged across command hierarchy levels.
|
|
768
|
+
An option is parsed with the spec of the current matching command (while parsing the command line from left to right):
|
|
769
|
+
- `--foo a` appears before any matching command, so is coerced with `root-spec`
|
|
770
|
+
- `--bar b` appears after a matching `sub1` but before `sub2`, so is coerced with `sub1`'s `sub1-spec`
|
|
771
|
+
- `--baz c` appears after matching `sub1` and `sub2`, so is coerced with `sub2`s `sub2-spec`
|
|
772
|
+
|
|
773
|
+
Let's compare with a different ordering on the command line:
|
|
774
|
+
```clojure
|
|
775
|
+
(cli/dispatch tree ["--foo" "a" "sub1" "sub2" "--bar" "b" "--baz" "c" "arg"])
|
|
776
|
+
;; => {:dispatch ["sub1" "sub2"],
|
|
777
|
+
;; :opts {:foo "global-a", :bar "b", :baz "sub2-c"},
|
|
778
|
+
;; :args ["arg"]}
|
|
779
|
+
```
|
|
780
|
+
Notice that `--bar` is now after `sub1` and `sub2` and gets default string coercion treatment.
|
|
781
|
+
This is because it was processed with `sub2-spec`, which has no specific coercion for `:bar`.
|
|
782
|
+
|
|
783
|
+
Let's explore how `:restrict` works with command hierarchies.
|
|
784
|
+
``` clojure
|
|
785
|
+
(def tree
|
|
786
|
+
{:cmd {"group"
|
|
787
|
+
{:spec {:registry {}}
|
|
788
|
+
:cmd {"sub"
|
|
789
|
+
{:fn identity :spec {:format {}}}}}}})
|
|
790
|
+
```
|
|
791
|
+
<details>
|
|
792
|
+
<summary>Equivalent table syntax</summary>
|
|
793
|
+
|
|
794
|
+
```clojure
|
|
795
|
+
(def table
|
|
796
|
+
[{:cmds ["group"] :spec {:registry {}}}
|
|
797
|
+
{:cmds ["group" "sub"] :fn identity :spec {:format {}}}])
|
|
798
|
+
```
|
|
799
|
+
</details>
|
|
800
|
+
|
|
801
|
+
Because `:registry` belongs to the `group` command, it is expected only to be used with the `group` command:
|
|
802
|
+
```clojure
|
|
803
|
+
(cli/dispatch tree ["group" "--registry" "X" "sub"] {:restrict true})
|
|
804
|
+
;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil}
|
|
805
|
+
```
|
|
806
|
+
and not the `group sub` command:
|
|
807
|
+
```clojure
|
|
808
|
+
(cli/dispatch tree ["group" "sub" "--registry" "X"] {:restrict true})
|
|
809
|
+
;; throws: Unknown option: --registry
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Mark an option with `:inherit true` to also accept it at any command hierarchy descendant level.
|
|
813
|
+
The option is coerced and restrict-checked wherever it appears:
|
|
814
|
+
``` clojure
|
|
815
|
+
(def tree
|
|
816
|
+
{:cmd {"group"
|
|
817
|
+
{:spec {:registry {:inherit true}} ;; <--
|
|
818
|
+
:cmd {"sub"
|
|
819
|
+
{:fn identity :spec {:format {}}}}}}})
|
|
820
|
+
|
|
821
|
+
(cli/dispatch tree ["group" "sub" "--registry" "X"] {:restrict true})
|
|
822
|
+
;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
<details>
|
|
826
|
+
<summary>Equivalent table syntax</summary>
|
|
827
|
+
|
|
828
|
+
```clojure
|
|
829
|
+
(def table
|
|
830
|
+
[{:cmds ["group"] :spec {:registry {:inherit true}}} ;; <--
|
|
831
|
+
{:cmds ["group" "sub"] :fn identity :spec {:format {}}}])
|
|
832
|
+
```
|
|
833
|
+
</details>
|
|
834
|
+
|
|
835
|
+
A descendant command may redefine an option in its own spec, in which case the descendant's spec wins.
|
|
836
|
+
|
|
837
|
+
Instead of marking individual options, you can pass an `:inherit` option to `dispatch`.
|
|
838
|
+
Specify `true` to inherit all options, or a set of keys to inherit only those options:
|
|
839
|
+
|
|
840
|
+
``` clojure
|
|
841
|
+
(def tree
|
|
842
|
+
{:cmd {"group"
|
|
843
|
+
{:spec {:registry {}}
|
|
844
|
+
:cmd {"sub"
|
|
845
|
+
{:fn identity :spec {:format {}}}}}}})
|
|
846
|
+
|
|
847
|
+
(cli/dispatch tree ["group" "sub" "--registry" "X"] {:inherit true})
|
|
848
|
+
;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil}
|
|
849
|
+
(cli/dispatch tree ["group" "sub" "--registry" "X"] {:inherit #{:registry}})
|
|
850
|
+
;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil}
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
You can use `:args->opts`, but command matching is always prioritized first:
|
|
854
|
+
``` clojure
|
|
855
|
+
(def tree
|
|
856
|
+
{:cmd {"sub1"
|
|
857
|
+
{:fn identity :spec sub1-spec :args->opts [:some-opt]
|
|
858
|
+
:cmd {"sub2"
|
|
859
|
+
{:fn identity :spec sub2-spec}}}}})
|
|
860
|
+
|
|
861
|
+
(cli/dispatch tree ["sub1" "dude"])
|
|
862
|
+
;; => {:dispatch ["sub1"], :opts {:some-opt "dude"}, :args nil}
|
|
863
|
+
(cli/dispatch tree ["sub1" "sub2"])
|
|
864
|
+
;; => {:dispatch ["sub1" "sub2"], :opts {}, :args nil}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
<details>
|
|
868
|
+
<summary>Equivalent table syntax</summary>
|
|
869
|
+
|
|
870
|
+
```clojure
|
|
871
|
+
(def table
|
|
872
|
+
[{:cmds ["sub1"] :fn identity :spec sub1-spec :args->opts [:some-opt]}
|
|
873
|
+
{:cmds ["sub1" "sub2"] :fn identity :spec sub2-spec}])
|
|
874
|
+
```
|
|
875
|
+
</details>
|
|
876
|
+
|
|
877
|
+
See [neil](https://github.com/babashka/neil) for a real-world CLI using multi-word commands.
|
|
878
|
+
|
|
879
|
+
### Command formats
|
|
880
|
+
Commands can be specified in a tree or table format.
|
|
881
|
+
Both formats are supported; use the format that best suits you.
|
|
882
|
+
|
|
883
|
+
The difference between formats becomes apparent when multi-word commands are used.
|
|
884
|
+
For example, let's say we have a CLI with commands:
|
|
885
|
+
- `copy` (hierarchy level 1)
|
|
886
|
+
- `cache` (hierarchy level 1)
|
|
887
|
+
- `cache clean` (hierarchy level 2)
|
|
888
|
+
|
|
889
|
+
The table format represents this structure flatly:
|
|
890
|
+
|
|
891
|
+
```clojure
|
|
892
|
+
(def table
|
|
893
|
+
[{:cmds [] :spec {:verbose {:coerce :boolean :inherit true :desc "Verbose output"}}} ;; top-level options
|
|
894
|
+
{:cmds ["copy"] :fn copy :doc "Copy a file" :args->opts [:file]
|
|
895
|
+
:spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}}
|
|
896
|
+
{:cmds ["cache"] :doc "Manage the cache"}
|
|
897
|
+
{:cmds ["cache" "clean"] :fn clean :doc "Clean the cache"}])
|
|
898
|
+
|
|
899
|
+
(cli/dispatch table args {:prog "example" :help true})
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
The tree format, as you would guess, uses nesting.
|
|
903
|
+
The root accepts a `:spec` for top-level options.
|
|
904
|
+
The first level of commands is specified under `:cmd`
|
|
905
|
+
in a map of strings to command options, which are the same as in the table
|
|
906
|
+
above, minus the `:cmds` entry. You can nest arbitrarily deep.
|
|
907
|
+
|
|
908
|
+
``` clojure
|
|
909
|
+
(def tree
|
|
910
|
+
{:spec {:verbose {:coerce :boolean :inherit true :desc "Verbose output"}}
|
|
911
|
+
:cmd {"copy" {:fn copy :doc "Copy a file" :args->opts [:file]
|
|
912
|
+
:spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}}
|
|
913
|
+
"cache" {:doc "Manage the cache"
|
|
914
|
+
;; clean is nested under cache
|
|
915
|
+
:cmd {"clean" {:fn clean :doc "Clean the cache"}}}}})
|
|
916
|
+
|
|
917
|
+
(cli/dispatch tree args {:prog "example" :help true})
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
The table or tree format can be used interchangeably in `dispatch`,
|
|
921
|
+
`format-command-help` and the like.
|
|
922
|
+
|
|
923
|
+
You'll want consistent ordering for help output.
|
|
924
|
+
The tree format uses a map; the nature of Clojure maps is that they become unordered hash-maps after 8 entries.
|
|
925
|
+
You probably don't want to rely on this implementation detail and can explicitly control order with `:cmd-order`.
|
|
926
|
+
Commands not mentioned in `:cmd-order` are left out of printed output, but are still callable on the
|
|
927
|
+
command line.
|
|
928
|
+
|
|
929
|
+
``` clojure
|
|
930
|
+
{:cmd-order ["copy" "cache"]
|
|
931
|
+
:cmd {"copy" {...}
|
|
932
|
+
"cache" {...}}}
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### Help
|
|
936
|
+
|
|
937
|
+
> For a guided walkthrough of automatic help and shell completions, see this
|
|
938
|
+
> [blog post](https://blog.michielborkent.nl/babashka-cli-help-and-completions.html).
|
|
939
|
+
|
|
940
|
+
Pass `:help true` to `dispatch` (and `:prog`, the program name) to add help to a
|
|
941
|
+
CLI:
|
|
942
|
+
|
|
943
|
+
``` clojure
|
|
944
|
+
(cli/dispatch tree args {:prog "some-prog" :help true})
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
- `--help`/`-h` alone prints help for all commands (or usage help for a command-less CLI) and exits with status 0.
|
|
948
|
+
- `some-command --help`/`some-command -h` prints help for `some-command` and exits with status 0.
|
|
949
|
+
This also works for multi-word commands, e.g., `some-prog deps outdated --help` would show help for the `deps outdated` command.
|
|
950
|
+
- A mistyped or missing command prints a terse message and exits with a status of 1.
|
|
951
|
+
- `-h, --help` is listed in each command's available options, appended last. To control
|
|
952
|
+
the order, give the command entry an `:order` (a vector of option keys); it
|
|
953
|
+
is used verbatim, so you decide the order, which options to list, and whether
|
|
954
|
+
to list `--help` at all (omit `:help` from `:order` to hide it; but note, it will still work).
|
|
955
|
+
An example `dispatch` command entry that lists `--help` first:
|
|
956
|
+
|
|
957
|
+
- tree format
|
|
958
|
+
```clojure
|
|
959
|
+
{:cmd {"foo" {:spec {...} :order [:help :port :verbose]}}}
|
|
960
|
+
```
|
|
961
|
+
- table format
|
|
962
|
+
``` clojure
|
|
963
|
+
{:cmds ["foo"] :spec {...} :order [:help :port :verbose]}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
Without `:order`, the order is taken from the spec (a vec-of-pairs spec keeps
|
|
967
|
+
its order; a map follows its key order, which Clojure does not guarantee), and
|
|
968
|
+
`--help` is appended.
|
|
969
|
+
- `--help`/`-h` are reserved when `:help true` is specified (a command may still define its
|
|
970
|
+
own `:help`).
|
|
971
|
+
- A command entry's `:epilog` (a string) is rendered verbatim after that command's
|
|
972
|
+
options, for examples, notes or links. Specify it at the root of the commands tree format (or `:cmds []` entry for commands table format) for the top-level help.
|
|
973
|
+
|
|
974
|
+
The `:help true` option works for a command-less CLI too.
|
|
975
|
+
`some-prog --help` then shows Usage + Options:
|
|
976
|
+
|
|
977
|
+
- tree format
|
|
978
|
+
```clojure
|
|
979
|
+
(cli/dispatch {:fn run :spec {:port {:coerce :long :desc "Port"}}}
|
|
980
|
+
args
|
|
981
|
+
{:prog "some-prog" :help true})
|
|
982
|
+
```
|
|
983
|
+
- table format
|
|
984
|
+
``` clojure
|
|
985
|
+
(cli/dispatch [{:cmds [] :fn run :spec {:port {:coerce :long :desc "Port"}}}]
|
|
986
|
+
args
|
|
987
|
+
{:prog "some-prog" :help true})
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
`--help`/`-h` are success paths: they print help and return naturally (no exit call), so
|
|
991
|
+
your `-main` ends and the process exits with a status of 0, like a normal command. Errors go
|
|
992
|
+
through the dynamic `*exit-fn*`, which exits non-zero:
|
|
993
|
+
|
|
994
|
+
| invocation | outcome |
|
|
995
|
+
|---|---|
|
|
996
|
+
| `--help` / `-h` | print help, return (status 0), no `*exit-fn*` |
|
|
997
|
+
| no command or incomplete multi-word command | terse message, `*exit-fn*` exit 1, `:cause :input-exhausted` |
|
|
998
|
+
| unknown command | terse message, `*exit-fn*` exit 1, `:cause :no-match` |
|
|
999
|
+
| option error | terse message, `*exit-fn*` exit 1, `:cause` = the babashka.cli cause |
|
|
1000
|
+
|
|
1001
|
+
You can include a `:doc` without a `:fn` to describe a grouping of multi-word commands.
|
|
1002
|
+
For example, `git bisect` is not something we can invoke, but we can get `--help` for it:
|
|
1003
|
+
```clojure
|
|
1004
|
+
(cli/dispatch
|
|
1005
|
+
{:cmd {"bisect"
|
|
1006
|
+
{:doc "general bisect help"
|
|
1007
|
+
:cmd {"start" {:fn identity :doc "start the bisect"}
|
|
1008
|
+
"good" {:fn identity :doc "commit is good"}
|
|
1009
|
+
"bad" {:fn identity :doc "commit is bad"}}}}}
|
|
1010
|
+
["bisect" "--help"]
|
|
1011
|
+
{:prog "git" :help true})
|
|
1012
|
+
```
|
|
1013
|
+
Outputs:
|
|
1014
|
+
```
|
|
1015
|
+
Usage: git bisect [options] <command>
|
|
1016
|
+
|
|
1017
|
+
general bisect help
|
|
1018
|
+
|
|
1019
|
+
Commands:
|
|
1020
|
+
start start the bisect
|
|
1021
|
+
good commit is good
|
|
1022
|
+
bad commit is bad
|
|
1023
|
+
|
|
1024
|
+
Options:
|
|
1025
|
+
-h, --help Show this help
|
|
1026
|
+
|
|
1027
|
+
Run "git bisect <command> --help" for more information on a command.
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
`*exit-fn*` is called on errors, with a map with keys:
|
|
1031
|
+
- `:exit` exit code
|
|
1032
|
+
- `:cause` can be `:no-match`, `:input-exhausted`, or an option cause.
|
|
1033
|
+
- `:dispatch` the matched command
|
|
1034
|
+
- `:data` raw dispatch error data
|
|
1035
|
+
|
|
1036
|
+
The default `*exit-fn*` implementation exits the process (`System/exit` on JVM, `js/process.exit` on Node).
|
|
1037
|
+
Rebind it to not exit (for tests, REPL use) or to remap codes by `:cause`, for example:
|
|
1038
|
+
|
|
1039
|
+
``` clojure
|
|
1040
|
+
;; treat a missing command or incomplete multi-word command as success (exit 0) instead of a usage error
|
|
1041
|
+
(binding [cli/*exit-fn* (fn [{:keys [exit cause]}]
|
|
1042
|
+
(System/exit (if (= :input-exhausted cause) 0 exit)))]
|
|
1043
|
+
(cli/dispatch table args {:prog "example" :help true}))
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
You can optionally override help and error handlers via `dispatch` `:help-fn` and `:error-fn` options.
|
|
1047
|
+
|
|
1048
|
+
To render the standard help and add to it, call `format-command-help`, the same renderer
|
|
1049
|
+
the default uses:
|
|
1050
|
+
|
|
1051
|
+
``` clojure
|
|
1052
|
+
(cli/dispatch table args
|
|
1053
|
+
{:prog "example" :help true
|
|
1054
|
+
:help-fn (fn [{:keys [tree dispatch prog inherit]}]
|
|
1055
|
+
(println "my-tool v1.2.3")
|
|
1056
|
+
(println (cli/format-command-help
|
|
1057
|
+
{:table tree :cmds dispatch :prog prog :inherit inherit})))})
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
The function `format-command-help` is also usable on its own (without `dispatch`): pass
|
|
1061
|
+
`:table` (a `dispatch` [table or tree](#tree-format)), `:cmds` (the command
|
|
1062
|
+
path, default `[]`), `:prog`, and optional `:inherit`. It returns the help
|
|
1063
|
+
string.
|
|
1064
|
+
|
|
1065
|
+
A custom `:error-fn` receives the dispatch error data
|
|
1066
|
+
(`{:cause :dispatch :prog :inherit :tree :msg ...}`) and is responsible for
|
|
1067
|
+
exiting (call `*exit-fn*` or exit yourself). To keep the standard terse message
|
|
1068
|
+
and add to it, call `format-command-error` (the same renderer the default uses)
|
|
1069
|
+
and exit afterwards:
|
|
1070
|
+
|
|
1071
|
+
``` clojure
|
|
1072
|
+
(cli/dispatch table args
|
|
1073
|
+
{:prog "example" :help true
|
|
1074
|
+
:error-fn (fn [data]
|
|
1075
|
+
(println (cli/format-command-error data))
|
|
1076
|
+
(println "See https://example.com/docs")
|
|
1077
|
+
(cli/*exit-fn* {:exit 1 :cause (:cause data)}))})
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
## Completions
|
|
1081
|
+
|
|
1082
|
+
The `dispatch` function can generate dynamic shell completions for `bash`,
|
|
1083
|
+
`zsh`, `fish`, `powershell` and `nushell`. Shells call back into your program on
|
|
1084
|
+
each TAB to generate completions. The `:prog` (program name) value is essential
|
|
1085
|
+
in the `dispatch` call. The generated snippet registers completion for that
|
|
1086
|
+
name, so it must match the command you type.
|
|
1087
|
+
|
|
1088
|
+
``` clojure
|
|
1089
|
+
(cli/dispatch table args {:prog "mycli" :help true})
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
Under babashka the snippet also registers the running script's own file name (from
|
|
1093
|
+
the `babashka.file` system property), so a script invoked directly by path
|
|
1094
|
+
(`./mycli.clj`, `/abs/mycli.clj`) completes without a `:prog`-named symlink. See
|
|
1095
|
+
[Developing completions](#developing-completions).
|
|
1096
|
+
|
|
1097
|
+
If the installed command has a different name, e.g., when a distro renames it, pass
|
|
1098
|
+
`--prog <name>` when generating the snippet to register that name instead. `--prog`
|
|
1099
|
+
may be repeated to register several names (aliases):
|
|
1100
|
+
|
|
1101
|
+
``` bash
|
|
1102
|
+
mycli org.babashka.cli/completions snippet --shell zsh --prog sq
|
|
1103
|
+
mycli org.babashka.cli/completions snippet --shell zsh --prog squint --prog sq
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
The completions call goes through a hidden `org.babashka.cli/completions`
|
|
1107
|
+
command group that `dispatch` adds for you. Running `mycli
|
|
1108
|
+
org.babashka.cli/completions snippet --shell <shell>` prints the install snippet
|
|
1109
|
+
for that specific shell to stdout. It does not write files or edit your shell
|
|
1110
|
+
config for you.
|
|
1111
|
+
|
|
1112
|
+
Commands and options come with completion support out of the box. Descriptions come from the
|
|
1113
|
+
same `:desc` (options) and `:doc` (commands) you already write for `--help`. A
|
|
1114
|
+
`:no-doc` command or option is hidden. Options that already appeared are filtered
|
|
1115
|
+
out of later suggestions, except repeatable options (e.g., `:coerce [:string]`).
|
|
1116
|
+
|
|
1117
|
+
Instructions follow to enable auto-completions in your shell.
|
|
1118
|
+
|
|
1119
|
+
### Bash
|
|
1120
|
+
|
|
1121
|
+
Add this code to your bash init file:
|
|
1122
|
+
|
|
1123
|
+
``` bash
|
|
1124
|
+
source <(mycli org.babashka.cli/completions snippet --shell bash)
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
Bash completes values only and does not show descriptions. For correct handling of
|
|
1128
|
+
`=` and `:` inside values, install the bash-completion package, which needs bash 4.1
|
|
1129
|
+
or newer. The macOS system bash 3.2 still works for the common cases.
|
|
1130
|
+
|
|
1131
|
+
### Zsh
|
|
1132
|
+
|
|
1133
|
+
Add this to your zsh init file, after `compinit`:
|
|
1134
|
+
|
|
1135
|
+
``` bash
|
|
1136
|
+
source <(mycli org.babashka.cli/completions snippet --shell zsh)
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
or save the output as `_mycli` on your `$fpath`. Option and command
|
|
1140
|
+
descriptions show inline. Completions also fire when the program is invoked by
|
|
1141
|
+
path, such as `./mycli`.
|
|
1142
|
+
|
|
1143
|
+
### Fish
|
|
1144
|
+
|
|
1145
|
+
``` fish
|
|
1146
|
+
mycli org.babashka.cli/completions snippet --shell fish | source
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
Option and command descriptions show inline. Completion also fires on a path
|
|
1150
|
+
invocation.
|
|
1151
|
+
|
|
1152
|
+
### Powershell
|
|
1153
|
+
|
|
1154
|
+
Add this to your `$PROFILE`:
|
|
1155
|
+
|
|
1156
|
+
``` powershell
|
|
1157
|
+
mycli org.babashka.cli/completions snippet --shell powershell | Out-String | Invoke-Expression
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
Descriptions show in menu-completion mode, which you can enable with
|
|
1161
|
+
`Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete`.
|
|
1162
|
+
|
|
1163
|
+
### Nushell
|
|
1164
|
+
|
|
1165
|
+
Nushell cannot `source` from a pipe, so save the snippet to a file in the
|
|
1166
|
+
autoload directory and restart `nu`:
|
|
1167
|
+
|
|
1168
|
+
``` nu
|
|
1169
|
+
mkdir ($nu.user-autoload-dirs | first)
|
|
1170
|
+
mycli org.babashka.cli/completions snippet --shell nushell | save -f (($nu.user-autoload-dirs | first) | path join "mycli.nu")
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
On nushell versions without autoload dirs, save it anywhere and add
|
|
1174
|
+
`source <literal path>` to your `config.nu`. Descriptions show in the completion
|
|
1175
|
+
menu.
|
|
1176
|
+
|
|
1177
|
+
Unlike the other shells, nushell has no per-command completion registration. Instead, one
|
|
1178
|
+
global hook (`$env.config.completions.external.completer`) handles TAB for all
|
|
1179
|
+
external commands. The snippet does not overwrite a completer you already have
|
|
1180
|
+
there: it saves the previous one and falls back to it for every command other
|
|
1181
|
+
than `mycli`, so several tools can install side by side.
|
|
1182
|
+
|
|
1183
|
+
### Developing completions
|
|
1184
|
+
|
|
1185
|
+
Under babashka, the snippet registers completion for the running script's file
|
|
1186
|
+
name (read from the `babashka.file` system property) in addition to `:prog`. A
|
|
1187
|
+
dev build invoked directly completes as-is, by bare name or by path, with no
|
|
1188
|
+
symlink to the `:prog` name required:
|
|
1189
|
+
|
|
1190
|
+
``` bash
|
|
1191
|
+
./run.clj <TAB>
|
|
1192
|
+
/abs/path/to/run.clj <TAB>
|
|
1193
|
+
run.clj <TAB> # when its directory is on PATH
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
Source the snippet for the shell you are testing, using the install command from
|
|
1197
|
+
its section above, and re-source it after each change to your CLI so new commands
|
|
1198
|
+
and options show up.
|
|
1199
|
+
|
|
1200
|
+
When `babashka.file` is not set (e.g. running via `clojure` or an uberjar) the
|
|
1201
|
+
script file name is unknown, so completion is registered for `:prog` only. Make
|
|
1202
|
+
the build callable under that name on `PATH`, for example with a symlink:
|
|
1203
|
+
|
|
1204
|
+
``` bash
|
|
1205
|
+
ln -sf "$PWD/run.clj" /tmp/mycli # name the link :prog
|
|
1206
|
+
export PATH="/tmp:$PATH" # bash and zsh
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
In fish use `set -gx PATH /tmp $PATH`, in nushell
|
|
1210
|
+
`$env.PATH = ($env.PATH | prepend /tmp)`. On Windows, put a `mycli` wrapper
|
|
1211
|
+
script on your `PATH` instead of a symlink.
|
|
1212
|
+
|
|
1213
|
+
You can also register one or more explicit names by passing `--prog` (repeatable)
|
|
1214
|
+
when generating the snippet, e.g. `--prog mycli --prog mc`.
|
|
1215
|
+
|
|
1216
|
+
Now `mycli <TAB>` completes commands and `mycli sub --<TAB>` its options. To see the
|
|
1217
|
+
completer's raw output directly, without a shell, call the hidden command
|
|
1218
|
+
yourself. The tokens after `--` are what the shell would pass on TAB, here the
|
|
1219
|
+
command `sub` and a `--` to complete its options. It prints one candidate per
|
|
1220
|
+
line, as the value, a tab, then the description:
|
|
1221
|
+
|
|
1222
|
+
``` bash
|
|
1223
|
+
mycli org.babashka.cli/completions complete --shell zsh -- sub --
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Completing option values
|
|
1227
|
+
|
|
1228
|
+
To complete an option's value, give it one of:
|
|
1229
|
+
|
|
1230
|
+
- `:complete` - a static collection of values (or `{:value .. :description ..}`
|
|
1231
|
+
maps)
|
|
1232
|
+
- A set-valued `:validate`, whose members double as completions
|
|
1233
|
+
- `:complete-fn` - a function for dynamic completion
|
|
1234
|
+
|
|
1235
|
+
``` clojure
|
|
1236
|
+
{:env {:coerce :string
|
|
1237
|
+
:complete ["dev" "staging" "prod"]} ; static list
|
|
1238
|
+
:level {:coerce :keyword
|
|
1239
|
+
:validate #{:local :global :system}} ; reused as completions
|
|
1240
|
+
:branch {:coerce :string
|
|
1241
|
+
:complete-fn (fn [{:keys [to-complete opts]}] ; dynamic
|
|
1242
|
+
(git-branches to-complete))}}
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
The `:complete-fn` is called with `{:to-complete <partial> :opts <opts parsed so
|
|
1246
|
+
far> :option <key>}` and returns values (strings) (or `{:value .. :description
|
|
1247
|
+
..}` maps). All three sources are prefix-filtered against the partial value for
|
|
1248
|
+
you.
|
|
1249
|
+
|
|
1250
|
+
An option value with none of these defaults to the shell's own file completion.
|
|
1251
|
+
For a value where file suggestions are not appropriate, you can opt out with
|
|
1252
|
+
`:complete false`.
|
|
1253
|
+
|
|
1254
|
+
Positional arguments mapped with [`:args->opts`](#args-opts) complete in the same
|
|
1255
|
+
way. A positional resolves to its spec key by position, so the same `:complete`,
|
|
1256
|
+
`:complete-fn` or set `:validate` on that key completes the positional too. With
|
|
1257
|
+
`:args->opts [:env]` and `:env {:complete ["dev" "prod"]}`, `mycli deploy <TAB>`
|
|
1258
|
+
completes `dev`/`prod`.
|
|
1259
|
+
|
|
1260
|
+
A positional declared in `:args->opts` with no value completion defaults to the
|
|
1261
|
+
shell's own file completion the same way. So `:args->opts [:file]` with a bare
|
|
1262
|
+
`:file` makes `mycli cat <TAB>` complete filenames and `:complete false` opts
|
|
1263
|
+
out here too.
|
|
1264
|
+
|
|
1265
|
+
## Creating a Standalone CLI
|
|
1266
|
+
We've covered how to run your CLI via a supported Clojure dialect, e.g.:
|
|
1267
|
+
- `clojure -M -m my-cli --foo bar`
|
|
1268
|
+
- `bb -m my-cli --foo bar`
|
|
1269
|
+
- `bb my_clj.clj --foo bar`
|
|
1270
|
+
|
|
1271
|
+
Sometimes you'll want to run your CLI as a standalone program, e.g. `my-cli --foo bar`.
|
|
1272
|
+
This hides the launching dialect as an implementation detail and supports [shell completions](#completions).
|
|
1273
|
+
|
|
1274
|
+
Some techniques to achieve this are:
|
|
1275
|
+
|
|
1276
|
+
| Technique | macOS & Linux | Windows | Babashka | Clojure | ClojureScript | ClojureDart | Description |
|
|
1277
|
+
|----------------------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------|
|
|
1278
|
+
| shebang on script | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | For self-contained scripts, a `#!/usr/bin/env bb` at the top of your script |
|
|
1279
|
+
| wrapper shell script | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | An appropriate shell-specific (e.g., bash, CMD, Powershell) wrapper script that launches your CLI. |
|
|
1280
|
+
| bbin | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :x: | An elegant way to distribute CLIs created with babashka. |
|
|
1281
|
+
| GraalVM native image | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: | :x: | :x: | Compile your Clojure CLI to an OS-specific executable |
|
|
1282
|
+
| ClojureDart native image | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :x: | :heavy_check_mark: | Compile your ClojureDart CLI to an OS-specific executable |
|
|
1283
|
+
| use JavaScript native image tool | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: | [Bun](https://bun.com/docs/bundler/executables), for example, supports compiling JavaScript to an os-specific executable |
|
|
1284
|
+
| shebang on JavaScript | :heavy_check_mark: | :x: | :x: | :x: | :heavy_check_mark: | :x: | Prepend compiled ClojureScript with `#!/usr/bin/env node` |
|
|
1285
|
+
| use npm | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: | Distribute your CLI through npm. For a stand-alone experience users would install globally via `npm install -g` |
|
|
1286
|
+
|
|
1287
|
+
## Adding Production Polish
|
|
1288
|
+
Babashka CLI lets you get up and running quickly.
|
|
1289
|
+
As you move toward production quality, it's helpful to let users know when their inputs are invalid.
|
|
1290
|
+
Strict validation can be introduced with [:restrict](#restrict), [:require](#require), and [:validate](#validate).
|
|
1291
|
+
|
|
1292
|
+
As you add polish, you'll likely make use of a [:spec](#spec) and maybe a custom [:error_fn](#error-handling). Even if your program does not use [commands](#commands), consider using `dispatch` with the `:help true` option (as shown in [Simple Example](#simple-example)) for printed terse error messages (instead of exceptions), and automatic `--help` generation.
|
|
1293
|
+
|
|
1294
|
+
## Restrict
|
|
1295
|
+
|
|
1296
|
+
Use the `:restrict` option to restrict options to only those explicitly mentioned in configuration:
|
|
1297
|
+
|
|
1298
|
+
``` clojure
|
|
1299
|
+
(cli/parse-args ["--foo"] {:spec {:bar {}} :restrict true})
|
|
1300
|
+
;;=>
|
|
1301
|
+
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:357).
|
|
1302
|
+
Unknown option: --foo
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
## Require
|
|
1306
|
+
|
|
1307
|
+
Mark an option required in its [spec](#spec) with `:require true`; parsing throws
|
|
1308
|
+
when it is not present:
|
|
1309
|
+
|
|
1310
|
+
``` clojure
|
|
1311
|
+
(cli/parse-args ["--foo"] {:spec {:bar {:require true}}})
|
|
1312
|
+
;;=>
|
|
1313
|
+
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:363).
|
|
1314
|
+
Required option: --bar
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
Required options are shown as `(required)` in `--help`, in the slot a default
|
|
1318
|
+
would otherwise occupy.
|
|
1319
|
+
|
|
1320
|
+
## Validate
|
|
1321
|
+
|
|
1322
|
+
``` clojure
|
|
1323
|
+
(cli/parse-args ["--foo" "0"] {:spec {:foo {:validate pos?}}})
|
|
1324
|
+
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:378).
|
|
1325
|
+
Invalid value for option --foo: 0
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
To gain more control over the error message, use `:pred` and `:ex-msg`:
|
|
1329
|
+
|
|
1330
|
+
``` clojure
|
|
1331
|
+
(cli/parse-args ["--foo" "0"] {:spec {:foo {:validate {:pred pos? :ex-msg (fn [m] (str "Not a positive number: " (:value m)))}}}})
|
|
1332
|
+
;;=>
|
|
1333
|
+
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:378).
|
|
1334
|
+
Not a positive number: 0
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
## Error handling
|
|
1338
|
+
|
|
1339
|
+
By default, an exception will be thrown in the following situations:
|
|
1340
|
+
- A restricted option is encountered
|
|
1341
|
+
- A required option is missing
|
|
1342
|
+
- Validation fails for an option
|
|
1343
|
+
- Coercion fails for an option
|
|
1344
|
+
|
|
1345
|
+
You may supply a custom error handler function with `:error-fn`. The function
|
|
1346
|
+
will be called with a map containing the following keys:
|
|
1347
|
+
- `:type` - `:org.babashka/cli` (for filtering out other types of errors).
|
|
1348
|
+
- `:cause` - one of:
|
|
1349
|
+
- `:restrict` - a restricted option was encountered.
|
|
1350
|
+
- `:require` - a required option was missing.
|
|
1351
|
+
- `:validate` - validation failed for an option.
|
|
1352
|
+
- `:coerce` - coercion failed for an option.
|
|
1353
|
+
- `:msg` - default error message.
|
|
1354
|
+
- `:option` - the option being parsed when the error occurred.
|
|
1355
|
+
- `:spec` - the spec passed into `parse-opts` (see the [Spec](#spec) section).
|
|
1356
|
+
|
|
1357
|
+
The following keys are present depending on `:cause`:
|
|
1358
|
+
- `:cause :restrict`
|
|
1359
|
+
- `:restrict` - the value of the `:restrict` opt to `parse-args` (see the
|
|
1360
|
+
[Restrict](#restrict) section).
|
|
1361
|
+
- `:cause :require`
|
|
1362
|
+
- `:require` - the value of the `:require` opt to `parse-args` (see the
|
|
1363
|
+
[Require](#require) section).
|
|
1364
|
+
- `:cause :validate`
|
|
1365
|
+
- `:value` - the value of the option that failed validation.
|
|
1366
|
+
- `:validate` - the value of the `:validate` opt to `parse-args` (see the
|
|
1367
|
+
[Validate](#validate) section).
|
|
1368
|
+
- `:cause :coerce`
|
|
1369
|
+
- `:value` - the value of the option that failed coercion.
|
|
1370
|
+
|
|
1371
|
+
By default, babashka CLI will throw exceptions on errors it detects.
|
|
1372
|
+
You can do the same from your custom error handler.
|
|
1373
|
+
|
|
1374
|
+
For a more polished user experience, you might choose to have your custom error handler print the error and exit. For example:
|
|
1375
|
+
``` clojure
|
|
1376
|
+
(cli/parse-opts
|
|
1377
|
+
[]
|
|
1378
|
+
{:spec {:foo {:desc "You know what this is."
|
|
1379
|
+
:ref "<val>"
|
|
1380
|
+
:require true}}
|
|
1381
|
+
:error-fn
|
|
1382
|
+
(fn [{:keys [spec type cause msg option] :as data}]
|
|
1383
|
+
(if (= :org.babashka/cli type)
|
|
1384
|
+
(case cause
|
|
1385
|
+
:require
|
|
1386
|
+
(println
|
|
1387
|
+
(format "Missing required argument:\n%s"
|
|
1388
|
+
(cli/format-opts {:spec (select-keys spec [option])})))
|
|
1389
|
+
(println msg))
|
|
1390
|
+
(throw (ex-info msg data)))
|
|
1391
|
+
(System/exit 1))})
|
|
1392
|
+
```
|
|
1393
|
+
would print:
|
|
1394
|
+
|
|
1395
|
+
```
|
|
1396
|
+
Missing required argument:
|
|
1397
|
+
--foo <val> You know what this is.
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
You can also choose to collect and then report all detected errors (see `babashka.cli-test/error-fn-test` for an example of this).
|
|
1401
|
+
|
|
1402
|
+
## Adding default args
|
|
1403
|
+
|
|
1404
|
+
You can supply default args with `:exec-args`:
|
|
1405
|
+
|
|
1406
|
+
``` clojure
|
|
1407
|
+
(cli/parse-args ["--foo" "0"] {:exec-args {:bar 1}})
|
|
1408
|
+
;;=> {:foo 0, :bar 1}
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
Note that args specified in `args` will override defaults in `:exec-args`:
|
|
1412
|
+
|
|
1413
|
+
``` clojure
|
|
1414
|
+
(cli/parse-args ["--foo" "0" "--bar" "42"] {:exec-args {:bar 1}})
|
|
1415
|
+
;;=> {:foo 0, :bar 42}
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
## Printing options
|
|
1419
|
+
|
|
1420
|
+
Given a [spec](#spec) (like the `from`/`to`/`paths`/`pretty` one above), print
|
|
1421
|
+
its options with `format-opts`:
|
|
1422
|
+
|
|
1423
|
+
``` clojure
|
|
1424
|
+
(println (cli/format-opts {:spec spec :order [:from :to :paths :pretty]}))
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
This will print:
|
|
1428
|
+
|
|
1429
|
+
```
|
|
1430
|
+
-i, --from <format> The input format. <format> can be edn, json or transit. (default: edn)
|
|
1431
|
+
-o, --to <format> The output format. <format> can be edn, json or transit. (default: json)
|
|
1432
|
+
--paths Paths of files to transform. (default: src test)
|
|
1433
|
+
-p, --pretty Pretty-print output.
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
As options can often be reused in multiple commands, you can determine the
|
|
1437
|
+
order _and_ selection of printed options with `:order`. If you don't want to use
|
|
1438
|
+
`:order` and simply want to present the options as written, you can also use a
|
|
1439
|
+
vector of vectors for the spec:
|
|
1440
|
+
|
|
1441
|
+
``` clojure
|
|
1442
|
+
[[:pretty {:desc "Pretty-print output."
|
|
1443
|
+
:alias :p}]
|
|
1444
|
+
[:paths {:desc "Paths of files to transform."
|
|
1445
|
+
:coerce []
|
|
1446
|
+
:default ["src" "test"]
|
|
1447
|
+
:default-desc "src test"}]]
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
If you need more flexibility, you can also use `opts->table`, which turns a spec into a vector of vectors, representing rows of a table.
|
|
1451
|
+
You can then use `format-table` to produce a table as returned by `format-opts`.
|
|
1452
|
+
For example, to add a header row with labels for each column, you could do something like:
|
|
1453
|
+
|
|
1454
|
+
``` clojure
|
|
1455
|
+
(cli/format-table
|
|
1456
|
+
{:rows (concat [["alias" "option" "ref" "default" "description"]]
|
|
1457
|
+
(cli/opts->table
|
|
1458
|
+
{:spec {:foo {:alias :f, :default "yupyupyupyup", :ref "<foo>"
|
|
1459
|
+
:desc "Thingy"}
|
|
1460
|
+
:bar {:alias :b, :default "sure", :ref "<bar>"
|
|
1461
|
+
:desc "Barbarbar" :default-desc "Mos def"}}}))
|
|
1462
|
+
:indent 2})
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
### Terminal width
|
|
1466
|
+
|
|
1467
|
+
`format-opts` and `format-table` wrap long descriptions to the terminal width,
|
|
1468
|
+
aligning continuation lines under the description column:
|
|
1469
|
+
|
|
1470
|
+
```
|
|
1471
|
+
--copy-resources <resource> Copy non cljs/cljc files from --paths as
|
|
1472
|
+
resources; a keyword matches by extension,
|
|
1473
|
+
otherwise by regex
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
On by default; `:wrap false` disables it.
|
|
1477
|
+
|
|
1478
|
+
The width comes from `:max-width-fn`, a `(fn [cfg] -> width)` defaulting to
|
|
1479
|
+
`cli/default-width-fn`: on node it reads `process.stdout.columns`; on the JVM it
|
|
1480
|
+
reads `$COLUMNS` then probes JLine (when on the classpath). Falls back to 80.
|
|
1481
|
+
Override per call:
|
|
1482
|
+
|
|
1483
|
+
``` clojure
|
|
1484
|
+
(cli/format-opts {:spec spec :max-width-fn (constantly 80)})
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
On the JVM, `default-width-fn` reads the real width via JLine when it is on the
|
|
1488
|
+
classpath. babashka bundles it, so bb scripts get it for free; without JLine the
|
|
1489
|
+
width falls back to `$COLUMNS`/80. If you want real-width detection on another
|
|
1490
|
+
JVM, you can add a JLine provider (FFM is the lightest):
|
|
1491
|
+
|
|
1492
|
+
``` clojure
|
|
1493
|
+
;; deps.edn
|
|
1494
|
+
org.jline/jline-terminal {:mvn/version "3.30.4"}
|
|
1495
|
+
org.jline/jline-terminal-ffm {:mvn/version "3.30.4"}
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
## Babashka tasks
|
|
1499
|
+
|
|
1500
|
+
For documentation on babashka tasks, go
|
|
1501
|
+
[here](https://book.babashka.org/#tasks).
|
|
1502
|
+
|
|
1503
|
+
Since babashka `0.9.160`, `babashka.cli` has become a built-in and has better
|
|
1504
|
+
integration through `-x` and `exec`. Read about that in the [babashka
|
|
1505
|
+
book](https://book.babashka.org/#cli).
|
|
1506
|
+
|
|
1507
|
+
## Clojure CLI
|
|
1508
|
+
|
|
1509
|
+
The Clojure CLI supports [invoking a function with a single map arg](https://clojure.org/reference/clojure_cli#use_fn) via the `-X` command line option.
|
|
1510
|
+
Because `-X` does no automatic coercion of values, getting the command-line correct can be, at best, awkward on macOS and Linux, and often an exercise of real frustration on Windows.
|
|
1511
|
+
|
|
1512
|
+
You can control parsing behavior by adding `:org.babashka/cli` metadata to
|
|
1513
|
+
Clojure functions. It does not introduce a dependency on `babashka.cli`
|
|
1514
|
+
itself. Not adding any metadata will result in string values, which in many
|
|
1515
|
+
cases may already be a reasonable default.
|
|
1516
|
+
|
|
1517
|
+
Adding support for babashka CLI will cause less friction with shell usage.
|
|
1518
|
+
You can support the same function for both `clojure -X` and `clojure -M` style invocations without
|
|
1519
|
+
writing extra boilerplate.
|
|
1520
|
+
|
|
1521
|
+
In your `deps.edn` `:aliases` entry, add:
|
|
1522
|
+
|
|
1523
|
+
``` clojure
|
|
1524
|
+
:exec {:extra-deps {org.babashka/cli {:mvn/version "<latest-version>"}}
|
|
1525
|
+
:main-opts ["-m" "babashka.cli.exec"]}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
Now you can call any function that accepts a map argument. E.g.:
|
|
1529
|
+
|
|
1530
|
+
``` clojure
|
|
1531
|
+
$ clojure -M:exec clojure.core prn :a 1 :b 2
|
|
1532
|
+
{:a "1", :b "2"}
|
|
1533
|
+
```
|
|
1534
|
+
|
|
1535
|
+
Use `:org.babashka/cli` metadata for coercions:
|
|
1536
|
+
|
|
1537
|
+
``` clojure
|
|
1538
|
+
(ns my-ns)
|
|
1539
|
+
|
|
1540
|
+
(defn foo
|
|
1541
|
+
{:org.babashka/cli {:coerce {:a :symbol
|
|
1542
|
+
:b :long}}}
|
|
1543
|
+
;; map argument:
|
|
1544
|
+
[m]
|
|
1545
|
+
;; print map argument:
|
|
1546
|
+
(prn m))
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
``` clojure
|
|
1550
|
+
$ clojure -M:exec my-ns foo :a foo/bar :b 2 :c vanilla
|
|
1551
|
+
{:a foo/bar, :b 2, :c "vanilla"}
|
|
1552
|
+
```
|
|
1553
|
+
|
|
1554
|
+
Note that any library can add support for babashka CLI without depending on
|
|
1555
|
+
babashka CLI.
|
|
1556
|
+
|
|
1557
|
+
An example that specializes `babashka.cli` usage to a function:
|
|
1558
|
+
|
|
1559
|
+
``` clojure
|
|
1560
|
+
:prn {:extra-deps {org.babashka/cli {:mvn/version "<latest-version>"}}
|
|
1561
|
+
:main-opts ["-m" "babashka.cli.exec" "clojure.core" "prn"]}
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
``` clojure
|
|
1565
|
+
$ clojure -M:prn --foo=bar --baz
|
|
1566
|
+
{:foo "bar" :baz true}
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
You can also pre-define the exec function in `:exec-fn`:
|
|
1570
|
+
|
|
1571
|
+
``` clojure
|
|
1572
|
+
:prn {:extra-deps {org.babashka/cli {:mvn/version "<latest-version>"}}
|
|
1573
|
+
:exec-fn clojure.core/prn
|
|
1574
|
+
:main-opts ["-m" "babashka.cli.exec"]}
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
To alter the parsing behavior of functions you don't control, you can add
|
|
1578
|
+
`:org.babashka/cli` data in the `deps.edn` alias:
|
|
1579
|
+
|
|
1580
|
+
``` clojure
|
|
1581
|
+
:prn {:deps {org.babashka/cli {:mvn/version "<latest-version>"}}
|
|
1582
|
+
:exec-fn clojure.core/prn
|
|
1583
|
+
:main-opts ["-m" "babashka.cli.exec"]
|
|
1584
|
+
:org.babashka/cli {:coerce {:foo :long}}}
|
|
1585
|
+
```
|
|
1586
|
+
|
|
1587
|
+
``` clojure
|
|
1588
|
+
$ clojure -M:prn --foo=1
|
|
1589
|
+
{:foo 1}
|
|
1590
|
+
```
|
|
1591
|
+
|
|
1592
|
+
### [antq](https://github.com/liquidz/antq)
|
|
1593
|
+
|
|
1594
|
+
`.clojure/deps.edn` alias:
|
|
1595
|
+
|
|
1596
|
+
``` clojure
|
|
1597
|
+
:antq {:deps {org.babashka/cli {:mvn/version "<latest-version>"}
|
|
1598
|
+
com.github.liquidz/antq {:mvn/version "1.7.798"}}
|
|
1599
|
+
:paths []
|
|
1600
|
+
:main-opts ["-m" "babashka.cli.exec" "antq.tool" "outdated"]
|
|
1601
|
+
:org.babashka/cli {:coerce {:skip []}}}
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
On the command line you can now run it with:
|
|
1605
|
+
|
|
1606
|
+
``` clojure
|
|
1607
|
+
$ clj -M:antq --upgrade
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
Note that we are calling the same `outdated` function that you normally call
|
|
1611
|
+
with `-T`:
|
|
1612
|
+
|
|
1613
|
+
``` clojure
|
|
1614
|
+
$ clj -Tantq outdated :upgrade true
|
|
1615
|
+
```
|
|
1616
|
+
even though antq has its own `-main` function.
|
|
1617
|
+
|
|
1618
|
+
Note that we added the `:org.babashka/cli {:coerce {:skip []}}` data in the
|
|
1619
|
+
alias to make sure that `--skip` options get collected into a vector:
|
|
1620
|
+
|
|
1621
|
+
``` clojure
|
|
1622
|
+
clj -M:antq --upgrade --skip github-action
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
vs.
|
|
1626
|
+
|
|
1627
|
+
``` clojure
|
|
1628
|
+
clj -Tantq outdated :upgrade true :skip '["github-action"]'
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
The following projects have added support for babashka CLI. Feel free to add a PR to
|
|
1632
|
+
list your project as well!
|
|
1633
|
+
|
|
1634
|
+
### [clj-new](https://github.com/seancorfield/clj-new#babashka-cli)
|
|
1635
|
+
|
|
1636
|
+
### [codox](https://github.com/weavejester/codox)
|
|
1637
|
+
|
|
1638
|
+
In `deps.edn` create an alias:
|
|
1639
|
+
|
|
1640
|
+
``` clojure
|
|
1641
|
+
:codox {:extra-deps {org.babashka/cli {:mvn/version "<latest-version>"}
|
|
1642
|
+
codox/codox {:mvn/version "0.10.8"}}
|
|
1643
|
+
:exec-fn codox.main/generate-docs
|
|
1644
|
+
;; default arguments:
|
|
1645
|
+
:exec-args {:source-paths ["src"]}
|
|
1646
|
+
:org.babashka/cli {:coerce {:source-paths []
|
|
1647
|
+
:doc-paths []
|
|
1648
|
+
:themes [:keyword]}}
|
|
1649
|
+
:main-opts ["-m" "babashka.cli.exec"]}
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
CLI invocation:
|
|
1653
|
+
|
|
1654
|
+
``` clojure
|
|
1655
|
+
$ clojure -M:codox --output-path /tmp/out
|
|
1656
|
+
```
|
|
1657
|
+
|
|
1658
|
+
### [deps-new](https://github.com/seancorfield/deps-new#babashka-cli)
|
|
1659
|
+
|
|
1660
|
+
### [kaocha](https://github.com/lambdaisland/kaocha)
|
|
1661
|
+
|
|
1662
|
+
In `deps.edn` create an alias:
|
|
1663
|
+
|
|
1664
|
+
``` clojure
|
|
1665
|
+
:kaocha {:extra-deps {org.babashka/cli {:mvn/version "<latest-version>"}
|
|
1666
|
+
lambdaisland/kaocha {:mvn/version "1.66.1034"}}
|
|
1667
|
+
:exec-fn kaocha.runner/exec-fn
|
|
1668
|
+
:exec-args {} ;; insert default arguments here
|
|
1669
|
+
:org.babashka/cli {:alias {:watch :watch?
|
|
1670
|
+
:fail-fast :fail-fast?}
|
|
1671
|
+
:coerce {:skip-meta :keyword
|
|
1672
|
+
:kaocha/reporter [:symbol]}}
|
|
1673
|
+
:main-opts ["-m" "babashka.cli.exec"]}
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
Now you are able to use kaocha's exec-fn to be used as a CLI:
|
|
1677
|
+
|
|
1678
|
+
``` clojure
|
|
1679
|
+
$ clj -M:kaocha --watch --fail-fast --kaocha/reporter kaocha.report/documentation
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
### [quickdoc](https://github.com/borkdude/quickdoc#clojure-cli)
|
|
1683
|
+
|
|
1684
|
+
### [tools.build](https://github.com/clojure/tools.build)
|
|
1685
|
+
|
|
1686
|
+
In `deps.edn` create an alias:
|
|
1687
|
+
|
|
1688
|
+
``` clojure
|
|
1689
|
+
:build {:deps {org.babashka/cli {:mvn/version "<latest-version>"}
|
|
1690
|
+
io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}}
|
|
1691
|
+
:paths ["."]
|
|
1692
|
+
:ns-default build
|
|
1693
|
+
:main-opts ["-m" "babashka.cli.exec"]}
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
Now you can call your build functions as CLIs:
|
|
1697
|
+
|
|
1698
|
+
``` clojure
|
|
1699
|
+
clj -M:build jar --verbose
|
|
1700
|
+
```
|
|
1701
|
+
|
|
1702
|
+
### [tools.deps.graph](https://github.com/clojure/tools.deps.graph)
|
|
1703
|
+
|
|
1704
|
+
In `deps.edn` create an alias:
|
|
1705
|
+
|
|
1706
|
+
``` clojure
|
|
1707
|
+
:graph {:deps {org.babashka/cli {:mvn/version "<latest-version>"}
|
|
1708
|
+
org.clojure/tools.deps.graph {:mvn/version "1.1.68"}}
|
|
1709
|
+
:exec-fn clojure.tools.deps.graph/graph
|
|
1710
|
+
:exec-args {} ;; insert default arguments here
|
|
1711
|
+
:org.babashka/cli {:coerce {:trace-omit [:symbol]}}
|
|
1712
|
+
:main-opts ["-m" "babashka.cli.exec"]}
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
Then invoke on the command line:
|
|
1716
|
+
|
|
1717
|
+
``` clojure
|
|
1718
|
+
clj -M:graph --size --output graph.png
|
|
1719
|
+
```
|
|
1720
|
+
|
|
1721
|
+
## Leiningen
|
|
1722
|
+
|
|
1723
|
+
This tool can be used to run clojure exec functions with [lein](https://leiningen.org/).
|
|
1724
|
+
|
|
1725
|
+
An example with `clj-new`:
|
|
1726
|
+
|
|
1727
|
+
In `~/.lein/profiles.clj` put:
|
|
1728
|
+
|
|
1729
|
+
``` clojure
|
|
1730
|
+
{:clj-1.11 {:dependencies [[org.clojure/clojure "1.11.1"]]}
|
|
1731
|
+
:clj-new {:dependencies [[org.babashka/cli "<latest-version>"]
|
|
1732
|
+
[com.github.seancorfield/clj-new "1.2.381"]]}
|
|
1733
|
+
:user {:aliases {"clj-new" ["with-profiles" "+clj-1.11,+clj-new"
|
|
1734
|
+
"run" "-m" "babashka.cli.exec"
|
|
1735
|
+
{:exec-args {:env {:description "My project"}}
|
|
1736
|
+
:coerce {:verbose :long
|
|
1737
|
+
:args []}
|
|
1738
|
+
:alias {:f :force}}
|
|
1739
|
+
"clj-new"]}}}
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
After that you can use `lein clj-new app` to create a new app:
|
|
1743
|
+
|
|
1744
|
+
``` clojure
|
|
1745
|
+
$ lein clj-new app --name foobar/baz --verbose 3 -f
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1748
|
+
<!-- ## Future ideas -->
|
|
1749
|
+
|
|
1750
|
+
<!-- ### Command line syntax for `:coerce` and `:collect` -->
|
|
1751
|
+
|
|
1752
|
+
<!-- Perhaps this library can consider a command line syntax for `:coerce` and -->
|
|
1753
|
+
<!-- `:collect`, e.g.: -->
|
|
1754
|
+
|
|
1755
|
+
<!-- ``` clojure -->
|
|
1756
|
+
<!-- $ clj -M:example --skip.0=github-actions --skip.1=clojure-cli -->
|
|
1757
|
+
<!-- ``` -->
|
|
1758
|
+
|
|
1759
|
+
<!-- ``` clojure -->
|
|
1760
|
+
<!-- $ clj -M:example --lib%sym=org.babashka/cli -->
|
|
1761
|
+
<!-- ``` -->
|
|
1762
|
+
|
|
1763
|
+
<!-- Things to look out for here is if the delimiter works well with bash / zsh / -->
|
|
1764
|
+
<!-- cmd.exe and Powershell. -->
|
|
1765
|
+
|
|
1766
|
+
<!-- ### Merge args from a file -->
|
|
1767
|
+
|
|
1768
|
+
<!-- Merge default arguments from a file so you don't have to write them on the command line: -->
|
|
1769
|
+
|
|
1770
|
+
<!-- ``` clojure -->
|
|
1771
|
+
<!-- --org.babashka/cli-defaults=foo.edn -->
|
|
1772
|
+
<!-- ``` -->
|
|
1773
|
+
|
|
1774
|
+
## JavaScript
|
|
1775
|
+
|
|
1776
|
+
Babashka CLI is published to NPM as [`@babashka/cli`](https://www.npmjs.com/package/@babashka/cli),
|
|
1777
|
+
compiled from the same source with [Squint](https://github.com/squint-cljs/squint).
|
|
1778
|
+
|
|
1779
|
+
Install it with:
|
|
1780
|
+
|
|
1781
|
+
```
|
|
1782
|
+
npm install @babashka/cli
|
|
1783
|
+
```
|
|
1784
|
+
|
|
1785
|
+
and use it:
|
|
1786
|
+
|
|
1787
|
+
```js
|
|
1788
|
+
import { parseOpts, parseArgs, dispatch, coerce, formatOpts } from '@babashka/cli';
|
|
1789
|
+
|
|
1790
|
+
parseOpts(['--foo', '1', '--bar']);
|
|
1791
|
+
// => { foo: 1, bar: true }
|
|
1792
|
+
|
|
1793
|
+
parseOpts(['--port', '8080'], { coerce: { port: 'int' } });
|
|
1794
|
+
// => { port: 8080 }
|
|
1795
|
+
|
|
1796
|
+
parseArgs(['--foo', '1', 'x', 'y']);
|
|
1797
|
+
// => { opts: { foo: 1 }, args: ['x', 'y'] }
|
|
1798
|
+
|
|
1799
|
+
coerce('42', 'int');
|
|
1800
|
+
// => 42
|
|
1801
|
+
|
|
1802
|
+
const tree = {
|
|
1803
|
+
fn: () => console.log('usage: add'),
|
|
1804
|
+
cmd: {
|
|
1805
|
+
add: {
|
|
1806
|
+
fn: (m) => console.log('add', m.opts),
|
|
1807
|
+
spec: {
|
|
1808
|
+
file: { desc: 'File to add', alias: 'f' },
|
|
1809
|
+
verbose: { coerce: 'boolean', desc: 'Verbose output' },
|
|
1810
|
+
},
|
|
1811
|
+
},
|
|
1812
|
+
},
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
// `help: true` gives every (sub)command an auto-generated --help:
|
|
1816
|
+
dispatch(tree, process.argv.slice(2), { help: true, prog: 'myapp' });
|
|
1817
|
+
```
|
|
1818
|
+
|
|
1819
|
+
```
|
|
1820
|
+
$ myapp add --help
|
|
1821
|
+
Usage: myapp add [options]
|
|
1822
|
+
|
|
1823
|
+
Options:
|
|
1824
|
+
-f, --file File to add
|
|
1825
|
+
--verbose Verbose output
|
|
1826
|
+
-h, --help Show this help
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1829
|
+
Every function is exported under a friendly camelCase name (`parseOpts`,
|
|
1830
|
+
`specToOpts`, `tableToTree`, ...) and under its squint-compiled name
|
|
1831
|
+
(`parse_opts`, `spec__GT_opts`, ...). The `parse-opts*` function is exposed also as `parseOptsRaw`.
|
|
1832
|
+
|
|
1833
|
+
## License
|
|
1834
|
+
|
|
1835
|
+
Copyright © 2022-2026 Michiel Borkent
|
|
1836
|
+
|
|
1837
|
+
Distributed under the MIT License. See LICENSE.
|