-
Notifications
You must be signed in to change notification settings - Fork 24
/
cli-parser.lisp
129 lines (113 loc) · 5.36 KB
/
cli-parser.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
(in-package #:pgcharts)
(eval-when (:load-toplevel :compile-toplevel :execute)
(defstruct command verbs bindings help lambda)
(defvar *commands* (make-array 0
:element-type 'command
:adjustable t
:fill-pointer t)
"Host commands defined with the DEFINE-COMMAND macro.")
(defmethod same-command ((a command) (b command))
"Return non-nil when a and b are commands with the same verbs"
(equal (command-verbs a) (command-verbs b))))
(defun destructuring-match (lambda-list args)
"Return non-nil when ARGS are matching against the given LAMBDA-LIST."
(ignore-errors
(funcall
(compile nil
`(lambda ()
;; hide a style warning that variables are defined
;; but never used here
(declare #+sbcl (sb-ext:muffle-conditions style-warning))
(destructuring-bind ,lambda-list ',args t))))))
(defmethod command-matches ((command command) args)
"When the given COMMAND matches given command line ARGS, then return it
and the argument to apply to it."
(declare (type list args))
(when (<= (length (command-verbs command)) (length args))
(let ((matches-p (loop :for verb :in (command-verbs command)
:for arg in args
:for matches-p := (string-equal verb arg)
:while matches-p
:finally (return matches-p))))
(when matches-p
(let ((fun-args (nthcdr (length (command-verbs command)) args)))
(when (destructuring-match (command-bindings command) fun-args)
(list (command-lambda command) fun-args)))))))
(defmacro define-command ((verbs bindings) help-string &body body)
"Define a command that is to be fired when VERBS are found at the
beginning of the command, assigning remaining arguments to given
bindings.
The help-string is used when displaying the program usage."
(let ((fun (gensym))
(command (gensym))
(position (gensym))
(output (gensym)))
`(eval-when (:load-toplevel :compile-toplevel :execute)
(let* ((,fun (lambda ,bindings
(read-config)
(let ((,output (progn ,@body)))
(typecase ,output
(string (format t "~a~%" ,output))
(t nil)))))
(,command (make-command :verbs ',verbs
:bindings ',bindings
:help ,help-string
:lambda (compile nil ,fun)))
(,position (position-if (lambda (c) (same-command c ,command))
*commands*)))
(if ,position
(setf (aref *commands* ,position) ,command)
(vector-push-extend ,command *commands*))))))
(defstruct (option
(:conc-name opt-)
(:constructor make-option (keyword short long
&optional fun eat-next-arg)))
keyword short long fun eat-next-arg)
(defun parse-option-name (arg)
"When ARG is an option name, return its keyword, otherwise return nil."
(loop :for option :in *options*
:when (or (string= arg (opt-short option))
(string= arg (opt-long option)))
:return option))
(defun process-argv-options (argv)
"Return the real args found in argv, and a list of the options used, as
multiple values."
(let ((args '())
(ignore nil)
(opts '()))
(values (loop :for (arg next) :on (rest argv)
:for opt := (unless ignore (parse-option-name arg))
:do (progn
;; sanity check
(when (and opt (opt-eat-next-arg opt) (null next))
(format t "Missing argument for option ~a~%" arg)
(push :help opts))
;; build the argument list
(unless (or opt ignore)
(push arg args))
;; we might have to ignore arg on next iterationa
(setf ignore (and opt (opt-eat-next-arg opt)))
;; deal with the option side effects
(when opt
(push (opt-keyword opt) opts)
(when (opt-fun opt)
(let ((args (when (opt-eat-next-arg opt) (list next))))
(apply (opt-fun opt) args)))))
:finally (return (nreverse args)))
opts)))
(defun find-command-function (args)
"Loop through *COMMANDS* to find the code to execute given ARGS."
(loop :for command :across *commands*
:for match := (command-matches command args)
:until match
:finally (return match)))
(defun usage (args &key help)
"Loop over all the commands and output the usage of the main program"
(format t "pgcharts [ --help ] [ --version ] [ --config filename ] command ...~%")
(unless help
(format t "~a: command line parse error.~%" (first args))
(format t "~@[Error parsing args: ~{~s~^ ~}~%~]~%" (rest args)))
(format t "~%Available commands:~%")
(loop :for command :across *commands*
:do (with-slots (verbs bindings help) command
(format t " ~{~a~^ ~} ~{~a~^ ~}~28T~a~%" verbs bindings help))))