Skip to content

Commit f9211ed

Browse files
committed
Add argument parsing
1 parent 3b5ff42 commit f9211ed

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed

args.lisp

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
(defpackage :kiln/args
2+
(:use :cl :alexandria :serapeum)
3+
(:export :with-argument-destructuring)
4+
(:local-nicknames
5+
(:cli :clingon)))
6+
(in-package :kiln/args)
7+
8+
;;; TODO Use types from declarations to parse.
9+
10+
(defclass param ()
11+
((var :type symbol :initarg :var :reader param-var)))
12+
13+
;;; Abstract.
14+
(defclass default-param (param)
15+
((default :initarg :default :reader param-default)
16+
(suppliedp :type symbol :initarg :suppliedp :reader param-suppliedp)))
17+
18+
(defclass required-param (param)
19+
())
20+
21+
(defun required-param (name)
22+
(make 'required-param :var name))
23+
24+
(defclass optional-param (default-param)
25+
())
26+
27+
(defun optional-param (name default suppliedp)
28+
(make 'optional-param :name name :default default :suppliedp suppliedp))
29+
30+
(defclass keyword-param (default-param)
31+
((keyword :type :keyword :initarg :keyword :reader param-keyword)))
32+
33+
(defun keyword-param (keyword name default suppliedp)
34+
(make 'keyword-param
35+
:keyword keyword
36+
:var name
37+
:default default
38+
:suppliedp suppliedp))
39+
40+
(defclass rest-param (param)
41+
())
42+
43+
(defun rest-param (name)
44+
(make 'rest-param :var name))
45+
46+
(defclass rules ()
47+
((required-params
48+
:type list
49+
:initarg :required-params
50+
:reader required-params)
51+
(optional-params
52+
:type list
53+
:initarg :optional-params
54+
:reader optional-params)
55+
(rest-param
56+
:type (or null rest-parame)
57+
:initarg :rest-param
58+
:reader rest-param-p)
59+
(keyword-params
60+
:type list
61+
:initarg :keyword-params
62+
:reader keyword-params)
63+
(allow-other-keys-p
64+
:type boolean
65+
:initarg :allow-other-keys
66+
:reader allow-other-keys-p)
67+
(allow-keys-p
68+
:type boolean
69+
:initarg :allow-keys
70+
:reader allow-keys-p)))
71+
72+
(defun keyword-dict (rules)
73+
(lret ((dict (make-hash-table :test #'equal)))
74+
(dolist (param (keyword-params rules))
75+
(let* ((str (string-invert-case (param-keyword param)))
76+
(prefix
77+
(case (length str)
78+
(0 (error "Empty keyword"))
79+
(1 "-")
80+
(t "--")))
81+
(key (string+ prefix str)))
82+
(setf (@ dict key) param)))))
83+
84+
(defun parse-args (args rules)
85+
(mvlet* ((rules (ensure-rules rules))
86+
(required args (parse-required-arguments args rules))
87+
(optional args (parse-optional-arguments args rules))
88+
(keywords args (parse-keyword-arguments args rules))
89+
(rest (parse-rest-argument args rules)))
90+
(args-hash-table
91+
(append required optional keywords rest))))
92+
93+
(defun args-hash-table (alist)
94+
(lret ((alist (reverse alist))
95+
(ht (make-hash-table)))
96+
;; The first (left-most) binding wins.
97+
(loop for (k . v) in alist do
98+
(ensure2 (@ ht k) v))))
99+
100+
(defun parse-required-arguments (args rules)
101+
(let* ((params (required-params rules))
102+
(len (length params)))
103+
(if (length< args len)
104+
(error "Missing required arguments: ~a"
105+
(drop (length args)
106+
(mapcar #'param-var
107+
params)))
108+
(multiple-value-bind (required-args rest)
109+
(halves args len)
110+
(values
111+
(mapcar (op (cons (param-var _) _))
112+
params
113+
required-args)
114+
rest)))))
115+
116+
(defun parse-optional-arguments (args rules)
117+
(if (equal (car args) "--")
118+
(values nil args)
119+
(if (no args)
120+
(values nil args)
121+
(let ((params (optional-params rules))
122+
alist)
123+
(loop for param in params do
124+
(if (string^= "-" (car args))
125+
(return)
126+
(push (cons (param-var param)
127+
(pop args))
128+
alist)))
129+
(values alist args)))))
130+
131+
(defun parse-keyword-arguments (args rules)
132+
(if (not (allow-keys-p rules))
133+
(values nil args)
134+
(let ((dict (keyword-dict rules))
135+
alist)
136+
(nlet parse ((args args))
137+
(if (no args)
138+
(values alist args)
139+
(if (string^= "-" (car args))
140+
(let ((param (@ dict (car args))))
141+
(if (no param)
142+
(if (allow-other-keys-p rules)
143+
(parse (cdr args))
144+
(econd
145+
((string^= "--" (car args))
146+
(error "Unknown long keyword argument: ~a"
147+
(car args)))
148+
((string^= "-" (car args))
149+
(error "Unknown short keyword argument: ~a"
150+
(car args)))))
151+
(if (string^= "-" (cadr args))
152+
(progn
153+
(push (cons (param-var param)
154+
nil)
155+
alist)
156+
(parse (cdr args)))
157+
(progn
158+
(push (cons (param-var param)
159+
(cadr args))
160+
alist)
161+
(parse (cddr args))))))))))))
162+
163+
(defun parse-rest-argument (args rules)
164+
(if-let (param (rest-param-p rules))
165+
(values (cons (param-var param) args)
166+
nil)
167+
(if args
168+
(error "Unbound extra arguments: ~a" args)
169+
(values nil nil))))
170+
171+
(defun lambda-list-rules (lambda-list)
172+
(multiple-value-bind
173+
(required-params
174+
optional-params
175+
rest-param-p
176+
keyword-params
177+
allow-other-keys-p
178+
aux-params
179+
allow-keys-p)
180+
(parse-ordinary-lambda-list lambda-list)
181+
(declare (ignore aux-params))
182+
(make 'rules
183+
:required-params
184+
(mapcar #'required-param required-params)
185+
:optional-params
186+
(mapply #'optional-param optional-params)
187+
:rest-param
188+
(and rest-param-p
189+
(rest-param rest-param-p))
190+
:keyword-params
191+
(loop for ((keyword var) init suppliedp) in keyword-params
192+
collect (keyword-param keyword var init suppliedp))
193+
:allow-other-keys
194+
allow-other-keys-p
195+
:allow-keys
196+
allow-keys-p)))
197+
198+
(defun ensure-rules (x)
199+
(etypecase x
200+
(list (lambda-list-rules x))
201+
(rules x)))
202+
203+
(defun generate-binding-lookups (lambda-list dict-var)
204+
(multiple-value-bind
205+
(required-params
206+
optional-params
207+
rest-param-p
208+
keyword-params
209+
allow-other-keys-p
210+
aux-params
211+
allow-keys-p)
212+
(parse-ordinary-lambda-list lambda-list)
213+
(declare (ignore allow-other-keys-p allow-keys-p))
214+
(append
215+
(mapcar (op `(,_1 (@ ,dict-var ',_1)))
216+
(append
217+
required-params
218+
(mapcar #'car optional-params)
219+
(ensure-list rest-param-p)
220+
(mapcar #'cadar keyword-params)))
221+
aux-params)))
222+
223+
(defmacro with-argument-destructuring ((&rest bindings)
224+
(&key
225+
(argv (uiop:command-line-arguments)))
226+
&body body)
227+
(with-unique-names (dict)
228+
`(let ((,dict (parse-args ,argv ',bindings)))
229+
(let ,(generate-binding-lookups bindings dict)
230+
,@body))))
231+
232+
;; (assert (equal '("x" "foo")
233+
;; (with-argument-destructuring (x &key y)
234+
;; (:argv '("x" "-y" "foo") )
235+
;; (list x y))))

0 commit comments

Comments
 (0)