|
92 | 92 | ([flags flagspec] |
93 | 93 | (add-middleware |
94 | 94 | (if-let [handler (:handler flagspec)] |
95 | | - (handler flags) |
| 95 | + (binding [*opts* flags] (handler flags)) |
96 | 96 | (assoc flags (:key flagspec) (:value flagspec))) |
97 | 97 | flagspec)) |
98 | 98 | ([flags flagspec & args] |
99 | 99 | (add-middleware |
100 | 100 | (if-let [handler (:handler flagspec)] |
101 | | - (apply handler flags args) |
| 101 | + (binding [*opts* flags] (apply handler flags args)) |
102 | 102 | (assoc flags (:key flagspec) |
103 | 103 | (if (= 1 (count args)) |
104 | 104 | (first args) |
|
108 | 108 | (defn update-flag [flags flagspec f & args] |
109 | 109 | (add-middleware |
110 | 110 | (if-let [handler (:handler flagspec)] |
111 | | - (apply handler flags args) |
| 111 | + (binding [*opts* flags] (apply handler flags args)) |
112 | 112 | (apply update flags (:key flagspec) f args)) |
113 | 113 | flagspec)) |
114 | 114 |
|
|
156 | 156 | (mapv (fn [[_ u l]] (keyword (str/lower-case (or u l)))) (re-seq args-re str))]) |
157 | 157 |
|
158 | 158 | (defn to-cmdspec [?var] |
159 | | - (if (var? ?var) (assoc (meta ?var) :command ?var) ?var)) |
| 159 | + (cond |
| 160 | + (var? ?var) |
| 161 | + (assoc (meta ?var) :command ?var) |
| 162 | + |
| 163 | + (var? (:command ?var)) |
| 164 | + (merge (meta (:command ?var)) ?var) |
| 165 | + |
| 166 | + :else |
| 167 | + ?var)) |
160 | 168 |
|
161 | 169 | (defn prepare-cmdpairs [commands] |
162 | 170 | (let [m (if (vector? commands) (apply hash-map commands) commands)] |
|
222 | 230 | (map (juxt :flag identity))) |
223 | 231 | flagpairs)) |
224 | 232 |
|
225 | | -(defn split-flags [cmdspec cli-args] |
| 233 | +(defn add-defaults [init flagpairs] |
| 234 | + (reduce (fn [opts flagspec] |
| 235 | + (if-let [d (:default flagspec)] |
| 236 | + (if-let [h (:handler flagspec)] |
| 237 | + (binding [*opts* opts] |
| 238 | + (h opts (if (and (string? d) (:parse flagspec)) |
| 239 | + ((:parse flagspec default-parse) d) |
| 240 | + d))) |
| 241 | + (assoc opts (:key flagspec) d)) |
| 242 | + opts)) |
| 243 | + init |
| 244 | + (map second flagpairs))) |
| 245 | + |
| 246 | +(defn add-extra-flags |
| 247 | + "We process flag information for easier use, this results in |
| 248 | + `:flagpairs` (ordered sequence of pairs, mainly used in printing help |
| 249 | + information), and `:flagmap` (for easy lookup), added to the `cmdspec`. As we |
| 250 | + process arguments we may need to add additional flags, based on the current |
| 251 | + subcommand. This function is used both for the top-level as for subcommand |
| 252 | + handling of flags." |
| 253 | + [cmdspec extra-flags] |
| 254 | + (let [flagpairs (prepare-flagpairs extra-flags) |
| 255 | + flagmap (parse-flagstrs flagpairs)] |
| 256 | + (-> cmdspec |
| 257 | + (update :flagpairs (fn [fp] |
| 258 | + (into (vec fp) |
| 259 | + ;; This prevents duplicates. Yes, this is not pretty. I'm very sorry. |
| 260 | + (remove #((into #{} (map first) fp) (first %))) |
| 261 | + flagpairs))) |
| 262 | + (update :flagmap merge flagmap)))) |
| 263 | + |
| 264 | +(defn split-flags |
| 265 | + "Main processing loop, go over raw arguments, split into positional and flags, |
| 266 | + building up an argument vector, and flag/options map." |
| 267 | + [cmdspec cli-args init] |
226 | 268 | (loop [cmdspec cmdspec |
227 | 269 | [arg & cli-args] cli-args |
228 | 270 | args [] |
229 | | - flags {}] |
| 271 | + flags init] |
230 | 272 | ;; Handle additional flags by nested commands |
231 | 273 | (let [extra-flags (cmd->flags cmdspec args) |
232 | | - flagpairs (prepare-flagpairs extra-flags) |
233 | | - flagmap (parse-flagstrs flagpairs) |
234 | | - cmdspec (-> cmdspec |
235 | | - (update :flagpairs (fn [fp] (into (vec fp) (remove #((into #{} (map first) fp) (first %))) flagpairs))) ; This prevents duplicates. Yes, this is not pretty. I'm very sorry. |
236 | | - (update :flagmap merge flagmap))] |
237 | | - |
| 274 | + flags (add-defaults flags (prepare-flagpairs extra-flags)) |
| 275 | + cmdspec (add-extra-flags cmdspec extra-flags)] |
238 | 276 | (cond |
239 | | - (nil? arg) [cmdspec args flags] |
240 | | - (= "--" arg) [cmdspec (into args cli-args) flags] |
241 | | - (= \- (first arg)) (let [[cli-args args flags] (handle-flag cmdspec arg cli-args args flags)] |
242 | | - (recur cmdspec cli-args args flags)) |
243 | | - :else (recur cmdspec cli-args (conj args arg) flags))))) |
| 277 | + (nil? arg) |
| 278 | + [cmdspec args flags] |
244 | 279 |
|
245 | | -(defn default-flags [flagpairs] |
246 | | - (reduce (fn [opts flagspec] |
247 | | - (if-let [d (:default flagspec)] |
248 | | - (if-let [h (:handler flagspec)] |
249 | | - (h opts (if (and (string? d) (:parse flagspec)) |
250 | | - ((:parse flagspec default-parse) d) |
251 | | - d)) |
252 | | - (assoc opts (:key flagspec) d)) |
253 | | - opts)) |
254 | | - {} |
255 | | - (map second flagpairs))) |
| 280 | + (= "--" arg) |
| 281 | + [cmdspec (into args cli-args) flags] |
| 282 | + |
| 283 | + (and (= \- (first arg)) |
| 284 | + (not= 1 (count arg))) ; single dash is considered a positional argument |
| 285 | + (let [[cli-args args flags] (handle-flag cmdspec arg cli-args args flags)] |
| 286 | + (recur (dissoc cmdspec :flags) cli-args args flags)) |
| 287 | + |
| 288 | + :else |
| 289 | + (recur (dissoc cmdspec :flags) cli-args (conj args (str/replace arg #"^\\(.)" (fn [[_ o]] o))) flags))))) |
256 | 290 |
|
257 | 291 | (defn dispatch |
| 292 | + "Main entry point for com.lambdaisland/cli. |
| 293 | +
|
| 294 | + Takes either a single var, or a map describing the commands and flags that |
| 295 | + your CLI tool accepts. At a minimum it should contain either a `:command` or |
| 296 | + `:commands`, optionally followed by a vector of positional command line |
| 297 | + arguments (this second argument can generally be omitted, since we can access |
| 298 | + these through [[*command-line-args*]]). |
| 299 | +
|
| 300 | + - `:name` Name of the script/command as used in the shell, used in the help text |
| 301 | + - `:command` Function that implements your command logic, receives a map of |
| 302 | + parsed CLI args. Can be a var, in which case additional configuration can be |
| 303 | + done through var metadata. |
| 304 | + - `:commands` Map or flat vector of command-string command-map pairs |
| 305 | + - `:doc` Docstring, taken from `:command` if it is a var. |
| 306 | + - `:flags` Map or flat vector of flag-string flag-map |
| 307 | + - `:argnames` Vector of positional argument names, only needed on the top |
| 308 | + level, for subcommands use the command-string to specify these. |
| 309 | + - `:init` map or zero-arity function that provides the base options map, that |
| 310 | + parsed flags and arguments are added onto |
| 311 | +
|
| 312 | + These flags can also be used in (sub)command maps, with the exception of |
| 313 | + `:name`, `:argnames`, and `:init`. |
| 314 | +
|
| 315 | + A command-string consists of the name of the command, optionally followed by |
| 316 | + any named positional argument, either in all-caps, or delineated by angle |
| 317 | + brackets, e.g. `create <id> <name>` or `delete ID`. |
| 318 | +
|
| 319 | + A flag-string consists of command separated short (single-dash) or |
| 320 | + long (double-dash) flags, optionally followed by an argument name, either in |
| 321 | + all-caps, or delineated by angle brackets. The flag and argument are separated |
| 322 | + by either a space or an equals sign. e.g. `--input=<filename>`, `-o, --output |
| 323 | + FILENAME`. |
| 324 | +
|
| 325 | + Flag-maps can contain |
| 326 | + - `:doc` Docstring, used in the help text |
| 327 | + - `:parse` Function that parses/coerces the flag argument from string. |
| 328 | + - `:default` Default value, gets passed through `:parse` if it's a string. |
| 329 | + - `:handler` Function that transforms the options map when this flag is |
| 330 | + present. Zero-arity for boolean (no-argument) flag, one-arity for flags that |
| 331 | + take an argument. |
| 332 | + - `:middleware` Function or sequence of functions that will wrap the command |
| 333 | + function if this flag is present. |
| 334 | +
|
| 335 | + This docstring is just a summary, see the `com.lambdaisland/cli` README for |
| 336 | + details. |
| 337 | + " |
258 | 338 | ([cmdspec] |
259 | 339 | (dispatch (to-cmdspec cmdspec) *command-line-args*)) |
260 | | - ([{:keys [flags] :as cmdspec} cli-args] |
261 | | - (let [flagpairs (prepare-flagpairs flags) |
262 | | - flagmap (parse-flagstrs flagpairs) |
263 | | - cmdspec (assoc cmdspec :flagpairs flagpairs :flagmap flagmap) |
264 | | - [cmdspec pos-args flags] (split-flags cmdspec cli-args) |
| 340 | + ([{:keys [flags init] :as cmdspec} cli-args] |
| 341 | + (let [init (if (or (fn? init) (var? init)) (init) init) |
| 342 | + [cmdspec pos-args flags] (split-flags cmdspec cli-args init) |
265 | 343 | flagpairs (get cmdspec :flagpairs)] |
266 | | - (dispatch (merge (meta (:command cmdspec)) cmdspec) |
267 | | - pos-args |
268 | | - (merge (default-flags flagpairs) flags)))) |
| 344 | + (dispatch cmdspec pos-args flags))) |
| 345 | + ;; Note: this three-arg version of dispatch is considered private, it's used |
| 346 | + ;; for internal recursion on subcommands. |
269 | 347 | ([{:keys [commands doc argnames command flags flagpairs flagmap] |
270 | 348 | :as cmdspec |
271 | 349 | program-name :name |
|
0 commit comments