|

Swift Argument Parser: One Model for CLI Commands

Daily (almost) Swift Open Source Overview hero for Swift Argument Parser

A CLI often begins with a small parsing problem. Given ["convert", "input.jpg", "--width", "320", "--verbose"], the program must decide which tokens are positional arguments, which belong to named options, and which are flags. It must also report missing values and explain the accepted syntax in --help.

Swift Argument Parser lets you describe those rules using Swift types rather than maintaining a separate parser and help text.

import ArgumentParser

@main
struct Resize: ParsableCommand {
    @Argument(help: "The input image.")
    var input: String

    @Option(help: "Output width in pixels.")
    var width: Int

    @Flag(help: "Print extra information.")
    var verbose = false

    mutating func run() throws {
        print("Resize \(input) to \(width) px")
        if verbose {
            print("Verbose output enabled")
        }
    }
}

Those declarations drive more than parsing. They also provide the information needed for validation, usage text, help screens, shell completions, and documentation. The connection becomes clearer when you follow how the wrappers become internal argument definitions.

Property wrappers describe argument definitions

@Argument, @Option, @Flag, and @OptionGroup are not just containers for parsed values.

Each wrapper conforms internally to ArgumentSetProvider. That protocol can produce an ArgumentSet for the property it represents.

When the library needs the grammar for a ParsableArguments type, ArgumentSet.init(_:visibility:parent:) creates an instance of the type and reflects over its stored properties. Wrapped properties provide their argument sets. Those sets are joined into the full definition.

That is an important design detail: Swift Argument Parser does not need a separate schema file beside your command type. The declaration of the type is the schema.

For a command like this:

struct Export: ParsableCommand {
    @Argument
    var input: String

    @Option
    var format: String = "json"

    @Flag
    var pretty = false

    mutating func run() throws {}
}

the wrappers describe three different command-line concepts:

  • input is positional;
  • format is a named option that consumes a value;
  • pretty is a named flag that changes state without consuming a separate value.

Those differences become ArgumentDefinition values inside an ArgumentSet.

ArgumentDefinition is the common internal language

The public wrappers have different APIs, but parsing needs a common representation.

That role belongs to ArgumentDefinition.

An argument definition carries information such as its kind, help metadata, completion behavior, and the update operation used when matching command-line input.

For example, a Boolean flag can be represented by an update closure that writes true into ParsedValues. Options and positional arguments use definitions that consume values and transform them into the requested Swift type.

ArgumentSet is then a collection of these definitions plus a lookup from names to their positions.

This gives the parser one model to work with after the higher-level Swift declarations have been translated.

Parsing and decoding are separate steps

ParsableArguments.parse(_:) eventually routes through a CommandParser.

For command types, parseAsRoot(_:) creates a CommandParser, takes either the supplied arguments or the process command line, and asks the parser to build the matching command.

The parser walks the command tree, handles subcommands, and parses input against the relevant ArgumentSet.

But matching tokens is not the final step.

The parsed values are decoded back into the user’s Swift type. This is why ParsableArguments inherits from Decodable, and why the property wrappers themselves are Decodable.

The library effectively moves through three representations:

Swift command type
        ↓
ArgumentSet / ArgumentDefinition
        ↓
ParsedValues
        ↓
decoded Swift command value

That separation keeps the command declaration strongly typed without forcing the low-level parser to know about every user-defined command type.

The grammar also produces usage text

One of the best ways to see this architecture is to look at usage generation.

The test suite defines types such as:

struct Options: ParsableArguments {
    @Option
    var firstName: String?

    @Argument
    var file: String
}

The generated synopsis follows the type:

example [--first-name <first-name>] <file>

Optionality comes from the Swift declaration. Named versus positional input comes from the wrapper. Value names come from the property or explicit help configuration.

UsageGenerator receives an ArgumentSet and turns those definitions into the command synopsis.

So the usage line is not maintained separately from the parser. It is another rendering of the same argument model.

Help uses the same definitions

HelpGenerator builds on the same idea.

It collects the arguments for the current command, adds help and version flags when appropriate, groups positional arguments, options, flags, and subcommands, and renders the sections for the terminal.

This is why changing a declaration can affect parsing and help together.

For example:

@Option(help: "Number of retries.")
var retries: Int = 3

The default value makes the option optional during parsing, and the help system can also show that default.

The relationship is structural rather than a convention that application code has to keep synchronized manually.

Command types add a tree above the arguments

ParsableCommand extends ParsableArguments.

A command adds CommandConfiguration, a run() method, and support for a tree of subcommands.

CommandParser stores that command tree and tracks the current node while parsing. That lets the library move from a root command into nested subcommands while keeping the argument definitions associated with the right command.

A small hierarchy can look like this:

import ArgumentParser

@main
struct Tool: ParsableCommand {
    static let configuration = CommandConfiguration(
        subcommands: [Status.self]
    )
}

struct Status: ParsableCommand {
    @Flag
    var short = false

    mutating func run() throws {
        print(short ? "ok" : "Everything is OK")
    }
}

Here the type hierarchy is also the command hierarchy.

That same tree is used when parsing the subcommand and when generating help for it.

Validation belongs to the parsed type

Parsing answers whether the input can be converted into the declared values. Application rules can still require more.

ParsableArguments includes validate() throws, which has an empty default implementation.

That gives a command a natural place for constraints involving several parsed properties:

struct Download: ParsableCommand {
    @Argument
    var url: String

    @Option
    var retries = 3

    mutating func validate() throws {
        if retries < 0 {
            throw ValidationError("Retries must be zero or greater.")
        }
    }

    mutating func run() throws {
        print("Downloading \(url)")
    }
}

The distinction is useful. The wrappers describe how text becomes values; validate() describes whether the resulting combination is acceptable for the command.

Async commands keep the same grammar

For commands that need asynchronous work, the library provides AsyncParsableCommand.

Its main difference is the execution method:

import ArgumentParser

@main
struct Fetch: AsyncParsableCommand {
    @Argument
    var url: String

    mutating func run() async throws {
        // Perform async work.
    }
}

The parsing model does not need a second set of wrappers or argument definitions.

Current versions expose asyncParse(_:) and asyncParseAsRoot(_:) as asynchronous parsing entry points. This naming matters because release 1.8.1 changed them from async overloads of parse and parseAsRoot after those overloads caused a source-compatibility regression.

That is a useful example of API design around Swift concurrency meeting an established synchronous API.

The package includes tooling built from the grammar

The package does more than expose the runtime library.

Its manifest defines the ArgumentParser library plus two command plugins: GenerateDoccReference and GenerateManual.

The first can generate DocC reference material for a command-line tool. The second generates manual-page content.

That is possible because the command declaration contains enough structured information to describe itself.

The same design also supports shell completion generation through ParsableArguments.completionScript(for:) and a JSON help representation through _dumpHelp().

The typed command is therefore not just executable configuration. It is a source of metadata for tooling around the executable.

Where the abstraction has limits

A generated grammar does not remove every CLI design problem.

The project still has open issues around details such as duplicate subcommand names and some help or plugin behavior. Those are good reminders that deriving several outputs from one model increases consistency, but it also makes the quality of that model and its renderers important.

There is also a point where custom parsing may be simpler. If a tool intentionally accepts an unusual, context-sensitive command syntax that does not map naturally to arguments, options, flags, and subcommands, forcing it into a declarative command type can work against the design.

For conventional command-line interfaces, however, keeping parsing rules and documentation close to the Swift declarations removes a large amount of duplicated structure.

Source worth reading

Start with ParsableArguments.swift.

Its ArgumentSet initializer is the bridge between the user’s Swift type and the parser’s internal representation. It reflects over the initialized command and asks property wrappers for their argument sets.

Then read ArgumentSet.swift and ArgumentDefinition.swift. They show the common grammar produced by @Argument, @Option, and @Flag.

CommandParser.swift adds the command tree and parsing flow.

Finally, UsageGenerator.swift and HelpGenerator.swift show why the internal grammar is useful beyond parsing: the same definitions become user-facing CLI documentation.

The usage-generation tests are also worth reading because they turn Swift declarations directly into expected command syntaxes.

Project health

Swift Argument Parser is maintained in Apple’s GitHub organization and is licensed under Apache License 2.0 with the Runtime Library Exception.

The current package manifest uses Swift tools 6.0 and has no external Swift package dependencies. It contains the core library, plugins, examples, tools, and multiple test targets.

The changelog lists 1.8.2 on June 4, 2026, with fixes for fish completion scripts and a Windows build warning. The current main branch also contains unreleased work, including a configurable help banner.

There are active open issues, including reports around duplicate subcommands, generated documentation plugins, and help output. They show ongoing edge cases without changing the core architecture described here.

Engineering takeaway

A CLI changes over time. An option becomes required, its default changes, or a new subcommand appears. When parsing rules and help text are maintained separately, they can describe different versions of the same command.

Swift Argument Parser reduces that risk by deriving both from the same typed declaration. The interesting part is not the syntax of @Option or @Flag, but the shared model underneath them. This approach works well when that model can express the command-line interface you actually need.

Sources