On Sun 25 Aug 2013 at 09:05:15PM -0500, gaz jones wrote:

> Hey, i am the current maintainer of tools.cli - i have very little
> time to make any changes to it at the moment (kids :) ). I'm not sure
> what the process is for adding you as a developer or transferring
> ownership etc but if I'm happy to do so as I have no further plans for
> working on it.

Hello Gareth,

Sorry for delay in action. I submitted my Clojure CA that week and have
been casually awaiting confirmation ever since.

Only today have I noticed that my name has appeared on
http://clojure.org/contributing, so I suppose that is confirmation
enough.

This is my proposal for tools.cli:

* Merge the CLI arguments lexer from github.com/guns/optparse-clj to
  support GNU option parsing conventions.

  Examples:
  https://github.com/guns/optparse-clj#features

  guns.cli.optparse/tokenize-arguments:
  https://github.com/guns/optparse-clj/blob/master/src-cljx/guns/cli/optparse.cljx#L25-74

  GNU options parsing spec:
  https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html

* Adapt tools.cli/cli to use the arguments lexer, then freeze it to
  maintain backwards compatibility.

* Create a new function tools.cli/parse-opts, based largely on the
  design of guns.cli.optparse/parse, that supports the following
  features:

  - Granular options specification map.

    Given the following setup:

    (ns my.ns
      …)

    (def cli-options
      [["-s" "--server HOSTNAME" "Remote server"
        :default (java.net.InetAddress/getByName \"example.com\")
        :default-desc "example.com"
        :parse-fn #(java.net.InetAddress/getByName %)
        :assert-fn (partial instance? Inet4Address)
        :assert-msg "%s is not an IPv4 host"]
       […]
       ])

    A call to (clojure.tools.cli/compile-option-specs cli-options)
    will result in the following PersistentArrayMap:

    {option-id                  ; :server
     {:short-opt    String      ; "-s"
      :long-opt     String      ; "--server"
      :required     String      ; "HOSTNAME"
      :desc         String      ; "Remote server"
      :default      Object      ; #<Inet4Address example.com/93.184.216.119>
      :default-desc String      ; "example.com"
      :parse-fn     IFn         ; #(InetAddress/getByName %)
      :assoc-fn     IFn         ; assoc
      :assert-fn    IFn         ; (partial instance? Inet4Address)
      :assert-msg   String      ; "%s is not an IPv4 host"
      }}

    The optspec compiler will verify uniqueness of option-id,
    :short-opt, and :long-opt values and throw an AssertionError on
    failure.

    The optspec map is a PAM to preserve options ordering for summary
    generation.

  - Customizable options summary.

    tools.cli/parse-opts will return an options summary string to
    the caller. Printing the summary with a banner will be the
    responsibility of the caller.

    The default options summary will look like:

      -p, --port NUMBER      8080         Remote port
      -s, --server HOSTNAME  example.com  Remote server
          --detach                        Detach and run in the background
      -h, --help

    The above format can be changed by supplying an optional :summary-fn
    flag that will receive the optspec map values from above and return
    a summary string. The default summary-fn will be a public var.

    This addresses TCLI-3.

  - Optional in-order options processing, with trailing options parsed
    by default.

    This is necessary for managing different option sets for
    subcommands. Indirectly addresses TCLI-5.

  - No runtime exceptions.

    While parse-opts will throw an AssertionError for duplicate
    option-id, :short-opt, and :long-opt values during compilation,
    option parsing errors will no longer throw exceptions.

    Instead, a map of {option-id error-string} will be provided, or nil
    if there are no errors. Correspondingly, parse-opts will have the
    following function prototype:

    parse-opts:
      [argument-seq [& option-vectors] & compiler-flags]
      ->
      {:options   {Keyword Object}   ; {:server "my-server.com"}
       :arguments PersistentVector   ; non-optarg arguments
       :summary   String             ; options summary produced by summary-fn
       :errors    {Keyword String}   ; error messages by option
       }

    The expected usage of this function will look like:

    (def usage-banner
      "Usage: my-program [options] arg1 arg2\n\nOptions:\n")

    (defn exit [status msg]
      (println msg)
      (System/exit status))

    (defn -main [& argv]
      (let [{:keys [options arguments summary errors]}
            (cli/parse-opts argv cli-options :in-order true)]
        (when (:help options)
          (exit 0 (str usage-banner summary)))
        (when (not= (count arguments) 2)
          (exit 1 (str usage-banner summary)))
        (when errors
          (exit 1 (string/join "\n" (cons "The following errors have occured:\n"
                                          (vals errors)))))
        (apply my-program! options arguments)))

  - ClojureScript support.

    github.com/guns/optparse-clj currently supports CLJS/node.js via
    the cljx preprocessor and an extern file for the Closure Compiler
    `process` namespace.

    If this is desirable for tools.cli, a similar setup will be applied,
    except that I will attempt to avoid cljx for easier hacking.

Comments are appreciated, and I am eager to amend this proposal to gain
community acceptance.

Cheers,
Sung Pae