# Swift ArgumentParser Tutorial: Building CLI Commands with vphone-cli

> Master Swift ArgumentParser to build type-safe CLI tools. Learn to define commands, options, and flags with practical examples from the vphone-cli repository.

- Repository: [Lakr/vphone-cli](https://github.com/Lakr233/vphone-cli)
- Tags: tutorial
- Published: 2026-09-09

---

**Use the Swift ArgumentParser library to create type-safe CLI tools by defining `ParsableCommand` structs with `@Option`, `@Flag`, and custom validation—exactly as implemented in the Lakr233/vphone-cli repository.**

The **Swift ArgumentParser** library transforms command-line argument parsing from string manipulation into compiler-checked Swift code. The open-source *vphone-cli* project by Lakr233 demonstrates production-grade patterns for building virtual iPhone management tools with subcommands, typed arguments, and automatic help generation. This guide walks through the actual implementation found in [`sources/vphone-cli/VPhoneCLI.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/vphone-cli/VPhoneCLI.swift).

## Setting Up the Root Command

Every ArgumentParser project starts with a root command conforming to `ParsableCommand`. The `CommandConfiguration` static property defines the CLI's name, description, and subcommand structure.

In [`VPhoneCLI.swift`](https://github.com/Lakr233/vphone-cli/blob/main/VPhoneCLI.swift) (lines 6–15), the top-level command declares two subcommands with a default:

```swift
struct VPhoneCLI: ParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "vphone-cli",
        abstract: "Boot a virtual iPhone or patch firmware with the Swift pipeline",
        subcommands: [
            VPhoneBootCLI.self,
            PatchFirmwareCLI.self,
        ],
        defaultSubcommand: VPhoneBootCLI.self
    )
}

```

**Key parameters:**
- `commandName` – the executable name shown in help
- `subcommands` – array of `ParsableCommand` types that become subcommands
- `defaultSubcommand` – runs when no subcommand is specified

## Defining Required Options with @Option

Options capture values from command-line arguments. Use `@Option` for named parameters that require data.

The boot command in [`VPhoneCLI.swift`](https://github.com/Lakr233/vphone-cli/blob/main/VPhoneCLI.swift) (lines 36–41) shows a required file path with automatic URL transformation:

```swift
@Option(
    name: .shortAndLong,
    help: "Path to VM manifest plist (config.plist). Required.",
    transform: URL.init(fileURLWithPath:)
)
var config: URL

```

**Critical details:**
- `name: .shortAndLong` generates both `-c` and `--config`
- The `transform` closure converts the raw string to `URL` before assignment
- Omitting a default value makes the option required—ArgumentParser enforces this at runtime

## Adding Boolean Flags with @Flag

Flags represent on/off switches without associated values. The `dfu` flag from `VPhoneBootCLI` (lines 43–45) demonstrates:

```swift
@Flag(name: .shortAndLong, help: "Boot into DFU mode")
var dfu: Bool = false

```

Flags default to `false` when unset. Custom names use `.customShort("d")` or `.customLong("device-firmware-update")` for precise control.

## Creating Typed Enumerations for Constrained Input

When an option should accept only specific values, define an `enum` conforming to `ExpressibleByArgument`. The `PatchFirmwareCLI` implementation (lines 25–33) shows this pattern:

```swift
enum VariantOption: String, CaseIterable, ExpressibleByArgument {
    case less, regular, dev, jb, exp
    
    var pipelineVariant: FirmwarePipeline.Variant { /* mapping */ }
    var virtualMachineVariant: VPhoneVirtualMachine.Variant { /* mapping */ }
}

@Option(name: [.customShort("V"), .long], help: "Firmware variant to patch.")
var variant: VariantOption = .regular

```

The `ExpressibleByArgument` conformance enables:
- Automatic help text listing valid cases
- Case-insensitive parsing of raw values
- Default value assignment via standard Swift initialization

## Validating Argument Combinations

Complex CLIs need cross-field validation. Implement `validate()` to enforce business rules before `run()` executes.

From `VPhoneBootCLI` (lines 76–94), mutual exclusion between `--dfu` and `--install-ipa`:

```swift
mutating func validate() throws {
    if dfu, let packageURL = installPackageURL {
        throw ValidationError("Cannot install IPA package when booting into DFU mode.")
    }
    
    // Additional validation for VM selection
    if let serial = vmSerial, vmIdentifier != nil {
        throw ValidationError("Cannot use both --vm-serial and --vm-identifier.")
    }
}

```

**Validation behavior:**
- Runs automatically after parsing but before `run()`
- Throws `ValidationError` with user-facing messages
- Supports `mutating` modifications to normalize input

## Implementing Command Execution

The `run()` method contains command logic. In *vphone-cli*, boot command execution delegates to external systems after option resolution:

```swift
mutating func run() throws {
    // Empty by design—vphone-cli processes parsed options externally
}

```

For self-contained tools, implement full execution:

```swift
mutating func run() throws {
    let vm = try loadVirtualMachine(config: config)
    if dfu {
        try vm.enterDFU()
    } else {
        try vm.boot()
    }
}

```

## Complete Working Example

Combine these patterns into a standalone tool. This minimal implementation mirrors the architecture of `vphone-cli`:

```swift
import ArgumentParser
import Foundation

// MARK: – Root command
struct DeviceManager: ParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "devman",
        abstract: "Manage iOS device simulators",
        subcommands: [Boot.self, Flash.self],
        defaultSubcommand: Boot.self
    )
}

// MARK: – Boot subcommand
struct Boot: ParsableCommand {
    @Option(name: .shortAndLong, help: "Device configuration plist", transform: URL.init(fileURLWithPath:))
    var config: URL
    
    @Flag(name: .shortAndLong, help: "Recovery mode boot")
    var recovery: Bool = false
    
    mutating func validate() throws {
        guard FileManager.default.fileExists(atPath: config.path) else {
            throw ValidationError("Configuration file not found: \(config.path)")
        }
    }
    
    mutating func run() throws {
        print("Booting from \(config.path)\(recovery ? " [recovery]" : "")")
    }
}

// MARK: – Flash subcommand with enum
struct Flash: ParsableCommand {
    enum ImageType: String, CaseIterable, ExpressibleByArgument {
        case developer, beta, release
    }
    
    @Option(name: .shortAndLong, help: "Firmware image type")
    var type: ImageType = .developer
    
    @Argument(help: "Path to firmware bundle")
    var source: String
    
    mutating func run() throws {
        print("Flashing \(type) build from \(source)")
    }
}

// Entry point
DeviceManager.main()

```

Run `devman --help` to see automatically generated documentation:

```

USAGE: devman [<options>] [<subcommand>]

DESCRIPTION:
  Manage iOS device simulators

SUBCOMMANDS:
  boot (default)          Boot a device simulator
  flash                   Flash firmware to device

  See 'devman help <subcommand>' for detailed help.

```

## Summary

- **Root command**: Define with `ParsableCommand` and `CommandConfiguration` to establish CLI structure
- **@Option**: Capture required or optional values with type transformations
- **@Flag**: Handle boolean switches with customizable names
- **ExpressibleByArgument**: Constrain input to valid enum cases
- **validate()**: Enforce cross-field constraints with descriptive errors
- **run()**: Execute business logic with parsed, validated arguments

The *vphone-cli* source code in `Lakr233/vphone-cli` provides reference implementations for production CLI tools using these exact patterns.

## Frequently Asked Questions

### What is the difference between @Option and @Flag in Swift ArgumentParser?

`@Option` captures values like `--config path.plist` and supports type transformations. `@Flag` represents boolean switches like `--verbose` or `-v` with no associated value. Use `@Option` for data input and `@Flag` for mode toggles.

### How do I make a command-line argument required in Swift ArgumentParser?

Omit a default value from the property declaration. ArgumentParser automatically enforces required options at runtime and generates appropriate error messages if missing.

### Can I validate combinations of arguments before execution?

Yes. Implement `mutating func validate() throws` in your `ParsableCommand` struct. This method runs after parsing but before `run()`, allowing you to throw `ValidationError` for invalid combinations.

### How does Swift ArgumentParser generate help documentation automatically?

The library inspects property wrappers (`@Option`, `@Flag`, `@Argument`) and their `help` parameters, combined with `CommandConfiguration` metadata, to produce `--help` output without manual formatting.