steinmetz

A Scheme command-line-parsing library

Contents

  1. Sources
  2. Using steinmetz
  3. Option type
    1. Syntactic interface
    2. Procedural interface
  4. Parsing
    1. Parsing conventions
    2. Procedures
  5. Conditions and Exceptions
    1. Invalid option
    2. Invalid argument
    3. Extra argument
    4. Missing argument
  6. Printing usage information
  7. To-do and not to-do
  8. Acknowledgements
  9. License

Sources

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.

Using steinmetz

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.

Option type

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).

Syntactic interface

(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.

Note

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.

Exception protocol

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.

Example

(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")))

Procedural interface

(make-option names arg-name arg-parser [docstring [cname [allowed-args [user-data]]]]) → option

Argument types:

names

A list of strings.

arg-name

A symbol or #f

arg-parser

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)))
docstring

A string describing the option, or #f.

cname

A string giving the option’s canonical name, or #f.

allowed-args

A list of strings giving the set of valid arguments for the option, or #f.

user-data

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.

Parsing

Parsing conventions

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.

Procedures

(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.

Exception protocol

process-command-line follows parse-command-line’s exception protocol.

Examples

(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:

options
A list of Options.
proc
A procedure as described below.
cl-list
A list of strings.
seed
An arbitrary value.

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.

Exception protocol

All these conditions are raised continuably.

Conditions and Exceptions

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:

Otherwise, returns #f.

All the condition types below inherit from the &error type defined in (rnrs conditions) or SRFI 35.

Invalid option condition

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.

Invalid argument condition

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.

Extra argument condition

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.

Missing argument condition

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.

Printing usage information

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:

port
A textural output port.
options
A list of Options.
header
footer
Strings. footer defaults to the empty string.
width
A positive exact integer. Currently defaults to 75.

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.)

Example

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

To-do and not to-do

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.

To-do

A better low-level interface

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.

Default arguments

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.

Other

(Probably) not to-do

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.)

Subcommands

(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.

Optional arguments

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.

X Windows–style long options

(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.)

Acknowledgements

I have taken inspiration from the following tools while designing steinmetz:

License

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.

Home