Skip to content

ouser4629/cmd-arg-lib

Repository files navigation

Command Argument Library

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.


Contents


Features

  • 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

Examples

Greet

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 WORLD

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


Greet with a Help Screen

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 for String
  • the default value of the help parameter refers to an array of ShowElement
  • the array of ShowElement defines 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 As a Leaf Node

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.


Greet in a Command Tree

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 BRANCUSI

Stateful Command Tree

This 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 KNUTH

Sed Wrapper With a Manual Page

This 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.
Manual Page
> ./mf8-sed  --manpage > mf8-sed.1
> man ./mf8-sed.1

ManpageInLess

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.


Core Concepts

  • 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 run functions 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

Overview

Command Function

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, or MetaOption
    • [T], T?, or Variadic<T> where T: BasicParameterType
  • Any String-backed enum can conform to BasicParameterEnum (and thus BasicParameterType)
  • Other types can be extended to conform to BasicParameterType

Macros

The library provides two peer macros: MainFunction and Command

  • MainFunction generates:
    • a run function for programmatic use
    • a main entry point for system invocation
  • Command generates a command node
    • The node has a run method that can be called directly or recursively by its parent node
  • If an annotated command function is exception-pure, the generated run function will also be exception-pure

Argument Parsing

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 run function and catch any exceptions
  • Alternatively, let the system call the generated main function, which handles all exceptions
  • The run and main functions parse command-line arguments and pass the parsed values to the command function

Meta-services

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 MetaFlag or MetaOption and 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

Exceptions are used to "return" messages and error messages back to the peer functions generated by the library's macros.

  • Exception is an enum that conforms to Error, CustomStringConvertible, and Sendable
  • It has four cases: stdout(String), stderr(String), error(String) and errors([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 .error or .errors appear 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 run functions exception-pure (assuming no other I/O)

Example Programs

The following repositories have short example programs:

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.


Usage in a SwiftPM project

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

Documentation

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.


Project Status

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.

About

A Swift library for defining and parsing command line arguments.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages