Defining Commands and Subcommands

Break complex command-line tools into a tree of subcommands.

CommandsAndSubcommands.md

Overview

When command-line programs grow larger, it can be useful to divide them into a group of smaller programs, providing an interface through subcommands. Utilities such as git and the Swift package manager are able to provide varied interfaces for each of their sub-functions by implementing subcommands such as git branch or swift package init.

Generally, these subcommands each have their own configuration options, as well as options that are shared across several or all aspects of the larger program.

You can build a program with commands and subcommands by defining multiple command types and specifying each command’s subcommands in its configuration. For example, here’s the interface of a math utility that performs operations on a series of values given on the command line.

% math add 10 15 7
32
% math multiply 10 15 7
1050
% math stats average 3 4 13 15 15
10.0
% math stats average --kind median 3 4 13 15 15
13.0
% math stats
OVERVIEW: Calculate descriptive statistics.

USAGE: math stats <subcommand>

OPTIONS:
  -h, --help              Show help information.

SUBCOMMANDS:
  average, avg            Print the average of the values.
  stdev                   Print the standard deviation of the values.
  quantiles               Print the quantiles of the values (TBD).

  See 'math help stats <subcommand>' for detailed help.

Start by defining the root Math command. You can provide a static configuration property for a command that specifies its subcommands and a default subcommand, if any.

struct Math: ParsableCommand {
    static let configuration = CommandConfiguration(
        abstract: "A utility for performing maths.",
        subcommands: [Add.self, Multiply.self, Statistics.self],
        defaultSubcommand: Add.self)
}

Math lists its three subcommands by their types; we’ll see the definitions of Add, Multiply, and Statistics below. Add is also given as a default subcommand — this means that it is selected if a user leaves out a subcommand name:

% math 10 15 7
32

Next, define a ParsableArguments type with properties that will be shared across multiple subcommands. Types that conform to ParsableArguments can be parsed from command-line arguments, but don’t provide any execution through a run() method.

In this case, the Options type accepts a --hexadecimal-output flag and expects a list of integers.

struct Options: ParsableArguments {
    @Flag(name: [.long, .customShort("x")], help: "Use hexadecimal notation for the result.")
    var hexadecimalOutput = false

    @Argument(help: "A group of integers to operate on.")
    var values: [Int]
}

It’s time to define our first two subcommands: Add and Multiply. Both of these subcommands include the arguments defined in the Options type by denoting that property with the @OptionGroup property wrapper (see OptionGroup). @OptionGroup doesn’t define any new arguments for a command; instead, it splats in the arguments defined by another ParsableArguments type.

extension Math {
    struct Add: ParsableCommand {
        static let configuration
            = CommandConfiguration(abstract: "Print the sum of the values.")

        @OptionGroup var options: Math.Options

        mutating func run() {
            let result = options.values.reduce(0, +)
            print(format(result: result, usingHex: options.hexadecimalOutput))
        }
    }

    struct Multiply: ParsableCommand {
        static let configuration = CommandConfiguration(
            abstract: "Print the product of the values.",
            aliases: ["mul"])

        @OptionGroup var options: Math.Options

        mutating func run() {
            let result = options.values.reduce(1, *)
            print(format(result: result, usingHex: options.hexadecimalOutput))
        }
    }
}

One thing to note is the aliases parameter for CommandConfiguration. This is useful for subcommands to define alternative names that can be used to invoke them. In this case we’ve defined a shorthand for multiply named mul, so you could invoke the Multiply command for our program by either of the below:

% math multiply 10 15 7
1050
% math mul 10 15 7
1050

Next, we’ll define Statistics, the third subcommand of Math. The Statistics command specifies a custom command name (stats) in its configuration, overriding the default derived from the type name (statistics). It also declares two additional subcommands, meaning that it acts as a forked branch in the command tree, and not a leaf.

extension Math {
    struct Statistics: ParsableCommand {
        static let configuration = CommandConfiguration(
            commandName: "stats",
            abstract: "Calculate descriptive statistics.",
            subcommands: [Average.self, StandardDeviation.self])
    }
}

Let’s finish our subcommands with the Average and StandardDeviation types. Each of them has slightly different arguments, so they don’t use the Options type defined above. Each subcommand is ultimately independent and can specify a combination of shared and unique arguments.

extension Math.Statistics {
    struct Average: ParsableCommand {
        static let configuration = CommandConfiguration(
            abstract: "Print the average of the values.",
            aliases: ["avg"])

        enum Kind: String, ExpressibleByArgument {
            case mean, median, mode
        }

        @Option(help: "The kind of average to provide.")
        var kind: Kind = .mean

        @Argument(help: "A group of floating-point values to operate on.")
        var values: [Double] = []

        func calculateMean() -> Double { ... }
        func calculateMedian() -> Double { ... }
        func calculateMode() -> [Double] { ... }

        mutating func run() {
            switch kind {
            case .mean:
                print(calculateMean())
            case .median:
                print(calculateMedian())
            case .mode:
                let result = calculateMode()
                    .map(String.init(describing:))
                    .joined(separator: " ")
                print(result)
            }
        }
    }

    struct StandardDeviation: ParsableCommand {
        static let configuration = CommandConfiguration(
            commandName: "stdev",
            abstract: "Print the standard deviation of the values.")

        @Argument(help: "A group of floating-point values to operate on.")
        var values: [Double] = []

        mutating func run() {
            if values.isEmpty {
                print(0.0)
            } else {
                let sum = values.reduce(0, +)
                let mean = sum / Double(values.count)
                let squaredErrors = values
                    .map { $0 - mean }
                    .map { $0 * $0 }
                let variance = squaredErrors.reduce(0, +) / Double(values.count)
                let result = variance.squareRoot()
                print(result)
            }
        }
    }
}

Last but not least, we add the @main attribute to the root of our command tree, to tell the compiler to use that as the program’s entry point. Upon execution, this parses the command-line arguments, determines whether a subcommand was selected, and then instantiates and calls the run() method on that particular subcommand.

@main
struct Math: ParsableCommand {
    // ...
}

That’s it for this doubly-nested math command! This example is also provided as a part of the swift-argument-parser repository, so you can see it all together and experiment with it here.