Skip to content

Instantly share code, notes, and snippets.

@guns
Last active December 28, 2015 22:48
Show Gist options
  • Save guns/7573819 to your computer and use it in GitHub Desktop.
Save guns/7573819 to your computer and use it in GitHub Desktop.
tools.cli proposal
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
@jszakmeister
Copy link

As mentioned in my email to you, I think it would be nice to consider offering much of the option handling for arguments too. argparse in Python does this, and it's a real pleasure to use. The idea is that if you're going to have a library that can help validate various aspects about your options, help convert them, and provide defaults then why would you foist that same work on your callers for argument? The specification is largely the same. The real difference is that argument order matters where option order generally does not. Since you're already using a list of lists, I think this would work out nicely.

I do think adding ClojureScript support would be a nice addition too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment