From 3db233a2d5b5ef3a822052cfc7cf8e6f3563bf92 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Fri, 15 Dec 2023 20:00:01 -0800 Subject: [PATCH] split readme; reorg code; start more detailed docs --- README.md | 137 +-------- doc/cljdoc.edn | 4 + doc/new-in-0-4.md | 134 +++++++++ doc/parse-opts.md | 206 ++++++++++++++ src/main/clojure/clojure/tools/cli.cljc | 352 ++++++++++++------------ 5 files changed, 527 insertions(+), 306 deletions(-) create mode 100644 doc/cljdoc.edn create mode 100644 doc/new-in-0-4.md create mode 100644 doc/parse-opts.md diff --git a/README.md b/README.md index 62ccc0d..c74f9ac 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINO Latest stable release: 1.0.219 -* [All Released Versions](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.clojure%22%20AND%20a%3A%22tools.cli%22) +* [All Released Versions](https://central.sonatype.com/artifact/org.clojure/tools.cli/versions) * [Development Snapshot Versions](https://oss.sonatype.org/index.html#nexus-search;gav~org.clojure~tools.cli~~~) @@ -17,7 +17,7 @@ Latest stable release: 1.0.219 org.clojure/tools.cli {:mvn/version "1.0.219"} ``` -[Leiningen](https://github.com/technomancy/leiningen) dependency information: +[Leiningen](https://leiningen.org/) dependency information: ```clojure [org.clojure/tools.cli "1.0.219"] ``` @@ -105,136 +105,11 @@ http://clojure.github.io/tools.cli/index.html#clojure.tools.cli/parse-opts An interesting library built on top of `tool.cli` that provides a more compact, higher-level API is [cli-matic](https://github.com/l3nz/cli-matic). -## Since Release 0.3.x - -### Better Option Tokenization - -In accordance with the [GNU Program Argument Syntax Conventions][GNU], two -features have been added to the options tokenizer: - -* Short options may be grouped together. - - For instance, `-abc` is equivalent to `-a -b -c`. If the `-b` option - requires an argument, the same `-abc` is interpreted as `-a -b "c"`. - -* Long option arguments may be specified with an equals sign. - - `--long-opt=ARG` is equivalent to `--long-opt "ARG"`. - - If the argument is omitted, it is interpreted as the empty string. - e.g. `--long-opt=` is equivalent to `--long-opt ""` - -[GNU]: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html - -### In-order Processing for Subcommands - -Large programs are often divided into subcommands with their own sets of -options. To aid in designing such programs, `clojure.tools.cli/parse-opts` -accepts an `:in-order` option that directs it to stop processing arguments at -the first unrecognized token. - -For instance, the `git` program has a set of top-level options that are -unrecognized by subcommands and vice-versa: - - git --git-dir=/other/proj/.git log --oneline --graph - -By default, `clojure.tools.cli/parse-opts` interprets this command line as: - - options: [[--git-dir /other/proj/.git] - [--oneline] - [--graph]] - arguments: [log] - -When :in-order is true however, the arguments are interpreted as: - - options: [[--git-dir /other/proj/.git]] - arguments: [log --oneline --graph] - -Note that the options to `log` are not parsed, but remain in the unprocessed -arguments vector. These options could be handled by another call to -`parse-opts` from within the function that handles the `log` subcommand. - -### Options Summary - -`parse-opts` returns a minimal options summary string: - - -p, --port NUMBER 8080 Required option with default - --host HOST localhost Short and long options may be omitted - -d, --detach Boolean option - -h, --help - -This may be inserted into a larger usage summary, but it is up to the caller. - -If the default formatting of the summary is unsatisfactory, a `:summary-fn` -may be supplied to `parse-opts`. This function will be passed the sequence -of compiled option specification maps and is expected to return an options -summary. - -The default summary function `clojure.tools.cli/summarize` is public and may -be useful within your own `:summary-fn` for generating the default summary. - -### Option Argument Validation - -By default, option validation is performed immediately after parsing, which -means that "flag" arguments will have a Boolean value, even if a `:default` -is specified with a different type of value. - -You can choose to perform validation after option processing instead, with -the `:post-validation true` flag. During option processing, `:default` values -are applied and `:assoc-fn` and `:update-fn` are invoked. If an option is -specified more than once, `:post-validation true` will cause validation to -be performed after each new option value is processed. - -There is a new option entry `:validate`, which takes a tuple of -`[validation-fn validation-msg]`. The validation-fn receives an option's -argument *after* being parsed by `:parse-fn` if it exists. The validation-msg -can either be a string or a function of one argument that can be called on -the invalid option argument to produce a string: - - ["-p" "--port PORT" "A port number" - :parse-fn #(Integer/parseInt %) - :validate [#(< 0 % 0x10000) #(str % " is not a number between 0 and 65536")]] - -If the validation-fn returns a falsey value, the validation-msg is added to the -errors vector. - -### Error Handling and Return Values - -Instead of throwing errors, `parse-opts` collects error messages into a vector -and returns them to the caller. Unknown options, missing required arguments, -validation errors, and exceptions thrown during `:parse-fn` are all added to -the errors vector. - -Any option can be flagged as required by providing a `:missing` key in the -option spec with a string that should be used for the error message if the -option is omitted. - -The error message when a required argument is omitted (either a short opt with -`:require` or a long opt describing an argument) is: - -`Missing required argument for ...` - -Correspondingly, `parse-opts` returns the following map of values: - - {:options A map of default options merged with parsed values from the command line - :arguments A vector of unprocessed arguments - :summary An options summary string - :errors A vector of error messages, or nil if no errors} - -During development, parse-opts asserts the uniqueness of option `:id`, -`:short-opt`, and `:long-opt` values and throws an error on failure. - -### ClojureScript Support - -As of 0.4.x, the namespace is `clojure.tools.cli` for both Clojure and -ClojureScript programs. The entire API, including the legacy (pre-0.3.x) -functions, is now available in both Clojure and ClojureScript. - -For the 0.3.x releases, the ClojureScript namespace was `cljs.tools.cli` and -only `parse-opts` and `summarize` were available. - ## Example Usage +This is an example of a program that uses most of the `tools.cli` features. +For detailed documentation, please see the docstring of `parse-opts`. + ```clojure (ns cli-example.core (:require [cli-example.server :as server] @@ -287,6 +162,8 @@ only `parse-opts` and `summarize` were available. ;; case any :default value is used as the initial option value rather than nil, ;; and :default-fn will be called to compute the final option value if none was ;; given on the command-line (thus, :default-fn can override :default) +;; Note: validation is *not* performed on the result of :default-fn (this is +;; an open issue for discussion and is not currently considered a bug). (defn usage [options-summary] (->> ["This is my program. There are many like it, but this one is mine." diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn new file mode 100644 index 0000000..e4f7389 --- /dev/null +++ b/doc/cljdoc.edn @@ -0,0 +1,4 @@ +{:cljdoc.doc/tree [["Readme" {:file "README.md"}] + ["Changes" {:file "CHANGELOG.md"}] + ["Command-Line Options" {:file "doc/parse-opts.md"}] + ["Changes since 0.3.x" {:file "doc/new-in-0-4.md"}]]} diff --git a/doc/new-in-0-4.md b/doc/new-in-0-4.md new file mode 100644 index 0000000..62ff83f --- /dev/null +++ b/doc/new-in-0-4.md @@ -0,0 +1,134 @@ +## Improvements in 0.4.x + +This section highlights the changes/improvents in the 0.4.x series of +releases, compared to the earlier 0.3.x series. + +As a general note, `clojure.tools.cli/cli` is deprecated and you should +use `clojure.tools.cli/parse-opts` instead. The legacy function will remain +for the foreseeable future, but will not get bug fixes or new features. + +### Better Option Tokenization + +In accordance with the [GNU Program Argument Syntax Conventions][GNU], two +features have been added to the options tokenizer: + +* Short options may be grouped together. + + For instance, `-abc` is equivalent to `-a -b -c`. If the `-b` option + requires an argument, the same `-abc` is interpreted as `-a -b "c"`. + +* Long option arguments may be specified with an equals sign. + + `--long-opt=ARG` is equivalent to `--long-opt "ARG"`. + + If the argument is omitted, it is interpreted as the empty string. + e.g. `--long-opt=` is equivalent to `--long-opt ""` + +[GNU]: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html + +### In-order Processing for Subcommands + +Large programs are often divided into subcommands with their own sets of +options. To aid in designing such programs, `clojure.tools.cli/parse-opts` +accepts an `:in-order` option that directs it to stop processing arguments at +the first unrecognized token. + +For instance, the `git` program has a set of top-level options that are +unrecognized by subcommands and vice-versa: + + git --git-dir=/other/proj/.git log --oneline --graph + +By default, `clojure.tools.cli/parse-opts` interprets this command line as: + + options: [[--git-dir /other/proj/.git] + [--oneline] + [--graph]] + arguments: [log] + +When :in-order is true however, the arguments are interpreted as: + + options: [[--git-dir /other/proj/.git]] + arguments: [log --oneline --graph] + +Note that the options to `log` are not parsed, but remain in the unprocessed +arguments vector. These options could be handled by another call to +`parse-opts` from within the function that handles the `log` subcommand. + +### Options Summary + +`parse-opts` returns a minimal options summary string: + + -p, --port NUMBER 8080 Required option with default + --host HOST localhost Short and long options may be omitted + -d, --detach Boolean option + -h, --help + +This may be inserted into a larger usage summary, but it is up to the caller. + +If the default formatting of the summary is unsatisfactory, a `:summary-fn` +may be supplied to `parse-opts`. This function will be passed the sequence +of compiled option specification maps and is expected to return an options +summary. + +The default summary function `clojure.tools.cli/summarize` is public and may +be useful within your own `:summary-fn` for generating the default summary. + +### Option Argument Validation + +By default, option validation is performed immediately after parsing, which +means that "flag" arguments will have a Boolean value, even if a `:default` +is specified with a different type of value. + +You can choose to perform validation after option processing instead, with +the `:post-validation true` flag. During option processing, `:default` values +are applied and `:assoc-fn` and `:update-fn` are invoked. If an option is +specified more than once, `:post-validation true` will cause validation to +be performed after each new option value is processed. + +There is a new option entry `:validate`, which takes a tuple of +`[validation-fn validation-msg]`. The validation-fn receives an option's +argument *after* being parsed by `:parse-fn` if it exists. The validation-msg +can either be a string or a function of one argument that can be called on +the invalid option argument to produce a string: + + ["-p" "--port PORT" "A port number" + :parse-fn #(Integer/parseInt %) + :validate [#(< 0 % 0x10000) #(str % " is not a number between 0 and 65536")]] + +If the validation-fn returns a falsey value, the validation-msg is added to the +errors vector. + +### Error Handling and Return Values + +Instead of throwing errors, `parse-opts` collects error messages into a vector +and returns them to the caller. Unknown options, missing required arguments, +validation errors, and exceptions thrown during `:parse-fn` are all added to +the errors vector. + +Any option can be flagged as required by providing a `:missing` key in the +option spec with a string that should be used for the error message if the +option is omitted. + +The error message when a required argument is omitted (either a short opt with +`:require` or a long opt describing an argument) is: + +`Missing required argument for ...` + +Correspondingly, `parse-opts` returns the following map of values: + + {:options A map of default options merged with parsed values from the command line + :arguments A vector of unprocessed arguments + :summary An options summary string + :errors A vector of error messages, or nil if no errors} + +During development, parse-opts asserts the uniqueness of option `:id`, +`:short-opt`, and `:long-opt` values and throws an error on failure. + +### ClojureScript Support + +As of 0.4.x, the namespace is `clojure.tools.cli` for both Clojure and +ClojureScript programs. The entire API, including the legacy (pre-0.3.x) +functions, is now available in both Clojure and ClojureScript. + +For the 0.3.x releases, the ClojureScript namespace was `cljs.tools.cli` and +only `parse-opts` and `summarize` were available. diff --git a/doc/parse-opts.md b/doc/parse-opts.md new file mode 100644 index 0000000..cedbe00 --- /dev/null +++ b/doc/parse-opts.md @@ -0,0 +1,206 @@ +# `clojure.tools.cli/parse-opts` + +[`parse-opts`][parse-opts] is the primary function in this library. + +## docstring + +This is the current docstring for `parse-opts` (I plan to expand +this into complete documentation for the library, with examples, over time): + +``` + Parse arguments sequence according to given option specifications and the + GNU Program Argument Syntax Conventions: + + https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html + + Option specifications are a sequence of vectors with the following format: + + [short-opt long-opt-with-required-description description + :property value] + + The first three string parameters in an option spec are positional and + optional, and may be nil in order to specify a later parameter. + + By default, options are toggles that default to nil, but the second string + parameter may be used to specify that an option requires an argument. + + e.g. [\"-p\" \"--port PORT\"] specifies that --port requires an argument, + of which PORT is a short description. + + The :property value pairs are optional and take precedence over the + positional string arguments. The valid properties are: + + :id The key for this option in the resulting option map. This + is normally set to the keywordized name of the long option + without the leading dashes. + + Multiple option entries can share the same :id in order to + transform a value in different ways, but only one of these + option entries may contain a :default(-fn) entry. + + This option is mandatory. + + :short-opt The short format for this option, normally set by the first + positional string parameter: e.g. \"-p\". Must be unique. + + :long-opt The long format for this option, normally set by the second + positional string parameter; e.g. \"--port\". Must be unique. + + :required A description of the required argument for this option if + one is required; normally set in the second positional + string parameter after the long option: \"--port PORT\". + + The absence of this entry indicates that the option is a + boolean toggle that is set to true when specified on the + command line. + + :missing Indicates that this option is required (not just an argument), + and provides the string to use as an error message if omitted. + + :desc A optional short description of this option. + + :default The default value of this option. If none is specified, the + resulting option map will not contain an entry for this + option unless set on the command line. Also see :default-fn + (below). + + This default is applied before any arguments are parsed so + this is a good way to seed values for :assoc-fn or :update-fn + as well as the simplest way to provide defaults. + + If you need to compute a default based on other command line + arguments, or you need to provide a default separate from the + seed for :assoc-fn or :update-fn, see :default-fn below. + + :default-desc An optional description of the default value. This should be + used when the string representation of the default value is + too ugly to be printed on the command line, or :default-fn + is used to compute the default. + + :default-fn A function to compute the default value of this option, given + the whole, parsed option map as its one argument. If no + function is specified, the resulting option map will not + contain an entry for this option unless set on the command + line. Also see :default (above). + + If both :default and :default-fn are provided, if the + argument is not provided on the command-line, :default-fn will + still be called (and can override :default). + + :parse-fn A function that receives the required option argument and + returns the option value. + + If this is a boolean option, parse-fn will receive the value + true. This may be used to invert the logic of this option: + + [\"-q\" \"--quiet\" + :id :verbose + :default true + :parse-fn not] + + :assoc-fn A function that receives the current option map, the current + option :id, and the current parsed option value, and returns + a new option map. The default is 'assoc'. + + For non-idempotent options, where you need to compute a option + value based on the current value and a new value from the + command line. If you only need the the current value, consider + :update-fn (below). + + You cannot specify both :assoc-fn and :update-fn for an + option. + + :update-fn Without :multi true: + + A function that receives just the existing parsed option value, + and returns a new option value, for each option :id present. + The default is 'identity'. + + This may be used to create non-idempotent options where you + only need the current value, like setting a verbosity level by + specifying an option multiple times. (\"-vvv\" -> 3) + + [\"-v\" \"--verbose\" + :default 0 + :update-fn inc] + + :default is applied first. If you wish to omit the :default + option value, use fnil in your :update-fn as follows: + + [\"-v\" \"--verbose\" + :update-fn (fnil inc 0)] + + With :multi true: + + A function that receives both the existing parsed option value, + and the parsed option value from each instance of the option, + and returns a new option value, for each option :id present. + The :multi option is ignored if you do not specify :update-fn. + + For non-idempotent options, where you need to compute a option + value based on the current value and a new value from the + command line. This can sometimes be easier than use :assoc-fn. + + [\"-f\" \"--file NAME\" + :default [] + :update-fn conj + :multi true] + + :default is applied first. If you wish to omit the :default + option value, use fnil in your :update-fn as follows: + + [\"-f\" \"--file NAME\" + :update-fn (fnil conj []) + :multi true] + + Regardless of :multi, you cannot specify both :assoc-fn + and :update-fn for an option. + + :multi true/false, applies only to options that use :update-fn. + + :validate A vector of [validate-fn validate-msg ...]. Multiple pairs + of validation functions and error messages may be provided. + + :validate-fn A vector of functions that receives the parsed option value + and returns a falsy value or throws an exception when the + value is invalid. The validations are tried in the given + order. + + :validate-msg A vector of error messages corresponding to :validate-fn + that will be added to the :errors vector on validation + failure. Can be plain strings, or functions to be applied + to the (invalid) option argument to produce a string. + + :post-validation true/false. By default, validation is performed after + parsing an option, prior to assoc/default/update processing. + Specifying true here will cause the validation to be + performed after assoc/default/update processing, instead. + + parse-opts returns a map with four entries: + + {:options The options map, keyed by :id, mapped to the parsed value + :arguments A vector of unprocessed arguments + :summary A string containing a minimal options summary + :errors A possible vector of error message strings generated during + parsing; nil when no errors exist} + + A few function options may be specified to influence the behavior of + parse-opts: + + :in-order Stop option processing at the first unknown argument. Useful + for building programs with subcommands that have their own + option specs. + + :no-defaults Only include option values specified in arguments and do not + include any default values in the resulting options map. + Useful for parsing options from multiple sources; i.e. from a + config file and from the command line. + + :strict Parse required arguments strictly: if a required argument value + matches any other option, it is considered to be missing (and + you have a parse error). + + :summary-fn A function that receives the sequence of compiled option specs + (documented at #'clojure.tools.cli/compile-option-specs), and + returns a custom option summary string. +``` diff --git a/src/main/clojure/clojure/tools/cli.cljc b/src/main/clojure/clojure/tools/cli.cljc index a063850..1e6284e 100644 --- a/src/main/clojure/clojure/tools/cli.cljc +++ b/src/main/clojure/clojure/tools/cli.cljc @@ -12,6 +12,16 @@ (:require [clojure.string :as s] #?(:cljs goog.string.format))) +;; +;; Utility Functions: +;; + +(defn- make-format + "Given a sequence of column widths, return a string suitable for use in + format to print a sequences of strings in those columns." + [lens] + (s/join (map #(str " %" (when-not (zero? %) (str "-" %)) "s") lens))) + (defn- tokenize-args "Reduce arguments sequence into [opt-type opt ?optarg?] vectors and a vector of remaining arguments. Returns as [option-tokens remaining-args]. @@ -61,181 +71,12 @@ (recur opts (conj argv car) cdr))) [opts argv])))) -(defn- make-format - "Given a sequence of column widths, return a string suitable for use in - format to print a sequences of strings in those columns." - [lens] - (s/join (map #(str " %" (when-not (zero? %) (str "-" %)) "s") lens))) -;; -;; Legacy API -;; - -(defn- build-doc [{:keys [switches docs default]}] - [(apply str (interpose ", " switches)) - (or (str default) "") - (or docs "")]) - #?(:cljs ;; alias to Google Closure string format - (defn format + (defn- format [fmt & args] (apply goog.string.format fmt args))) -(defn- banner-for [desc specs] - (when desc - (println desc) - (println)) - (let [docs (into (map build-doc specs) - [["--------" "-------" "----"] - ["Switches" "Default" "Desc"]]) - max-cols (->> (for [d docs] (map count d)) - (apply map (fn [& c] (apply vector c))) - (map #(apply max %))) - vs (for [d docs] - (mapcat (fn [& x] (apply vector x)) max-cols d))] - (doseq [v vs] - (let [fmt (make-format (take-nth 2 v))] - (print (apply format fmt (take-nth 2 (rest v))))) - (prn)))) - -(defn- name-for [k] - (s/replace k #"^--no-|^--\[no-\]|^--|^-" "")) - -(defn- flag-for [^String v] - (not (s/starts-with? v "--no-"))) - -(defn- opt? [^String x] - (s/starts-with? x "-")) - -(defn- flag? [^String x] - (s/starts-with? x "--[no-]")) - -(defn- end-of-args? [x] - (= "--" x)) - -(defn- spec-for - [arg specs] - (->> specs - (filter (fn [s] - (let [switches (set (s :switches))] - (contains? switches arg)))) - first)) - -(defn- default-values-for - [specs] - (reduce (fn [m s] - (if (contains? s :default) - ((:assoc-fn s) m (:name s) (:default s)) - m)) - {} specs)) - -(defn- apply-specs - [specs args] - (loop [options (default-values-for specs) - extra-args [] - args args] - (if-not (seq args) - [options extra-args] - (let [opt (first args) - spec (spec-for opt specs)] - (cond - (end-of-args? opt) - (recur options (into extra-args (vec (rest args))) nil) - - (and (opt? opt) (nil? spec)) - (throw #?(:clj (Exception. (str "'" opt "' is not a valid argument")) - :cljr (Exception. (str "'" opt "' is not a valid argument")) - :cljs (js/Error. (str "'" opt "' is not a valid argument")))) - - (and (opt? opt) (spec :flag)) - (recur ((spec :assoc-fn) options (spec :name) (flag-for opt)) - extra-args - (rest args)) - - (opt? opt) - (recur ((spec :assoc-fn) options (spec :name) ((spec :parse-fn) (second args))) - extra-args - (drop 2 args)) - - :else - (recur options (conj extra-args (first args)) (rest args))))))) - -(defn- switches-for - [switches flag] - (-> (for [^String s switches] - (cond (and flag (flag? s)) - [(s/replace s #"\[no-\]" "no-") (s/replace s #"\[no-\]" "")] - - (and flag (s/starts-with? s "--")) - [(s/replace s #"--" "--no-") s] - - :else - [s])) - flatten)) - -(defn- generate-spec - [raw-spec] - (let [[switches raw-spec] (split-with #(and (string? %) (opt? %)) raw-spec) - [docs raw-spec] (split-with string? raw-spec) - options (apply hash-map raw-spec) - aliases (map name-for switches) - flag (or (flag? (last switches)) (options :flag))] - (merge {:switches (switches-for switches flag) - :docs (first docs) - :aliases (set aliases) - :name (keyword (last aliases)) - :parse-fn identity - :assoc-fn assoc - :flag flag} - (when flag {:default false}) - options))) - -(defn- normalize-args - "Rewrite arguments sequence into a normalized form that is parsable by cli." - [specs args] - (let [required-opts (->> specs - (filter (complement :flag)) - (mapcat :switches) - (into #{})) - ;; Preserve double-dash since this is a pre-processing step - largs (take-while (partial not= "--") args) - rargs (drop (count largs) args) - [opts largs] (tokenize-args required-opts largs)] - (concat (mapcat rest opts) largs rargs))) - -(defn cli - "THIS IS A LEGACY FUNCTION and may be deprecated in the future. Please use - clojure.tools.cli/parse-opts in new applications. - - Parse the provided args using the given specs. Specs are vectors - describing a command line argument. For example: - - [\"-p\" \"--port\" \"Port to listen on\" :default 3000 :parse-fn #(Integer/parseInt %)] - - First provide the switches (from least to most specific), then a doc - string, and pairs of options. - - Valid options are :default, :parse-fn, and :flag. See - https://github.com/clojure/tools.cli/wiki/Documentation-for-0.2.4 for more - detailed examples. - - Returns a vector containing a map of the parsed arguments, a vector - of extra arguments that did not match known switches, and a - documentation banner to provide usage instructions." - [args & specs] - (let [[desc specs] (if (string? (first specs)) - [(first specs) (rest specs)] - [nil specs]) - specs (map generate-spec specs) - args (normalize-args specs args) - [options extra-args] (apply-specs specs args) - banner (with-out-str (banner-for desc specs))] - [options extra-args banner])) - -;; -;; New API -;; - (def ^{:private true} spec-keys [:id :short-opt :long-opt :required :desc :default :default-desc :default-fn @@ -495,7 +336,7 @@ [(select-keys m ids) errors] [m errors])))))) -(defn ^{:added "0.3.0"} make-summary-part +(defn make-summary-part "Given a single compiled option spec, turn it into a formatted string, optionally with its default values if requested." [show-defaults? spec] @@ -519,7 +360,7 @@ [opt dd (or desc "")] [opt (or desc "")]))) -(defn ^{:added "0.3.0"} format-lines +(defn format-lines "Format a sequence of summary parts into columns. lens is a sequence of lengths to use for parts. There are two sequences of lengths if we are not displaying defaults. There are three sequences of lengths if we @@ -536,7 +377,7 @@ s)) #{} specs)) -(defn ^{:added "0.3.0"} summarize +(defn summarize "Reduce options specs into a options summary for printing at a terminal. Note that the specs argument should be the compiled version. That effectively means that you shouldn't call summarize directly. When you call parse-opts @@ -552,7 +393,7 @@ (s/join \newline lines)) "")) -(defn ^{:added "0.3.2"} get-default-options +(defn get-default-options "Extract the map of default options from a sequence of option vectors. As of 0.4.1, this also applies any :default-fn present." @@ -566,8 +407,7 @@ vals (default-option-map specs :default-fn)))) - -(defn ^{:added "0.3.0"} parse-opts +(defn parse-opts "Parse arguments sequence according to given option specifications and the GNU Program Argument Syntax Conventions: @@ -775,3 +615,163 @@ :arguments rest-args :summary ((or summary-fn summarize) specs) :errors (when (seq errors) errors)})) + +;; +;; Legacy API +;; + +(defn- build-doc [{:keys [switches docs default]}] + [(apply str (interpose ", " switches)) + (or (str default) "") + (or docs "")]) + +(defn- banner-for [desc specs] + (when desc + (println desc) + (println)) + (let [docs (into (map build-doc specs) + [["--------" "-------" "----"] + ["Switches" "Default" "Desc"]]) + max-cols (->> (for [d docs] (map count d)) + (apply map (fn [& c] (apply vector c))) + (map #(apply max %))) + vs (for [d docs] + (mapcat (fn [& x] (apply vector x)) max-cols d))] + (doseq [v vs] + (let [fmt (make-format (take-nth 2 v))] + (print (apply format fmt (take-nth 2 (rest v))))) + (prn)))) + +(defn- name-for [k] + (s/replace k #"^--no-|^--\[no-\]|^--|^-" "")) + +(defn- flag-for [^String v] + (not (s/starts-with? v "--no-"))) + +(defn- opt? [^String x] + (s/starts-with? x "-")) + +(defn- flag? [^String x] + (s/starts-with? x "--[no-]")) + +(defn- end-of-args? [x] + (= "--" x)) + +(defn- spec-for + [arg specs] + (->> specs + (filter (fn [s] + (let [switches (set (s :switches))] + (contains? switches arg)))) + first)) + +(defn- default-values-for + [specs] + (reduce (fn [m s] + (if (contains? s :default) + ((:assoc-fn s) m (:name s) (:default s)) + m)) + {} specs)) + +(defn- apply-specs + [specs args] + (loop [options (default-values-for specs) + extra-args [] + args args] + (if-not (seq args) + [options extra-args] + (let [opt (first args) + spec (spec-for opt specs)] + (cond + (end-of-args? opt) + (recur options (into extra-args (vec (rest args))) nil) + + (and (opt? opt) (nil? spec)) + (throw #?(:clj (Exception. (str "'" opt "' is not a valid argument")) + :cljr (Exception. (str "'" opt "' is not a valid argument")) + :cljs (js/Error. (str "'" opt "' is not a valid argument")))) + + (and (opt? opt) (spec :flag)) + (recur ((spec :assoc-fn) options (spec :name) (flag-for opt)) + extra-args + (rest args)) + + (opt? opt) + (recur ((spec :assoc-fn) options (spec :name) ((spec :parse-fn) (second args))) + extra-args + (drop 2 args)) + + :else + (recur options (conj extra-args (first args)) (rest args))))))) + +(defn- switches-for + [switches flag] + (-> (for [^String s switches] + (cond (and flag (flag? s)) + [(s/replace s #"\[no-\]" "no-") (s/replace s #"\[no-\]" "")] + + (and flag (s/starts-with? s "--")) + [(s/replace s #"--" "--no-") s] + + :else + [s])) + flatten)) + +(defn- generate-spec + [raw-spec] + (let [[switches raw-spec] (split-with #(and (string? %) (opt? %)) raw-spec) + [docs raw-spec] (split-with string? raw-spec) + options (apply hash-map raw-spec) + aliases (map name-for switches) + flag (or (flag? (last switches)) (options :flag))] + (merge {:switches (switches-for switches flag) + :docs (first docs) + :aliases (set aliases) + :name (keyword (last aliases)) + :parse-fn identity + :assoc-fn assoc + :flag flag} + (when flag {:default false}) + options))) + +(defn- normalize-args + "Rewrite arguments sequence into a normalized form that is parsable by cli." + [specs args] + (let [required-opts (->> specs + (filter (complement :flag)) + (mapcat :switches) + (into #{})) + ;; Preserve double-dash since this is a pre-processing step + largs (take-while (partial not= "--") args) + rargs (drop (count largs) args) + [opts largs] (tokenize-args required-opts largs)] + (concat (mapcat rest opts) largs rargs))) + +(defn ^{:deprecated "since 0.4.x"} cli + "THIS IS A LEGACY FUNCTION and is deprecated. Please use + clojure.tools.cli/parse-opts in new applications. + + Parse the provided args using the given specs. Specs are vectors + describing a command line argument. For example: + + [\"-p\" \"--port\" \"Port to listen on\" :default 3000 :parse-fn #(Integer/parseInt %)] + + First provide the switches (from least to most specific), then a doc + string, and pairs of options. + + Valid options are :default, :parse-fn, and :flag. See + https://github.com/clojure/tools.cli/wiki/Documentation-for-0.2.4 for more + detailed examples. + + Returns a vector containing a map of the parsed arguments, a vector + of extra arguments that did not match known switches, and a + documentation banner to provide usage instructions." + [args & specs] + (let [[desc specs] (if (string? (first specs)) + [(first specs) (rest specs)] + [nil specs]) + specs (map generate-spec specs) + args (normalize-args specs args) + [options extra-args] (apply-specs specs args) + banner (with-out-str (banner-for desc specs))] + [options extra-args banner]))