README.org 6.31 KB
Newer Older
Eric Timmons's avatar
Eric Timmons committed
1 2 3 4 5 6 7 8 9 10 11 12
#+TITLE: Adopt Subcommands
#+AUTHOR: Eric Timmons <eric@timmons.dev>

This library is an extension of Steve Losh's [[https://docs.stevelosh.com/adopt/][Adopt]] library (hands down my
favorite CLI options parser for CL) to support arbitrarily nested
subcommands. Not all features of Adopt are nicely supported yet (like producing
man pages for subcommands), but the basic functionality is there.

Subcommands are organized into folders and terminals. Both folders and
terminals can have options associated with them. Folders can additionally
contain other folders and terminals. Terminals *must* have a function
associated with them that will be called when a CLI command is dispatched. This
Eric Timmons's avatar
Eric Timmons committed
13 14 15
function must accept three arguments: a list of arguments (strings), an =EQL=
hash table of processed arguments, and a path object denoting all the folders
and terminal traversed.
Eric Timmons's avatar
Eric Timmons committed
16 17

Folders *may* have a function associated with them that is called before
Eric Timmons's avatar
Eric Timmons committed
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
attempting to further process the remaining arguments. This is useful to bind
dynamic variables or munge the options passed down to the later levels. This
function, if provided, must take four arguments: a list of arguments (strings),
an =EQL= hash table of options, a path object denoting all the folders and
traversed so far, and a thunk that must be called to continue processing.

This library tries to follow the same aesthetic as Adopt (such as interfaces
encouraged to be defined with =defparameter= and errors signaled with some
restarts provided) with two primary exceptions:

1. Any function you provide for terminals or folders is called with the results
   of processing the command line. You do not need to call
   =adopt:parse-options= or similar yourself.
2. Printing help and exiting is a common enough operation that this library
   provides native support for it, so you do not need to duplicate the checks
   for your program's help option everywhere.

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
* How to use

  In this section, we describe how to use the library, partially illustrated by
  examples from [[file:simple-demo.lisp][=simple-demo.lisp=]] (see the History section for more details on
  the provenance of this file).

  First, you must define your program's hierarchy. Terminals are defined using
  =make-subcommand-terminal=. It accepts the same arguments as
  =adopt:make-interface=, with the addition of the mandatory =:function= and
  optional =:key= keywords.

  The provided function will be called once we dispatch based on the command
  line arguments.

  The key is used if you want to specify the name of the subcommand; it is
  normally not required as it is derived from =:name=.

  For example:

  #+begin_src common-lisp
    (defparameter *ui/foo*
      (adopt-subcommands:make-subcommand-terminal
        :name "subex foo"
        :usage "foo [-a|-b]"
        :summary "foo some things"
        :help "foo some things"
        :contents (list *o/foo/a* *o/foo/b*)
        :function 'run/foo))
  #+end_src

  Folders are defined using =make-subcommand-folder=. It accepts the same
  arguments as =adopt:make-interface=, with the addition of the optional
  =:function= and =:key= keywords.

  If provided, the function will be called once we dispatch based on the
  command line arguments, before continuing to process the remainder of the
  arguments. It can be used to establish dynamic bindings or otherwise munge
  the arguments before continuing to process them.

  The key is used if you want to specify the name of the folder; it is normally
  not required as it is derived from =:name=.

  Additionally, the =:contents= of a folder can include other folders or
  terminals. For example:

  #+begin_src common-lisp
    (defparameter *ui*
      (adopt-subcommands:make-subcommand-folder
        :name "subex"
        :usage "[subcommand] [options]"
        :help "subcommand example program"
        :summary "an example program that uses subcommands"
        :contents (list *o/help* *o/version* *ui/foo* *ui/bar*)
        :function 'run/global))
  #+end_src

  Next, you must actually process the arguments and call the desired
  functions. This is accomplished using the =dispatch= function. It requires a
  folder as an argument. It can additionally be provided the list of
  arguments. Last, if =print-help-and-exit= is non-NIL, it should name our
  program's help option. If provided, this library processes as much of the
  command line as it can. Then, if the help option is non-NIL, prints the help
  and exits the program.

  For example:

  #+begin_src common-lisp
    (defun toplevel ()
      (handler-bind ((adopt-subcommands:folder-is-terminal 'adopt-subcommands:print-help-and-exit))
        (adopt-subcommands:dispatch *ui* :print-help-and-exit 'help)))
  #+end_src

Eric Timmons's avatar
Eric Timmons committed
107 108 109 110 111 112 113 114 115
* History

  This work was largely inspired by Steve Losh's [[https://old.reddit.com/r/Common_Lisp/comments/m7gjno/writing_small_cli_programs_in_common_lisp_steve/grdqq1j/][Reddit comment]] on a quick and
  dirty method to implement subcommands using Adopt. For a while, I had my own
  way of doing this (that also supported arbitrarily nested subcommands), but it
  was very nasty because I had somehow missed the existence of the
  =adopt:treat-as-argument= restart (d'oh). So after seeing that, I cleaned up my
  code and packaged it up as a library for others to use.

Eric Timmons's avatar
Eric Timmons committed
116 117
  For reference, the file =simple-demo.lisp= shows how to implement Steve's
  Reddit example using this library.
Eric Timmons's avatar
Eric Timmons committed
118 119 120 121 122

* Deficiencies

  These are the current deficiencies that I would like to fix some day:

Eric Timmons's avatar
Eric Timmons committed
123 124 125 126
  1. We reach into Adopt's internals a bit. Need to reach out to Steve and
     figure out if he's amenable to exporting the accessors we need and/or
     refactoring the help/man page generation to be more modular (and export
     the new components).
Eric Timmons's avatar
Eric Timmons committed
127

Eric Timmons's avatar
Eric Timmons committed
128
  2. Man page generation is not yet supported.
Eric Timmons's avatar
Eric Timmons committed
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148

  3. Options need to come after the subcommand that defines them. For example,
     if =-a= is defined as part of =foo='s interface, the following will not
     work:

     #+begin_src shell
       my-program -a foo
     #+end_src

     But this will:

     #+begin_src shell
       my-program foo -a
     #+end_src

     This could be easily fixed for parameterless options. It could also be
     easily fixed for options with parameters, so long as the argument list has
     the parameter in the same token as the option (e.g. =--foo=5= instead of
     =--foo 5=). But a general purpose solution is harder and it is not obvious
     it's worth it.