cmd
is a Janet library for parsing command-line arguments. It features:
- parsing named and positional arguments
- autogenerated
--help
text - hierarchical subcommands
- custom type parsers
- two kinds of
--
escapes - no dependencies
- pure Janet
If you want to use cmd
, add it to the dependencies
in your project.janet
file like this:
(declare-project
:name "my-neat-command-line-app"
:dependencies [
{:url "https://github.com/ianthehenry/cmd.git"
:tag "v1.1.0"}
])
A minimal usage in a script looks like this:
(import cmd)
(cmd/def
--greeting (optional :string "Hello")
name :string)
(printf "%s, %s!" greeting name)
$ greet Janet
Hello, Janet!
$ greet Janet --greeting "Howdy there"
Howdy there, Janet!
While a compiled program looks like this:
(import cmd)
(cmd/main (cmd/fn
[--greeting (optional :string "Hello")
name :string]
(printf "%s, %s!" greeting name)))
By adding a few more annotations, cmd
will autogenerate nice --help
output as well:
(import cmd)
(cmd/def "Print a friendly greeting"
--greeting (optional :string "Hello")
"What to say. Defaults to hello."
name ["NAME" :string])
(printf "%s, %s!" greeting name)
$ greet --help
Print a friendly greeting
greet NAME
=== flags ===
[--greeting STRING] : What to say. Defaults to hello.
[--help] : Print this help text and exit
You will mostly use the following macros:
(cmd/def DSL)
parses(cmd/args)
immediately and puts the results in the current scope. You can use this to quickly parse arguments in scripts.(cmd/fn "docstring" [DSL] & body)
returns a simple command, which you can use in acmd/group
.(cmd/group "docstring" & name command)
returns a command made up of subcommands created fromcmd/fn
orcmd/group
.(cmd/main command)
declares a function calledmain
that ignores its arguments and then calls(cmd/run command (cmd/args))
.
There are also some convenience helpers:
(cmd/peg name ~(<- (some :d)))
returns an argument parser that uses the provided PEG, raising if the PEG fails to parse or if it does not produce exactly one capture. You can use this to easily create custom types.(cmd/defn name "docstring" [DSL] & body)
gives a name to a simple command.(cmd/defgroup name "docstring" & name command)
gives a name to a command group.
You probably won't need to use any of these, but if you want to integrate cmd
into an existing project you can use some lower level helpers:
(cmd/spec DSL)
returns a spec as a first-class value.(cmd/parse spec args)
parses the provided arguments according to the spec, and returns a table of keywords, not symbols. Note that this might have side effects if you supply an(effect)
argument (like--help
).(cmd/run command args)
runs a command returned by(cmd/fn)
or(cmd/group)
with the provided arguments.(cmd/print-help command)
prints the help for a command.(cmd/args)
returns(dyn *args*)
, normalized according to the rules described below.
There is currently no way to produce a command-line spec except by using the DSL, so it's difficult to construct one dynamically.
You can specify multiple aliases for named parameters:
(cmd/def
[--foo -f] :string)
(print foo)
$ run -f hello
hello
By default cmd
will create a binding based on the first provided alias. If you want to change this, specify a symbol without any leading dashes:
(cmd/def
[custom-name --foo -f] :string)
(print custom-name)
$ run -f hello
hello
Named parameters can have the following handlers:
Count | --param |
--param value |
---|---|---|
1 | required |
|
0 or 1 | flag , effect |
optional |
0 or more | counted |
tuple , array , last |
1 or more | tuple+ , array+ , last+ |
Positional parameters can only have the values in the rightmost column.
There is also a special handler called (escape)
, described below.
You can omit this handler if your type is a keyword, struct, table, or inline PEG. The following are equivalent:
(cmd/def
--foo :string)
(cmd/def
--foo (required :string))
However, if you are providing a custom type parser, you need to explicitly specify the required
handler.
(defn my-custom-parser [str] ...)
(cmd/def
--foo (required my-custom-parser))
(cmd/def
--foo (optional :string "default value"))
(print foo)
$ run --foo hello
hello
$ run
default value
If left unspecified, the default default value is nil
.
(cmd/def
--dry-run (flag))
(printf "dry run: %q" dry-run)
$ run
dry run: false
$ run --dry-run
dry run: true
(cmd/def
[verbosity -v] (counted))
(printf "verbosity level: %q" verbosity)
$ run
verbosity: 0
$ run -vvv
verbosity: 3
(cmd/def
[words --word] (tuple :string))
(pp words)
$ run --word hi --word bye
("hi" "bye")
(tuple+)
and (array+)
require that at least one argument is provided.
last
is like optional
, but the parameter can be specified multiple times, and only the last argument matters.
last+
is like required
, but the parameter can be specified multiple times, and only the last argument matters.
(cmd/def
--foo (last :string "default"))
(print foo)
$ run
default
$ run --foo hi --foo bye
bye
(effect)
allows you to create a flag that, when supplied, calls an arbitrary function.
(cmd/def
--version (effect (fn []
(print "1.0")
(os/exit 0))))
$ run --version
1.0
You usually don't need to use the (effect)
handler, because you can do something similar with a (flag)
:
(cmd/def
--version (flag))
(when version
(print "1.0")
(os/exit 0))
$ run --version
1.0
There are three differences:
(effect)
s run even if there are other arguments that did not parse successfully (just as value parsers do).(effect)
handlers do not create bindings.(effect)
handlers run without any of the parsed command-line arguments in scope.
(effect)
mostly exists to support the default --help
handler, and is a convenient way to specify other "subcommand-like" flags.
There are two kinds of escape: hard escape and soft escape.
A "soft escape" causes all subsequent arguments to be parsed as positional arguments. Soft escapes will not create a binding.
(cmd/def
name :string
-- (escape))
(printf "Hello, %s!" name)
$ run -- --bobby-tables
Hello, --bobby-tables!
A hard escape stops all argument parsing, and creates a new binding that contains all subsequent arguments parsed according to their provided type.
(cmd/def
name (optional :string "anonymous")
--rest (escape :string))
(printf "Hello, %s!" name)
(pp rest)
$ run --rest Janet
Hello, anonymous!
("Janet")
You can mix required, optional, and variadic positional parameters, although you cannot specify more than one variadic positional parameter.
(cmd/def
first (required :string)
second (optional :string)
third (required :string))
(pp [first second third])
$ run foo bar
("foo" nil "bar")
$ run foo bar baz
("foo" "bar" "baz")
The variadic positional parameter for a spec can be a hard escape, if it appears as the final positional parameter in your spec. The value of a hard positional escape is a tuple containing the value of that positional argument followed by all subsequent arguments (whether or not they would normally parse as --params
).
Only the final positional argument can be an escape, and like normal variadic positional arguments, it will take lower priority than optional positional arguments.
(cmd/def
name (optional :string "anonymous")
rest (escape :string))
(printf "Hello, %s!" name)
(pp rest)
$ run Janet all the other args
Hello, Janet!
("all" "the" "other" "args")
If the type of a parameter is a struct, it should enumerate a list of named parameters:
(cmd/def
format {--text :plain
--html :rich})
(print format)
$ script --text
:plain
The keys of the struct are parameter names, and the values of the struct are literal Janet values.
You can use structs with the last
handler to implement a toggleable flag:
(cmd/def
verbose (last {--verbose true --no-verbose :false} false)
(print verbose)
$ run --verbose --verbose --no-verbose
false
You can specify aliases inside a struct like this:
(cmd/def
format {[--text -t] :plain
--html :rich})
(print format)
$ script -t
:plain
If the type of a parameter is a table, it's parsed similarly to an enum, but will result in a value of the form [:tag arg]
.
(cmd/def
format @{--text :string
--html :string})
(pp format)
$ run --text ascii
(:text "ascii")
$ run --html utf-8
(:html "utf-8")
You can also specify an arbitrary expression to use as a custom tag, by making the values of the table bracketed tuples of the form [tag type]
:
(cmd/def
format @{--text :string
--html [(+ 1 2) :string]})
(pp format)
$ run --text ascii
(:text "ascii")
$ run --html utf-8
(3 "utf-8")
There are a few built-in argument parsers:
:string
:file
- like:string
, but prints differently in help output:number
:int
- any integer, positive or negative:int+
- non-negative integer (>= 0
):int++
- positive integer (> 0
)
You can also use any function as an argument. It should take a single string, and return the parsed value or error
if it could not parse the argument.
There is also a helper, cmd/peg
, which you can use to create ad-hoc argument parsers:
(def host-and-port (cmd/peg "HOST:PORT" ~(group (* (<- (to ":")) ":" (number :d+)))))
(cmd/def address (required host-and-port))
(def [host port] address)
(print "host = " host ", port = " port)
cmd
will automatically generate a --help
flag that prints the full docstring for a command.
When printing the help for groups, cmd
will only print the first line of each subcommand's docstring.
You can give useful names to arguments by replacing argument types with a tuple of ["ARG-NAME" type]
. For example:
(def name ["NAME" :string])
(cmd/def
name (required name))
(printf "Hello, %s!" name)
$ greet --help
script.janet NAME
=== flags ===
[--help] : Print this help text and exit
If you're supplying an argument name for a required parameter, you must use an explicit (required)
clause: --foo (required ["ARG" :string])
, not --foo ["ARG" :string]
.
If you're writing a variant, the argument name must come after the tag:
(cmd/def
variant @{--foo [:tag ["ARG" :string]]})
By default, cmd
performs the following normalizations:
Before | After |
---|---|
-xyz |
-x -y -z |
--foo=bar |
--foo bar |
-xyz=bar |
-x -y -z bar |
Additionally, cmd
will detect when your script is run with the Janet interpreter (janet foo.janet --flag
), and will automatically ignore the foo.janet
argument.
You can bypass these normalizations by using cmd/parse
or cmd/run
, which will parse exactly the list of arguments you provide them.
These are not fundamental limitations of this library, but merely unimplemented features that you might wish for. If you wish for them, let me know!
- You cannot make "hidden" aliases. All aliases will appear in the help output.
- You cannot specify separate docstrings for different enum or variant choices. All of the parameters will be grouped into a single entry in the help output, so the docstring has to describe all of the choices.
- There is no good way to re-use common flags across multiple subcommands.
- There is no auto-generated shell completion file, even though we have sufficient information to create one.
- Docstrings no longer have to be string literals, so you can construct a dynamic docstring with
(string/format ...)
. Note that the expression has to be a form to disambiguate it from a parameter name, so if you have the docstring in a variable already you have to write(|docstring)
instead ofdocstring
in order for the macro to parse it correctly. cmd/parse
now errors if there was a parse error, instead of returning just the arguments that parsed correctly
- Fix
--help
output when used in a compiled executable.
- Fix
cmd
when used in a compiled executable.
--help
output only prints the basename of the executable in the usage line, regardless of the path that it was invoked with--help
output forgroup
ed commands now includes the subcommand path in the usage line- positional arguments print more nicely in the usage line
- improved error message for unknown subcommands when using
cmd/group
cmd/peg
can now take a pre-compiled PEG
- Initial release.