A Swift library for defining and parsing command line arguments.
The library provides three macros, each of which generates a peer function that wraps an annotated work function.
MainFunction(shadowGroups:)- generatesmain()a function meant to be called by the operating systemCallFunction(shadowGroups:)- generates a function that can be called by the programmerCommand<T>(shadowGroups:synopsis:children)- generates a command node and a command action
The generated functions parse command arguments lists and call the annotated work functions with the parsed values.
The generated functions can detect "meta-flags" which trigger the generation of "meta-services" like help screens, manual pages, shell completion scripts, version strings, etc. If one is detected in the command argument list, the meta-service is called rather than the wrapped work function.
You can generate a fully functional command line tool, with meaningful error reporting, just by annotating a work function with the MainFunction macro.
Code
import CmdArgLib
import CmdArgLibMacros
@main
struct Main{
@MainFunction
static func greet(
u upper: Flag = false,
count: Int = 1,
_ greeting: String)
{
for _ in 0..<max(count, 1) { print(upper ? greeting.uppercased() : greeting)}
}
}Command Calls
> ./greet "Hello World"
Hello World
> greet -u --count 2 "Hello World"
HELLO WORLD
HELLO WORLD
Note that the command line calls mirror Swift function calls:
greet("Hello World") // --> Hello World
greet(u: true, count: 2, "Hello World") // --> HELLO WORLD\nHHELLO WORLD
This is more evident if you think of the flag "-u" as an abreviation for "-u true".
One of the library's goals is to preserve, to the extent possible, this correspondence between work function parameters and command line arguments.
This is why the syntax of command line arguments accepted by a utiltiy produced using the library differs slightly from that of utilities produced by many other argument parsers. For example, many argument parsers allow option arguments that may or may not consume a value. This is never allowed in Swift, and, accordingly, it is not allowed when using the library.
Command Argument Errors
The main function generated by MainFunction macro detects most command argument syntax errors in a single pass:
> greet -xuy --lower --count 1.5
Errors:
unrecognized options: "-x" and "-y", in "-xuy"
unrecognized option: "--lower"
missing a "<phrase>"
"1.5" is not a valid <int>
There is no reference like "see greet --help ..." in the error screen because "--help" has not been defined.
You can add a help screen to the previous example by adding a parameter with type MetaFlag and
default value MetaFlag(helpElements: [ShowElement]) to the annotated work function.
Code
The code is the same as in the previous example except for the Phrase typealias and
the work function's new help parameter.
import CmdArgLib
import CmdArgLibMacros
typealias Phrase = String
@main
struct Main {
@MainFunction
static func greet(
u upper: Flag = false,
count: Int = 1,
_ greeting: Phrase,
h__help help: MetaFlag = MetaFlag(helpElements: helpElements))
{
for _ in 0..<max(count, 1) { print(upper ? greeting.uppercased() : greeting)}
}
// Lay out the help screen
static let helpElements: [ShowElement] = [
.text("DESCRIPTION:", "Print a greeting."),
.synopsis("\nUSAGE:"),
.text("\nPARAMETERS:"),
.parameter("greeting","A friendly greeting"),
.parameter("upper", "Uppercase the greeting"),
.parameter("count", "The number of times to print the greeting"),
.parameter("help", "Show this help message."),
]
}The typealias adds clarity to the work function's positional parameter, greeting. In this
case, however, its main purpose is to provide a meaningful name for the parameter as it
is represented in the built-in help screen (and in the built-in error screen).
Help Screen
> ./greet --help
DESCRIPTION: Print a greeting.
USAGE: greet [-uh] [--count <int>] <phrase>
PARAMETERS:
<phrase> A friendly greeting.
-u Uppercase the greeting.
--count <int> The number of times to print the greeting (default: 1).
-h/--help Show this help message.
Error Screen
As opposed to the first example, which does hot have a help screen, the error screen refers to the "--help" meta-flag.
> ./greet
Error:
missing a "<phrase>"
See 'greet --help' for more information.
Use a simple command tree when state is not passed from parent node to child node.
You can generate a command node suitable for use in a simple command tree
by annotating a work function with the Command(shadowGroups:synopsis:children) macro. The synopsis parameter
is required. The shadowGroups and children parameters have default values of [].
Top Command Node Code
This node has two children, specified here in Self.subcommands.
import CmdArgLib
import CmdArgLibMacros
@main
struct Main {
@Command(synopsis: "Print quotes by famous people.", children: Self.subcommands)
static func ca1Simple(
t tree: MetaFlag = MetaFlag(treeFor: "ca1-simple"),
h__help help: MetaFlag = MetaFlag(helpElements: help)
) {}
private static let subcommands = [GeneralQuotes.command, ComputingQuotes.command]
private static let help: [ShowElement] = [
.text("DESCRIPTION\n", "Print quotes by famous people."),
.synopsis("\nUSAGE\n", trailer: "subcommand"),
.text("\nOPTIONS"),
.parameter("tree", "Show a hierarchical list of commands"),
.parameter("help", "Show help information"),
.text("\nSUBCOMMANDS"),
.commandNode(GeneralQuotes.command.asNode),
.commandNode(ComputingQuotes.command.asNode),
]
static func main() async {
await runAsMain(command)
}
}The runAsMain(_:) function
- collects the command argument list and passes to
.command'srunmethod - catches and prints errors thrown by the by the
runmethod.
Child Command Node Code
Here is the code for the general quotes command node.
@Command(shadowGroups: ["lower upper"], synopsis: "Print quotes about life in general.")
static func general(
_ count: Count,
l lower: Flag,
u upper: Flag,
h__help help: MetaFlag = MetaFlag(helpElements: help)
) {
let formatter = PhraseFormatter(upper: upper, lower: lower)
printQuotesWith(formatter, count: count, quotes: generalQuotes)
}
private static let help: [ShowElement] = [
.text("DESCRIPTION\n", "Print quotes about life in general."),
.synopsis("\nUSAGE\n"),
.text("\nOPTIONS"),
.parameter("count", "The number of times to print the quote"),
.parameter("lower", "Lowercase the output"),
.parameter("upper", "Uppercase the output"),
.parameter("help", "Show help information"),
.text("\nNOTE\n", "The $L{lower} and $L{upper} options shadow each other."),
]
}The code for the computing quotes command node is similar.
Tree Hierarchy
> ./ca1-simple -t
ca1-simple
├── general - print quotes about life in general
└── computing - print quotes about computing
Help Screens
The help screen for the top node shows its subcommands. The only options are for meta-services which do not affect the operation of the program.
> ./ca1-simple -h
DESCRIPTION
Print quotes by famous people.
USAGE
ca1-simple [-th] <subcommand>
OPTIONS
-t Show a hierarchical list of commands.
-h/--help Show help information.
SUBCOMMANDS
general Print quotes about life in general.
computing Print quotes about computing.
The help screen for the subcommand shows options that affect the operation of the program.
> ./ca1-simple general -h
DESCRIPTION
Print quotes about life in general.
USAGE
ca1-simple general [-luh] <count>
OPTIONS
<count> The number of times to print the quote.
-l Lowercase the output.
-u Uppercase the output.
-h/--help Show help information.
NOTE
The -l and -u options shadow each other.
Command Calls
> ./ca1-simple general -u 1
Quote
SIMPLICITY IS COMPLEXITY RESOLVED. - CONSTANTIN BRANCUSI
> ./ca1-simple general -uxz
Errors:
unrecognized options: "-x" and "-z", in "-uxz"
missing a "<count>"
See 'ca1-simple general --help' for more information.
Use a stateful command tree when you want state to be passed from parent node to child node.
You can generate a command node suitable for use in a stateful command tree
by annotating a stateful work function with the Command<T>(shadowGroups:synopsis:children) where
T conforms to Sendable. The state passed from parent node to child node is an instance of [T].
The synopsis parameter is required. The shadowGroups and children parameters have default values of [].
This example is the same as the previous example except that state, an instance of [PhraseFormatter], is
passed from parent node to child node.
Top Command Node Code
import CmdArgLib
import CmdArgLibMacros
import LocalHelpers
@main
struct Main {
@Command<PhraseFormatter>(
shadowGroups: ["lower upper"],
synopsis: "Print quotes by famous people.",
children: subcommands)
private static func ca2Stateful(
t tree: MetaFlag = MetaFlag(treeFor: "ca2-stateful"),
u__upper upper: Flag,
l__lower lower: Flag,
h__help help: MetaFlag = MetaFlag(helpElements: helpElements),
state: [PhraseFormatter]) -> [PhraseFormatter]
{
let formatter = PhraseFormatter(upper: upper, lower: lower)
return [formatter]
}
private static let subcommands = [GeneralQuotes.command, ComputingQuotes.command]
private static let helpElements: [ShowElement] = [
.text("DESCRIPTION\n", "Print quotes by famous people."),
.synopsis("\nUSAGE\n", trailer: "subcommand"),
.text("\nOPTIONS"),
.parameter("upper", "Show the uppercase version of the quotes"),
.parameter("lower", "Show the lowercase version of the quotes"),
.parameter("tree", "Show a hierarchical list of commands"),
.parameter("help", "Show this help screen"),
.text("\nSUBCOMMANDS"),
.commandNode(GeneralQuotes.command.asNode),
.commandNode(ComputingQuotes.command.asNode),
.text("\nNOTE\n", "The $L{lower} and $L{upper} options shadow each other."),
]
static func main() async {
await runAsMain(command)
}
}Child Command Node Code
Here is the code for general command node.
import CmdArgLib
import CmdArgLibMacros
import LocalHelpers
struct GeneralQuotes {
@Command<PhraseFormatter>(synopsis: "Print quotes about life in general.")
static func general(
_ count: Count,
h__help help: MetaFlag = MetaFlag(helpElements: help),
state: [PhraseFormatter]) -> [PhraseFormatter]
{
let formatter = state.first ?? PhraseFormatter()
printQuotesWith(formatter, count: count, quotes: generalQuotes)
return []
}
private static let help: [ShowElement] = [
.text("DESCRIPTION\n", "Print quotes about computing."),
.synopsis("\nUSAGE\n"),
.text("\nOPTIONS"),
.parameter("count", "The number of times to print the quote"),
.parameter("help", "Show this help screen"),
]
}The code for the computing quotes node is similar`.
Tree Hierarchy
> ./ca2-stateful -t
ca2-stateful
├── general - print quotes about life in general
└── computing - print quotes about computing
Help Screens
> ./ca2-stateful -h
DESCRIPTION
Demonstrate CLI command completion with a stateful hierarchical command.
USAGE
quotes [-tulh] <subcommand>
OPTIONS
-u/--upper Show the uppercase version of the quotes.
-l/--lower Show the lowercase version of the quotes.
-t Show a hierarchical list of commands.
-h/--help Show this help screen.
SUBCOMMANDS
general Print quotes about life in general.
computing Print quotes about computing.
NOTE
The --lower and --upper options shadow each other.
The upper and lower flags are processed by the top node to be passed down to all of the node's children.
The leaves, general and computing, cannot (in this case) override the formatting options processed by the top node.
> ./ca2-stateful computing -h
DESCRIPTION
Print quotes about computing.
USAGE
quotes computing [-h] <count>
OPTIONS
<count> The number of times to print the quote.
-h/--help Show this help screen.
Command Calls
> ./ca2-stateful -u computing 1
Quote
IF A MACHINE IS EXPECTED TO BE INFALLIBLE, IT CANNOT ALSO BE INTELLIGENT. - ALAN TURING
This example wraps sed soley to demonstate some advanced features of the built-in help screen and manual page generators.
WorkFunction
Here is the work function annotated by @MainFunction:
@main
struct Example_8_Sed {
@MainFunction
static func mf8Sed(
n__quiet noEcho: Flag,
p__preview preview: Flag,
i__inplace inplaceEdit: Extension?,
e__expression commands: [Command] = [],
f__commandFile commandFiles: [CommandFile] = [],
_ command: Command?,
_ files: Variadic<File> = [],
generateManpage: MetaFlag = MetaFlag(manPageElements: manPageElements),
h__help help: MetaFlag = MetaFlag(helpElements: helpElements),
version: MetaFlag = MetaFlag(string: "Version 1.0")
) throws { ... }
}Note that function's generateManpage parameter has a default value that references
manPageElements, the array of ShowElements` that "lays out" the generated manual page.
ShowElements
This is the array of ShowElements that defines the generated manual page.
extension Example_8_Sed {
static let manPageElements: [ShowElement] = [
// The prologue (with name section)
.prologue(description: "wrap sed to demonstrate use of manpage support"),
// The synopsis
.synopsis(parameterNameLists: [synopsisLine1Names, synopsisLine2Names]),
// The description
.lines("DESCRIPTION", description01),
.lines("", description02),
.lines("", "The following options are available:"),
.parameter("commands", commands),
.parameter("commandFiles", commandFiles),
.parameter("inplaceEdit", inplaceEdit),
.parameter("noEcho", noEcho),
.parameter("preview", preview),
.lines("", note1),
// Other sections
.lines("", exitStatus),
.lines("", examples),
.lines("", seeAlso),
.lines("", authors),
]
private static let exitStatus = """
.Sh EXIT STATUS
The mf8-sed utility exits 0 on success, and >0 if an error occurs.
"""
private static let examples = """
.Sh EXAMPLES
.Pp
Use quiet mode $S{noEcho}, $L{noEcho}:
.Pp
.Dl > mf8-sed -n 's/foo/zap/gp' test.txt
.Pp
Replace all occurances of ‘foo’ with ‘bar’ in the file test.txt, without creating
a backup of the file:
.Pp
.Dl > mf8-sed -i '' -e 's/foo/bar/g' test.txt
"""
private static let seeAlso = """
.Sh SEE ALSO
.Xr man 1 ,
.Xr mandoc 1 ,
.Xr sed 1 ,
.Xr mdoc 7 ,
.Xr re_format 7
.Rs
.%A Arnold Robbins
.%B sed and awk Pocket Reference, 2nd Edition
.%I O'Reilly Media
.%D 2002
.Re
"""
private static let authors = """
.Sh AUTHORS
The sed utility wrapped by mf8-sed, was written by
.%A Diomidis D. Spinellis <dds@FreeBSD.org> .
.Pp
The $N{} utiility was written (with help screen and manual page text lifted
from the sed utiltiy's manual page) by
.%A Frankie Lee
.%A Judas Priest .
"""
}The synopsisLine1Names, synopsisLine2Names and description01 elements of manPageElements are
defined elsewhere, available for use when defining mf8-sed's help screen well as its manual page:
let synopsisLine1Names = ["noEcho", "preview", "inplaceEdit", "_:Command", "files"]
let synopsisLine2Names = ["noEcho", "preview", "inplaceEdit", "commands", "commandFiles", "files"]
let description01 = """
The mf8-sed utility reads the specified $E{files}s, or the standard input if no
$E{files}s are specified, modifying the input as specified by a list
of $E{command}s. The input is then written to the standard output.
"""Help Screen
> ./mf8-sed --help
DESCRIPTION
A sed wrapper.
USAGE
mf8-sed [-np] [-i <extension>] <command> [<file>...]
mf8-sed [-np] [-i <extension>] [-e <command>] [-f <command_file>] [<file>...]
OPTIONS
-n/--quiet By default, each line of input is echoed to
the standard output after all of the
commands have been applied to it. The -n
option suppresses this behavior.
-p/--preview Print the genrated sed command without
executing it.
-i/--inplace <extension> Edit the <file>s in-place, saving backups
with the specified <extension>. If a
zero-length extension is given (""), no
backup will be saved.
-e/--expression <command> Append <command> to the list of editing
<command>s (may be repeated).
-f/--command-file <command_file> Append the editing <command>s found in the
file <command_file> to the list of editing
<command>s (may be repeated). The editing
commands should each be listed on a
separate line. The <command>s are read from
the standard input if <command_file> is
“-”.
--generate-manpage Generate a man page.
-h/--help Show help information.
NOTES
The mf8-sed tool reads the specified <file>s, or the standard input if no
<file>s are specified, modifying the input as specified by a list of
<command>s. The input is then written to the standard output.
A single <command> may be specified as the first argument to mf8-sed, in
which case no -e or -f options are allowed. Multiple <command>s may be
specified by using the -e or -f options, in which case all arguments are
<file>s. All <command>s are applied to the input in the order they are
specified regardless of their origin.
Regular expressions are always interpreted as extended (modern) regular
expressions.
Command line utilities that use the macros are included in the following repositories:
MainFunctionmacro - CmdArgLib_MainFunctionCallFunctionmacro - CmdArgLib_CallFunctionCommandmacro - CmdArgLib_Command
In addition, CmdArgLib_Completion has an example that generates fish shell and zsh shell completion scripts.
If you are interested in the examples, please start with CmdArgLib_MainFunction.
If you want to experiment further, you can
use cmd-arg-lib-package-manager to initialize
fully functional packages that use cmd-arg-lib. This repository is itself a good example for generating completion
scripts and manual pages for a command with subcommands.
The library's documentation consists of a reference that defines the library's components, example programs, a features list, and Xcode quick help for the library's public functions and methods.
The overall design of the library is based on certain design principles used by Fish Shell, paraphrased and adapted as follows:
-
“The library should have a small set of orthogonal features. Any situation where two features are related but not identical, one of them should be removed, and the other should be made powerful and general enough to handle all common use cases of either feature.”
-
“Every configuration option in a program is a place where the program is too *** to figure out for itself what the user really wants.”
-
“The library's features should be as easy as possible to discover for the user.”
-
“The command argument syntax generated by the library should be uniform.”
The library has the following specific goals:
-
The command line syntax for a program written using the library should map directly to a Swift work function.
-
Command argument syntax errors should be detected in one pass and listed collectively in an error screen.
-
The library should provide full-featured help screen and manual page generators.
-
The library should provide a uniform interface for defining any number of custom meta-flags (not just, say, "--help" and "--version").
-
The library should provide support for stateful command hierarchies.
-
The library should be complete without the need for plug-ins and auxilary tools.
This software is licensed under the Apache License Version 2.0 "ALv2".
The library is in beta phase, version 0.5.7, and has only been implemented for macOS.
The library requires Swift 6.2 and macOS 26.1, or above.
Bug identification and feedback are welcome.
