Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f21b12f

Browse files
author
Ryan Neufeld
authored
Merge pull request clojure-cookbook#451 from dz-cies/patch6
Update 3.07 to use tools.cli 0.3.1 and parse-opts.
2 parents 49d1ce1 + 4fd67ce commit f21b12f

File tree

1 file changed

+74
-61
lines changed

1 file changed

+74
-61
lines changed

03_general-computing/3-07_parse-command-line-arguments.asciidoc

Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,59 @@ by Ryan Neufeld; originally submitted by Nicolas Bessi
66
==== Problem
77

88
You want to write command-line tools in Clojure that can parse input
9-
arguments.(((command lines, parsing input arguments)))(((parsing, input arguments)))((("development ecosystem", "command line parsing")))(((tools.cli library)))((("Clojure", "clojure.tools.cli/cli")))
9+
arguments.(((command lines, parsing input arguments)))(((parsing, input arguments)))((("development ecosystem", "command line parsing")))(((tools.cli library)))((("Clojure", "clojure.tools.cli/parse-opts")))
1010

1111
==== Solution
1212

1313
Use the https://github.com/clojure/tools.cli[+tools.cli+]
1414
library.
1515

16-
Before starting, add `[org.clojure/tools.cli "0.2.4"]` to your project's
16+
Before starting, add `[org.clojure/tools.cli "0.3.1"]` to your project's
1717
dependencies, or start a REPL using +lein-try+:
1818

1919
[source,shell-session]
2020
----
2121
$ lein try org.clojure/tools.cli
2222
----
2323

24-
Use the +clojure.tools.cli/cli+ function in your project's +-main+
24+
Use the +clojure.tools.cli/parse-opts+ function in your project's +-main+
2525
function entry point to parse command-line arguments:footnote:[Since
2626
+tools.cli+ is so cool, this example can run entirely at the REPL.]
2727

2828
[source,clojure]
2929
----
30-
(require '[clojure.tools.cli :refer [cli]])
30+
(require '[clojure.tools.cli :refer [parse-opts]])
3131
3232
(defn -main [& args]
33-
(let [[opts args banner] (cli args
34-
["-h" "--help" "Print this help"
35-
:default false :flag true])]
36-
(when (:help opts)
37-
(println banner))))
33+
(let [{:keys [:options :arguments :summary :errors]} (parse-opts args
34+
[["-h" "--help" "Print this help"
35+
:default false]])]
36+
(when (:help options)
37+
(println summary))))
3838
3939
;; Simulate entry into -main at the command line
4040
(-main "-h")
4141
;; *out*
42-
;; Usage:
43-
;;
44-
;; Switches Default Desc
45-
;; -------- ------- ----
46-
;; -h, --no-help, --help false Print this help
42+
;; -h, --help Print this help
4743
----
4844

4945
==== Discussion
5046

5147
Clojure's +tools.cli+ is a simple library, with only one function,
52-
+cli+, and a slim data-oriented API for specifying how arguments
48+
+parse-opts+, and a slim data-oriented API for specifying how arguments
5349
should be parsed. Handily enough, there isn't much special about this
5450
function: an arguments vector and specifications go in, and a map of parsed
55-
options, variadic arguments, and a help banner come out. It's really the
51+
options, variadic arguments, a help summary, and error messages come out. It's really the
5652
epitome of good, composable functional programming.
5753

58-
To configure how options are parsed, pass any number of spec vectors
54+
To configure how options are parsed, pass a sequence of spec vectors
5955
after the +args+ list. To specify a +:port+ parameter, for example,
60-
you would provide the spec `["-p" "--port"]`. The +"-p"+ isn't
56+
you would provide the spec `["-p" "--port PORT"]`. The +"-p"+ isn't
6157
strictly necessary, but it is customary to provide a single-letter
62-
shortcut for command-line options (especially long ones). In the
63-
returned +opts+ map, the text of the last option name will be interned
58+
shortcut for command-line options (especially long ones). The +PORT+ is
59+
added to indicate the option requires an argument, of which +PORT+ is a
60+
short description. In the
61+
returned +options+ map, the text of the last option name will be interned
6462
to a keyword (less the +--+). For example, +"--port"+ would become
6563
+:port+, and +"--super-long-option"+ would become +:super-long-option+.
6664

@@ -83,19 +81,20 @@ optional string following the final argument name:
8381

8482
[source,clojure]
8583
----
86-
["-p" "--port" "The incoming port the application will listen on."]
84+
["-p" "--port PORT" "The incoming port the application will listen on."]
8785
----
8886

8987
Everything after the argument name and description will be interpreted
9088
as options in key/value pairs. +tools.cli+ provides the following
9189
options:
9290

9391
+:default+:: The default value returned in the absence of user input.
94-
Without specifying, the default of +:default+ is +nil+.
92+
Without specifying, the resulting option map will not contain an entry
93+
for this option unless set by user.
9594

96-
+:flag+:: If truthy (not +false+ or +nil+), indicates an argument
97-
behaves like a flag or switch. This argument will _not_ take any
98-
value as its input.
95+
+:default-desc+:: An optional description of the default value. This should
96+
be used when the string representation of the default value is too ugly
97+
to be printed on the command line.
9998

10099
+:parse-fn+:: The function used to parse an argument's value. This can
101100
be used to turn string values into integers, floats, or other
@@ -104,6 +103,21 @@ options:
104103
+:assoc-fn+:: The function used to combine multiple values for a
105104
single argument.
106105

106+
+:validate-fn+:: A function that receives the parsed option value and returns
107+
a falsy value when the value is invalid.
108+
109+
+:validate-msg+:: An optional message that will be added to the +:errors+
110+
vector on validation failure.
111+
112+
+:validate+:: A vector of +[validate-fn validate-msg]+. You can either set
113+
+:validate-fn+ and +:validate-msg+ seperately or bundle them together this
114+
way.
115+
116+
+:id+, +:short-opt+, +:long-opt+, +:required+, +:desc+:: These options will
117+
overwrite the values specified or indicated by the positional arguments, e.g.
118+
+:port+, +"-p"+, +"--port"+, +PORT+, +"The incoming port the application
119+
will listen on."+, respectively.
120+
107121
Here's a complete example:
108122

109123
[source,clojure]
@@ -116,36 +130,35 @@ Here's a complete example:
116130
(update-in m [k] max v)
117131
(assoc m k v)))
118132
119-
(def app-specs [["-n" "--count" :default 5
133+
(def app-specs [["-n" "--count COUNT" :default 5
134+
:default-desc "FIVE"
120135
:parse-fn #(Integer. %)
121-
:assoc-fn assoc-max]
122-
["-v" "--verbose" :flag true
123-
:default true]])
136+
:assoc-fn assoc-max
137+
:validate [#(< % 100) "Reached the maximum."]]
138+
["-v" nil :long-opt "--verbose"
139+
:default false]])
124140
125-
(first (apply cli ["-n" "2" "-n" "50"] app-specs))
126-
;; -> {:count 50, :verbose true}
141+
(select-keys (parse-opts ["-n" "2" "-n" "50"] app-specs) [:options :errors])
142+
;; -> {:errors nil, :options {:verbose false, :count 50}}
127143
128-
(first (apply cli ["--no-verbose"] app-specs))
129-
;; -> {:count 5, :verbose false}
130-
----
144+
(select-keys (parse-opts ["-n" "2" "-n" "200"] app-specs) [:options :errors])
145+
;; -> {:errors ["Failed to validate \"-n 200\": Reached the maximum."], :options {:verbose false, :count 5}}
131146
132-
When writing flag options, a useful shortcut is to omit the +:flag+
133-
option and add a "`[no-]`" prefix to the argument's name. +cli+ will
134-
interpret this argument spec as including +:flag true+ without you having
135-
to specify it as such:
147+
(select-keys (parse-opts ["--verbose"] app-specs) [:options :errors])
148+
;; -> {:errors nil, :options {:verbose true, :count 5}}
136149
137-
[source,clojure]
138-
----
139-
["-v" "--[no-]verbose" :default true]
150+
(println (:summary (parse-opts ["--verbose"] app-specs)))
151+
;; *out*
152+
;; -n, --count COUNT FIVE
153+
;; -v, --verbose
140154
----
141155

142156
One thing the +tools.cli+ library _doesn't_ provide is a hook into the
143157
application container's launch life cycle. It is your responsibility to
144-
add a +cli+ call to your +-main+ function and know when to print the
145-
help banner. A general pattern for use is to capture the results of
146-
+cli+ in a +let+ block and determine if help needs to be printed. This
147-
is also useful for ensuring the validity of arguments (especially since
148-
there is no +:required+ option):
158+
add a +parse-opts+ call to your +-main+ function and know when to print the
159+
help summary. A general pattern for use is to capture the results of
160+
+parse-opts+ in a +let+ block and determine if help needs to be printed. This
161+
is also useful for ensuring the validity of arguments:
149162

150163
[source,clojure]
151164
----
@@ -157,40 +170,40 @@ there is no +:required+ option):
157170
(not-every? opts required-opts))
158171
159172
(defn -main [& args]
160-
(let [[opts args banner] (cli args
161-
["-h" "--help" "Print this help"
162-
:default false :flag true]
163-
["-p" "--port" :parse-fn #(Integer. %)])]
164-
(when (or (:help opts)
165-
(missing-required? opts))
166-
(println banner))))
173+
(let [{:keys [:options :arguments :summary :errors]} (parse-opts args
174+
[["-h" "--help" "Print this help"
175+
:default false]
176+
["-p" "--port PORT" :parse-fn #(Integer. %)]])]
177+
(when (or (:help options)
178+
(missing-required? options))
179+
(println summary))))
167180
----
168181

169182
As with many applications, you may want to accept a variable number of
170183
arguments; for example, a list of filenames.
171184
In most cases, you don't need to do anything special to capture these
172185
arguments--just supply them after any other options. These variadic
173-
arguments will be returned as the second item in ++cli++'s returned vector:
186+
arguments will be returned as the value of key +:auguments+ in ++parse-opts++'s returned map:
174187

175188
[source,clojure]
176189
----
177-
(second (apply cli ["-n" "5" "foo.txt" "bar.txt"] app-specs))
190+
(:arguments (parse-opts ["-n" "5" "foo.txt" "bar.txt"] app-specs))
178191
;; -> ["foo.txt" "bar.txt"]
179192
----
180193

181194
If your variadic arguments look like flags, however, you'll need(((variadic arguments)))((("arguments, variadic")))
182-
another trick. Use +--+ as an argument to indicate to +cli+ that
195+
another trick. Use +--+ as an argument to indicate to +parse-opts+ that
183196
everything that follows is a variadic argument. This is useful if
184197
you're invoking another program with the options originally passed to
185198
your program:
186199

187200
[source,clojure]
188201
----
189-
(second (apply cli ["-n" "5" "--port" "80"] app-specs))
190-
;; -> Exception '--port' is not a valid argument ...
202+
(select-keys (parse-opts ["-n" "5" "--port" "80"] app-specs) [:arguments :errors])
203+
;; -> {:errors ["Unknown option: \"--port\""], :arguments ["80"]}
191204
192-
(second (apply cli ["-n" "5" "--" "--port" "80"] app-specs))
193-
;; -> ["--port" "80"]
205+
(select-keys (parse-opts ["-n" "5" "--" "--port" "80"] app-specs) [:arguments :errors])
206+
;; -> {:errors nil, :arguments ["--port" "80"]}
194207
----
195208

196209
Once you've finished toying with your application's option parsing at
@@ -202,7 +215,7 @@ pass on to subsequent programs, so too must you use +--+ to indicate to
202215
[source,shell-session]
203216
----
204217
# If app-specs were rigged up to a project...
205-
$ lein run -- -n 5 --no-verbose
218+
$ lein run -- -n 5 --verbose
206219
----
207220

208221
==== See Also

0 commit comments

Comments
 (0)