Skip to content

Commit afd4eb0

Browse files
committed
Changes around middleware/handler handling
- `:middleware` can now be a single function, not only a sequence of functions - [BREAKING] When setting a `:handler` for a boolean flag (one that looks like `--[no-]...`), the handler now receives the boolean value as a second argument - [BREAKING] Invoke flag handlers/middleware in the order they are specified, so that later flags can override earlier flags
1 parent dd8795d commit afd4eb0

File tree

4 files changed

+86
-9
lines changed

4 files changed

+86
-9
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
## Added
44

5-
## Fixed
5+
- `:middleware` can now be a single function, not only a sequence of functions
66

77
## Changed
88

9+
- [BREAKING] When setting a `:handler` for a boolean flag (one that looks like
10+
`--[no-]...`), the handler now receives the boolean value as a second argument
11+
- [BREAKING] Invoke flag handlers/middleware in the order they are specified, so
12+
that later flags can override earlier flags
13+
914
# 0.21.84 (2025-02-09 / fa56d06)
1015

1116
## Added

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ must always be passed:
282282
- `:default` default value
283283
- `:value` value to be associated with the key (for flags that don't take arguments)
284284
- `:handler` handler function, `(fn [opts & flag-args] ,,,)`
285-
- `:middelware` functions which wrap the final command handler
285+
- `:middleware` function(s) which wrap the final command handler (function or sequence of functions)
286286
- `:coll?` flag can be specified multiple times, will result in a vector
287287
- `:parse` function used to coerce the flag argument
288288

@@ -399,6 +399,35 @@ This is my cool CLI tool. Use it well.
399399
-l, --long Use long format
400400
```
401401

402+
#### Command Middleware
403+
404+
Commands, too, can receive middleware, including in the top level cmdspec. If
405+
you imagine middleware as getting wrapped in layers around the final command
406+
handler function, then the outer layers are the middleware for flags, and the
407+
inner layers are for command middleware, with subcommand middleware sitting
408+
inside their parent middleware.
409+
410+
Flags that occur first on the command line get their middleware wrapped around
411+
the middleware for flags that occur later, so that later flags may override the
412+
behavior or earlier flags.
413+
414+
What this means effectively is that is flags manipiulate the options, that this
415+
is visible to command middleware, and that if parent commands manipulate the
416+
options map, that this is visible to child commands. Similarly a parent could
417+
bind a dynamic var, and have it be visible to its children.
418+
419+
#### Command option reference
420+
421+
- `:name` (top-level only) Name of the command, used in the help text
422+
- `:doc` docstring, first line should be a short description, followed by a
423+
blank line, and a longer description
424+
- `:command` handler function, `(fn [opts] ,,,)`, or var
425+
- `:commands` subcommands, mutually exclusive with `:command`
426+
- `:flags` flags, see the section of flags
427+
- `:middleware` function(s) which wrap the final command handler (function or sequence of functions)
428+
- `:init` Initial options maps, which gets threaded through middleware and flag
429+
handlers, and passed to the final command handler
430+
402431
### Processing Order
403432

404433
Most of what lambdaisland/cli does is combine the command and flag descriptions
@@ -427,8 +456,12 @@ available.
427456
Sub-commands can add additional flag specifications, if we encounter those then
428457
their defaults are added to the opts map, either directly or through their
429458
defined handler. This does mean that these kind of flags can only be used
430-
*after* the command. This may change in the future, since this is an unfortunate
431-
asymmetry.
459+
*after* the command. This is an unfortunate necessity, since doing it otherwise,
460+
by reparsing with the additional flags added, can lead to ambiguity between flag
461+
arguments and positional arguments. Note that in non-strict mode flags that
462+
precede the subcommand that defines them will use the default flag processing,
463+
without any regard for the flagspec defined on the subcommand (`:parse`,
464+
`:default`, `:handler`, `:middleware`, etc are all ignored).
432465

433466
Finally the opts map gets bound to `cli/*opts*`, middleware gets invoked (so
434467
`*opts*` can be accessed during middleware execution).

repl_sessions/poke.clj

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
(ns poke
2+
(:require
3+
[lambdaisland.cli :as cli]))
4+
5+
(def ^:dynamic *v* "o")
6+
7+
(defn make-mw [ch]
8+
(fn [cmd]
9+
(fn [opts]
10+
(prn ch opts)
11+
(cmd (assoc opts ch true))
12+
#_
13+
(binding [*v* (str ch *v* ch)]
14+
(cmd opts)))))
15+
16+
(def commands
17+
["xxx"
18+
{:middleware [(make-mw "x")]
19+
:flags ["--bar XXX" {:middleware [(make-mw "b")]}]
20+
:commands
21+
["yyy"
22+
{:middleware [(make-mw "y")]
23+
:command
24+
(fn [opts]
25+
(prn *v*)
26+
(prn opts))
27+
} ]}])
28+
29+
(cli/dispatch*
30+
{:commands commands
31+
:flags ["--foo" {:middleware [(make-mw "f")]}]
32+
:middleware [(make-mw "*")]}
33+
["--foo" "xxx" "yyy" "--bar" "zzz"])

src/lambdaisland/cli.clj

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
(if required "(required)" "")]]
9292
(map (fn [l]
9393
["" l ""]))
94-
(next (str/split doc #"\R")))))))))
94+
(next (str/split (or doc "") #"\R")))))))))
9595
(when (seq command-pairs)
9696
(println "\nSUBCOMMANDS")
9797
(print-table
@@ -128,7 +128,7 @@
128128
(vec middleware)
129129
:else
130130
[middleware])]
131-
(append-middleware*
131+
(prepend-middleware*
132132
opts
133133
(conj mw
134134
(fn [cmd]
@@ -172,7 +172,9 @@
172172
(cond
173173
(or (nil? argcnt)
174174
(= 0 argcnt))
175-
[cli-args args (assoc-flag flags flagspec)]
175+
(if (:boolean? flagspec)
176+
[cli-args args (assoc-flag flags flagspec value)] ;; --[no-]... style flag handlers get a bool passed in
177+
[cli-args args (assoc-flag flags flagspec)])
176178
(= 1 argcnt)
177179
(cond
178180
arg
@@ -279,7 +281,8 @@
279281
(seq args)
280282
(assoc :args args)
281283
no
282-
(assoc :value (not negative?))
284+
(assoc :value (not negative?)
285+
:boolean? true)
283286
:->
284287
(merge flagopts)))))
285288

@@ -406,7 +409,10 @@
406409
pos-args
407410
opts]
408411

409-
(let [opts (prepend-middleware* opts middleware)]
412+
(let [opts (prepend-middleware* opts (if (or (fn? middleware)
413+
(instance? clojure.lang.MultiFn middleware))
414+
[middleware]
415+
middleware))]
410416
(cond
411417
command
412418
(let [middleware (into [(bind-opts-mw)

0 commit comments

Comments
 (0)