logs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Logs-devel] cl-cli proposal -- defopt


From: Vijay Lakshminarayanan
Subject: Re: [Logs-devel] cl-cli proposal -- defopt
Date: Mon, 26 Jun 2006 19:36:19 -0500

Hi Jim,

Here are the modified cl-cli functions:

;; process all options given on the command line
(defun process-options (options-list args)
 (declare (ignorable options-list))
 (flet ((nextarg ()
          (loop with seenflag = 'nil
                as arg in args
                if (not seenflag)
                  do (setq seenflag t)
                else if (optionp arg)
                  do (loop-finish)
                collect (pop args))))
   (loop as cdr on args until (optionp (car cdr)) finally (setq args cdr))
   (loop as nextopt = (nextarg)
         while nextopt
         do (process-switch nextopt options-list))))

(defun optionp (string)
 "Is STRING a command line option?"
 (or (starts-with-p string "--") (starts-with-p string "-")))

(defun starts-with-p (string1 string2 &key (test #'string=))
 "Does STRING1 start with STRING2?"
 (and (>= (length string1) (length string2))
      (funcall test string1 string2 :end1 (length string2))))

(defun same-name-p (string name)
 "Is NAME the same as STRING in terms of command line options?"
 (or (equal name (subseq string 1)) (equal name (subseq string 2))))

;; process an individual switch
(defun process-switch (list options-list)
 (let* ((switch (pop list))
        (opt (car (member switch options-list
                          :test #'(lambda (a b) (same-name-p a (name b)))))))
   (when opt
     (let ((numargs (length list)))
       (multiple-value-bind (minargs maxargs)
           (option-lengths opt)

         (cond
           ((< numargs minargs)
            (error "too few arguments for flag: ~A expecting ~A to
~A, given ~A~%"
                   (name opt) minargs maxargs numargs))
           ((and (numberp maxargs) (> numargs maxargs))
            (error "too many arguments for flag: ~A expecting ~A to
~A, given ~A~%"
                   (name opt) minargs maxargs numargs))
           ((action opt)
            (apply (action opt) list))))))))

;; pull the next flag & its args off the args list
(defun option-lengths (option)
 (let ((min-args 0) ; minimum arguments to this flag
       (max-args 0) ; maximum arguments to this flag
       (seen-optional-arg nil) ; has an optional argument been seen?
       (multiargs nil) ; are an infinite number of arguments possible?
       )
   (flet ((specialp (arg)
            (and (symbolp arg) (char= (elt (symbol-name arg) 0) #\&)))
          (special (arg)
            (let ((arg (intern (symbol-name arg))))
              (ecase arg
                (&optional (setq seen-optional-arg t))
                (&rest (setq multiargs t))
                ((&body &key)
                 (error "Cannot support ~a for command line." arg))))))
     (loop as arg in (arguments option)
           if (specialp arg)
           do (special arg)
           else
           do (incf max-args)
           and unless (or seen-optional-arg multiargs)
               do (incf min-args)
           finally (return (if multiargs
                               (values min-args ())
                               (values min-args max-args)))))))

tell me if you'd rather have the diff file.

I tested it using SBCL on my machine (sbcl allows #! scripts)

Here are the contents of my cli.lisp file:

#!/usr/bin/sbcl --noinform

(require 'cl-cli)

(use-package :cl-cli)

(defun print-file (filename)
 (with-open-file (file filename)
   (loop as line = (read-line file nil nil)
         while line
         do (format *standard-output* "~a~%" line))))

(defvar *opts* NIL)

(defmacro defopt (&key name arguments action description)
 (let ((opt (gensym)))
  `(let ((,opt (make-instance 'cli-opt
                              :name ,name
                              :arguments ',arguments
                              :action ,action
                              :description ,description)))
    (setq *opts* (nconc *opts* (list ,opt)))
    ,opt)))

(defopt
 :name "sum"
 :arguments (&rest args)
 :action #'(lambda (&rest args)
             (loop as i in args
                   summing (parse-integer i) into sum
                   finally (format *standard-output* "~a~%"
                                   sum)))
 :description "Adds the arguments it receives")

(defopt
 :name "cat"
 :arguments (filename)
 :action #'print-file
 :description "Does the equivalent of the UNIX `cat' command.")

(eval-when (:execute)
 (let ((args (cl-cli:get-application-args)))
   (cl-cli:process-options *opts* args)))

;;;;;

And here is my bash interactions:

address@hidden foo]$ ./cli.lisp --sum 12 23 34 45
114
address@hidden foo]$ ./cli.lisp --sum 10 12 --cat ~/.sbclrc
22
;;; -*- Mode: Lisp; -*-
;;; If the first user-processable command-line argument is a filename,
;;; disable the debugger, load the file handling shebang-line and quit.
(let ((script (and (second *posix-argv*)
                  (probe-file (second *posix-argv*)))))
 (when script
   ;; Handle shebang-line
   (set-dispatch-macro-character #\# #\!
                                 (lambda (stream char arg)
                                   (declare (ignore char arg))
                                   (read-line stream)))
   ;; Disable debugger
   (setf *invoke-debugger-hook*
         (lambda (condition hook)
           (declare (ignore hook))
           ;; Uncomment to get backtraces on errors
           ;; (sb-debug:backtrace 20)
           (format *error-output* "Error: ~A~%" condition)
           (quit)))
   (load script)
   (quit)))

(require '#:asdf)
address@hidden foo]$

Thanks
Vijay

On 6/26/06, Vijay Lakshminarayanan <address@hidden> wrote:
Hi Jim

I also think that PROCESS-OPTIONS is better written as

(defun process-options (options-list args)
  (declare (ignorable options-list))
  (macrolet ((nextarg ()
               '(loop with seenflag = 'nil
                      as arg in args
                      if (not seenflag)
                        do (setq seenflag t)
                      else if (optionp arg)
                        do (loop-finish)
                      collect (pop args))))
    (loop initially (loop as cdarg on args
                          ;; Removes the elements that don't start
                          ;; with an option.  So
                          ;; ("lisp" "LoGS.lisp" "--help")
                          ;; will become ("--help")
                          until (optionp (car cdarg))
                          finally (setq args cdarg))
          as nextopt = (nextarg)
          while nextopt
          collect nextopt)))

(defun optionp (string)
  "Is STRING an option?"
  (or (starts-with "-" string) (starts-with "--" string)))

(defun starts-with-p (string1 string2 &key (test #'string=))
  "Does STRING1 start with STRING2?"
  (and (>= (length string1) (length string2))
       (funcall test string1 string2 :end1 (length string2))))

The macrolet is so simple now that maybe we could just make it a flet.

I really love the LOOP macro :-)

Thanks Jim
Vijay

On 6/25/06, Vijay Lakshminarayanan <address@hidden> wrote:
> On 6/25/06, Jim Prewett <address@hidden> wrote:
> >
> > Hello,
> >
> > I'm thinking a more elegant way to specify a command line option might be
> > a macro I'm working on called defopt.  Here is an example usage:
> >
> > (defopt
> >         ;; the name of the flag is "file"
> >         "file"
> >         ;; the flag takes a filename parameter and optional position
> >         ;; parameter
> >         (filename &optional position)
> >         ;; expression (with parameters bound)
> >         ;; to set up things properly for that flag
> >         ;; (this is the body of the lambda function)
> >         (let ((ff
> >                (make-instance
> >                 'org.prewett.LoGS::File-Follower
> >                 :FileName
> >                 filename)))
> >                 ;; if position is specified, start there
> >                 (when position
> >                 (when (not ff) (error "no ff~%"))
> >                 (org.prewett.LoGS::set-file-follower-position
> >                  ff
> >                  (read-from-string position)))
> >                 (push ff *file-list*))
> >         ;; a list of (optional) aliases for this flag
> >         :aliases '("f")
> >         ;; the description of this flag, used in the help text
> >         :description "file to read from")
> >
> > Does this look somewhat reasonable?
> >
> > I'm thinking that by specifying an option this way, the user only has to
> > specify the argument list once, and in a much more lispy way IMO.
>
> It is better than the old scheme :-)  but I don't like many things.
>
> Also, why not have defopt accept the parameters as keyword arguments?
> It makes things clearer and reduces the need for comments.
>
> Something like:
> (defopt :name "file" :alias '("f" "ff")
>         :params (filename &optional position)
>         :action (list filename position))
>
> There are several problems with :action
>
> First, you cannot have recursive functions.  All actions must start
> with an explicit progn or a let or block.  Take the case where an
> action must start with a DECLARE form.
>
> Also we cannot reuse functions with this syntax.  I think we should
> keep the original syntax but instead of strings use sexps.  It doesn't
> matter that we are forced to repeat because of the benefits and
> freedoms we get as a consequence.
>
> Here are a few examples:
>
> :parameters (one two &optional three)
> :actions #'list
>
> :parameters (one two three)
> :actions (lambda (&rest args)
>
> Here's an implementation of option-lengths that uses symbols:
>
> (defun option-lengths (option)
>   (let ((min-args 0) ; minimum arguments to this flag
>         (max-args 0) ; maximum arguments to this flag
>         (seen-optional-arg nil) ; has an optional argument been seen?
>         (multiargs nil) ; are an infinite number of arguments possible?
>         )
>     (flet ((specialp (arg)
>              (and (symbolp arg) (char= (elt (symbol-name arg) 0) #\&)))
>            (special (arg)
>              (let ((arg (intern (symbol-name arg))))
>                (ecase arg
>                  (&optional (setq seen-optional-arg t))
>                  (&rest (setq multiargs t))
>                  ((&body &key)
>                   (error "Cannot support ~a for command line." arg))))))
>       (loop as arg in (arguments option)
>             if (specialp arg)
>             do (special arg)
>             else
>             do (incf max-args)
>             and unless (or seen-optional-arg multiargs)
>             do (incf min-args)
>             finally (return (if (or multiargs seen-optional-arg)
>                                 (values min-args max-args)
>                                 (values min-args ())))))))
>
> While writing this, it occured to me that you could very easily parse
> the options that took single options using:
> (destructuring-bind (&key foo bar baz) (get-application-args)
>   &body)
>
> IMHO there will be places where using a different lambda-list for the
> action and the explicit arguments might make life easier for the
> programmer.  At the same time, as a lisper, I do think life can be
> made simpler with having to specify the arglist only in one place
> since that is the majority of how anyone would like it.  Duplicating
> the code is also error-prone, I admit.
>
> But I would recommend that we keep the arguments and actions
> parameters independant of each other.
>
> > Jim
> Thanks
> Vijay
>





reply via email to

[Prev in Thread] Current Thread [Next in Thread]