A little brother for Swift Argument Parser

I mentioned way back that I was working on an argument parser as a hobby project.

After some thought, I decided to give it a name, Command Argument Library, and make it available for comment on GitHub.

This might pique your interest:

  • The Library's architecture is completely different from that of Swift Argument Parser

    • Swift Argument Parser is based on conforming structs and property wrappers
    • The Library is based on macros (30 percent of its source code is macro support)
    • The Library's code base is much smaller than that of Swift Argument Library
  • Programs built using the Library are smaller, by over a megabyte

Here are the relevant links:

9 Likes

Are there any plans to add support for Embedded Swift?

That is interesting. Up until now, I wasn’t thinking about size, just API.

Based on the following, would it be worth the trouble?

Here are the sizes for the examples in the MainFunction guide:

[I] release> ls  -lF | grep 'mf.*\*'
-rwxr-xr-x   1 po  staff    400408 Dec  9 17:13 mf1-basic*
-rwxr-xr-x   1 po  staff    425384 Dec  9 17:13 mf2-enums*
-rwxr-xr-x   1 po  staff    419464 Dec  9 17:13 mf3-lists*
-rwxr-xr-x   1 po  staff    403768 Dec  9 17:13 mf4-errors*
-rwxr-xr-x   1 po  staff    420656 Dec  9 17:13 mf5-positionals*
-rwxr-xr-x   1 po  staff    414464 Dec  9 17:13 mf6-show-macros*
-rwxr-xr-x   1 po  staff    400456 Dec  9 17:13 mf7-man*
-rwxr-xr-x   1 po  staff    406216 Dec  9 17:13 mf8-sed*

These could be reduced, possibly by about 150K as follows:

  • remove support for command hierarchies
  • remove support for help
  • keep manual page support (but compile only in debug mode)

You could still have -h/β€”help just refer to β€œman this-or-that”, or open a web page , etc.

In theory, the Library could provide package traits to churn out a minimal footprint version.

For which shells does CAL generate shell completion scripts?

If none, do you plan to generate them in the future? For which shells?

Do you have a feature comparison table for CAL & SAP?

Can you relax the project constraints to permit older macOS & Swift versions?

Thanks.

No. Frankly I don't know enough about SAP to even try. I have never used it in a project (because I have always needed multiple flexible headers and notes in my help screens).

You need Swift-Syntax, preferably with prebuilts for macros. That is a recent enhancement, :folded_hands:.

By the way, it appears that CAL builds a little faster than SAP, thanks to the prebuilts.

Thanks for the info.

For which shells does CAL generate shell completion scripts?

If none, do you plan to generate them in the future? For which shells?

Swift Syntax requires only macOS 10.15 & Swift 5.9.

Pre-builts are useful only for build performance, right? If so, it makes sense to me to allow people to use CAL if at all possible, just note in your README / documentation that building will be much slower without pre-builts, which are only available on Swift X+ & specified OS versions & newer.

Requiring macOS 26+ will probably be a no-go for every public tool, and for at least some private tools.

Requiring Swift newer than 6.0 precludes using CAL for any projects that want to be distributed via a Homebrew Core formula, until Homebrew drops support for macOS 14 in late 2026, or until Homebrew supports cross-OS-version builds (not likely anytime soon or possibly ever).

Thank you for the feedback. I need all the help I can get. Eventually this must be fixed.

As to the completion script generator. I noticed a bug in the fish completion script generator (which I want to fix before responding to your questions).

These are sizes for source files not binary code?

Have you verified the actual final binary size for executables built for any of the platforms supported by Embedded Swift?

With Embedded Swift dead code elimination will remove features not used by a top level of a dependency graph (usually an executable target). Users don't pay for what they don't use, so there's no need to exclude source code unless it's incompatible.

So what I'm inquiring about is Embedded Swift compatibility (maybe CI jobs that build with Embedded Swift to verify that), not source code size.

They are binary, static library.

Not at all. CAL is at 0.1.5 - made pubic just for comments re the API. No way is this at the "Announce" forum level. That said. Your comment is valid and well appreciated.

Once again, I am blown away by Swift.

1 Like

CAL does not support any shell completion generator. One size (even for a given shell) does not fit all. CAL lets you roll your own (or use/hack one written by someone else).


I have written a package that lets you add a fish-shell generator to your project.

import FishCompletion

@MainFunction
private static func cfChoice(
       ...
        fish: MetaFlag = MetaFlag(fishElements: helpElements),  // <-- just add this
        help: MetaFlag = MetaFlag(helpElements: helpElements)) 
{ ... }
 

The FishCompletion package also supports completion for stateful (and simple) command trees:

@main
struct TopLevel {

    private static let mainCommand = StatefulCommand<PhraseFormatter>(
        name: "cf-ca2-stateful",
        synopsis: "Cmd_2 - Stateful commands.",
        config: actionConfig(),
        showElements: help,
        action: Self.action,
        children: [
            Cmd02GeneralQuotes.command,
            Cmd02ComputingQuotes.command,
            FishCompletionScript.command,  // <-- generic, supplied by FishCompletion
            CmdTree.command, // <-- generic, supplied by CmeArgLib
        ])

Here is a sample using the simple command tool, with a MetaFlag:

First the help screen:

[I] CmdArgLib_Completion> cf-choice --help
DESCRIPTION: Print the names of selected animals and plants.

USAGE: cf-choice [-b <bird>] [-r <reptile>] [-i <insect>] [-m <mammal>...] -f <fruit>
                 -v <vegetable> -h <herb>... <tree>...

ANIMALS:
  -b/--bird <bird>             A "canary", "dove" or "penguin" (default: "dove").
  -r/--reptile <reptile>       A "snake", "lizard" or "turtle" (default: "none").
  -i/--insect <insect>         A "wasp", "bee" or "ant" (can be repeated).
  -m/--mammals <mammal>...     One or more instances of "dog", "cat" or "cow".

PLANTS:
  -f/--fruit <fruit>           A "banana", "orange" or "apple".
  -v/--vegetables <vegetable>  A "spinach", "broccoli" or "carrot" (can be repeated).
  -h/--herbs <herb>...         One or more instances of "basil", "mint" or "dill".
  <tree>...                    One or more instances of "oak", "pine" or "maple".

Then some completions:

[I] release> cf-choice <TAB>
oak  pine  maple

[I] release> cf-choice oak <TAB>
oak  pine  maple

[I] release> cf-choice -v   <TAB>
spinach  broccoli  carrot

[I] release> cf-choice -v carrot <TAB>
oak  pine  maple

[I] release> cf-choice -m  <TAB>
dog  cat  cow

[I] release> cf-choice -m dog  <TAB>
dog  cat  cow


Here is a. sample with a stateful command tree, using the FishCompletionScript.command:

First the help screen:

[I] > cf-ca2-stateful --help
DESCRIPTION: Print quotes by famous people.

USAGE: cf-ca2-stateful [-lu] [-f <phrase_format>] <subcommand>

SUBCOMMANDS:
  general     Print quotes about life in general.
  computing   Print quotes about computing.
  fish        Print fish completion script.
  tree        Print the tree hierarchy.

OPTIONS:
  -l                    Lowercase the output.
  -u                    Uppercase the output.
  -f <phrase_format>    A "white", "yellow" or "underlined".

NOTE:
  The -l and -u options shadow each other.
  The possible <phrase_format>s are "white", "yellow" or "underlined".

Then some completions:

[I] CmdArgLib_Completion> cf-ca2-stateful <TAB>
general  computing  fish  tree

[I] CmdArgLib_Completion> cf-ca2-stateful -<TAB>
-f  -l  -u  --help

[I] CmdArgLib_Completion> cf-ca2-stateful -f <TAB>
white  yellow  underlined

[I] CmdArgLib_Completion> cf-ca2-stateful -f white  <TAB>
general  computing  fish  tree

[I] CmdArgLib_Completion> cf-ca2-stateful -f white general 1 <RET>
Quote:
  Well done is better than well said. - Benjamin Franklin

[I] CmdArgLib_Completion> cf-ca2-stateful -<TAB>  -f white general 1
-f  -l  -u  --help

I have no intention of writing a similar package for any other shell. Only fish-shell :heart:.

I should have added that the .parameter ShowElementConstructor does have a third parameter: ( ..., completionHint: CompletionHint) where:

public enum CompletionHint: Equatable, Sendable {

    /// do not suggest anything - not even the label. Normally all labels are suggested.
    case ignore

    /// require a value,  like the name of a person, no  suggestion are made
    case exclusive

    /// require a file name, suggestions are the shell's full on path expansion
    case pathExpansion

    /// require a value, suggesion is based on white-space  seperated choices in the string
    case choice(String)

    /// require the name of a file in the current directory that matched the glob pattern (full on path expansion is off)
    case file(String)

    /// require the name of a subdirectory in the current directory, suggestion is sub -directoriy ed in current directory that mtches the glob pattern,
    /// (full on path expansion is off)
    case directory(String)
}

So CAL does have some scant upport for shell completion script generators.

Here is how it was used in the cf-choice example mention earlier:

extension CaseIterable where AllCases.Element: CustomStringConvertible {
    static var oneOf:  String { "A \(Self.casesJoinedWith("or"))" }
    static var arrayOf:  String {"A \(Self.casesJoinedWith("or")) (can be repeated)"}
    static var listOf:  String {"One or more instances of \(Self.casesJoinedWith("or"))"}
}
@main
struct Choice {

       @MainFunction
    private static func cfChoice(
        b__bird bird: Bird = .dove,
        r__reptile maybeReptile: Reptile?,
        i__insect insects: [Insect] = [],
        m__mammals mammals: Variadic<Mammal> = [],
        f__fruit fruit: Fruit,
        v__vegetables vegetables: [Vegetable],
        h__herbs herbs: Variadic<Herb>,
        _ trees: Variadic<Tree>,
        fish: MetaFlag = MetaFlag(fishElements: helpElements),
        help: MetaFlag = MetaFlag(helpElements: helpElements)
    )  { ... }

   private static let helpElements: [ShowElement] = [
        .text("DESCRIPTION:", "Print the names of selected animals and plants."),
        .synopsis("\nUSAGE:"),
        .text("\nANIMALS:"),
        .parameter("bird", Bird.oneOf, completionHint: .choice(Bird.names)),
        .parameter("maybeReptile", "\(Reptile.oneOf) (default: \"none\")", completionHint: .choice(Reptile.names)),
        .parameter("insects", Insect.arrayOf, completionHint: .choice(Insect.names)),
        .parameter("mammals", Mammal.listOf, completionHint: .choice(Mammal.names)),
        .text("\nPLANTS:"),
        .parameter("fruit", Fruit.oneOf, completionHint: .choice(Fruit.names)),
        .parameter( "vegetables", Vegetable.arrayOf, completionHint: .choice(Vegetable.names)),
        .parameter( "herbs", Herb.listOf, completionHint: .choice(Herb.names)),
        .parameter( "trees", Tree.listOf, completionHint: .choice(Tree.names)),
    ]

Thanks for all the info about completions. I don't currently have time to digest all of what you wrote, but a few questions / comments:

  1. It seems like FishCompletionScript.command provides fish completions, so what is the purpose of fish: MetaFlag = MetaFlag(fishElements: helpElements)?
  2. I might not want to have a command to exclusively generate a fish completion script. I might want it as a flag instead. Or I might want an option or command that accepts an argument specifying the shell for which I want to generate a completion script. Etc. Can the completion script generator be used outside of the command?
  3. How does one configure FishCompletionScript.command? There are many completion setups that would require more info than standard parameter definitions. It would be nice to specify such completion settings either via attributes or macros (I don't know much about macros). If you are able to configure completions for FishCompletionScript.command, it would be useful for the same configuration to be used for all shells.
  4. pathExpansion DocC should be something like:
/// accept a file system; suggestions are provided by the shell's built-in completion path suggestion mechanism to existing file system items
case pathExpansion
  1. case pathExpansion could be expanded to case pathExpansion(String? = nil) or case pathExpansion(String = ""), where the associated value is a shell glob that limits suggestions. In zsh, at least, you can limit the suggestions via a glob to directories only (append (/) to the glob), plain files only (append (.)), etc. I assume you can do the same in fish. file & directory place restrictions for the use of globs, and can be replaced by glob arguments to case pathExpansion(String? = nil), at least on zsh. I don't see why anyone would want to not allow completing in other directories via absolute or relative paths. Maybe there's a use case, but I think it could be handled in zsh by *(.) & *(/) for files & directories, respectively. To include dot files, use *(D.) & *(D/).

There is no point talking about shell completion scripts in CAL until you understand how use meta-flags. I know it is a lot to ask, but I would suggest that you read the CAL README and run/play with the examples in the MainFunction guide before trying to understand what I wrote about the FishCompletion module.

That said, I will try to answer your questions.

But first, the module is not part of CAL. I mentioned it in answer to your question regarding completion scripts to show that CAL has a uniform, open interface for adding any number of goodies, including shell completion generators with any number of configuration parameters.


There are two examples. One is a simple command tool, the other has subcommands. The FishCompletionScript.command is a generic node in a command tree that you do not have to write yourself. It is obviously not applicable to the first example.

I only showed the subcommand example because the completion script is a little more complex when parent commands have their own completions, which have to be deactivated when the curser is moved to the next command. Let's forget about that for today.

The fish in fish: MetaFlag = MetaFlag(fishElements: helpElements) is both the name of the parameter and the parameter's label (Swift 101). As a label, it means that the simple command tool will have a meta-flag "--flag" that will trigger the MetaFlagFunction contained in the the default value, an instance of MetaFlag(fishElements: helpElements).

CAL has six built-in MetaFlag constructors - MetaFlag(metaFlagFunction:) MetaFlag(helpElements:), MetaFlag(manPageElements:), MetaFlag(string:), MetaFlag(text:) and MetaFlag(textBlocks:).

You can write your own MetaFlag initializers - specifying our own custom function that will be triggered. That is what the my personal 'FishCompletion' module does.


As Louie the King put it "Yes, I think it can easily done". :smiling_face_with_sunglasses:

Given a function (of type MetaFlagFunction) that produced a shell completion script for, say, zsh, it would be easy to implement, say, MetaFlag(parameterDescriptions: ShowElements, shellName: String, shellHints: [String]), or better perhaps, simply `MetaFlag(zshElements: ShowElements, zshStuff: ZhsStuff).'


That's another topic. IMHO, CompletionHint is more than sufficient, as is. In particular I don't see the need for, say, 'case: Script(String, String)`, where the first string is the name of the shell and the second is a script.

By the way, the FishCompletion module has been tested for all of the CompletionHint cases.

This is a cool project! I'm moving the thread to Community Showcase, though, since it's not about Argument Parser per se.

4 Likes

I have updated the beta release from 0.1.5 to 0.1.8. It still requires Swift 6.2 and MacOS 26.1, or above..

The only change is that functions that get called from generated code that are implemented as public in the macro support framework or the "runtime" framework are now all prefixed with "__."

Functions starting with "__" are meant to be ignored by users of the library.

Consider the expanded @MainFunciton macro in this code:

typealias Greeting = String
typealias Name = String
typealias Count = Int

@main
struct Example_1_Basic {
    
    @MainFunction(shadowGroups: ["lower upper"])
    private static func mf1Basic(
        i includeIndex: Flag,
        u upper: Flag = false,
        l lower: Flag = false,
        c__count repeats: Count? = nil,
        g__greeting greeting: Greeting = "Hello",
        _ name: Name,
        authors: MetaFlag = MetaFlag(string: "Robert Ford and Jesse James"),
        h__help help: MetaFlag = MetaFlag(helpElements: helpElements),
        v__version version: MetaFlag = MetaFlag(string: "version 0.1.0 - 2025-10-14"))
    { ...}
}

The expanded macro is the same as in 0.1.5, except that, for example:__config__.addShadowGroups(__shadowGroups__) has been changed to __config__.__addShadowGroups(__shadowGroups__)

Here is the new version of the expanded macro:

private static func main() {
    func __flagCheck__(_ x: Bool.Type) {
    }
    __flagCheck__(Flag.self)
    func __typeCheck__(_ xx: any BasicParameterType.Type) {
    }
    __typeCheck__(Count.self)
    __typeCheck__(Greeting.self)
    __typeCheck__(Name.self)
    let __tokens__ = Array(CommandLine.arguments.dropFirst(1))
    let __metaFlagDefs__: [(String, MetaFlagProtocol)] = [
        ("authors", MetaFlag(string: "Robert Ford and Jesse James")),
        ("help", MetaFlag(helpElements: helpElements)),
        ("version", MetaFlag(string: "version 0.1.0 - 2025-10-14"))
    ]
    let __shadowGroups__: [String] =  ["lower upper"]
    var __config__ = PeerFunctionConfig()
    __config__.__addShadowGroups(__shadowGroups__)
    __config__.__addMetaFlags(__metaFlagDefs__)
    let __greeting__default: Greeting? = "Hello"
    let __name__default: Name? = nil
    let __parameters__: [Parameter] = [
    Parameter(
        name: "includeIndex",
        labelTriple: ("-i", nil, nil),
        defaultValue: nil,
        typeName: "Flag",
        minMaxNumberOfValues: (0, 0),
        minMaxNumberOfOccurances: (0, 9223372036854775807),
        minMaxNumberOfElements: (0, 0),
        isMetaFlag: false),
    Parameter(
        name: "upper",
        labelTriple: ("-u", nil, nil),
        defaultValue: nil,
        typeName: "Flag",
        minMaxNumberOfValues: (0, 0),
        minMaxNumberOfOccurances: (0, 9223372036854775807),
        minMaxNumberOfElements: (0, 0),
        isMetaFlag: false),
    Parameter(
        name: "lower",
        labelTriple: ("-l", nil, nil),
        defaultValue: nil,
        typeName: "Flag",
        minMaxNumberOfValues: (0, 0),
        minMaxNumberOfOccurances: (0, 9223372036854775807),
        minMaxNumberOfElements: (0, 0),
        isMetaFlag: false),
    Parameter(
        name: "repeats",
        labelTriple: ("-c", nil, "--count"),
        defaultValue: nil,
        typeName: "Count?",
        minMaxNumberOfValues: (1, 1),
        minMaxNumberOfOccurances: (0, 1),
        minMaxNumberOfElements: (0, 1),
        isMetaFlag: false),
    Parameter(
        name: "greeting",
        labelTriple: ("-g", nil, "--greeting"),
        defaultValue: __quotedOrNil(__greeting__default),
        typeName: "Greeting",
        minMaxNumberOfValues: (1, 1),
        minMaxNumberOfOccurances: (0, 1),
        minMaxNumberOfElements: (0, 1),
        isMetaFlag: false),
    Parameter(
        name: "name",
        labelTriple: (nil, nil, nil),
        defaultValue: __quotedOrNil(__name__default),
        typeName: "Name",
        minMaxNumberOfValues: (1, 1),
        minMaxNumberOfOccurances: (1, 1),
        minMaxNumberOfElements: (1, 1),
        isMetaFlag: false),
    Parameter(
        name: "authors",
        labelTriple: (nil, nil, "--authors"),
        defaultValue: nil,
        typeName: "MetaFlag",
        minMaxNumberOfValues: (0, 0),
        minMaxNumberOfOccurances: (0, 9223372036854775807),
        minMaxNumberOfElements: (0, 0),
        isMetaFlag: true),
    Parameter(
        name: "help",
        labelTriple: ("-h", nil, "--help"),
        defaultValue: nil,
        typeName: "MetaFlag",
        minMaxNumberOfValues: (0, 0),
        minMaxNumberOfOccurances: (0, 9223372036854775807),
        minMaxNumberOfElements: (0, 0),
        isMetaFlag: true),
    Parameter(
        name: "version",
        labelTriple: ("-v", nil, "--version"),
        defaultValue: nil,
        typeName: "MetaFlag",
        minMaxNumberOfValues: (0, 0),
        minMaxNumberOfOccurances: (0, 9223372036854775807),
        minMaxNumberOfElements: (0, 0),
        isMetaFlag: true)]
    __config__.__addParameters(__parameters__)
    do {
        let __parseResult__ = try ParseResult(
            callNames: ["mf1-basic"],
            tokens: __tokens__,
            stopWords: [],
            config: __config__)
        var __messages__ = [String]() // avoids never mutated message if parseBlock is empty
        __messages__ += __parseResult__.parsedErrors.map {
            $0.description
        }
        let __initializer = __ValueInitializer(parsedValues: __parseResult__.parsedValues)
        let __includeIndex__value: Flag = __initializer.__parseFlag(for: "includeIndex", &__messages__)
        let __upper__value: Flag = __initializer.__parseFlag(for: "upper", &__messages__)
        let __lower__value: Flag = __initializer.__parseFlag(for: "lower", &__messages__)
        let __repeats__value: Count? = __initializer.__parseOptionalValue(for: "repeats",  &__messages__)
        let __greeting__value: Greeting? = __initializer.__parseValue(for: "greeting", __greeting__default, &__messages__)
        let __name__value: Name? = __initializer.__parseValue(for: "name", __name__default, &__messages__)
        if !__messages__.isEmpty {
            try __config__.__throwErrorScreen(
            callNames: ["mf1-basic"],
            messages: __messages__)
        }
        mf1Basic(
            i: __includeIndex__value,
            u: __upper__value,
            l: __lower__value,
            c__count: __repeats__value,
            g__greeting: __greeting__value!,
            __name__value!)
   }
   catch {
       printErrorAndExit(for: error, callNames: ["mf1-basic"])
   }
   return
}

Please clone the newer versions of CmdArgLib_MainFunction et. al., which depend on cmd-arg-lib 0.1.8.

First, thank you for moving this to the correct forum.

I wanted to mention a few developments:

  • I messed up using Github. Anything using cmd-arg-lib 0.2.1 or before doesn't work. All the example packages have been updated to use version 0.2.3.

  • Version 0.2.3 has a rewrite of StatefulCommand<T> and friends.

  • Help at each node in a command tree is vastly improved

    • A new ShowElement constructor .subcommand(_ cmd: any CmdArgLib.CommandProtocol) has been added
    • With this you can add line-wrapped subcommand descriptions anywhere in your help show element array
    • This implies any number of subcommand headers, in any order
  • All binaries produced using the library dropped in size by about 40K

  • The tree command is removed; replaced by MetaFlag(treeFor:)

    • a tree diagram is akin to a help screen - if one is a flag, so be it for the other
    • tree, the subcommand, was out of place
    • only one of the two is provided due to the library's orthogonal features goal
    • this allows for arbitary names - could be --print-tree, etc.
  • Every example in CmdArgLib_CommandAction changed, so I deleted it and started over

Code snippit:

@CommandAction
 private static func work(
     h__help: MetaFlag = MetaFlag(helpElements: help),
     t__tree: MetaFlag = MetaFlag(treeFor: phoneyCommand),
 ) {}

 private static let help: [ShowElement] = [
     .text("DESCRIPTION:", "Print a greeting or print some famous quotes."),
     .synopsis("\nUSAGE:", trailer: "Command"),
     .text("\nOPTIONS:"),
     .parameter("h__help", "Show help information"),
     .parameter("t__tree", "Show command tree"),
     .text("\nSUBCOMMANDS:"),
     .subcommand(Greet.node.asCommand),
     .subcommand(Quotes.node.asCommand),
 ]

Help Screen:

> ./ca1-simple --help
DESCRIPTION: Print a greeting or print some famous quotes.

USAGE: ca1-simple [-ht] <command>

OPTIONS:
  -h/--help             Show help information.
  -t/--tree             Show command tree.

SUBCOMMANDS:
  greet     Print a greeting.
  quotes    Print some quotes.

Tree:

> ./ca1-simple --tree
cf-ca1-simple
β”œβ”€β”€ greet - Print a greeting.
└── quotes
    β”œβ”€β”€ general - Print quotes about life in general.
    └── computing - Print quotes about computing.

Thanks again

I just bumped cmd-arg-lib to 0.2.5, and updated the samples to use the new version.

Changes that support hierarchical command structures

  • Method and protocol names have been changed to clarify the difference between a Node and a StatefulCommandAction<T>
  • Node conform to CommandNode, without adding any new properties
  • StatefulCommandAction<T> conforms to CommandNode but adds new properties. In particular, it holds a function of type StatefulCommandAction<T> that does the work associated with the command.

The names were confusing before the change. Here are the amended declarations:

public protocol CommandNode: Sendable {
    var name: String { get }
    var synopsis: String { get }
    var subnodes: [CommandNode] { get }
}
 public struct StatefulCommand<T: Sendable>: CommandNode, Sendable {
     public let name: String
     public let synopsis: String
     let commandAction: StatefulCommandAction<T>
     public let children: [StatefulCommand]
     public var subnodes: [CommandNode] { children.map { $0 as CommandNode } }
     public var asNode: CommandNode { self as CommandNode }

DeadWood

  • StatefulCommand<T> has been simplified, resulting in a further reduction in the library's binary footprint.

  • Here are the sizes of some sample executables on December 22, 2025:

    What Size Difference
    SPM --type executable 56,576 0
    CAL mf1-basic 337,528 280,952
    SAP repeat 1,538,312 1,481,737

Changes to the help screen generator

  • The .subcommand ShowElement constructor has been renamed to .commandNode
  • The new .customSynopsis ShowElement constructor has been added.

Drop down help screens

This constructor makes it possible, for example, to have "drop down" help screens.

Here is how this might be applied SPM.

[I] release> ./xswift --help
DESCRIPTION:
 Invoke swift package manager commands.

USAGE:
 xswift [-PCLSRBThptv] [<path-settings>] [<cashing-settings>]
        [<logging-settings>] [<security-settings>] [<resolution-settings>]
        [<build-settings>] [<trait-settings>] <subcommand>

SETTINGS:
 -P/--paths            Describe the path and location settings.
 -C/--caching          Describe the caching settings.
 -L/--logging          Describe the logging settings.
 -S/--security         Describe the security settings.
 -R/--resolution       Describe the resolution settings.
 -B/--build            Describe the build and linker settings.
 -T/--traits           Describe the trait settings.

OPTIONS:
 -h/--help             Show help information.
 -p/--preview          Print the generate swift command arguments.
 -t/--tree             Show command tree.
 -v/--version          Show version.

SUBCOMMANDS:
 init      initialize a new package.
 build     Build a package.
 run       run executable in package.

.customSynopsis is necessary here because .synopsis works (only) with parameter names, not arbitrary strings. Other than that .customSynopsis works just like .synopsis (same command call list, same line wrapping, etc.)

Here is the drop down help for paths and locations:

[I] release> ./xswift --help -P
PATHS & LOCATIONS OPTIONS:
  --package-path <directory_path>     Specify the package directory to operate
                                      on (default current directory).
  --cache-path <directory_path>       Specify the shared cache directory.
  --config-path <directory_path>      Specify the shared configuration
                                      directory.
  --security-path <directory_path>    Specify the shared security directory.
  --scratch-path <directory_path>     Specify a custom scratch directory path
                                      (default .build).
  --swift-sdk-path <directory_path>   Path to the directory containing
                                      installed Swift SDKs.
  --toolset <file_path>               Path to a toolset JSON file to use when
                                      building for the target platform (can be
                                      repeated).
  --pkg-config-path <directory_path>  Path to a pkg-config `.pc` file (can be
                                      repeated).

The -P meta-flag applies, not --help, because all meta-flags automatically shadow each other. I.e., ./xswift --help -P is the same as ./xswift -P.

The idea is that while entering a command you can, for example:

  • enter as much as you can until you want to add a security option
  • enter -S and return, to see the security settings drop down help
  • Hit the up arrow to bring up the line before the menu dropped.
  • Delete the -S
  • Enter (using shell completion) the desired option.

I pushed cmd-arg-lib version 0.2.7.

I made changes to the MetaFlag(manPageElements:) meta-flag so that subcommands get rendered. This is reflected in the latest version of CmdArgLib_Completion, example ca1-simple.

The modified example code has an added parameter, m__manpage. The manpage show element array is the same the help show element array, except for the first two elements. I.e., if you have a help layout, you can add a manage in minutes. No plugin to learn.

 @main
struct TopNode {

    private static let topNode = SimpleCommand(
        name: "ca1-simple",
        synopsis: "Cmd_1 - Simple Commands.",
        action: action,
        config: actionConfig(),
        children: [Greet.command, Quotes.command])

    @CommandAction
    private static func work(
        h__help: MetaFlag = MetaFlag(helpElements: help),
        t__tree: MetaFlag = MetaFlag(treeFor: "cf-ca1-simple", synopsis: "Print a greeting or print some famous quotes."),
        m__manpage: MetaFlag = MetaFlag(manPageElements: manpage)
    ) {}

    private static let manpage: [ShowElement] = [
        .prologue(description: "Print a greeting or print some famous quotes."),
        .synopsis("SYNOPSIS", trailer: "subcommand"),
        .text("\nOPTIONS"),
        .parameter("h__help", "Show help information"),
        .parameter("t__tree", "Show command tree"),
        .parameter("m__manpage", "Print manpage mdoc code"),
        .lines("\nSUBCOMMANDS"),
        .commandNode(Greet.command.asNode),
        .commandNode(Quotes.command.asNode),
    ]

    private static let help: [ShowElement] = [
        .text("DESCRIPTION:", "Print a greeting or print some famous quotes."),
        .synopsis("\nUSAGE:", trailer: "Command"),
        .text("\nOPTIONS:"),
        .parameter("h__help", "Show help information"),
        .parameter("t__tree", "Show command tree"),
        .parameter("m__manpage", "Print manpage mdoc code"),
        .text("\nSUBCOMMANDS:"),
        .commandNode(Greet.command.asNode),
        .commandNode(Quotes.command.asNode),
    ]

    private static func main() async {
        do {
            var (_, tokens) = commandLineNameAndWords()
            if tokens.isEmpty { tokens = ["--help"] }
            try await topNode.run(tokens: tokens, nodePath: [])
        } catch {
            printErrorAndExit(for: error, callNames: [topNode.name])
        }
    }
}

Here is help:

[I] release> ./ca1-simple -h
DESCRIPTION: Print a greeting or print some famous quotes.

USAGE: ca1-simple [-htm] <command>

OPTIONS:
  -h/--help             Show help information.
  -t/--tree             Show command tree.
  -m/--manpage          Print manpage mdoc code.

SUBCOMMANDS:
  greet     Print a greeting.
  quotes    Print some quotes.

Here is the tree:

[I] release> ./ca1-simple -ht
cf-ca1-simple
β”œβ”€β”€ greet - Print a greeting.
└── quotes
    β”œβ”€β”€ general - Print quotes about life in general.
    └── computing - Print quotes about computing.
[I] release> ./ca1-simple quotes general 1
Quote:
  Well done is better than well said. - Benjamin Franklin
[I] release> ./ca1-simple -htm > ca1-simple.1 && man ./ca1-simple.1