Skip to content

ouser4629/cmd-arg-lib

Repository files navigation

Command Argument Library

A Swift library for defining and parsing command line arguments.


Contents


Overview

The library provides three macros, each of which generates a peer function that wraps an annotated work function.

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.


Samples

Command Line Tool

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.


Command Line Tool With A Help Screen

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.

Simple Command Tree

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's run method
  • catches and prints errors thrown by the by the run method.
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.

Stateful Command Tree

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

Sed Wrapper With a Manual Page

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

ManPageInLess


Example Programs

Command line utilities that use the macros are included in the following repositories:

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.


Documentation

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.


Design

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.


License

This software is licensed under the Apache License Version 2.0 "ALv2".


Project Status

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.

About

A Swift library for defining and parsing command line arguments.

Topics

Resources

License

Stars

Watchers

Forks

Languages