Swift ArgumentParser Tutorial: Building CLI Commands with vphone-cli
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.
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 (lines 6–15), the top-level command declares two subcommands with a default:
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 helpsubcommands– array ofParsableCommandtypes that become subcommandsdefaultSubcommand– 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 (lines 36–41) shows a required file path with automatic URL transformation:
@Option(
name: .shortAndLong,
help: "Path to VM manifest plist (config.plist). Required.",
transform: URL.init(fileURLWithPath:)
)
var config: URL
Critical details:
name: .shortAndLonggenerates both-cand--config- The
transformclosure converts the raw string toURLbefore 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:
@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:
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:
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
ValidationErrorwith user-facing messages - Supports
mutatingmodifications 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:
mutating func run() throws {
// Empty by design—vphone-cli processes parsed options externally
}
For self-contained tools, implement full execution:
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:
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
ParsableCommandandCommandConfigurationto 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →