@@ -6,61 +6,59 @@ by Ryan Neufeld; originally submitted by Nicolas Bessi
6
6
==== Problem
7
7
8
8
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 ")))
10
10
11
11
==== Solution
12
12
13
13
Use the https://github.com/clojure/tools.cli[+tools.cli+]
14
14
library.
15
15
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
17
17
dependencies, or start a REPL using +lein-try+:
18
18
19
19
[source,shell-session]
20
20
----
21
21
$ lein try org.clojure/tools.cli
22
22
----
23
23
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+
25
25
function entry point to parse command-line arguments:footnote:[Since
26
26
+tools.cli+ is so cool, this example can run entirely at the REPL.]
27
27
28
28
[source,clojure]
29
29
----
30
- (require '[clojure.tools.cli :refer [cli ]])
30
+ (require '[clojure.tools.cli :refer [parse-opts ]])
31
31
32
32
(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 ))))
38
38
39
39
;; Simulate entry into -main at the command line
40
40
(-main "-h")
41
41
;; *out*
42
- ;; Usage:
43
- ;;
44
- ;; Switches Default Desc
45
- ;; -------- ------- ----
46
- ;; -h, --no-help, --help false Print this help
42
+ ;; -h, --help Print this help
47
43
----
48
44
49
45
==== Discussion
50
46
51
47
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
53
49
should be parsed. Handily enough, there isn't much special about this
54
50
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
56
52
epitome of good, composable functional programming.
57
53
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
59
55
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
61
57
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
64
62
to a keyword (less the +--+). For example, +"--port"+ would become
65
63
+:port+, and +"--super-long-option"+ would become +:super-long-option+.
66
64
@@ -83,19 +81,20 @@ optional string following the final argument name:
83
81
84
82
[source,clojure]
85
83
----
86
- ["-p" "--port" "The incoming port the application will listen on."]
84
+ ["-p" "--port PORT " "The incoming port the application will listen on."]
87
85
----
88
86
89
87
Everything after the argument name and description will be interpreted
90
88
as options in key/value pairs. +tools.cli+ provides the following
91
89
options:
92
90
93
91
+: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.
95
94
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 .
99
98
100
99
+:parse-fn+:: The function used to parse an argument's value. This can
101
100
be used to turn string values into integers, floats, or other
@@ -104,6 +103,21 @@ options:
104
103
+:assoc-fn+:: The function used to combine multiple values for a
105
104
single argument.
106
105
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
+
107
121
Here's a complete example:
108
122
109
123
[source,clojure]
@@ -116,36 +130,35 @@ Here's a complete example:
116
130
(update-in m [k] max v)
117
131
(assoc m k v)))
118
132
119
- (def app-specs [["-n" "--count" :default 5
133
+ (def app-specs [["-n" "--count COUNT" :default 5
134
+ :default-desc "FIVE"
120
135
: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]])
124
140
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} }
127
143
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}}
131
146
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}}
136
149
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
140
154
----
141
155
142
156
One thing the +tools.cli+ library _doesn't_ provide is a hook into the
143
157
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:
149
162
150
163
[source,clojure]
151
164
----
@@ -157,40 +170,40 @@ there is no +:required+ option):
157
170
(not-every? opts required-opts))
158
171
159
172
(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 ))))
167
180
----
168
181
169
182
As with many applications, you may want to accept a variable number of
170
183
arguments; for example, a list of filenames.
171
184
In most cases, you don't need to do anything special to capture these
172
185
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 :
174
187
175
188
[source,clojure]
176
189
----
177
- (second (apply cli ["-n" "5" "foo.txt" "bar.txt"] app-specs))
190
+ (:arguments (parse-opts ["-n" "5" "foo.txt" "bar.txt"] app-specs))
178
191
;; -> ["foo.txt" "bar.txt"]
179
192
----
180
193
181
194
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
183
196
everything that follows is a variadic argument. This is useful if
184
197
you're invoking another program with the options originally passed to
185
198
your program:
186
199
187
200
[source,clojure]
188
201
----
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"]}
191
204
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"]}
194
207
----
195
208
196
209
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
202
215
[source,shell-session]
203
216
----
204
217
# If app-specs were rigged up to a project...
205
- $ lein run -- -n 5 --no- verbose
218
+ $ lein run -- -n 5 --verbose
206
219
----
207
220
208
221
==== See Also
0 commit comments