-
Notifications
You must be signed in to change notification settings - Fork 13
/
unix-opts.lisp
612 lines (555 loc) · 24.3 KB
/
unix-opts.lisp
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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp -*-
;;;
;;; Unix-opts—a minimalistic parser of command line options.
;;;
;;; Copyright © 2015–2018 Mark Karpov
;;; Copyright © 2018–2020 Thomas Schaper
;;;
;;; Permission is hereby granted, free of charge, to any person obtaining a
;;; copy of this software and associated documentation files (the
;;; "Software"), to deal in the Software without restriction, including
;;; without limitation the rights to use, copy, modify, merge, publish,
;;; distribute, sublicense, and/or sell copies of the Software, and to
;;; permit persons to whom the Software is furnished to do so, subject to
;;; the following conditions:
;;;
;;; The above copyright notice and this permission notice shall be included
;;; in all copies or substantial portions of the Software.
;;;
;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
;;; OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
;;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
;;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
;;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
(defpackage :unix-opts
(:nicknames :opts)
(:use #:common-lisp)
(:export
;; Conditions
#:unknown-option
#:unknown-option-provided
#:troublesome-option
#:missing-arg
#:missing-required-option
#:arg-parser-failed
;; Restarts
#:skip-option
#:use-value
#:reparse-arg
;; Classes
#:option
;; Readers
#:missing-options
#:raw-arg
;; Functions
#:exit
#:argv
#:get-opts
#:describe
#:make-options
;; Macros
#:define-opts)
(:shadow #:describe))
(in-package #:unix-opts)
(defclass option ()
((name
:initarg :name
:accessor name
:documentation "keyword that will be included in list returned by
`get-opts' function if this option is given by user")
(description
:initarg :description
:accessor description
:documentation "description of the option")
(short
:initarg :short
:accessor short
:documentation "NIL or single char - short variant of the option")
(long
:initarg :long
:accessor long
:documentation "NIL or string - long variant of the option")
(required
:initarg :required
:accessor required
:initform nil
:documentation "If not NIL this argument is required.")
(arg-parser
:initarg :arg-parser
:accessor arg-parser
:documentation "if not NIL, this option requires an argument, it will be
parsed with this function")
(meta-var
:initarg :meta-var
:accessor meta-var
:documentation "if this option requires an argument, this is how it will
be printed in option description")
(default
:initarg :default
:accessor default
:documentation "if the option is not passed this value will be used,
cannot be used in combination with REQUIRED"))
(:documentation "representation of an option"))
(define-condition troublesome-option (simple-error)
((option
:initarg :option
:reader option))
(:report (lambda (c s) (format s "troublesome option: ~s" (option c))))
(:documentation "Generalization over conditions that have to do with some
particular option."))
(define-condition unknown-option (troublesome-option)
()
(:report (lambda (c s) (format s "unknown option: ~s" (option c))))
(:documentation "This condition is thrown when parser encounters
unknown (not previously defined with `define-opts') option."))
(define-condition unknown-option-provided (troublesome-option)
()
(:report (lambda (c s) (format s "Provided a unknown option: ~s" (option c))))
(:documentation "This condition is signaled when the restart `USE-VALUE'
is called with an undefined option."))
(define-condition missing-arg (troublesome-option)
()
(:report (lambda (c s) (format s "missing arg for option: ~s" (option c))))
(:documentation "This condition is thrown when some option OPTION wants
an argument, but there is no such argument given."))
(define-condition missing-required-option (troublesome-option)
((missing-options
:initarg :missing-options
:reader missing-options))
(:report (lambda (c s)
(format s "missing required options: ~{\"~a\"~^, ~}"
(mapcar (lambda (opt)
(with-slots (short long name) opt
(apply #'format nil
(cond
(long (list "--~A" long))
(short (list "-~A" short))
(t (list "~A" name))))))
(missing-options c)))))
(:documentation "This condition is thrown when required options are missing."))
(define-condition arg-parser-failed (troublesome-option)
((raw-arg
:initarg :raw-arg
:reader raw-arg))
(:report (lambda (c s)
(format s
"argument parser failed (option: ~s, string to parse: ~s)"
(option c)
(raw-arg c))))
(:documentation "This condition is thrown when some option OPTION wants
an argument, it's given but cannot be parsed by argument parser."))
(defparameter *options* nil
"List of all defined options.")
(defun make-options (opts)
(mapcar #'make-option opts))
(defun make-option (args)
"Register an option according to ARGS."
(let ((name (getf args :name))
(description (getf args :description "?"))
(short (getf args :short))
(long (getf args :long))
(arg-parser (getf args :arg-parser))
(required (getf args :required))
(default (getf args :default))
(meta-var (getf args :meta-var "ARG")))
(unless (or short long)
(error "at least one form of the option must be provided"))
(check-type name keyword)
(check-type description string)
(check-type short (or null character))
(check-type long (or null string))
(check-type arg-parser (or null function))
(check-type meta-var string)
(check-type required boolean)
(when required
(check-type default null))
(when (and default
(or (consp default) (and
(not (stringp default))
(arrayp default))
(hash-table-p default) (typep default 'standard-object)))
(warn "Providing mutable object as default value, please provide a function that returns a fresh instance of this object. ~
Default value of ~A was provided." default))
(make-instance 'option
:name name
:description description
:short short
:long long
:required required
:arg-parser arg-parser
:default default
:meta-var meta-var)))
(defmacro define-opts (&body descriptions)
"Define command line options. Arguments of this macro must be plists
containing various parameters. Here we enumerate all allowed parameters:
:NAME—keyword that will be included in list returned by GET-OPTS function if
actual option is supplied by user.
:DESCRIPTION—description of the option (it will be used in DESCRIBE
function). This argument is optional, but it's recommended to supply it.
:SHORT—single character, short variant of the option. You may omit this
argument if you supply :LONG variant of option.
:LONG—string, long variant of option. You may omit this argument if you
supply :SHORT variant of option.
:ARG-PARSER—if actual option must take an argument, supply this argument, it
must be a function that takes a string and parses it.
:META-VAR—if actual option requires an argument, this is how it will be
printed in option description.
:REQUIRED—whether the option is required. This only makes sense if the
option takes an argument.
:DEFAULT—the default value used if the option was not found. This can either
be a function (which will be called to generate the default value) or a
literal value. This option cannot be combined with :REQUIRED. The default
value will not be provided to the :ARG-PARSER."
`(progn
(setf *options* (make-options (list ,@(mapcar (lambda (desc) (cons 'list desc))
descriptions))))
(values)))
(defun argv ()
"Return a list of program's arguments, including command used to execute
the program as first elements of the list. Portable across implementations."
#+abcl ext:*command-line-argument-list*
#+allegro (sys:command-line-arguments)
#+:ccl ccl:*command-line-argument-list*
#+clisp (cons *load-truename* ext:*args*)
#+clozure ccl:*command-line-argument-list*
#+cmu extensions:*command-line-words*
#+ecl (ext:command-args)
#+gcl si:*command-args*
#+lispworks system:*line-arguments-list*
#+sbcl sb-ext:*posix-argv*)
(defun split-short-opts (arg)
"Split short options, for example \"-ab\" will produce \"-a\" and
\"-b\". ARG must be a string, return value is list of strings."
(if (and (> (length arg) 1)
(char= #\- (char arg 0))
(char/= #\- (char arg 1)))
(mapcar (lambda (c) (format nil "-~c" c))
(cdr (coerce arg 'list)))
(list arg)))
(defun split-on-= (arg)
"Split string ARG on \"=\", return value is list of strings."
(if (and (> (length arg) 1)
(char= #\- (char arg 0))
(char/= #\= (char arg 1)))
(let ((pos (position #\= arg :test #'char=)))
(if pos
(list (subseq arg 0 pos)
(subseq arg (1+ pos) (length arg)))
(list arg)))
(list arg)))
(defun shortp (opt)
"Predicate that checks if OPT is a short option."
(and (= (length opt) 2)
(char= #\- (char opt 0))
(char/= #\- (char opt 1))))
(defun longp (opt)
"Predicate that checks if OPT is a long option."
(and (> (length opt) 2)
(char= #\- (char opt 0))
(char= #\- (char opt 1))))
(defun optionp (str)
"This predicate checks if string STR is an option."
(or (shortp str) (longp str)))
(defun argp (str)
"Check if string STR is an argument (not option)."
(and (typep str 'string)
(not (optionp str))))
(defun maybe-funcall (value-or-fun)
(if (functionp value-or-fun)
(funcall value-or-fun)
value-or-fun))
(defun map-options-to-hash-table (options callback)
(loop :with table = (make-hash-table)
:for option :in options
:when (funcall callback option)
:do (setf (gethash (name option) table) option)
:finally (return table)))
(defun find-option (opt options)
"Find option OPT and return object that represents it or NIL."
(multiple-value-bind (opt key)
(if (shortp opt)
(values (subseq opt 1) #'short)
(values (subseq opt 2) #'long))
(flet ((prefix-p (x)
(let ((x (string x)))
(when (>= (length x) (length opt))
(string= x opt :end1 (length opt))))))
(let* ((matches (remove-if-not #'prefix-p options :key key))
(exact-match (find-if #'(lambda (x) (string= x opt))
matches :key key)))
(cond
(exact-match exact-match)
((cadr matches) nil)
(t (car matches)))))))
(defun get-opts (&optional (options nil options-supplied-p) (defined-options *options*))
"Parse command line options. If OPTIONS is given, it should be a list to
parse. If it's not given, the function will use `argv' function to get list
of command line arguments.
Return two values:
* a list that contains keywords associated with command line options with
`define-opts' macro, and
* a list of free arguments.
If some option requires an argument, you can use `getf' to
test presence of the option and get its argument if the option is present.
The parser may signal various conditions. Let's list them all specifying
which restarts are available for every condition, and what kind of
information the programmer can extract from the conditions.
`unknown-option' is thrown when parser encounters unknown (not previously
defined with `define-opts') option. Use the `option' reader to get name of
the option (string). Available restarts: `use-value' (substitute the option
and try again), `skip-option' (ignore the option).
`missing-arg' is thrown when some option wants an argument, but there is no
such argument given. Use the `option' reader to get name of the
option (string). Available restarts: `use-value' (supplied value will be
used), `skip-option' (ignore the option).
`arg-parser-failed' is thrown when some option wants an argument, it's given
but cannot be parsed by argument parser. Use the `option' reader to get name
of the option (string) and `raw-arg' to get raw string representing the
argument before parsing. Available restarts: `use-value' (supplied value
will be used), `skip-option' (ignore the option), `reparse-arg' (supplied
string will be parsed instead).
`missing-required-option' is thrown when some option was required but was
not given. Use the `missing-options' reader to get the list of options that
are missing. Available restarts: `use-value' (supplied list of values will
be used), `skip-option' (ignore all these options, effectively binding them
to `nil')"
(do ((tokens (mapcan #'split-short-opts
(mapcan #'split-on-=
(if options-supplied-p
options
(cdr (argv)))))
(cdr tokens))
(required (map-options-to-hash-table defined-options #'required))
(default-values (map-options-to-hash-table defined-options #'default))
poption-name
poption-raw
poption-parser
options
free-args)
((and (null tokens)
(null poption-name))
(progn
(when (/= (hash-table-count required) 0)
(let ((missing (loop :for val :being :the :hash-values :of required
:collect val)))
(restart-case
(error 'missing-required-option
:missing-options missing)
(skip-option ())
(use-value (values)
(loop :for option :in missing
:for value :in values
:do (push (name option) options)
:do (push value options))))))
(loop :for option :being :the :hash-values :of default-values
:do (progn
(push (name option) options)
(push (maybe-funcall (default option)) options)))
(values (nreverse options)
(nreverse free-args))))
(labels ((push-option (name value)
(push name options)
(push value options)
(setf poption-name nil))
(process-arg (arg)
(restart-case
(handler-case
(push-option poption-name
(funcall poption-parser arg))
(error (condition)
(declare (ignore condition))
(error 'arg-parser-failed
:option poption-raw
:raw-arg arg)))
(use-value (value)
(push-option poption-name value))
(skip-option ()
(setf poption-name nil))
(reparse-arg (str)
(process-arg str))))
(process-option (opt)
(let ((option (find-option opt defined-options)))
(if option
(progn
(remhash (name option) required)
(remhash (name option) default-values)
(let ((parser (arg-parser option)))
(if parser
(setf poption-name (name option)
poption-raw opt
poption-parser parser)
(push-option (name option) t))))
(restart-case
(error 'unknown-option
:option opt)
(use-value (value)
(if (find-option value defined-options)
(process-option value)
(restart-case
(error 'unknown-option-provided
:option value)
(skip-option ()))))
(skip-option ()))))))
(let ((item (car tokens)))
(cond ((and poption-name (argp item))
(process-arg item))
(poption-name
(restart-case
(error 'missing-arg
:option poption-raw)
(use-value (value)
(push-option poption-name value)
(when item
(process-option item)))
(skip-option ()
(setf poption-name nil)
(when item
(process-option item)))))
((string= item "--")
(dolist (tok (cdr tokens))
(push tok free-args))
(setf tokens nil))
((optionp item)
(process-option item))
(t (push item free-args)))))))
(defun add-text-padding (str &key padding newline)
"Add padding to text STR. Every line except for the first one, will be
prefixed with PADDING spaces. If NEWLINE is non-NIL, newline character will
be prepended to the text making it start on the next line with padding
applied to every single line."
(let ((pad (make-string padding :initial-element #\Space))
(pad-next-lines (make-string (max 0 (1- padding)) :initial-element #\Space)))
(with-output-to-string (s)
(when newline
(format s "~%~a" pad))
(map nil
(lambda (x)
(write-char x s)
(when (char= x #\Newline)
(write pad-next-lines :stream s :escape nil)))
str))))
(defun print-opts (defined-options &optional (stream *standard-output*) (argument-block-width 25))
"Print info about defined options to STREAM. Every option get its own line
with description. A newline is printed after the options if this part of the
text is wider than ARGUMENT-BLOCK-WIDTH."
(flet ((pad-right (string max-size)
(concatenate 'string
string
(make-string (- max-size
(length string))
:initial-element #\Space))))
(let* ((option-strings (mapcar
(lambda (opt)
(with-slots (short long description required arg-parser meta-var default) opt
(let ((opts-and-meta
(concatenate
'string
(if short (format nil "-~c" short) "")
(if (and short long) ", " "")
(if long (format nil "--~a" long) "")
(if arg-parser (format nil " ~a" meta-var) "")
(if required (format nil " (Required)") "")))
(full-description
(concatenate
'string
description
(if default
(format nil " [Default: ~A]" (maybe-funcall default))
""))))
(cons opts-and-meta full-description))))
defined-options))
(max-opts-length (reduce #'max
(mapcar (lambda (el)
(length (car el)))
option-strings)
:initial-value 0)))
(loop
:for (opt-meta . opt-description) :in option-strings
:for newline = (>= (length opt-meta)
argument-block-width)
:do (format stream " ~a~a~%"
(pad-right opt-meta (+ (if newline 0 1) max-opts-length))
(add-text-padding opt-description
:padding (+ 3 max-opts-length)
:newline newline)))
(terpri stream))))
(defun print-opts* (margin defined-options)
"Return a string containing info about defined options. All options are
displayed on one line, although this function tries to print it elegantly if
it gets too long. MARGIN specifies margin."
(let ((fill-col (- 80 margin))
(i 0)
(last-newline 0))
(with-output-to-string (s)
(dolist (opt defined-options)
(with-slots (short long required arg-parser meta-var) opt
(let ((str
(format nil " [~a]"
(concatenate
'string
(if short (format nil "-~c" short) "")
(if (and short long) "|" "")
(if long (format nil "--~a" long) "")
(if arg-parser (format nil " ~a" meta-var) "")
(if required (format nil " (Required)") "")))))
(incf i (length str))
(when (> (- i last-newline) fill-col)
(terpri s)
(dotimes (x margin)
(princ #\space s))
(setf last-newline i))
(princ str s)))))))
(defun describe (&key prefix suffix usage-of args (stream *standard-output*) (argument-block-width 25)
(defined-options *options*) (usage-of-label "Usage") (available-options-label "Available options")
brief)
"Return string describing options of the program that were defined with
`define-opts' macro previously. You can supply PREFIX and SUFFIX arguments
that will be printed before and after options respectively. If USAGE-OF is
supplied, it should be a string, name of the program for \"Usage: \"
section. This section is only printed if this name is given.
If your program takes arguments (apart from options), you can specify how to
print them in 'usage' section with ARGS option (should be a string
designator).
For the 'available options' block: if the text that describes how to pass the
option is wider than ARGUMENT-BLOCK-WIDTH a newline is printed before the
description of that option.
The 'usage' section will be prefixed with the value of the key
argument `usage-of-label` (default value: \"Usage\"), and the
'available options' block will starts with the value of the key
argument `available-options-label'
(default value: \"Available options\")
on a single line
If USAGE-OF is provided and BRIEF is non-NIL, the 'available options'
block will be omitted from the output.
The output goes to STREAM."
(flet ((print-part (str)
(when str
(princ str stream)
(terpri stream))))
(print-part prefix)
(when usage-of
(terpri stream)
(format stream "~a: ~a~a~@[ ~a~]~%~%"
usage-of-label
usage-of
(print-opts* (+ (length usage-of-label)
(length usage-of)
2) ; colon and space
defined-options)
args))
(when (and (not (and usage-of brief)) defined-options)
(format stream "~a:~%" available-options-label)
(print-opts defined-options stream argument-block-width))
(print-part suffix)))
(defun exit (&optional (status 0))
"Exit the program returning `status'."
#+sbcl (sb-ext:exit :code status)
#+cmu (unix:unix-exit status)
#+ccl (ccl:quit status)
#+ecl (ext:quit status)
#+clisp (ext:exit status)
#+abcl (extensions:exit :status status)
#+allegro (excl:exit status :quiet t)
#+lispworks (lispworks:quit :status status))