Contents

ouser4629/cmd-arg-lib

## Command Argument Library

Command Argument Library

An experimental Swift library for defining and parsing command line arguments.


Contents

- Command Line Tool - Command Line Tool With A Help Screen - Simple Command Tree - Stateful Command Tree - Sed Wrapper With a Manual Page

- Overview - MainFunction - Command - Meta-Parameters


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](REFERENCE.md#mainfunction) macro.

<details>
<summary>Code</summary>

```swift
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)}
    }
}
```

</details>

<details>
<summary>Command Calls</summary>

```
> ./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:

```swiff
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.

</details>

<details>
<summary>Command Argument Errors</summary>

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.

</details>

---

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

<details>
<summary>Code</summary>

The code is the same as in the previous example except for the `Phrase` typealias and
the work function's new `help` parameter.

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

</details>

<details>
<summary>Help Screen</summary>

```
> ./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.
```

</details>

<details>
<summary>Error Screen</summary>

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

</details>

---

### Simple Command Tree

Use a [simple command tree](REFERENCE.md#simple-command-tree) when state is not passed
from parent node to child node.

You can generate a [command node](REFERENCE.md#command-node) suitable for use in a simple command tree
by annotating a [work function](REFERENCE.md#work-function) with the `Command(shadowGroups:synopsis:children)` macro. The `synopsis` parameter
is required. The `shadowGroups` and `children` parameters have default values of `[]`.

<details>
<summary>Top Command Node Code</summary>

This node has two children, specified here in `Self.subcommands`.

```swift
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.


</details>

<details>
<summary>Child Command Node Code</summary>

Here is the code for the general quotes command node.

```swift
    @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)
        try 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.

</details>

<details>
<summary>Tree Hierarchy</summary>

```
> ./ca1-simple -t
ca1-simple
├── general - print quotes about life in general
└── computing - print quotes about computing
```

</details>

<details>
<summary>Help Screens</summary>

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

</details>

<details>
<summary>Command Calls</summary>

```
> ./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.
```

</details>

---

### Stateful Command Tree

Use a [stateful command tree](REFERENCE.Md#command-tree) when you want state to be passed
from parent node to child node.

You can generate a [command node](REFERENCE.md#command-node) suitable for use in a stateful command tree
by annotating a [stateful work function](REFERENCE.md#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.

<details>
<summary>Top Command Node Code</summary>

```swift
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)
    }
}
```

</details>

<details>
<summary>Child Command Node Code</summary>

Here is the code for general command node.

```swift
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()
        try 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`.

</details>

<details>
<summary>Tree Hierarchy</summary>

```
> ./ca2-stateful -t
ca2-stateful
├── general - print quotes about life in general
└── computing - print quotes about computing
```

</details>

<details>
<summary>Help Screens</summary>

```
> ./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.
```

</details>

<details>
<summary>Command Calls</summary>

```
> ./ca2-stateful -u computing 1
Quote
  IF A MACHINE IS EXPECTED TO BE INFALLIBLE, IT CANNOT ALSO BE INTELLIGENT. - ALAN TURING
```

</details>

---

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

<details>
<summary>WorkFunction</summary>

Here is the work function annotated by `@MainFunction`:

```swift
@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.


</details>

<details>
<summary>ShowElements</summary>

This is the array of `ShowElements` that defines the generated manual page.

```swift
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:

```swift
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.
    """
```

</details>


<details>
<summary>Help Screen</summary>

```
> ./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.
```

</details>

<details>
<summary>Manual Page</summary>

```
> ./mf8-sed  --manpage > mf8-sed.1
> man ./mf8-sed.1
```

![ManPageInLess](ZOther/MF8_ManPage.jpg)

</details>

---

How it Works

Overview

The library is based on the concept of a work function, a function of with simple parameter types, like String, String?, [String], and Flag (a typealias for Bool), that implements all or part of your program's logic.

The library provides a pair of peer macros that are meant to annotate work functions. They generate functions that parse a command arguments list, typically passed in by a terminal shell, and pass the parsed values to the annotated work function.

A work function can have meta-parmeters, that can be used to trigger "meta-services" like help screens, manual pages, shell completion scripts, version strings, etc. When a meta-parameter's argument is encountered in the command argument list, a function provided by the meta-parameter's default value is called instead of the work function itself.

The work function's parameter names and types determine the names and types of options that can be in a valid command argument list.

The peer macros are as follows:

  • [MainFunction(shadowGroups: [String] = )](REFERENCE.md#mainfunction)
  • [Command<T>(shadowGroups: [String] = , synopsis: String, children: [Command<T>])](REFERENCE.md#command).

The first is the work horse, used for simple command line tools. The second is used for tools with hierachical commands.

Each shadowGroup string is a whitespace-separated list of work function parameter names. The shadow group defined by the string is the set of options corresponding to the parameters named in the list. If an option is a member of a given shadow group, only the last member of the group in a command argument list will be recognized by the parser.


MainFunction

The MainFunction macro generates two functions:

  • main() - a function meant to be called by the operating system
  • run(_ names: [String], with: [String]) - a function meant to be called by the programmer

run takes an array of "call names" that will appear in help screens, etc., and an array of "words" that constitute the command arguments list to be parsed. If the annotated work function returns a value run will return the value. If the work function is async, run will be async. run always throws because errors can be detected during parsing.

main() obtains the command arguments list provided by CommandLine.arguments and calls run. It disregards any values returned by the annotated work function, and catches and processes any exceptions thrown during parsing or by the work function.


Command

The Command<T> macro generates a command node, an instance of StatefulCommand<T:Sendable>.

The last parameter of a work function annotated by a Command<T> must be state: [T], and the function must return [T].

A command node has an internal command action, which basically is the same as the run function generated by the WorkFunction macro, except that, along with parsed values, it passes a state argument to the annotated work function.

A command node also has a "public" 'run` method:

func run(with: [String], state: [T] = [], commandPath: [StatefulCommand<T>] = []) async throws -> ([T], [String])

A command node's run method calls the node's command action with its words and state to obtain new state and remaining unparsed words, if any. If the command node has children, the method calls the appropriate child's run method with the new state and unprocessed words and returns the value returned by the child node's run method. If all goes well, all command node run methods return (final-state, ), otherwise appropriate errors are thrown.

Typically, you will call only the top in a command heirarchy, ignoring the state and commandPath parameters whcih are used internally to recurse down the heirarchy.


Meta-Parameters

An annotad work function can have meta-parmeters, that can be used to trigger "meta-services" like help screens, manual pages, shell completion scripts, version strings, etc.

A meta-parameter has type MetaFlag or MetaOption, and always has a default value. Instances of these types have a function of type MetaTypeFunction that generate the meta-service.

When a meta-parameter is "encountered" in the command argument list, its default value's meta-type-function is called rather than the work function itself. All of a work function's meta-parameters" automatically shadow each other so at most one will be called. In addition, they meta-parameters trump all other parameters and are called whether or not the argument list contains errors.


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 alpha phase, version 0.1.0, 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.

Package Metadata

Repository: ouser4629/cmd-arg-lib

Default branch: main

README: README.md