arguments.lisp 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. ;; arguments.lisp -- Command line option parsing
  2. ;; Copyright (C) 2024 Alexander Rosenberg
  3. ;;
  4. ;; This program is free software: you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation, either version 3 of the License, or
  7. ;; (at your option) any later version.
  8. ;;
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. (in-package :truth-table/args)
  17. (define-condition command-line-error (error)
  18. ((message :initarg :message
  19. :accessor command-line-error-message))
  20. (:report (lambda (con stream)
  21. (format stream "~a"
  22. (command-line-error-message con))))
  23. (:documentation "The parent condition of all command line errors."))
  24. (define-condition cli-argument-error (command-line-error)
  25. ((opt :initarg :opt
  26. :accessor cli-argument-error-opt))
  27. (:report (lambda (con stream)
  28. (with-slots (opt message) con
  29. (format stream
  30. "~a: ~:[--~a~;-~c~]" message (characterp opt) opt))))
  31. (:documentation "Condition representing an error that occurred during
  32. processing of command line arguments."))
  33. (define-condition unknown-option-error (cli-argument-error)
  34. ((message :initform "unknown option"))
  35. (:documentation "Condition representing an unknown command line option."))
  36. (define-condition option-no-arg-error (cli-argument-error)
  37. ((message :initform "option requires an argument"))
  38. (:documentation "Condition representing an error that occurred because a
  39. command line option did not have its required argument."))
  40. (define-condition no-input-error (command-line-error)
  41. ((message :initform "no propositions given"))
  42. (:documentation "Condition representing no propositions given on the command
  43. line."))
  44. (defparameter *cli-parse-continue-string*
  45. "Continue paring arguments normally."
  46. "String to use for `cerror' during argument parsing.")
  47. (defun parse-long-option (spec arg next-arg)
  48. "Parse the long option ARG. Return a list of its symbol, its value (or t if
  49. it did not have one), and weather it consumed NEXT-ARG or not."
  50. (destructuring-bind (name &optional value)
  51. (uiop:split-string (subseq arg 2)
  52. :max 2
  53. :separator "=")
  54. (loop for (short long symbol has-arg-p dest) in spec
  55. when (equal name long) do
  56. (if has-arg-p
  57. (cond
  58. (value
  59. (return (list symbol value nil)))
  60. (next-arg
  61. (return (list symbol next-arg t)))
  62. (t
  63. (cerror *cli-parse-continue-string*
  64. 'option-no-arg-error :opt name)
  65. (return (list symbol nil nil))))
  66. (return (list symbol t nil)))
  67. finally
  68. (cerror *cli-parse-continue-string*
  69. 'unknown-option-error :opt name)
  70. (return (list symbol nil nil)))))
  71. (defun parse-short-option (spec arg next-arg)
  72. "Parse the short options in ARG according to SPEC. Return a list of options
  73. with each entry being similar to the return value of `parse-long-option'."
  74. (loop with output = '()
  75. for i from 1 to (1- (length arg))
  76. for char = (elt arg i)
  77. for (short long symbol has-arg-p desc) = (assoc char spec) do
  78. (cond
  79. (has-arg-p
  80. (cond
  81. ((< i (1- (length arg)))
  82. (push (list symbol (subseq arg (1+ i)) nil) output)
  83. (return output))
  84. (next-arg
  85. (push (list symbol next-arg t) output)
  86. (return output))
  87. (t
  88. (cerror *cli-parse-continue-string*
  89. 'option-no-arg-error :opt char))))
  90. (short
  91. (push (list symbol t nil) output))
  92. (t
  93. (cerror *cli-parse-continue-string*
  94. 'unknown-option-error :opt char)))
  95. finally (return output)))
  96. (defun parse-command-line (spec argv)
  97. "Parse command line arguments in ARGV according to SPEC. Return an alist with
  98. the car being the option's symbol (as specified in SPEC), and the cdr being
  99. the argument it had on the command line, or t if it had none. The rest of the
  100. arguments will be placed in a list at the beginning of the alist."
  101. (let ((output-opts '())
  102. (output-other '()))
  103. (loop for (arg . rest) = argv then rest
  104. while (and arg (not (equal arg "--"))) do
  105. (cond
  106. ((uiop:string-prefix-p "--" arg)
  107. (destructuring-bind (symbol value skip-next-p)
  108. (parse-long-option spec arg (car rest))
  109. (push (cons symbol value) output-opts)
  110. (when skip-next-p
  111. (setq rest (cdr rest)))))
  112. ((uiop:string-prefix-p "-" arg)
  113. (loop for (symbol value skip-next-p) in (parse-short-option
  114. spec arg (car rest))
  115. do
  116. (push (cons symbol value) output-opts)
  117. (when skip-next-p
  118. (setq rest (cdr rest)))))
  119. (t
  120. (push arg output-other)))
  121. finally (setf output-other (nconc (nreverse rest) output-other)))
  122. (cons (nreverse output-other) output-opts)))
  123. (defun print-usage (stream spec exec-name &key general-args
  124. (print-astrisk t))
  125. "Print the command line usage corresponding to SPEC to STREAM."
  126. (format stream "usage: ~a [options]~@[ ~a~]~%~%" exec-name general-args)
  127. (loop with longest-option
  128. = (apply 'max (mapcar
  129. (lambda (entry)
  130. (destructuring-bind (short long sym has-arg-p &rest other)
  131. entry
  132. (declare (ignorable other sym))
  133. (+ (if short 2 0)
  134. (if long (+ 2 (length long)) 0)
  135. (if (and short long) 2 0)
  136. (if has-arg-p 6 0))))
  137. spec))
  138. for (short long symbol has-arg-p desc) in spec
  139. do
  140. (format stream " ~v@<~@[-~c~]~@[, ~*~]~@[--~a~]~@[=<arg>~*~]~> ~a~%"
  141. longest-option
  142. short (and short long) long has-arg-p desc))
  143. (format stream
  144. "~%~@[The choice surrounded by '*' is the default. ~]Arguments to long~:[ ~;~%~]~
  145. options are also required for their short variant.~%" print-astrisk print-astrisk))
  146. (defun option-value (opt opts)
  147. "Get the value of command line option OPT from OTPS, which is an alist as
  148. returned as the second output of `parse-command-line'."
  149. (cdr (assoc opt opts)))