A Swift library for defining and parsing command line arguments.
Define CLI interfaces directly from Swift command function signatures, preserving a close correspondence between Swift function calls and command-line usage.
Layout help screens and manpages with show elements, allowing any number of headers, synopsis sections, parameter description sections, notes, etc., in any order.
- Features
- Examples
- Core Concepts
- Overview
- Example Programs
- Usage in a SwiftPM project
- Documentation
- Project Status
- Automatic, type-safe parsing of CLI arguments
- Consistent, predictable Swift-based CLI syntax
- Separation of concerns
- Definition of CLI syntax via function signatures
- Generated run functions for parsing CLI arguments and invoking program logic
- Generated context functions for documentation of CLI syntax and program logic via meta-services
- Compile-time error reporting, e.g.,
- Duplicate short label names
- Meta-flag parameters without default values
- Parameter types not allowed by the library's macros
- Hierarchical command structures with state propagation
- Composable help screens (not template-based)
- Manual page generation with full mdoc support
- Tree diagram generation
- Reusable error screens (for parser and user code)
- Multiple syntax errors reported in a single pass
- Completion script generation for zsh and fish
In this example, the library's @MainFunction macro annotates a simple command function, greet:
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)
}
}
}The macro generates a CLI interface, argument parser, and error reporting.
Command Calls
> greet "Hello World"
Hello World
> greet -u --count 2 "Hello World"
HELLO WORLD
HELLO WORLD
This mirrors Swift function calls:
greet("Hello World") // --> Hello World
greet(u: true, count: 2, "Hello World") // --> HELLO WORLD\nHELLO WORLDConceptually, flags behave like boolean parameters (e.g., -u corresponds to u: true), reinforcing the mapping between CLI syntax and Swift function calls.
A core goal of the library is to preserve this correspondence.
Error Screen
> greet -xuy --lower --count 1.5
Errors:
unrecognized options: "-x" and "-y", in "-xuy"
unrecognized option: "--lower"
missing a "<string>"
"1.5" is not a valid <int>
The error screen does not refer to help (because no help flag was defined in the command function).
This is the same as the previous example except:
- the command function has an additional parameter arbitrarily named "help"
- the type of the greeting parameter is changed to
Phrase, a typealias forString - the default value of the help parameter refers to an array of
ShowElement - the array of
ShowElementdefines the function's help screen
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)
}
}
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 help parameter's label, h__help, indicates that the corresponding option in the CLI should have two forms: short, -h, and long, --help.
Because the greeting parameter's type is Phrase instead of String, the value placeholder for the greeting parameter in the function's help and error screens is <phrase> instead of <string>.
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
> ./greet
Errors:
unrecognized options: "-x" and "-y", in "-xuy"
unrecognized option: "--lower"
missing a "<phrase>"
"1.5" is not a valid <int>
See 'greet --help' for more information.
As opposed to the first example, which does not have a help screen, the error screen refers to the --help meta-flag.
Greet can be converted to a leaf node, ready to be included in a hierarchal command structure, simply
by changing the MainFunction macro to a Command macro:
import CmdArgLib
import CmdArgLibMacros
public typealias Phrase = String
public struct Greet {
@Command(synopsis: "Print a greeting.")
public static func greet(...) { ... }
public static let helpElements: [ShowElement] = [ ... ]
}Everything has been marked public so that Greet can be imported by the code in the next example.
This is the top node of a simple command tree that has Greet as a subnode.
import CmdArgLib
import CmdArgLibCompletions
import CmdArgLibMacros
import Greet
@main
struct Top {
@Command(synopsis: "Show quotes and recommended books", children: subcommands)
static func cf1Simple(
generateFishCompletionScript: MetaFlag = MetaFlag(
completionScriptFor: .fish, name: "cf1-simple", showElements: helpElements),
generateZshCompletionScript: MetaFlag = MetaFlag(
completionScriptFor: .zsh, name: "cf1-simple", showElements: helpElements),
v__version version: MetaFlag = MetaFlag(string: "0.1.0"),
t__tree tree: MetaFlag = MetaFlag(treeFor: "cf1-simple", synopsis: ""),
h__help help: MetaFlag = MetaFlag(helpElements: helpElements)
) {}
private static let subcommands = [
Quotes.commandNode, Books.commandNode, Greet.commandNode
]
static let helpElements: [ShowElement] = [
.text("DESCRIPTION\n", "Show quotes and recommended books."),
.synopsis("\nUSAGE\n", line: ["help", "tree", "$_:Subcommand"]),
.text("\nOPTIONS"),
.parameter("tree", "Show a hierarchical list of commands"),
.parameter("help", "Show this help screen"),
.parameter("version", "Show the version."),
.text("\nSUBCOMMANDS"),
.commandContext(Quotes.commandNode.context),
.commandContext(Books.commandNode.context),
.commandContext(Greet.commandNode.context),
]
private static func main() async {
await runAsMain(commandNode)
}
}Two of the subcommands are in the same target as the top node. The greet subcommand is imported.
The Command macro generates a command node with three subcommands. The annotated command function, cf1Simple offers five meta-services, but
does not perform any program logic. The completion and tree generators will automatically include subcommands, including imported subcommands.
The completion script options are purposely excluded from the help screen and the generated completion scripts. They could, of course, be included by adding the corresponding show elements (e.g., .parameter("generateFishCompletionScript", "Generate ..."),).
Tree Hierarchy
> cf1-simple -t
cf1-simple
├── quotes
│ ├── general - print quotes about life
│ └── computing - print quotes about computing
├── books - print a list of recommended books
└── greet - print a greeting
Help Screen
> cf1-simple -h
DESCRIPTION
Show quotes and recommended books.
USAGE
[-ht] <subcommand>
OPTIONS
-t/--tree Show a hierarchical list of commands.
-h/--help Show this help screen.
-v/--version Show the version.
SUBCOMMANDS
quotes Print quotes by famous people.
books Print a list of recommended books.
greet Print a greeting.Command Calls
> cf1-simple greet -h
DESCRIPTION: Print a greeting.
USAGE: [-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.> cf1-simple greet -xyzu --count 2.0
Errors:
unrecognized options: "-x", "-y" and "-z", in "-xyzu"
missing value: "<phrase>"
"2.0" is not a valid <int>
See "cf1-simple greet --help" for more information.> cf1-simple quotes general -u 1
QUOTE
SIMPLICITY IS COMPLEXITY RESOLVED. - CONSTANTIN BRANCUSIThis example shows a hierarchical command structure in which state is passed down from parent node to child node.
Tree Hierarchy
> cf2-stateful -t
cf2-stateful
├── quotes
│ ├── general - print quotes about life in general
│ └── computing - print quotes about computing
└── books - print a list of recommended books
Top-Level Node Help Screen
> cf2-stateful -h
DESCRIPTION
Show quotes and recommended books.
USAGE
[-hlut] [-f <text_format>] <subcommand>
OPTIONS
-h/--help Show this help screen.
-l/--lower Show the lowercase version of the quotes.
-u/--upper Show the uppercase version of the quotes.
-t/--tree Show a hierarchical list of commands.
-v/--version Show the version.
-f/--format <text_format> A text format to use when displaying quotes ("red",
"yellow" or "underlined").
SUBCOMMANDS
quotes Print quotes by famous people.
books Print a list of recommended books.The formatting options are specified at the top level, to be passed down to lower level nodes.
Command Calls
> cf2-stateful --upper quotes computing 1
QUOTE
IT IS MUCH MORE REWARDING TO DO MORE WITH LESS. - DONALD KNUTHThis example has both a help screen and a manual page.
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 generated 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 Function
@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 the function's generateManpage parameter has a default value that references
manpageElements, the array of ShowElements that lays out the generated manual page.
Manpage ShowElements
import CmdArgLib
import Foundation
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(lines: [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 occurrences 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{} utility was written (with help screen and manual page text lifted
from the sed utilitiy's manual page) by
.%A Frankie Lee
.%A Judas Priest .
"""
}The synopsisLine1Names, synopsisLine2Names, and description01 elements of manpageElements are
defined elsewhere and are available for use when defining mf8-sed's help screen as 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.
"""The "$_:Command" is a "dummy synopsis parameter", a synopsis element that can be useful when defining complex synopsis sections.
- A command function is a Swift function:
- whose signature defines a program's CLI
- and whose body implements its behavior
- Peer macros annotate command functions, generating
runfunctions that:- parse command-line arguments
- pass parsed values to the command functions
- Meta-services:
- provide program information (e.g., help screens)
- provide usage tooling (e.g., shell completion scripts)
- Show elements:
- are composable layout primitives used to construct help screens and manual pages
- support show macros for string interpolation of parameter names, labels, etc.
- support synopsis elements for precise control over synopsis line content
A command function defines a command-line tool’s interface and behavior.
- It's parameter types must be:
- types conforming to
BasicParameterType(e.g.,String,Int,Double) Flag,MetaFlag, orMetaOption[T],T?, orVariadic<T>whereT: BasicParameterType
- types conforming to
- Any
String-backed enum can conform toBasicParameterEnum(and thusBasicParameterType) - Other types can be extended to conform to
BasicParameterType
The library provides two peer macros: MainFunction and Command
MainFunctiongenerates:- a
runfunction for programmatic use - a
mainentry point for system invocation
- a
Commandgenerates a command node- The node has a
runmethod that can be called directly or recursively by its parent node
- The node has a
- If an annotated command function is exception-pure, the generated
runfunction will also be exception-pure
Command arguments and how they are parsed are defined using Swift function parameters.
- Define a command function
- Define a CLI by annotating the command function with one of the library's macros
- Pass command-line arguments to the generated
runfunction and catch any exceptions - Alternatively, let the system call the generated
mainfunction, which handles all exceptions - The
runandmainfunctions parse command-line arguments and pass the parsed values to the command function
Meta-services are command-line behaviors that replace normal execution, such as help screens, manual pages, and shell completion generation.
- A command function's meta-services are defined by its meta-parameters
- A meta-parameter has type
MetaFlagorMetaOptionand must have a default value - The default value has a function of "context" that performs the meta-service
- The library supplies the required context
- The function is called when the corresponding meta-paramter is encountered in the command line argument list
- Meta-parameters trump all others, and they are triggered even if the argument list has syntax errors
- A command function can have any number of meta-parameters
- Meta-parameters shadow each other; only the last one in the argument list is treated as encountered
- There are no reserved names (e.g.,
--help,--version) for meta-parameters
Exceptions are used to "return" messages and error messages back to the peer functions generated by the library's macros.
Exceptionis an enum that conforms toError,CustomStringConvertible, andSendable- It has four cases:
stdout(String),stderr(String),error(String)anderrors([String]) - It has a static function
printAndExit(for error: Error, callNames: [String]):- Intended for use at the system invocation level
- For
Exception: prints to stdout, stderr, or an error screen - For other errors: formats and prints an error screen
- Exceptions with case
.erroror.errorsappear in the same format as those detected by the parser: with a header, call names, list of errors and a reference to a help screen (if available) - Exceptions can be used to keep generated
runfunctions exception-pure (assuming no other I/O)
The following repositories have short example programs:
- CmdArgLib_MainFunction - Basic features
- CmdArgLib_Command - Hierarchical commands and completion scripts
The complete code for the first two examples and the sed example can be found in the CmdArgLib_MainFunction repository. The code for the two hierarchical command examples can be found in the CmdArgLib_Command repository.
Add this to the project's overall dependencies:
.package(url: "https://github.com/ouser4629/cmd-arg-lib.git", branch: "main"),Add this to the target dependencies for each target that uses cmd-arg-lib:
.product(name: "CmdArgLib", package: "cmd-arg-lib"),
.product(name: "CmdArgLibMacros", package: "cmd-arg-lib"),
.product(name: "CmdArgLibCompletions", package: "cmd-arg-lib"),The library's documentation consists of a reference that defines the library's components, example programs and Xcode quick help for the library's public functions and methods.
This software is licensed under the Apache License Version 2.0 "ALv2".
The library is in beta, version 0.1.4, and has only been implemented for macOS.
The library requires Swift 6.2 and macOS 26.1, or later.
