Skip to content

Commit 98afc43

Browse files
committed
Fixes TCLI-98 by adding :post-validation flag
1 parent 23828b0 commit 98afc43

4 files changed

Lines changed: 43 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Change Log
22

33
* Release 1.0.next (on **master**)
4+
* Allow validation to be performed either after parsing (before option processing) -- current default -- or after option processing, via the `:post-validation true` flag [TCLI-98](https://clojure.atlassian.net/browse/TCLI-98).
45
* Allow validation message to be a function (of the invalid argument) in addition to being a plain string [TCLI-97](https://clojure.atlassian.net/browse/TCLI-97).
56
* Add `:multi true` to modify behavior of `:update-fn` [TCLI-96](https://clojure.atlassian.net/browse/TCLI-96).
67

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ be useful within your own `:summary-fn` for generating the default summary.
172172

173173
### Option Argument Validation
174174

175+
By default, option validation is performed immediately after parsing, which
176+
means that "flag" arguments will have a Boolean value, even if a `:default`
177+
is specified with a different type of value.
178+
179+
You can choose to perform validation after option processing instead, with
180+
the `:post-validation true` flag. During option processing, `:default` values
181+
are applied and `:assoc-fn` and `:update-fn` are invoked. If an option is
182+
specified more than once, `:post-validation true` will cause validation to
183+
be performed after each new option value is processed.
184+
175185
There is a new option entry `:validate`, which takes a tuple of
176186
`[validation-fn validation-msg]`. The validation-fn receives an option's
177187
argument *after* being parsed by `:parse-fn` if it exists. The validation-msg

src/main/clojure/clojure/tools/cli.cljc

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@
237237

238238
(def ^{:private true} spec-keys
239239
[:id :short-opt :long-opt :required :desc :default :default-desc :default-fn
240-
:parse-fn :assoc-fn :update-fn :multi :validate-fn :validate-msg :missing])
240+
:parse-fn :assoc-fn :update-fn :multi :post-validation
241+
:validate-fn :validate-msg :missing])
241242

242243
(defn- select-spec-keys
243244
"Select only known spec entries from map and warn the user about unknown
@@ -306,6 +307,7 @@
306307
:validate-msg [String] ; [\"Must be an IPv4 host\"
307308
; \"Must not be a multicast address\"]
308309
; can also be a function (of the invalid argument)
310+
:post-validation Boolean ; default false
309311
:missing String ; \"server must be specified\"
310312
}
311313
@@ -382,17 +384,17 @@
382384
(defn- parse-error [opt optarg msg]
383385
(str "Error while parsing option " (pr-join opt optarg) ": " msg))
384386

385-
(defn- validation-error [opt optarg msg]
387+
(defn- validation-error [value opt optarg msg]
386388
(str "Failed to validate " (pr-join opt optarg)
387-
(if msg (str ": " (if (string? msg) msg (msg optarg))) "")))
389+
(if msg (str ": " (if (string? msg) msg (msg value))) "")))
388390

389391
(defn- validate [value spec opt optarg]
390392
(let [{:keys [validate-fn validate-msg]} spec]
391393
(or (loop [[vfn & vfns] validate-fn [msg & msgs] validate-msg]
392394
(when vfn
393395
(if (try (vfn value) (catch #?(:clj Throwable :cljs :default) _))
394396
(recur vfns msgs)
395-
[::error (validation-error opt optarg msg)])))
397+
[::error (validation-error value opt optarg msg)])))
396398
[value nil])))
397399

398400
(defn- parse-value [value spec opt optarg]
@@ -403,9 +405,12 @@
403405
(catch #?(:clj Throwable :cljs :default) e
404406
[nil (parse-error opt optarg (str e))]))
405407
[value nil])]
406-
(if error
407-
[::error error]
408-
(validate value spec opt optarg))))
408+
(cond error
409+
[::error error]
410+
(:post-validation spec)
411+
[value nil]
412+
:else
413+
(validate value spec opt optarg))))
409414

410415
(defn- neg-flag? [spec opt]
411416
(and (:long-opt spec)
@@ -452,13 +457,17 @@
452457
(or (find-spec specs :short-opt optarg)
453458
(find-spec specs :long-opt optarg)))
454459
[m ids (conj errors (missing-required-error opt (:required spec)))]
455-
[(if-let [update-fn (:update-fn spec)]
456-
(if (:multi spec)
457-
(update m id update-fn value)
458-
(update m id update-fn))
459-
((:assoc-fn spec assoc) m id value))
460-
(conj ids id)
461-
errors])
460+
(let [m' (if-let [update-fn (:update-fn spec)]
461+
(if (:multi spec)
462+
(update m id update-fn value)
463+
(update m id update-fn))
464+
((:assoc-fn spec assoc) m id value))]
465+
(if (:post-validation spec)
466+
(let [[value error] (validate (get m' id) spec opt optarg)]
467+
(if (= value ::error)
468+
[m ids (conj errors error)]
469+
[m' (conj ids id) errors]))
470+
[m' (conj ids id) errors])))
462471
[m ids (conj errors error)]))
463472
[m ids (conj errors (str "Unknown option: " (pr-str opt)))]))
464473
[defaults [] []] tokens)

src/test/clojure/clojure/tools/cli_test.cljc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,18 @@
127127
:validate [#(not= \/ (first %)) "Must be a relative path"
128128
;; N.B. This is a poor way to prevent path traversal
129129
#(not (re-find #"\.\." %)) "No path traversal allowed"]]
130+
["-l" "--level"
131+
:default 0 :update-fn inc
132+
:post-validation true
133+
:validate [#(<= % 2) #(str "Level " % " is more than 2")]]
130134
["-q" "--quiet"
131135
:id :verbose
132136
:default true
133137
:parse-fn not]])]
134138
(is (= (parse-option-tokens specs [[:long-opt "--port" "80"] [:short-opt "-q"] [:short-opt "-f" "FILE"]])
135-
[{:port (int 80) :verbose false :file "FILE"} []]))
139+
[{:port (int 80) :verbose false :file "FILE" :level 0} []]))
136140
(is (= (parse-option-tokens specs [[:short-opt "-f" "-p"]])
137-
[{:file "-p" :verbose true} []]))
141+
[{:file "-p" :verbose true :level 0} []]))
138142
(is (has-error? #"Unknown option"
139143
(peek (parse-option-tokens specs [[:long-opt "--unrecognized"]]))))
140144
(is (has-error? #"Missing required"
@@ -145,6 +149,9 @@
145149
(peek (parse-option-tokens specs []))))
146150
(is (has-error? #"0 is not between"
147151
(peek (parse-option-tokens specs [[:long-opt "--port" "0"]]))))
152+
(is (has-error? #"Level 3 is more than 2"
153+
(peek (parse-option-tokens specs [[:short-opt "-f" "FILE"]
154+
[:short-opt "-l"] [:short-opt "-l"] [:long-opt "--level"]]))))
148155
(is (has-error? #"Error while parsing"
149156
(peek (parse-option-tokens specs [[:long-opt "--port" "FOO"]]))))
150157
(is (has-error? #"Must be a relative path"

0 commit comments

Comments
 (0)