|
1 | 1 | (ns lambdaisland.cli |
2 | | - (:require [clojure.string :as str])) |
| 2 | + (:require [clojure.string :as str] |
| 3 | + [clojure.set :as set])) |
3 | 4 |
|
4 | 5 | ;; I've tried to be somewhat consistent with variable naming |
5 | 6 |
|
|
60 | 61 | (let [has-short? (some short? (mapcat (comp :flags second) flagpairs)) |
61 | 62 | has-long? (some long? (mapcat (comp :flags second) flagpairs))] |
62 | 63 | (print-table |
63 | | - (for [[_ {:keys [flags argdoc] :as flagopts}] flagpairs] |
| 64 | + (for [[_ {:keys [flags argdoc required] :as flagopts}] flagpairs] |
64 | 65 | (let [short (some short? flags) |
65 | 66 | long (some long? flags)] |
66 | 67 | [(str (cond |
|
72 | 73 | "") |
73 | 74 | long |
74 | 75 | argdoc) |
75 | | - (desc flagopts)])))) |
| 76 | + (desc flagopts) |
| 77 | + (if required "(required)" "") |
| 78 | + ])))) |
76 | 79 | (println)) |
77 | 80 | (print-table |
78 | 81 | (for [[cmd cmdopts] command-pairs] |
|
82 | 85 | (desc cmdopts)])))) |
83 | 86 |
|
84 | 87 | (defn parse-error! [& msg] |
85 | | - (println "[FATAL]" (str/join " " msg)) |
86 | | - (System/exit 1) |
87 | | - #_(throw (ex-info (str/join " " msg) {:type ::parse-error}))) |
| 88 | + (throw (ex-info (str/join " " msg) {:type ::parse-error}))) |
88 | 89 |
|
89 | 90 | (defn add-middleware [opts {mw :middleware}] |
90 | 91 | (let [mw (if (or (nil? mw) (sequential? mw)) mw [mw])] |
|
150 | 151 | :else |
151 | 152 | [(drop argcnt cli-args) args (assoc-flag flags flagspec (map parse (take argcnt cli-args)))]) |
152 | 153 | (if strict? |
153 | | - (parse-error! "Unknown flag: " f) |
| 154 | + (parse-error! "Unknown flag:" f) |
154 | 155 | [cli-args args (update-flag flags {:key (keyword (str/replace f #"^-+" ""))} #(or arg ((fnil inc 0) %)))])))) |
155 | 156 | [cli-args args flags] |
156 | 157 | (if (re-find #"^-\w+" flag) |
|
304 | 305 | :else |
305 | 306 | (recur (dissoc cmdspec :flags) cli-args (conj args (str/replace arg #"^\\(.)" (fn [[_ o]] o))) (conj seen-prefixes args) flags))))) |
306 | 307 |
|
| 308 | +(defn missing-flags |
| 309 | + "Return a set of required flags in `flagmap` not present in `opts`, or `nil` if |
| 310 | + all required flags are present." |
| 311 | + [flagmap opts] |
| 312 | + (let [required (->> flagmap vals (filter (comp true? :required)) (map :key) set) |
| 313 | + received (->> opts keys set) |
| 314 | + missing (map (fn [key] |
| 315 | + (->> flagmap vals (map #(vector (:key %) (:flags %))) (into {}) key)) |
| 316 | + (set/difference required received))] |
| 317 | + (seq missing))) |
| 318 | + |
| 319 | +(defn dispatch* |
| 320 | + ([cmdspec] |
| 321 | + (dispatch* (to-cmdspec cmdspec) *command-line-args*)) |
| 322 | + ([{:keys [flags init] :as cmdspec} cli-args] |
| 323 | + (let [init (if (or (fn? init) (var? init)) (init) init) |
| 324 | + [cmdspec pos-args flags] (split-flags cmdspec cli-args init) |
| 325 | + flagpairs (get cmdspec :flagpairs)] |
| 326 | + (dispatch* cmdspec pos-args flags))) |
| 327 | + ;; Note: this three-arg version of dispatch* is considered private, it's used |
| 328 | + ;; for internal recursion on subcommands. |
| 329 | + ([{:keys [commands doc argnames command flags flagpairs flagmap] |
| 330 | + :as cmdspec |
| 331 | + program-name :name |
| 332 | + :or {program-name "cli"}} |
| 333 | + pos-args opts] |
| 334 | + |
| 335 | + (cond |
| 336 | + command |
| 337 | + (if (:help opts) |
| 338 | + (print-help program-name doc [] flagpairs) |
| 339 | + (binding [*opts* (-> opts |
| 340 | + (dissoc ::middleware) |
| 341 | + (assoc ::argv pos-args) |
| 342 | + (merge (zipmap argnames pos-args)))] |
| 343 | + (if-let [missing (missing-flags flagmap opts)] |
| 344 | + (parse-error! "Missing required flags:" (->> missing (map #(str/join " " %)) (str/join ", "))) |
| 345 | + ((reduce #(%2 %1) command (::middleware opts)) *opts*)))) |
| 346 | + |
| 347 | + commands |
| 348 | + (let [[cmd & pos-args] pos-args |
| 349 | + pos-args (vec pos-args) |
| 350 | + cmd (when cmd (first (str/split cmd #"[ =]"))) |
| 351 | + opts (if cmd (update opts ::command (fnil conj []) cmd) opts) |
| 352 | + command-pairs (prepare-cmdpairs commands) |
| 353 | + command-map (into {} command-pairs) |
| 354 | + command-match (get command-map cmd)] |
| 355 | + |
| 356 | + (cond |
| 357 | + command-match |
| 358 | + (dispatch* (assoc (merge (dissoc cmdspec :command :commands) command-match) |
| 359 | + :name (str program-name " " cmd)) pos-args opts) |
| 360 | + |
| 361 | + (or (nil? command-match) |
| 362 | + (nil? commands) |
| 363 | + (:help opts)) |
| 364 | + (print-help program-name doc (for [[k v] command-pairs] |
| 365 | + [k (if (:commands v) |
| 366 | + (update v :commands prepare-cmdpairs) |
| 367 | + v)]) |
| 368 | + flagpairs) |
| 369 | + |
| 370 | + :else |
| 371 | + (parse-error! "Expected either :command or :commands key in" cmdspec)))))) |
| 372 | + |
307 | 373 | (defn dispatch |
308 | 374 | "Main entry point for com.lambdaisland/cli. |
309 | 375 |
|
|
347 | 413 | take an argument. |
348 | 414 | - `:middleware` Function or sequence of functions that will wrap the command |
349 | 415 | function if this flag is present. |
| 416 | + - `:required` Boolean value to indicate if the flag is required. |
350 | 417 |
|
351 | 418 | This docstring is just a summary, see the `com.lambdaisland/cli` README for |
352 | 419 | details. |
353 | 420 | " |
354 | | - ([cmdspec] |
355 | | - (dispatch (to-cmdspec cmdspec) *command-line-args*)) |
356 | | - ([{:keys [flags init] :as cmdspec} cli-args] |
357 | | - (let [init (if (or (fn? init) (var? init)) (init) init) |
358 | | - [cmdspec pos-args flags] (split-flags cmdspec cli-args init) |
359 | | - flagpairs (get cmdspec :flagpairs)] |
360 | | - (dispatch cmdspec pos-args flags))) |
361 | | - ;; Note: this three-arg version of dispatch is considered private, it's used |
362 | | - ;; for internal recursion on subcommands. |
363 | | - ([{:keys [commands doc argnames command flags flagpairs flagmap] |
364 | | - :as cmdspec |
365 | | - program-name :name |
366 | | - :or {program-name "cli"}} |
367 | | - pos-args opts] |
368 | | - (cond |
369 | | - command |
370 | | - (if (:help opts) |
371 | | - (print-help program-name doc [] flagpairs) |
372 | | - (binding [*opts* (-> opts |
373 | | - (dissoc ::middleware) |
374 | | - (assoc ::argv pos-args) |
375 | | - (merge (zipmap argnames pos-args)))] |
376 | | - ((reduce #(%2 %1) command (::middleware opts)) *opts*))) |
377 | | - |
378 | | - commands |
379 | | - (let [[cmd & pos-args] pos-args |
380 | | - pos-args (vec pos-args) |
381 | | - cmd (when cmd (first (str/split cmd #"[ =]"))) |
382 | | - opts (if cmd (update opts ::command (fnil conj []) cmd) opts) |
383 | | - command-pairs (prepare-cmdpairs commands) |
384 | | - command-map (into {} command-pairs) |
385 | | - command-match (get command-map cmd)] |
386 | | - |
387 | | - (cond |
388 | | - command-match |
389 | | - (dispatch (assoc (merge (dissoc cmdspec :command :commands) command-match) |
390 | | - :name (str program-name " " cmd)) pos-args opts) |
391 | | - |
392 | | - (or (nil? command-match) |
393 | | - (nil? commands) |
394 | | - (:help opts)) |
395 | | - (print-help program-name doc (for [[k v] command-pairs] |
396 | | - [k (if (:commands v) |
397 | | - (update v :commands prepare-cmdpairs) |
398 | | - v)]) |
399 | | - flagpairs) |
400 | | - |
401 | | - :else |
402 | | - (parse-error! "Expected either :command or :commands key in" cmdspec)))))) |
| 421 | + [& args] |
| 422 | + (try |
| 423 | + (apply dispatch* args) |
| 424 | + (catch Exception e |
| 425 | + (binding [*out* *err*] |
| 426 | + (println "[FATAL]" (.getMessage e))) |
| 427 | + (System/exit 1)))) |
403 | 428 |
|
404 | 429 |
|
405 | 430 | ;; |
|
0 commit comments