Skip to content

Commit fa56d06

Browse files
committed
Support :middleware in command specs, not just in flag handlers
1 parent a191a78 commit fa56d06

File tree

2 files changed

+104
-101
lines changed

2 files changed

+104
-101
lines changed

CHANGELOG.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
## Added
44

5-
## Fixed
6-
7-
## Changed
5+
- Support `:middleware` in command specs, not just in flag handlers
86

97
# 0.20.81 (2025-02-06 / 4f86a6e)
108

src/lambdaisland/cli.clj

Lines changed: 103 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,14 @@
104104
(defn parse-error! [& msg]
105105
(throw (ex-info (str/join " " msg) {:type ::parse-error})))
106106

107-
(defn add-middleware* [opts mw]
107+
(defn prepend-middleware* [opts mw]
108+
(update opts ::middleware #(into (vec mw) %)))
109+
110+
(defn append-middleware* [opts mw]
108111
(update opts ::middleware (fnil into []) mw))
109112

110-
(defn add-middleware [opts {mw :middleware}]
111-
(add-middleware*
113+
(defn append-middleware [opts {mw :middleware}]
114+
(append-middleware*
112115
opts
113116
(if (or (nil? mw) (sequential? mw)) mw [mw])))
114117

@@ -125,7 +128,7 @@
125128
(vec middleware)
126129
:else
127130
[middleware])]
128-
(add-middleware*
131+
(append-middleware*
129132
opts
130133
(conj mw
131134
(fn [cmd]
@@ -288,7 +291,7 @@
288291
(map (juxt :flag identity)))
289292
flagpairs))
290293

291-
(defn add-defaults [init flagpairs]
294+
(defn add-defaults [init-opts flagpairs]
292295
(reduce (fn [opts flagspec]
293296
(if-let [d (:default flagspec)]
294297
(let [d (if (and (string? d) (:parse flagspec))
@@ -300,7 +303,7 @@
300303
(assoc (:key flagspec) d)
301304
(assoc-in [::sources (:key flagspec)] (str (:flagstr flagspec) " (default value)")))))
302305
opts))
303-
init
306+
init-opts
304307
(map second flagpairs)))
305308

306309
(defn add-processed-flags
@@ -324,35 +327,35 @@
324327
(defn split-flags
325328
"Main processing loop, go over raw arguments, split into positional and flags,
326329
building up an argument vector, and flag/options map."
327-
[cmdspec cli-args init]
330+
[cmdspec cli-args opts]
328331
(loop [cmdspec cmdspec
329332
[arg & cli-args] cli-args
330333
args []
331334
seen-prefixes #{}
332-
flags init]
335+
opts opts]
333336
;; Handle additional flags by nested commands
334337
(let [extra-flags (when-not (seen-prefixes args)
335338
(cmd->flags cmdspec args))
336-
flags (add-defaults flags (prepare-flagpairs extra-flags))
339+
opts (add-defaults opts (prepare-flagpairs extra-flags))
337340
cmdspec (add-processed-flags cmdspec extra-flags)]
338341
(cond
339342
(nil? arg)
340-
[cmdspec args flags]
343+
[cmdspec args opts]
341344

342345
(= "--" arg)
343-
[cmdspec (into args cli-args) flags]
346+
[cmdspec (into args cli-args) opts]
344347

345348
(and (= \- (first arg))
346349
(not= 1 (count arg))) ; single dash is considered a positional argument
347-
(let [[cli-args args flags] (handle-flag cmdspec arg cli-args args flags)]
348-
(recur (dissoc cmdspec :flags) cli-args args (conj seen-prefixes args) flags))
350+
(let [[cli-args args opts] (handle-flag cmdspec arg cli-args args opts)]
351+
(recur (dissoc cmdspec :flags) cli-args args (conj seen-prefixes args) opts))
349352

350353
:else
351354
(recur (dissoc cmdspec :flags)
352355
cli-args
353-
(conj args (str/replace arg #"^\\(.)" (fn [[_ o]] o)))
356+
(conj args (str/replace arg #"^\\(.)" (fn [[_ o]] o))) ; remove initial backslash, allows args to be escaped
354357
(conj seen-prefixes args)
355-
flags)))))
358+
opts)))))
356359

357360
(defn missing-flags
358361
"Return a set of required flags in `flagmap` not present in `opts`, or `nil` if
@@ -396,92 +399,94 @@
396399
(dispatch* cmdspec pos-args flags)))
397400
;; Note: this three-arg version of dispatch* is considered private, it's used
398401
;; for internal recursion on subcommands.
399-
([{:keys [commands doc argnames command flags flagpairs flagmap]
402+
([{:keys [commands doc argnames command flags flagpairs flagmap middleware]
400403
:as cmdspec
401404
program-name :name
402405
:or {program-name "cli"}}
403-
pos-args opts]
404-
405-
(cond
406-
command
407-
(let [middleware (into [(bind-opts-mw)
408-
(missing-flags-mw cmdspec)
409-
(help-mw cmdspec)]
410-
(::middleware opts))
411-
opts (-> opts
412-
(dissoc ::middleware)
413-
(update ::argv (fnil into []) pos-args)
414-
(merge (zipmap argnames pos-args)))]
415-
(binding [*opts* opts]
416-
((reduce #(%2 %1) command middleware) opts)))
417-
418-
commands
419-
(let [[cmd & pos-args] pos-args
420-
pos-args (vec pos-args)
421-
cmd (when cmd (first (str/split cmd #"[ =]")))
422-
opts (if cmd (update opts ::command (fnil conj []) cmd) opts)
423-
command-pairs (prepare-cmdpairs commands)
424-
command-map (update-keys (into {} command-pairs)
425-
#(first (str/split % #"[ =]")))
426-
command-match (get command-map cmd)
427-
argnames (:argnames command-match)
428-
arg-count (count argnames)]
429-
(cond
430-
(and command-match
431-
(<= arg-count (count pos-args)))
432-
(dispatch*
433-
(-> cmdspec
434-
(dissoc :command :commands)
435-
(merge command-match)
436-
(assoc :name (str program-name " " cmd)))
437-
(drop arg-count pos-args)
438-
(-> opts
439-
(update ::argv (fnil into []) (take arg-count pos-args))
440-
(merge (when-let [i (:init cmdspec)]
441-
(if (or (fn? i) (var? i)) (i) i)))
442-
(merge (zipmap argnames pos-args))
443-
(update ::sources merge (zipmap argnames (map (fn [idx] (str "positional command line argument idx=" idx))
444-
(range (count pos-args)))))))
445-
446-
(or (nil? command-match)
447-
(:help opts)
448-
(< (count pos-args) arg-count))
449-
(do
450-
(cond
451-
(and cmd (nil? command-match))
452-
(println "No matching command found:" cmd "\n")
453-
(< (count pos-args) arg-count)
454-
(println "Positional arguments missing:"
455-
(->> argnames
456-
(drop (count pos-args))
457-
(map #(str "<" (name %) ">"))
458-
(str/join " "))
459-
"\n"))
460-
(if cmd
461-
(print-help (str program-name (when-not (nil? command-match)
462-
(str " " cmd)))
463-
(if command-match
464-
(:doc command-match)
465-
doc)
466-
(for [[k v] (if command-match
467-
(-> command-match :commands prepare-cmdpairs)
468-
command-pairs)]
469-
[k (if (:commands v)
470-
(update v :commands prepare-cmdpairs)
471-
v)])
472-
argnames
473-
flagpairs)
474-
(print-help program-name
475-
doc
476-
(for [[k v] command-pairs]
477-
[k (if (:commands v)
478-
(update v :commands prepare-cmdpairs)
479-
v)])
480-
argnames
481-
flagpairs)))
482-
483-
:else
484-
(parse-error! "Expected either :command or :commands key in" cmdspec))))))
406+
pos-args
407+
opts]
408+
409+
(let [opts (prepend-middleware* opts middleware)]
410+
(cond
411+
command
412+
(let [middleware (into [(bind-opts-mw)
413+
(missing-flags-mw cmdspec)
414+
(help-mw cmdspec)]
415+
(::middleware opts))
416+
opts (-> opts
417+
(dissoc ::middleware)
418+
(update ::argv (fnil into []) pos-args)
419+
(merge (zipmap argnames pos-args)))]
420+
(binding [*opts* opts]
421+
((reduce #(%2 %1) command middleware) opts)))
422+
423+
commands
424+
(let [[cmd & pos-args] pos-args
425+
pos-args (vec pos-args)
426+
cmd (when cmd (first (str/split cmd #"[ =]")))
427+
opts (if cmd (update opts ::command (fnil conj []) cmd) opts)
428+
command-pairs (prepare-cmdpairs commands)
429+
command-map (update-keys (into {} command-pairs)
430+
#(first (str/split % #"[ =]")))
431+
command-match (get command-map cmd)
432+
argnames (:argnames command-match)
433+
arg-count (count argnames)]
434+
(cond
435+
(and command-match
436+
(<= arg-count (count pos-args)))
437+
(dispatch*
438+
(-> cmdspec
439+
(dissoc :command :commands :middleware)
440+
(merge command-match)
441+
(assoc :name (str program-name " " cmd)))
442+
(drop arg-count pos-args)
443+
(-> opts
444+
(update ::argv (fnil into []) (take arg-count pos-args))
445+
(merge (when-let [i (:init cmdspec)]
446+
(if (or (fn? i) (var? i)) (i) i)))
447+
(merge (zipmap argnames pos-args))
448+
(update ::sources merge (zipmap argnames (map (fn [idx] (str "positional command line argument idx=" idx))
449+
(range (count pos-args)))))))
450+
451+
(or (nil? command-match)
452+
(:help opts)
453+
(< (count pos-args) arg-count))
454+
(do
455+
(cond
456+
(and cmd (nil? command-match))
457+
(println "No matching command found:" cmd "\n")
458+
(< (count pos-args) arg-count)
459+
(println "Positional arguments missing:"
460+
(->> argnames
461+
(drop (count pos-args))
462+
(map #(str "<" (name %) ">"))
463+
(str/join " "))
464+
"\n"))
465+
(if cmd
466+
(print-help (str program-name (when-not (nil? command-match)
467+
(str " " cmd)))
468+
(if command-match
469+
(:doc command-match)
470+
doc)
471+
(for [[k v] (if command-match
472+
(-> command-match :commands prepare-cmdpairs)
473+
command-pairs)]
474+
[k (if (:commands v)
475+
(update v :commands prepare-cmdpairs)
476+
v)])
477+
argnames
478+
flagpairs)
479+
(print-help program-name
480+
doc
481+
(for [[k v] command-pairs]
482+
[k (if (:commands v)
483+
(update v :commands prepare-cmdpairs)
484+
v)])
485+
argnames
486+
flagpairs)))
487+
488+
:else
489+
(parse-error! "Expected either :command or :commands key in" cmdspec)))))))
485490

486491
(defn dispatch
487492
"Main entry point for com.lambdaisland/cli.

0 commit comments

Comments
 (0)