|
104 | 104 | (defn parse-error! [& msg] |
105 | 105 | (throw (ex-info (str/join " " msg) {:type ::parse-error}))) |
106 | 106 |
|
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] |
108 | 111 | (update opts ::middleware (fnil into []) mw)) |
109 | 112 |
|
110 | | -(defn add-middleware [opts {mw :middleware}] |
111 | | - (add-middleware* |
| 113 | +(defn append-middleware [opts {mw :middleware}] |
| 114 | + (append-middleware* |
112 | 115 | opts |
113 | 116 | (if (or (nil? mw) (sequential? mw)) mw [mw]))) |
114 | 117 |
|
|
125 | 128 | (vec middleware) |
126 | 129 | :else |
127 | 130 | [middleware])] |
128 | | - (add-middleware* |
| 131 | + (append-middleware* |
129 | 132 | opts |
130 | 133 | (conj mw |
131 | 134 | (fn [cmd] |
|
288 | 291 | (map (juxt :flag identity))) |
289 | 292 | flagpairs)) |
290 | 293 |
|
291 | | -(defn add-defaults [init flagpairs] |
| 294 | +(defn add-defaults [init-opts flagpairs] |
292 | 295 | (reduce (fn [opts flagspec] |
293 | 296 | (if-let [d (:default flagspec)] |
294 | 297 | (let [d (if (and (string? d) (:parse flagspec)) |
|
300 | 303 | (assoc (:key flagspec) d) |
301 | 304 | (assoc-in [::sources (:key flagspec)] (str (:flagstr flagspec) " (default value)"))))) |
302 | 305 | opts)) |
303 | | - init |
| 306 | + init-opts |
304 | 307 | (map second flagpairs))) |
305 | 308 |
|
306 | 309 | (defn add-processed-flags |
|
324 | 327 | (defn split-flags |
325 | 328 | "Main processing loop, go over raw arguments, split into positional and flags, |
326 | 329 | building up an argument vector, and flag/options map." |
327 | | - [cmdspec cli-args init] |
| 330 | + [cmdspec cli-args opts] |
328 | 331 | (loop [cmdspec cmdspec |
329 | 332 | [arg & cli-args] cli-args |
330 | 333 | args [] |
331 | 334 | seen-prefixes #{} |
332 | | - flags init] |
| 335 | + opts opts] |
333 | 336 | ;; Handle additional flags by nested commands |
334 | 337 | (let [extra-flags (when-not (seen-prefixes args) |
335 | 338 | (cmd->flags cmdspec args)) |
336 | | - flags (add-defaults flags (prepare-flagpairs extra-flags)) |
| 339 | + opts (add-defaults opts (prepare-flagpairs extra-flags)) |
337 | 340 | cmdspec (add-processed-flags cmdspec extra-flags)] |
338 | 341 | (cond |
339 | 342 | (nil? arg) |
340 | | - [cmdspec args flags] |
| 343 | + [cmdspec args opts] |
341 | 344 |
|
342 | 345 | (= "--" arg) |
343 | | - [cmdspec (into args cli-args) flags] |
| 346 | + [cmdspec (into args cli-args) opts] |
344 | 347 |
|
345 | 348 | (and (= \- (first arg)) |
346 | 349 | (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)) |
349 | 352 |
|
350 | 353 | :else |
351 | 354 | (recur (dissoc cmdspec :flags) |
352 | 355 | 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 |
354 | 357 | (conj seen-prefixes args) |
355 | | - flags))))) |
| 358 | + opts))))) |
356 | 359 |
|
357 | 360 | (defn missing-flags |
358 | 361 | "Return a set of required flags in `flagmap` not present in `opts`, or `nil` if |
|
396 | 399 | (dispatch* cmdspec pos-args flags))) |
397 | 400 | ;; Note: this three-arg version of dispatch* is considered private, it's used |
398 | 401 | ;; for internal recursion on subcommands. |
399 | | - ([{:keys [commands doc argnames command flags flagpairs flagmap] |
| 402 | + ([{:keys [commands doc argnames command flags flagpairs flagmap middleware] |
400 | 403 | :as cmdspec |
401 | 404 | program-name :name |
402 | 405 | :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))))))) |
485 | 490 |
|
486 | 491 | (defn dispatch |
487 | 492 | "Main entry point for com.lambdaisland/cli. |
|
0 commit comments