Steinmetz exists in both R6RS and R7RS versions.
The R6RS version of steinmetz is on GitHub. In addition to the R6RS standard libraries, it depends on SRFIs 1, 115, and 152. SRFI 64 is required to run the included tests.
A tarball of the R7RS version of steinmetz is available on FTP. In addition to the SRFIs mentioned above, it uses SRFIs 35, 128, and 125.
There are five libraries of general utility:
(steinmetz parse)Exports the two primary parsing procedures.
(steinmetz exceptions)Exports procedures for raising and catching parsing exceptions.
(steinmetz options)Exports the low-level option interface.
(steinmetz syntax)Exports the options
syntactic interface.
(steinmetz usage)Exports put-usage,
a procedure for printing
formatted usage information drawn from a set of options.
The catch-all (steinmetz) library exports
everything exported by the above libraries.
Command-line options are described by the Option type.
The high-level syntactic interface to creating Options will be
described first, followed by the high-level procedural interface,
and then finally the lower-level core procedures exported by
(steinmetz options).
(options clause
…) → (list Option)
The options form is a convenient interface to the
options library. Each clause describes a single option and is
of one of the following forms:
(flag name-or-names [docstring])
(option name-or-names [arg-spec [docstring]])
The (flag …) form describes a boolean flag
which takes no arguments; (option …) describes
an option taking an argument.
name-or-names is either a string, an
identifier, or a list of strings or identifiers, and gives
the acceptable forms of an option (with leading dashes
omitted). docstring-text is a string
describing the option. arg-spec describes
the option’s argument and takes one of the following forms:
arg-name
(arg-name (enum …))
(arg-name conv)
arg-name is an identifier giving a metalinguistic
name for the argument.
If the second component of an args-spec is of the form
(enum …), where each enum is
an identifier or string, then these describe the only allowed
values of an “enumerated” argument. Otherwise, the second
component must be an expression evaluating to a procedure. The
resulting procedure is the converter (see
make-cli-option) of
the new option.
Rationale: In this simplified interface, argument converters and enumerated arguments are treated as mutually exclusive. Converting symbolic names to non-string values doesn’t seem useful enough to justify additional syntax.
Option names and allowed arguments may be written as identifiers for convenience, but will be stored as strings in the actual Option objects. For example,
(map option-names (options (flag (v verbose))))
⇒ (("v" "verbose"))
The acceptable command-line form of each name is determined
by its length. A single-character name like o
will be parsed as a “short” option, i.e. -o,
while a two-or-more-character name like output
will be parsed as a “long” option, i.e.
--output. See
Parsing conventions below for
more details on how options and their arguments are parsed.
The value of an options form is a list of option
structures representing the CLI options described by its
clauses.
If an option is constructed with the “enumerated” argument
syntax described above, then it is given an argument parser that
validates the option’s argument. If the argument isn’t one of
the allowed values, the parser raises a condition satisfying
invalid-argument-condition?.
TODO: An exception should be raised if the names of two or more clauses overlap.
(define my-opts
(options
(option (f file) (FILE "-") "input file")
(option (k chunk) (NUM string->number) "chunk to operate on")
(option (e) (ENDIANNESS (big little)) "stream endianess")
(flag (v verbose) "verbose output")
(flag ("1") "a numeric option")))
(make-option names arg-name
arg-parser [docstring
[cname
[allowed-args
[user-data]]]]) → option
Argument types:
A list of strings.
A symbol or #f
A procedure that accepts a single string-list
argument and return two values: the value of the option’s
argument and a suffix of the list argument. The list
argument passed by parse-command-line will
never be empty. An argument
parser should signal a parser exception (see
Conditions and
Exceptions) if its input list
is invalid. For example, the following is a parser
suitable for use with parse-command-line:
(lambda (tokens)
(values (car tokens) (cdr tokens)))
A string describing the option, or
#f.
A string giving the option’s canonical name, or
#f.
A list of strings giving the set of valid arguments
for the option, or #f.
An arbitrary Scheme value. The user-data field can be used to attach custom metadata to an option.
Returns a new Option with fields determined by the
arguments. Omitted optional arguments default to
#f.
(option-names option) →
(list string)
Returns a list of the accepted names of option.
(option-argument-name option) →
(or symbol #f)
Returns the symbolic name of option’s argument.
(option-argument-parser option)
→ (or procedure false)
Returns the argument parser associated with option,
or #f if the option takes no argument.
(option-docstring option)
→ (or string #f)
Returns option’s documentation string.
(option-allowed-arguments option)
→ (or (list string) #f)
Returns the list of valid arguments for option.
(option-canonical-name option)
→ (or string #f)
Returns option’s canonical name.
(option? obj) → boolean
Returns #t if obj is an Option and
#f otherwise.
A single option may have multiple “short” (single-character)
and “long” (two-or-more-character) names. Following tradition
(and POSIX), short options without arguments followed by a
single option that may take an argument can be run together.
For example, -ab is equivalent to -a
-b if the -a option takes no argument. The argument
of a short option is either the next token, or the rest of
the option string. e.g. -ab is equivalent to
-a b (option a, argument
b) whenever the -a option takes an argument.
(The distinction between option clusters and short options
with run-in arguments is clear but rather subtle.)
For a long option, the argument is the next token unless an
argument was supplied as part of the option token, with the two
components being separated by an = character.
For example, --output=foo.log is equivalent to
--output foo.log.
The first non-argument occurrence of --
terminates option parsing. All remaining tokens are treated
as arguments.
(process-command-line options
[cl-list]) → [alist, list]
A easy-to-use high-level parsing procedure.
options is a list of Option objects. Parses
cl-list (a list of strings, or the value of
(cdr (command-line)) by default) and returns
two values: an alist associating the name of each option
encountered (as a string) with a list of arguments, and a
list of tokens unassociated with any of the options (the
“operands”). The order of cl-list is preserved
in the operand list and in each option’s argument list, but
the order of the options-and-arguments alist is
unspecified.
For the keys of the alist return value,
process-command-line uses each
option’s canonical name.
If an option has no canonical name,
the first element of the option’s names
list is used.
process-command-line follows
parse-command-line’s
exception
protocol.
(let ((opts (options
(option (f) FILE "input file")
(flag (v) "verbose output")))
(cl '("-f" "file1" "-v" "-f" "file2" "frobnitz")))
(process-command-line opts cl))
⇒ (("v" #t) ("f" "file1" "file2"))
("frobnitz")
(let ((opts
(options
(option (e endianness) (ENDIANNESS (big little))))))
(parse-command-line opts '("-e" "little" "foo"))
; ⇒ (("endianness" "little")) ("foo")
(parse-command-line opts '("-e" "medium" "foo")))
; parser exception: invalid argument "medium"
FIXME: Flags aren’t treated specially at the moment, so
n appearances of a single flag will result in a list
of n #t values.
(parse-command-line options proc
cl-list seed …) → *
Argument types:
A general-purpose parsing procedure similar to SRFI 37’s
args-fold. Applies proc to options
and arguments derived from cl-list. At each step,
proc is applied to an Option, the option’s argument
value (or #f if the option has no argument), and
the current seeds. For example, if the first two elements of
cl-list are "-f" and "foo",
then proc will be invoked as (proc f-opt
"foo"), where f-opt is the Option whose names
include f. proc is expected to return
one or more values, the first of which is interpreted as a
boolean indicating whether to keep parsing: if it is false,
parse-command-line halts immediately, returning the
current seeds and the remaining suffix of cl-list
as multiple values.
The seed arguments are used as the initial seeds, and the remaining values returned by proc (those after the “keep parsing?” boolean) are used as the seeds for the next step.
If a token is encountered which is not an option or
an option’s argument, it is passed as the second argument
to proc, and proc’s first (option)
argument will be #f. For example, if the first
element of cl-list is "frob", then
proc will be invoked as
(proc #f "frob").
parse-command-line returns the values returned
by the final invocation of proc, followed by the
remaining suffix of cl-list.
TODO: An example using parse-command-line to
do things that are difficult to do with the higher-level
form.
If an unknown option is encountered while parsing,
parse-command-line raises a
condition satisfying
invalid-option-condition?.
If an option’s argument is missing, a condition satisfying
missing-argument-condition? is raised. Note
that this will normally only happen when an option appears
at the end of the command-line list. In particular,
arguments that look like options will not trigger an
exception. (This is conventional but unfortunate, since an
argument like "-y" is probably a mistake.)
If a long-style flag (a nullary option) is given an
argument using opt=arg
syntax, a condition satisfying
extra-argument-condition? is raised. (It would
be nice to raise this for short-style flags with run-in
arguments, but that case seems to be syntactically
ambiguous.)
All these conditions are raised continuably.
The forms in this section are exported by (steinmetz
exceptions).
Since inheritance is the least-portable part of the
R6RS condition system, I have not yet given steinmetz
a condition-type hierarchy. Steinmetz-specific conditions can
nevertheless be identified using the
parser-condition? predicate.
The R7RS version of steinmetz uses
SRFI 35
conditions. Since that SRFI doesn’t define the
&irritants condition type, (steinmetz
exceptions) defines its own irritants condition type and
exports its accessors.
(parser-condition? obj) → boolean
Returns #t if obj satisfies one of the
following predicates:
invalid-option-condition?invalid-argument-condition?extra-argument-condition?missing-argument-condition?Otherwise, returns #f.
All the condition types below inherit from the
&error type defined in (rnrs
conditions) or SRFI 35.
Constructor: (make-invalid-option-condition)
→ condition
Predicate: (invalid-option-condition?
obj) → boolean
A condition indicating that an unknown option was encountered during parsing.
(invalid-option-exception .
irritants)
Raises continuably an invalid-option condition compounded with a condition encapsulating the irritants.
Constructor: (make-invalid-argument-condition
option-name) → condition
Predicate: (invalid-argument-condition?
obj) → boolean
Accessor: (invalid-argument-condition-option-name
condition) → string
A condition indicating that the option with the given option-name was given an invalid argument. (This is different from an extra-argument condition, which indicates that the option doesn’t take an argument but was given one anyway.)
(invalid-argument-exception option-name .
irritants)
Raises continuably an invalid-argument condition compounded with a condition encapsulating the irritants.
Constructor: (make-extra-argument-condition
option-name) → condition
Predicate: (extra-argument-condition?
obj) → boolean
Accessor: (extra-argument-condition-option-name
condition) → string
A condition indicating that the option with the given option-name was given an argument even though it doesn’t accept one. (This is different from an invalid-argument condition, which indicates that the option does take an argument but was given one that was unsuitable.)
(extra-argument-exception option-name .
irritants)
Raises continuably an extra-argument condition compounded with a condition encapsulating the irritants.
Constructor: (make-missing-argument-condition
option-name) → condition
Predicate: (missing-argument-condition?
obj) → boolean
Accessor: (missing-argument-condition-option-name
condition) → string
A condition indicating that the option with the given option-name expected an argument but didn’t get one.
(missing-argument-exception option-name .
irritants)
Raises continuably an missing-argument condition compounded with a condition encapsulating the irritants.
The following procedure can be used to document a command-line program using information drawn from a set of Options.
(put-usage port options
header [footer [width]])
→ unspecified
Argument types:
Writes to port the header, followed by a formatted description of the options, followed by footer. The description of options is columnated and spans at most width characters whenever possible. header and footer are printed verbatim, i.e. they are not reflowed to width. (TODO: Consider whether it might better to reflow footer.)
Note that the exact appearance of put-usage’s
output is subject to change.
;; From the sox(1) usage, modified.
;; (SoX is GPLv2 software maintained by Chris Bagwell and SoX
;; Contributors.)
(define sox-opts
(options
(option (buffer) BYTES "Set the size of all processing buffers")
(flag (clobber) "Don't prompt to overwrite output file")
(flag (D no-dither) "Don't dither automatically")
(option (dft-min) NUM "Minimum size (log2) for DFT processing")
(flag (G guard) "Use temporary files to guard against clipping")
(flag (h help) "Display version number and usage information")
(option (replay-gain) (TYPE (track album off)) "Apply ReplayGain")))
(put-usage (current-output-port) sox-opts "Usage: sox [options] ...")
;; Prints:
Usage: sox [options] ...
--buffer BYTES Set the size of all processing buffers
--clobber Don't prompt to overwrite output file
-D, --no-dither Don't dither automatically
--dft-min NUM Minimum size (log2) for DFT processing
-G, --guard Use temporary files to guard against
clipping
-h, --help Display version number and usage
information
--replay-gain track|album|off
Apply ReplayGain
Those who have read this far may have noticed that steinmetz has some limitations. Some of these are intentional and others remain to be removed.
Building a command-line parser with make-option
and parse-command-line takes some effort. Much that
would be taken care of automatically by options and
process-command-line must be done by hand, and some
things (like default arguments) are no easier with the low-level
interface than they are with the high-level one.
Part of the difficulty stems from having two interfaces, one of
which is implemented with the other. It has been a constant
temptation to add clever features to
process-command-line which would have added an
unacceptable amount of complexity to
parse-command-line. I’m confident that putting the
general interface before the convenience layers has been a Good
Thing for the overall library design, but the general interface
still seems to be pushing the boundaries of usability.
These have proved surprisingly difficult. They are an excellent
example of a feature that could be added to
process-command-line only by horribly twisting
parse-command-line (or so it seems). Since the
low-level parsing procedure operates on user-defined seeds, it is
hard to see how default arguments could be inserted.
Shell (bash, zsh, etc.) completions. I don’t use these shells, so this is currently a low priority.
Possibly split an option’s argument on a delimiter (POSIX recommends a comma) if one is given.
process-command-line: It’s clumsy to have to
deal with a list of arguments (instead of a single argument)
in many cases. Possibly provide some way to indicate that an
option should appear at most once.
process-command-line could use this information
to associate such an option with a single argument, and to
raise an exception if the option occurred again.
The goal of this section is to briefly explain why I think certain features shouldn’t be supported, and not to criticize other people’s ideas. Many, many different approaches to option parsing exist, and they all work fine for the tools that use them. Difficulties arise when trying to support a range of approaches. To avoid complexity, it is necessary to be selective. (Despite this, some of the features below can still be implemented using steinmetz.)
(e.g. cvs import.)
Subcommands can be implemented on top of steinmetz, e.g. by dispatching on the command word to one of several parsing procedures with their own options lists. (An example of this would be nice.) Supporting them explicitly would mean using a different option “grammar” depending on the command word. This would drastically change steinmetz’s parsing strategy, which currently assumes all command-line tokens are created equal.
These are deprecated by POSIX and seem to be impossible to
implement unambiguously, unless we take a principled stand on
what an argument looks like. Otherwise, how do you decide
whether -b in -a -b is an option or an
argument? I’ll look into how other systems handle optionals, but
I suspect such arguments are just too messy.
(e.g. -geometry)
These are still popular, but they conflict very badly with POSIX
short-option clusters and with short options with run-together
arguments. (e.g. -geometry could be equivalent to
-g eometry, or to -g -e ometry, or, at
the extreme, to -g -e -o -m -e -t -r -y.)
I have taken inspiration from the following tools while designing steinmetz:
args-fold
(SRFI 37) by
Anthony Carrico.
optparse-applicative by Paolo Capriotti and others.
let-posix
by Daphne Preston-Kendal.
Copyright © 2022–2026 Wolfgang Corcoran-Mathe
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.