File: argparse.l

package info (click to toggle)
euslisp 9.27%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye
  • size: 55,344 kB
  • sloc: ansic: 41,162; lisp: 3,339; makefile: 256; sh: 208; asm: 138; python: 53
file content (251 lines) | stat: -rw-r--r-- 10,218 bytes parent folder | download
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
;;; argparse.l

;; -------------------
;; Command Line Argument Parser for EusLisp
;; Author: Guilherme de Campos Affonso
;; Created at 2019
;; -------------------


;; Example usage

#|
(require :argparse "argparse.l")

(defvar argparse (instance argparse:argument-parser :init
                           :description "Program Description (optional)"))
(send argparse :add-argument "--foo" :default 10 :read t
      :help "the foo description")
(send argparse :add-argument '("--bar" "-b") :action :store-true
      :help "the bar description")

(send argparse :parse-args)
(format t "foo: ~A~%" (send argparse :foo))
(format t "bar: ~A~%" (send argparse :bar))
(exit)
|#

;; Code

(unless (find-package "ARGPARSE")
  (make-package "ARGPARSE"))
(in-package "ARGPARSE")

(export '(argument-parser))

;; Utility
(defun mklst (obj) (if (listp obj) obj (list obj)))

(defun no-args-p (fn)
  "Return t if the function has null argument list. Unable to handle compiled functions"
  (if (symbolp fn) (setq fn (symbol-function fn)))
  (and (listp fn) (not (nth 4 fn))))

(defun find-any (item-lst seq &rest find-args)
  (dolist (item item-lst)
    (if (apply #'find item seq find-args) (return-from find-any t))))

(defun comma-separate (lst)
  (with-output-to-string (s)
    (princ "{" s)
    (maplist #'(lambda (a) (format s "~S" (car a)) (if (cdr a) (format s ","))) lst)
    (princ "}" s)))

(defmacro set-synonyms (&rest names)
  `(progn ,@(mapcar #'(lambda (place) `(send self :set-val ',place ,place)) names)))

(defun cli-error (format-string &rest args)
  (eval
   `(let ((lisp::*max-callstack-depth* 0))
      (let ((*error-output* *standard-output*))
        (warning-message 1 ,(apply #'format nil format-string args))
        (terpri *error-output*))
      (error ,(apply #'format nil format-string args)))))

(defun flag-name-p (str)
  (and (stringp str) (not (string= (string-left-trim "-" str) "")) (= (elt str 0) #\-)))
(defun check-flag (str)
  (and (flag-name-p str) (not (find #\= str))))
(defun split-flag (str)
  "Returns name, value, setp"
  (let ((pos (position #\= str)))
    (values
     (if pos (subseq str 0 pos) str)
     (if pos (subseq str (1+ pos)))
     (not (not pos)))))
(defun make-flag-dest (obj)
  (if (and (symbolp obj) (not (symbol-package obj)))
      obj
      (intern (string-left-trim "-" (string-upcase obj)) *keyword-package*)))

;; Class Definition
(defclass argparse-docstring
  :slots (usage-stream required-stream optional-stream description epilog))
(defmethod argparse-docstring
  (:init (&key prog description epilog)
    (set-synonyms description epilog)
    (setq usage-stream (make-string-output-stream))
    (setq required-stream (make-string-output-stream))
    (setq optional-stream (make-string-output-stream))
    ;; print initial message
    (princ "usage:" usage-stream)
    (if prog (format usage-stream " ~A" prog))
    (format required-stream "required arguments:~%")
    (format optional-stream "optional arguments:~%")
    self)
  (:add-argument (flags help default required &optional name)
    (setq flags (sort flags #'< #'length))
    (if name (setq name (string-upcase name)))
    (let ((name-str (format nil "~A~A" (car flags) (if name (format nil "=~A" name) "")))
          (details-stream (if required required-stream optional-stream)))
      (if required
          (format usage-stream " ~A" name-str)
          (format usage-stream " [~A]" name-str))
      (princ "  " details-stream)
      (maplist #'(lambda (a)
                   (format details-stream "~A~A" (car a)
                           (if name (format nil "=~A" name) ""))
                   (if (cdr a) (format details-stream ", ")))
               flags)
      (if help (format details-stream "~T~A" help))
      (if default (format details-stream "~C(default: ~S)" (if help #\Space #\Tab) default))
      (terpri details-stream)))
  (:print-usage (&optional (stream *standard-output*))
    (format stream "~A~%" (get-output-stream-string usage-stream)))
  (:print-help (&optional (stream *standard-output*))
    (send self :print-usage stream)
    (terpri stream)
    (if description (format stream "~A~%~%" description))
    (if (> (send required-stream :count) 20)
        ;; Have something more than the initial message 'required arguments:~%'
        (format stream "~A~%" (get-output-stream-string required-stream)))
    (format stream "~A~%" (get-output-stream-string optional-stream))
    (if epilog (format stream "~A~%~%" epilog))))

(defclass argparse-argument
    :super propertied-object
    :slots (value action const choices check flagp read required))
(defmethod argparse-argument
  (:init (&key action const default choices check read required)
    ;; check arguments
    (unless (or (functionp action) (memq action (send self :methods)))
      (cli-error "Action ~A not found!" action))
    (unless (listp choices)
      (cli-error "List expected in :choices!"))
    (unless (or (null check) (functionp check))
      (cli-error "Function expected in :check!"))
    ;; set slots
    (set-synonyms action const choices check read required)
    (setq value (case action
                  (:store-true nil)
                  (:store-false t)
                  (t default)))
    (setq flagp (if (functionp action)
                    (no-args-p action)
                    (not (not (memq action '(:store-true :store-false :store-const :count))))))
    self)
  (:value () value)
  (:store (val)
    (let ((val (if (and read (not (functionp action))) (read-from-string val) val)))
      (if check
          (unless (funcall check val)
            (cli-error "Argument check in ~A failed for value ~S" (send self :name) val)))
      (if (and choices (not (member val choices :test #'equal)))
          (cli-error "Invalid choice ~S~A (choose from ~A)"
                     val
                     (if (send self :name) (format nil " in ~A" (send self :name)) "")
                     (comma-separate choices)))
      (setq value val)))
  (:store-true () (send self :store t))
  (:store-false () (send self :store nil))
  (:store-const () (send self :store const))
  (:count () (send self :store (1+ (or value 0))))
  (:append (val)
    (let ((current value))
      (send self :store val)
      (setq value (append current (list value))))))

(defclass argument-parser
  :super propertied-object
  :slots (flaglst docstring))
(defmethod argument-parser
  (:init (&key prog description epilog (add-help t))
    (setq docstring (instance argparse-docstring :init
                              :prog prog
                              :description description
                              :epilog epilog))
    (if add-help
        (send self :add-argument '("--help" "-h") :help "show this help message and exit"
              :dest (gensym)
              :action `(lambda () (send ,docstring :print-help) (exit))))
    self)
  (:add-argument (flags &key (action :store) const default choices check read required help dest)
    (let* ((flags (mklst flags))
           (name (make-flag-dest (or dest (car flags))))
           (arg (instance argparse-argument :init
                          :action action
                          :const const
                          :default default
                          :choices choices
                          :check check
                          :read read
                          :required required)))
      (send arg :name name)
      ;; check type
      (dolist (a flags)
        (unless (check-flag a) (cli-error "Invalid argument name in ~S" a)))
      ;; ensure name is unique
      (if (assoc name (send self :plist))
          (cli-error "Already have argument with name ~S" name)
          (setf (get self name) arg))
      ;; add to flaglst
      (dolist (a flags)
        (if (assoc a flaglst :test #'string=)
            (cli-error "Multiple flags for ~S" a))
        (push (cons a name) flaglst))
      ;; add to docstring
      (send docstring :add-argument flags help (send arg :value) required
            (cond (choices (comma-separate choices))
                  ((not (argparse-argument-flagp arg)) name)))
      ;; add method
      (if (keywordp name)
          (eval `(defmethod argument-parser (,name () (send (get self ,name) :value)))))
      ;; return name
      name))
  (:parse-args ()
    (let ((required-args
           (remove-if-not #'(lambda (obj) (argparse-argument-required (cdr obj))) plist)))
      (flet ((cli-error (&rest args)
               (send docstring :print-usage)
               (apply #'cli-error args)))
        (dolist (flag (remove-if-not #'flag-name-p lisp::*eustop-argument*))
          (multiple-value-bind (name value setp) (split-flag flag)
            (let* ((arg-name (cdr (assoc name flaglst :test #'string=)))
                   (arg (get self arg-name)))
              (if arg
                  ;; has flag
                  (let ((action (argparse-argument-action arg))
                        (flagp (argparse-argument-flagp arg)))
                    (if required-args
                        (setq required-args (delete arg required-args :count 1 :key #'cdr)))
                    (cond
                      ((functionp action)
                       (if setp
                           (send arg :store (funcall action
                                                     (if (argparse-argument-read arg)
                                                         (read-from-string value)
                                                         value)))
                           (send arg :store (funcall action))))
                      ((and setp flagp)
                       (cli-error "Argument ~S do not take values!" flag))
                      ((and (not setp) (not flagp))
                       (cli-error "Value expected in ~S" flag))
                      (t
                       (if setp
                           (send arg action value)
                           (send arg action)))))
                  ;; doesn't have flag
                  (cli-error "Argument ~S not found!~%" flag)))))
        ;; check required arguments
        (when required-args
          (cli-error "Argument ~A is required!" (caar (last required-args))))))))