Skip to content
Browse files

CLJS: Port cli/parse-opts

Again, we change the arg parameter "arguments" to avoid shadowing the JS
reserved word.

Porting this library turned out to be quite easy!
  • Loading branch information...
1 parent 0b8326d commit d63f15867bff2a4bb68c2441472da369c7a4fc52 @guns guns committed Dec 9, 2013
View
124 src/main/clojure/cljs/tools/cli.cljs
@@ -219,3 +219,127 @@
lens (apply map (fn [& cols] (apply max (map count cols))) parts)
lines (format-lines lens parts)]
(s/join \newline lines)))
+
+(defn- required-arguments [specs]
+ (reduce
+ (fn [s {:keys [required short-opt long-opt]}]
+ (if required
+ (into s (remove nil? [short-opt long-opt]))
+ s))
+ #{} specs))
+
+(defn parse-opts
+ "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 boolean flags that are set to true when toggled, 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.
+
+ Must be a unique truthy value.
+
+ :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.
+
+ :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.
+
+ :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.
+
+ :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.
+
+ This may be used to create non-idempotent options, like
+ setting a verbosity level by specifying an option multiple
+ times. (\"-vvv\" -> 3)
+
+ [\"-v\" \"--verbose\"
+ :default 0
+ :assoc-fn (fn [m k _] (update-in m [k] inc))]
+
+ :validate A vector of [validate-fn validate-msg].
+
+ :validate-fn A function that receives the parsed option value and returns
+ a falsy value when the value is invalid.
+
+ :validate-msg An optional message that will be added to the :errors vector
+ on validation failure.
+
+ 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.
+
+ :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.
+ "
+ [args option-specs & options]
+ (let [{:keys [in-order summary-fn]} options
+ specs (compile-option-specs option-specs)
+ req (required-arguments specs)
+ [tokens rest-args] (tokenize-args req args :in-order in-order)
+ [opts errors] (parse-option-tokens specs tokens)]
+ {:options opts
+ :arguments rest-args
+ :summary ((or summary-fn summarize) specs)
+ :errors (when (seq errors) errors)}))
View
4 src/main/clojure/clojure/tools/cli.clj
@@ -486,11 +486,11 @@
(documented at #'clojure.tools.cli/compile-option-specs), and
returns a custom option summary string.
"
- [arguments option-specs & options]
+ [args option-specs & options]
(let [{:keys [in-order summary-fn]} options
specs (compile-option-specs option-specs)
req (required-arguments specs)
- [tokens rest-args] (tokenize-args req arguments :in-order in-order)
+ [tokens rest-args] (tokenize-args req args :in-order in-order)
[opts errors] (parse-option-tokens specs tokens)]
{:options opts
:arguments rest-args
View
79 src/test/clojure/clojure/tools/cli_test.clj
@@ -70,13 +70,16 @@
(defn has-error? [re coll]
(seq (filter (partial re-seq re) coll)))
+(defn parse-int [x]
+ ^:clj (Integer/parseInt x)
+ #_(:cljs (do (assert (re-seq #"^\d" x))
+ (js/parseInt x))))
+
(deftest test-parse-option-tokens
(testing "parses and validates option arguments"
(let [specs (compile-option-specs
[["-p" "--port NUMBER"
- :parse-fn (fn [x]
- ^:clj (Integer/parseInt x)
- #_(:cljs (do (assert (re-seq #"^\d" x)) (js/parseInt x))))
+ :parse-fn parse-int
:validate [#(< 0 % 0x10000) "Must be between 0 and 65536"]]
["-f" "--file PATH"
:validate [#(not= \/ (first %)) "Must be a relative path"]]
@@ -151,40 +154,42 @@
(is (= (summarize (compile-option-specs [["-m" "--minimal" "A minimal option summary"]]))
" -m, --minimal A minimal option summary"))))
-; (deftest test-parse-opts
-; (testing "parses options to :options"
-; (is (= (:options (parse-opts ["-abp80"] [["-a" "--alpha"]
-; ["-b" "--beta"]
-; ["-p" "--port PORT"
-; :parse-fn #(Integer/parseInt %)]]))
-; {:alpha true :beta true :port (int 80)})))
-; (testing "collects error messages into :errors"
-; (let [specs [["-f" "--file PATH"
-; :validate [#(not= \/ (first %)) "Must be a relative path"]]
-; ["-p" "--port PORT"
-; :parse-fn #(Integer/parseInt %)
-; :validate [#(< 0 % 0x10000) "Must be between 0 and 65536"]]]
-; errors (:errors (parse-opts ["-f" "/foo/bar" "-p0"] specs))]
-; (is (has-error? #"Must be a relative path" errors))
-; (is (has-error? #"Must be between 0 and 65536" errors))))
-; (testing "collects unprocessed arguments into :arguments"
-; (is (= (:arguments (parse-opts ["foo" "-a" "bar" "--" "-b" "baz"]
-; [["-a" "--alpha"] ["-b" "--beta"]]))
-; ["foo" "bar" "-b" "baz"])))
-; (testing "provides an option summary at :summary"
-; (is (re-seq #"-a\W+--alpha" (:summary (parse-opts [] [["-a" "--alpha"]])))))
-; (testing "processes arguments in order if :in-order is true"
-; (is (= (:arguments (parse-opts ["-a" "foo" "-b"]
-; [["-a" "--alpha"] ["-b" "--beta"]]
-; :in-order true))
-; ["foo" "-b"])))
-; (testing "accepts optional summary-fn for generating options summary"
-; (is (= (:summary (parse-opts [] [["-a" "--alpha"] ["-b" "--beta"]]
-; :summary-fn (fn [specs]
-; (str "Usage: myprog ["
-; (join \| (map :long-opt specs))
-; "] arg1 arg2"))))
-; "Usage: myprog [--alpha|--beta] arg1 arg2"))))
+(deftest test-parse-opts
+ (testing "parses options to :options"
+ (is (= (:options (parse-opts ["-abp80"] [["-a" "--alpha"]
+ ["-b" "--beta"]
+ ["-p" "--port PORT"
+ :parse-fn parse-int]]))
+ {:alpha true :beta true :port (int 80)})))
+ (testing "collects error messages into :errors"
+ (let [specs [["-f" "--file PATH"
+ :validate [#(not= \/ (first %)) "Must be a relative path"]]
+ ["-p" "--port PORT"
+ :parse-fn (fn [x]
+ ^:clj parse-int
+ #_(:cljs (do (assert (re-seq #"^\d" x)) (js/parseInt x))))
+ :validate [#(< 0 % 0x10000) "Must be between 0 and 65536"]]]
+ errors (:errors (parse-opts ["-f" "/foo/bar" "-p0"] specs))]
+ (is (has-error? #"Must be a relative path" errors))
+ (is (has-error? #"Must be between 0 and 65536" errors))))
+ (testing "collects unprocessed arguments into :arguments"
+ (is (= (:arguments (parse-opts ["foo" "-a" "bar" "--" "-b" "baz"]
+ [["-a" "--alpha"] ["-b" "--beta"]]))
+ ["foo" "bar" "-b" "baz"])))
+ (testing "provides an option summary at :summary"
+ (is (re-seq #"-a\W+--alpha" (:summary (parse-opts [] [["-a" "--alpha"]])))))
+ (testing "processes arguments in order if :in-order is true"
+ (is (= (:arguments (parse-opts ["-a" "foo" "-b"]
+ [["-a" "--alpha"] ["-b" "--beta"]]
+ :in-order true))
+ ["foo" "-b"])))
+ (testing "accepts optional summary-fn for generating options summary"
+ (is (= (:summary (parse-opts [] [["-a" "--alpha"] ["-b" "--beta"]]
+ :summary-fn (fn [specs]
+ (str "Usage: myprog ["
+ (join \| (map :long-opt specs))
+ "] arg1 arg2"))))
+ "Usage: myprog [--alpha|--beta] arg1 arg2"))))
(comment
;; Chas Emerick's PhantomJS test runner

0 comments on commit d63f158

Please sign in to comment.
Something went wrong with that request. Please try again.