/
utils.cljc
373 lines (304 loc) · 9.84 KB
/
utils.cljc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
(ns cli-matic.utils
"
### Utilities used in the project
* the *general* section contains low-level
stuff that could be anywhere
* the *cli-matic* section contain low-level functions
used by the parser and the help generators.
"
(:require [clojure.string :as str]
[cli-matic.presets :as PRESETS]
[cli-matic.specs :as S]
[clojure.spec.alpha :as s]
)
)
; ================================================
; GENERAL TOOLS
; ================================================
(defn asString
"Turns a collection of strings into one string,
or the string itself.
If the collection includes multiple sub-arrays,
those are flattened into lines as well.
"
[s]
(if (string? s)
s
(str/join "\n" (flatten s))))
(defn asStrVec
"Whatever we get in, we want a vector of strings out."
[s]
(cond
(nil? s) []
(string? s) [s]
:else s))
(defn indent-string
"Indents a single string by one space."
[s]
(str " " s))
(defn indent
"Indents a single string, or each string
in a collection of strings."
[s]
(if (string? s)
(indent-string s)
(map indent-string (flatten s))))
(defn pad
"Pads 's[, s1]' to so many characters"
[s s1 len]
(subs (str s
(when s1
(str ", " s1))
" ")
0 len))
(defn deep-merge
"
Merges a number of maps, considering values in inner maps.
See https://gist.github.com/danielpcox/c70a8aa2c36766200a95#gistcomment-2308595
"
[& maps]
(apply merge-with (fn [& args]
(if (every? map? args)
(apply deep-merge args)
(last args)))
maps))
;
; String distance
; Patch by https://github.com/l3nz/cli-matic/pull/49/commits/07c392301a9e12c2f8ad76fd8a9115e0632e175a
;
(defn- deep-merge-with
[f & maps]
(apply
(fn m [& maps]
(if (every? map? maps)
(apply merge-with m maps)
(apply f maps)))
maps))
(defn- levenshtein-distance
"Ref https://en.wikipedia.org/wiki/Levenshtein_distance "
[a b]
(let [m (count a)
n (count b)
init (apply deep-merge-with (fn [a b] b)
(concat
(for [i (range 0 (+ 1 m))]
{i {0 i}})
(for [j (range 0 (+ 1 n))]
{0 {j j}})))
table (reduce
(fn [d [i j]]
(deep-merge-with
(fn [a b] b)
d
{i {j (if (= (nth a (- i 1))
(nth b (- j 1)))
((d (- i 1)) (- j 1))
(min
(+ ((d (- i 1))
j) 1)
(+ ((d i)
(- j 1)) 1)
(+ ((d (- i 1))
(- j 1)) 1)))
}}))
init
(for [j (range 1 (+ 1 n))
i (range 1 (+ 1 m))] [i j]))]
((table m) n)))
(defn str-distance
"Distance between two strings, as expressed in percentage
of changes to the length of the longest string.
"
[a b]
(/ (levenshtein-distance a b)
(max (count a) (count b) 1)))
(defn candidate-suggestions
"Returns candidate suggestions, in order of
reliability."
[candidates cmd max-str-distance]
(let [valid (filter #(<= (str-distance % cmd) max-str-distance) candidates)]
(sort-by (partial str-distance cmd) valid)))
; ==================================================================
; CLI-matic specific stuff
; ==================================================================
(defn assoc-new-multivalue
"Associates a new multiple value to the
current parameter map.
If the current value is not a vector, creates
a new vector with the new value."
[parameter-map option v]
(let [curr-val (get parameter-map option [])
new-val (if (vector? curr-val)
(conj curr-val v)
[v])]
(assoc parameter-map option new-val)))
(defn mk-env-name
"Writes a description with the env name by the end."
[description env for-parsing?]
(if (and (not for-parsing?)
(some? env))
(str description " [$" env "]")
description))
(defn mk-cli-option
"Builds a tools.cli option out of our own format.
If for-parsing is true, the option will be used for parsing;
if false, for generating help messages.
"
[{:keys [option short as type default multiple env] :as cm-option}]
(let [preset (get PRESETS/known-presets type :unknown)
placeholder (str (:placeholder preset)
(if (= :present default) "*" ""))
positional-opts [(if (string? short)
(str "-" short)
nil)
(str "--" option " " placeholder)
(mk-env-name as env false)]
;; step 1 - remove :placeholder
opts-1 (dissoc preset :placeholder)
;; step 2 - add default if present and is not ":present"
opts-2 (if (and (some? default)
(not= :present default))
(assoc opts-1 :default default)
opts-1)
;; step 3 - if multivalue, add correct assoc-fns
opts-3 (if multiple
(assoc opts-2 :assoc-fn assoc-new-multivalue)
opts-2)]
(apply
conj positional-opts
(flatten (seq opts-3)))))
(s/fdef mk-cli-option
:args (s/cat :opts ::S/climatic-option)
:ret some?)
(defn get-subcommand
"Given args and the canonical name of a subcommand,
returns the map describing it.
"
[climatic-args subcmd]
(let [subcommands (:commands climatic-args)]
(first (filter #(= (:command %) subcmd) subcommands))))
(s/fdef get-subcommand
:args (s/cat :args ::S/climatic-cfg :subcmd string?)
:ret ::S/a-command)
(defn all-subcommands-aliases
"Maps all subcommands and subcommand aliases
to their canonical name.
E.g. {'add': 'add', 'a': 'add'}.
We basically add them all, then remove
nil keys.
"
[climatic-args]
(let [subcommands (:commands climatic-args)]
(dissoc
(merge
;; a map of 'cmd' -> 'cmd'
(into {}
(map
(fn [{:keys [command short]}]
[command command])
subcommands))
(into {}
(map
(fn [{:keys [command short]}]
[short command])
subcommands)))
nil)))
(s/fdef all-subcommands-aliases
:args (s/cat :args ::S/climatic-cfg)
:ret (s/map-of string? string?))
(defn all-subcommands
"Returns all subcommands, as strings.
We get all versions of all subcommands.
"
[climatic-args]
(set (keys (all-subcommands-aliases climatic-args))))
(s/fdef all-subcommands
:args (s/cat :args ::S/climatic-cfg)
:ret set?)
(defn canonicalize-subcommand
"Returns the 'canonical' name of a subcommand,
i.e. the one that appears in :command, even
if we pass an alias or short version."
[commands subcmd]
(get (all-subcommands-aliases commands) subcmd))
(s/fdef canonicalize-subcommand
:args (s/cat :args ::S/climatic-cfg :sub string?)
:ret string?)
(defn get-options-for
"Gets specific :options for a subcommand or,
if nil, for global."
[climatic-args subcmd]
(if (nil? subcmd)
(:global-opts climatic-args)
(:opts (get-subcommand climatic-args subcmd))))
;; Out of a cli-matic arg list,
;; generates a set of commands for tools.cli
(defn cm-opts->cli-opts
"
Out of a cli-matic arg list, generates a set of
options for tools.cli.
It also adds in the -? and --help options
to trigger display of helpness.
"
[climatic-opts]
(conj
(mapv mk-cli-option climatic-opts)
["-?" "--help" "" :id :_help_trigger]))
(defn rewrite-opts
"
Out of a cli-matic arg list, generates a set of
options for tools.cli.
It also adds in the -? and --help options
to trigger display of helpness.
"
[climatic-args subcmd]
(cm-opts->cli-opts (get-options-for climatic-args subcmd)))
(s/fdef rewrite-opts
:args (s/cat :args some?
:mode (s/or :common nil?
:a-subcommand string?))
:ret some?)
;; -------------------------------------------------------------
;; POSITIONAL PARAMETERS
; Positional parameters:
; 1- are only valid in subcommands
; 2- appear on help
; 3- capture from the "leftovers" vector :_arguments
;; --------------------------------------------------------------
(defn list-positional-parms
"Extracts all positional parameters from the configuration."
[cfg subcmd]
;;(prn "CFG" cfg "Sub" subcmd)
(let [opts (get-options-for cfg subcmd)
rv (filterv #(integer? (:short %)) opts)]
;;(prn "Subcmd" subcmd "OPTS" opts "RV" rv )
rv))
(s/fdef
list-positional-parms
:args (s/cat :cfg ::S/climatic-cfg :cmd (s/or :cmd ::S/command :global nil?))
:ret (s/coll-of ::S/climatic-option))
(defn a-positional-parm
"Reads one positional parameter from the arguments.
Returns a vector [parm value]
The value is NOT solved, so it's always a string."
[args option]
(let [pos (:short option)
lbl (:option option)
val (get args pos nil)]
[lbl val]))
(s/fdef
a-positional-parm
:args (s/cat :args sequential?
:opt ::S/climatic-option)
:ret vector?)
(defn capture-positional-parms
"Captures positional parameters in the remaining-args of
a subcommand."
[cfg subcmd remaining-args]
(let [pp (list-positional-parms cfg subcmd)]
(into {}
(map (partial a-positional-parm remaining-args) pp))))
(s/fdef
capture-positional-parms
:args (s/cat :cfg ::S/climatic-cfg :cmd ::S/command :args sequential?)
:ret ::S/mapOfCliParams)