Import command-line-arguments from XCVB 0.315.
authorFrancois-Rene Rideau <fare@tunes.org>
Wed, 8 Jul 2009 13:54:29 +0000 (09:54 -0400)
committerFrancois-Rene Rideau <fare@tunes.org>
Wed, 8 Jul 2009 13:54:29 +0000 (09:54 -0400)
XCVB itself imported a version from ITA's QUUX in early 2009, and modified it.

BUILD.lisp [new file with mode: 0644]
command-line-arguments.asd [new file with mode: 0644]
get-command-line-arguments.lisp [new file with mode: 0644]
parse-command-line-arguments.lisp [new file with mode: 0644]
pkgdcl.lisp [new file with mode: 0644]

diff --git a/BUILD.lisp b/BUILD.lisp
new file mode 100644 (file)
index 0000000..0b1cfb1
--- /dev/null
@@ -0,0 +1,23 @@
+;;; -*- mode: lisp -*-
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Free Software available under an MIT-style license. See LICENSE  ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2008-2009 ITA Software, Inc.  All rights reserved. ;;;
+;;;                                                                  ;;;
+;;; Original authors: Francois-Rene Rideau, Dan Weinreb              ;;;
+;;;                                                                  ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+#+xcvb
+(module
+ (:fullname "itasoftware.com/quux/command-line-arguments"
+  :nicknames ("command-line-arguments")
+  :supersedes-asdf ("command-line-arguments")
+  :author ("Francois-Rene Rideau" "Dan Weinreb")
+  :maintainer "Francois-Rene Rideau"
+  :licence "MIT"
+  :description "small library to deal with command-line arguments"
+  :long-description "A library to abstract away the parsing of Unix-style command-line arguments"
+  :depends-on ("parse-command-line-arguments" "get-command-line-arguments")
+  :build-image nil))
diff --git a/command-line-arguments.asd b/command-line-arguments.asd
new file mode 100644 (file)
index 0000000..b83646b
--- /dev/null
@@ -0,0 +1,22 @@
+;;; -*- mode: lisp -*-
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Free Software available under an MIT-style license. See LICENSE  ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2008-2009 ITA Software, Inc.  All rights reserved. ;;;
+;;;                                                                  ;;;
+;;; Original authors: Francois-Rene Rideau, Dan Weinreb              ;;;
+;;;                                                                  ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(asdf:defsystem :command-line-arguments
+  :author ("Francois-Rene Rideau" "Dan Weinreb")
+  :maintainer "Francois-Rene Rideau"
+  :licence "MIT"
+  :description "small library to deal with command-line arguments"
+  :long-description "A library to abstract away the parsing of Unix-style command-line arguments"
+  :depends-on (:cl-launch)
+  :components
+  ((:file "pkgdcl")
+   (:file "get-command-line-arguments" :depends-on ("pkgdcl"))
+   (:file "parse-command-line-arguments" :depends-on ("get-command-line-arguments"))))
diff --git a/get-command-line-arguments.lisp b/get-command-line-arguments.lisp
new file mode 100644 (file)
index 0000000..4f253ad
--- /dev/null
@@ -0,0 +1,16 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Confidential and proprietary information of ITA Software, Inc.   ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2009 ITA Software, Inc.  All rights reserved.      ;;;
+;;;                                                                  ;;;
+;;; Original author: Francois-Rene Rideau                            ;;;
+;;;                                                                  ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+#+xcvb (module (:depends-on ((:asdf "cl-launch") "pkgdcl")))
+
+(in-package :command-line-arguments)
+
+(defun get-command-line-arguments ()
+  cl-launch:*arguments*)
diff --git a/parse-command-line-arguments.lisp b/parse-command-line-arguments.lisp
new file mode 100644 (file)
index 0000000..d4c2e77
--- /dev/null
@@ -0,0 +1,371 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Confidential and proprietary information of ITA Software, Inc.   ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2003-2009 ITA Software, Inc.  All rights reserved. ;;;
+;;;                                                                  ;;;
+;;; Original author: Francois-Rene Rideau                            ;;;
+;;;                                                                  ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+#+xcvb (module (:depends-on ("get-command-line-arguments")))
+
+(in-package :command-line-arguments)
+
+
+(defvar *command-line-arguments* nil
+  "a list of strings, the arguments passed to the program on its command-line,
+or what's currently left of them as they are processed")
+
+(defvar *command-line-options* nil
+  "command-line options as parsed into a plist")
+
+(defvar *command-line-option-specification* nil
+  "the (prepared) specification for how to parse command-line options")
+
+;; A raw specification is a list of individual option specifications.
+;; An individual option specification is:
+;; A single option name or a list of option names, and a keyword/value list of option options.
+;; An option name is a single character #\x for short option -x,
+;; or a string "foo" for long option --foo.
+;; option options are:
+
+;; :type for specifying a parameter type for the option.
+;;  A type may be any of:
+;;  NIL - the option takes no parameter.
+;;  BOOLEAN - the option takes a boolean parameter.  The value can be true, false, yes, no, t, nil, y, n.
+;;       If it's a long option, --no-foo is defined, too.
+;;  STRING - the option takes a string as parameter
+;;  INTEGER - the option takes an integer as parameter, interpreted in decimal.
+
+;; :optional for allowing the option to have no parameter
+;;  for a list, it allows the final list to be empty.
+
+;; :action for specifying an action to do when the option is found
+;;  an action may be a symbol to set, a function to call, nil to do nothing,
+;;  or a keyword to push on the option plist.
+;;  default action is to make a keyword from the first name.
+
+;; :list The value is a plist with keywords :initial-contents and :symbol.
+;;  The :type must be integer or string.
+;;  :symbol is a special variable and :initial-contents is a list.
+;;  While the options are being processed, the special variable is bound to the
+;;  initial contents, reversed.
+;;  At the end of option processing, the finalizer reverses the list.
+
+;; TODO: add this feature, useful for verbose flags.
+;; :count The value is a plist with keywords :initial-value and :symbol.
+;;  A counter is initialized with initial-value (by default 0),
+;;  incremented each time the option is invoked, decremented each time.
+;;  Alternatively, if the option is given a numeric argument, the counter
+;;  is set to the provided argument value.
+
+;; :negation  Creates string called "no-XXX", or "disable-XXX" if the original name
+;;  is "enable-XXX".
+
+;; A *prepared* specification is an EQUAL-hash-table that maps option names to
+;; a simple-vector #(action type optional) that specifies what to do when the option
+;; is encountered in the command-line. It also includes three special entries for
+;; keywords :local-symbol :local-values :finalizers that specify the local symbols
+;; to bind when parsing options for this specification, the values to which to bind them,
+;; and a list of finalizers to run after the parsing is done.
+
+(defun make-option-action (p name &key (action nil actionp) list optional &allow-other-keys)
+
+  "This is called for one option specification.
+   P is the hash table of actions.  NAME is the first name of this option, a string
+   or a character.  The keywords are option options for this option specification."
+
+  (let ((actual-action
+         ;; This is usually the same as ACTION, but if ACTION is #'FOO,
+         ;; then it's the symbol-function of FOO, and if no action is
+         ;; provided, it's a keyword named NAME.
+         (cond
+           ((and (consp action) (eq 'function (car action))
+                 (consp (cdr action)) (null (cddr action)))
+            (symbol-function (cadr action)))
+           (actionp
+            action)
+           (t
+           (intern (string-upcase name) :keyword)))))
+    ;; If the :LIST option is not specified, just return the actual-action.
+    (if list
+      (destructuring-bind (&key initial-contents (symbol (gensym (string-upcase name))))
+         (and (listp list) list)
+       (let ((final-action #'(lambda ()
+                               (let ((value (symbol-value symbol)))
+                                 (unless (or optional value)
+                                   (error "No option ~A defined" (option-name name)))
+                                 (command-line-action actual-action (reverse value))))))
+         (push symbol (gethash :local-symbols p))
+         (push (reverse initial-contents) (gethash :local-values p))
+         (push final-action (gethash :finalizers p))
+         #'(lambda (value)
+             (case value
+               ((nil) (set symbol nil))
+               ((t)   (error "Option ~A requires a parameter" (option-name name)))
+               (otherwise (push value (symbol-value symbol)))))))
+      actual-action)))
+
+
+(defun prepare-command-line-options-specification (specification)
+
+  "Given a SPECIFICATION, return a hash table with one entry
+   whose key is the name and whose value is a vector of the action,
+   the type, and whether it's optional."
+
+  (etypecase specification
+    (hash-table specification)
+    (list
+     (let ((p (make-hash-table :test 'equal)))
+       (dolist (spec specification)
+         (destructuring-bind (names &rest option-options
+                                    &key type optional list negation
+                                    action documentation negation-documentation)
+             spec
+           (declare (ignorable action documentation negation-documentation))
+           (when list
+             (unless (member type '(integer string))
+               (error "option specification wants list but doesn't specify string or integer")))
+           (let* ((namelist (if (listp names) names (list names)))
+                  (firstname (car namelist))
+                  (pos-action (apply 'make-option-action p firstname option-options)))
+             ;; For each name of this spec, put an entry into the hash table
+             ;; mapping that name to a vector of the action, the type, and
+             ;; whether it's optional.
+             (loop with spec = (vector pos-action type (and optional (not list)))
+                   for name in namelist do
+                   (setf (gethash name p) spec))
+             ;; Deal with negation.
+             (when (or (eq type 'boolean) list optional)
+               (let ((neg-action #'(lambda (value)
+                                     (command-line-action pos-action (not value))))
+                     (neg-names (make-negated-names namelist negation)))
+                 (loop with spec = (vector neg-action nil nil nil)
+                       for name in neg-names do
+                       (setf (gethash name p) spec)))))))
+       p))))
+
+(defun make-negated-names (namelist &optional negation)
+  (let ((negation-list (if (listp negation) negation (list negation))))
+    (loop for name in namelist
+          when (stringp name) do
+          (push (concatenate 'string "no-" name) negation-list)
+          (when (and (<= 7 (length name))
+                     (string= "enable-" (subseq name 0 7)))
+            (push (concatenate 'string "disable-" (subseq name 7 nil))
+                  negation-list)))
+    negation-list))
+
+(defun command-line-option-specification (option)
+  (let ((v (gethash option *command-line-option-specification*)))
+    (if v (values t (svref v 0) (svref v 1) (svref v 2)) (values nil nil nil nil))))
+
+(defun command-line-action (action &optional value)
+  (etypecase action
+    (null nil)
+    (keyword  (setf *command-line-options*
+                   (list* action value *command-line-options*)))
+    (symbol   (set action value))
+    (function (funcall action value))))
+
+(defun short-option-p (arg)
+
+  "ARG is a string.  Is it like -A, but not --?"
+
+  (check-type arg simple-string)
+  (and (<= 2 (length arg))
+       (char= #\- (schar arg 0))
+       (char/= #\- (schar arg 1))))
+
+(defun negated-short-option-p (arg)
+
+  "ARG is a string.  Is it like +A?"
+
+  (check-type arg simple-string)
+  (and (<= 2 (length arg))
+       (char= #\+ (schar arg 0))))
+
+(defun long-option-p (arg)
+
+  "ARG is a string.  Is it like --A?"
+
+  (check-type arg simple-string)
+  (and (<= 3 (length arg))
+       (char= #\- (schar arg 0) (schar arg 1))))
+
+(defun option-end-p (arg)
+  (check-type arg simple-string)
+  (string= arg "--"))
+
+(defun option-like-p (arg)
+  (check-type arg simple-string)
+  (and (<= 2 (length arg))
+       (or (char= #\- (schar arg 0))
+           (char= #\+ (schar arg 0)))))
+
+(defun option-name (option-designator)
+  (etypecase option-designator
+    (character (format nil "-~A" option-designator))
+    (string    (format nil "--~A" option-designator))))
+
+(defun coerce-option-parameter (option string type)
+
+  "Given a STRING option value and a TYPE, return the value as
+   a Lisp object.  OPTION is the name of the option, just for
+   error messages."
+
+  (flet ((fail ()
+           (error "parameter for option ~A not of type ~A" (option-name option) type)))
+    (ecase type
+      ((nil)
+       (error "option ~A does not take a parameter" (option-name option)))
+      ((string)
+       string)
+      ((boolean)
+       (cond
+         ((member string '("true" "t" "1" "yes" "y") :test #'string-equal)
+          t)
+         ((member string '("false" "nil" "0" "no" "n") :test #'string-equal)
+          nil)
+         (t
+          (fail))))
+      ((integer)
+       (multiple-value-bind (value end) (parse-integer string :junk-allowed t)
+         (unless (and (integerp value) (= end (length string))) (fail))
+         value)))))
+
+(defun get-option-parameter (option type optional)
+  (cond
+    ((member type '(boolean t nil))
+     t)
+    ((and optional
+          (or (null *command-line-arguments*)
+              (option-like-p (car *command-line-arguments*))))
+     t)
+    (t
+     (coerce-option-parameter option (pop *command-line-arguments*) type))))
+
+(defun process-option (option validp action parameter type optional)
+  (unless validp (error "Undefined option ~A" (option-name option)))
+  (typecase parameter
+    (null
+     (unless (or (eq type 'boolean) optional)
+       (error "Option ~A cannot be negated" (option-name option))))
+    (string
+     (setf parameter (coerce-option-parameter option parameter type)))
+    (t
+     (setf parameter (get-option-parameter option type optional))))
+  (command-line-action action parameter))
+
+(defun process-short-option (c &key negated)
+  (multiple-value-bind (validp action type optional)
+      (command-line-option-specification c)
+    (process-option c validp action (not negated) type optional)))
+
+(defun decompose-long-option-string (string)
+  (let* ((separator (position #\= string :start 2))
+         (name (subseq string 2 separator))
+         (parameter (if separator (subseq string (1+ separator)) t)))
+    (values name parameter)))
+
+(defun process-long-option (s)
+  (multiple-value-bind (name parameter) (decompose-long-option-string s)
+    (multiple-value-bind (validp action type optional)
+        (command-line-option-specification name)
+      (process-option name validp action parameter type optional))))
+
+(defun do-process-command-line-options ()
+
+  "Remove all the options and values from *COMMAND-LINE-ARGUMENTS*.
+   Process each option."
+
+  (progv
+      (gethash :local-symbols *command-line-option-specification*)
+      (gethash :local-values *command-line-option-specification*)
+    (loop for arg = (pop *command-line-arguments*) do
+      (cond
+       ((or (null arg) (option-end-p arg))
+        (return))
+       ((short-option-p arg)
+        (loop for c across (subseq arg 1 nil) do
+          (process-short-option c)))
+       ((negated-short-option-p arg)
+        (loop for c across (subseq arg 1 nil) do
+          (process-short-option c :negated t)))
+       ((long-option-p arg)
+        (process-long-option arg))
+       (t
+         (push arg *command-line-arguments*)
+        (return))))
+    (loop for f in (gethash :finalizers *command-line-option-specification*)
+          do (funcall f))))
+
+(defun process-command-line-options (specification command-line)
+
+  "SPECIFICATION is a list as described above.  COMMAND-LINE
+   is the list of tokens to be parsed.  Return two values:
+   a list of alternating actions and values,
+   and a list of the rest of the arguments after the
+   various options and their values (a tail of the
+   COMMAND-LINE argument)."
+
+  (let*
+      ((*command-line-option-specification*
+        ;; The hash table describing each name.
+       (prepare-command-line-options-specification specification))
+       (*command-line-arguments*
+        command-line)
+       (*command-line-options* nil))
+    (do-process-command-line-options)
+    (values *command-line-options* *command-line-arguments*)))
+
+(defun compute-and-process-command-line-options (specification)
+  (process-command-line-options specification (get-command-line-arguments)))
+
+(defun show-option-help (specification &optional (stream *standard-output*))
+  ;; TODO: be clever when trying to align stuff vertically
+  (loop :for spec :in specification :do
+        (destructuring-bind (names &key negation documentation negation-documentation
+                                   type optional list &allow-other-keys) spec
+          (declare (ignorable negation documentation negation-documentation type optional list))
+          (when documentation
+            (format stream " ~25A  ~A~%"
+                    (format nil "~{ ~A~}" (mapcar 'option-name names))
+                    documentation))
+          (when negation-documentation
+            (format stream " ~25A  ~A~%"
+                    (format nil "~{ ~A~}" (mapcar 'option-name (make-negated-names names negation)))
+                    negation-documentation)))))
+
+#| Testing:
+
+(defparameter *opt-spec*
+ '((("all" #\a) :type boolean :documentation "do it all")
+   (("verbose" #\v) :type boolean :documentation "include debugging output")
+   (("file" #\f) :type string :documentation "read from file instead of standard input")
+   (("xml-port" #\x) :type integer :optional t :documentation "specify port for an XML listener")
+   ("enable-cache" :type boolean :documentation "enable cache for queries")
+   ("path" :type string :list t :optional t :documentation "add given directory to the path")
+   ("port" :type integer :list (:initial-contents (1 2)) :optional t :documentation "add a normal listen on given port")))
+
+(defun foo (args &key all verbose file xml-port enable-cache port path)
+  (list args :all all :verbose verbose :file file :xml-port xml-port
+        :enable-cache enable-cache :port port :path path))
+
+(multiple-value-bind (options arguments)
+    (process-command-line-options
+     *opt-spec*
+     '("--all" "--no-verbose" "--file" "foo" "-f" "-v" "-v"
+       "-x" "--disable-cache"
+       "--no-port" "--port" "3" "--port=4"
+       "--path" "/foo" "--path" "/bar"
+       "--" "--foo" "bar" "baz"))
+  (write arguments :pretty nil) (terpri)
+  (write options :pretty nil) (terpri)
+  (write (apply 'foo arguments options) :pretty nil)
+  (terpri))
+
+(show-option-help *opt-spec*)
+
+|#
diff --git a/pkgdcl.lisp b/pkgdcl.lisp
new file mode 100644 (file)
index 0000000..216180e
--- /dev/null
@@ -0,0 +1,15 @@
+#+xcvb
+(module
+ (:description "package for command-line-arguments"))
+
+(cl:defpackage :command-line-arguments
+  (:use :cl)
+  (:export
+   #:*command-line-arguments*
+   #:*command-line-options*
+   #:*command-line-option-specification*
+   #:process-command-line-options
+   #:compute-and-process-command-line-options
+   #:get-command-line-arguments
+   #:show-option-help
+   ))