Getting Started with ArgumentParser
Learn to set up and customize a simple command-line tool.
Overview
This guide walks through building an example command. You’ll learn about the different tools that ArgumentParser
provides for defining a command’s options, customizing the interface, and providing help text for your user.
Adding ArgumentParser as a Dependency
Let’s write a tool called count
that reads an input file, counts the words, and writes the result to an output file.
First, we need to add swift-argument-parser
as a dependency to our package, and then include "ArgumentParser"
as a dependency for our executable target. Our “Package.swift” file ends up looking like this:
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "Count",
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
],
targets: [
.executableTarget(
name: "count",
dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]),
]
)
Building Our First Command
Once we’ve built the count
tool, we’ll be able to run it like this:
% count readme.md readme.counts
Counting words in 'readme.md' and writing the result into 'readme.counts'.
We’ll define the initial version of the command as a type that conforms to the ParsableCommand
protocol:
import ArgumentParser
@main
struct Count: ParsableCommand {
@Argument var inputFile: String
@Argument var outputFile: String
mutating func run() throws {
print("""
Counting words in '\(inputFile)' \
and writing the result into '\(outputFile)'.
""")
// Read 'inputFile', count the words, and save to 'outputFile'.
}
}
In the code above, the inputFile
and outputFile
properties use the @Argument
property wrapper. ArgumentParser
uses this wrapper to denote a positional command-line input — because inputFile
is specified first in the Count
type, it’s the first value read from the command line, and outputFile
is the second.
The command’s logic is implemented in its run()
method. Here, it prints out a message confirming the names of the files the user gave. (You can find a full implementation of the completed command at the end of this guide.)
Finally, the Count
command is designated as the program’s entry point by applying the @main
attribute. When running your command, the ArgumentParser
library parses the command-line arguments, verifies that they match up with what we’ve defined in Count
, and either calls the run()
method or exits with a helpful message.
Working with Named Options
Our count
tool may have a usability problem — it’s not immediately clear whether a user should provide the input file first, or the output file. Instead of using positional arguments for our two inputs, let’s specify that they should be labeled options:
% count --input-file readme.md --output-file readme.counts
Counting words in 'readme.md' and writing the result into 'readme.counts'.
We do this by using the @Option
property wrapper instead of @Argument
:
@main
struct Count: ParsableCommand {
@Option var inputFile: String
@Option var outputFile: String
mutating func run() throws {
print("""
Counting words in '\(inputFile)' \
and writing the result into '\(outputFile)'.
""")
// Read 'inputFile', count the words, and save to 'outputFile'.
}
}
The @Option
property wrapper denotes a command-line input that looks like --name <value>
, deriving its name from the name of your property.
This interface has a trade-off for the users of our count
tool: With @Argument
, users don’t need to type as much, but they have to remember whether to provide the input file or the output file first. Using @Option
makes the user type a little more, but the distinction between values is explicit. Options are order-independent, as well, so the user can name the input and output files in either order:
% count --output-file readme.counts --input-file readme.md
Counting words in 'readme.md' and writing the result into 'readme.counts'.
Adding a Flag
Next, we want to add a --verbose
flag to our tool, and only print the message if the user specifies that option:
% count --input-file readme.md --output-file readme.counts
(no output)
% count --verbose --input-file readme.md --output-file readme.counts
Counting words in 'readme.md' and writing the result into 'readme.counts'.
Let’s change our Count
type to look like this:
@main
struct Count: ParsableCommand {
@Option var inputFile: String
@Option var outputFile: String
@Flag var verbose = false
mutating func run() throws {
if verbose {
print("""
Counting words in '\(inputFile)' \
and writing the result into '\(outputFile)'.
""")
}
// Read 'inputFile', count the words, and save to 'outputFile'.
}
}
The @Flag
property wrapper denotes a command-line input that looks like --name
, deriving its name from the name of your property. Flags are most frequently used for Boolean values, like the verbose
property here.
Using Custom Names
We can customize the names of our options and add an alternative to the verbose
flag so that users can specify -v
instead of --verbose
. The new interface will look like this:
% count -v -i readme.md -o readme.counts
Counting words in 'readme.md' and writing the result into 'readme.counts'.
% count --input readme.md --output readme.counts -v
Counting words in 'readme.md' and writing the result into 'readme.counts'.
% count -o readme.counts -i readme.md --verbose
Counting words in 'readme.md' and writing the result into 'readme.counts'.
Customize the input names by passing name
parameters to the @Option
and @Flag
initializers:
@main
struct Count: ParsableCommand {
@Option(name: [.short, .customLong("input")])
var inputFile: String
@Option(name: [.short, .customLong("output")])
var outputFile: String
@Flag(name: .shortAndLong)
var verbose = false
mutating func run() throws { ... }
}
The default name specification is .long
, which uses a property’s name with a two-dash prefix. .short
uses only the first letter of a property’s name with a single-dash prefix, and allows combining groups of short options. You can specify custom short and long names with the .customShort(_:)
and .customLong(_:)
methods, respectively, or use the combined .shortAndLong
property to specify the common case of both the short and long derived names.
Providing Help
ArgumentParser
automatically generates help for any command when a user provides the -h
or --help
flags:
% count --help
USAGE: count --input <input> --output <output> [--verbose]
OPTIONS:
-i, --input <input>
-o, --output <output>
-v, --verbose
-h, --help Show help information.
This is a great start — you can see that all the custom names are visible, and the help shows that values are expected for the --input
and --output
options. However, our custom options and flag don’t have any descriptive text. Let’s add that now by passing string literals as the help
parameter:
@main
struct Count: ParsableCommand {
@Option(name: [.short, .customLong("input")], help: "A file to read.")
var inputFile: String
@Option(name: [.short, .customLong("output")], help: "A file to save word counts to.")
var outputFile: String
@Flag(name: .shortAndLong, help: "Print status updates while counting.")
var verbose = false
mutating func run() throws { ... }
}
The help screen now includes descriptions for each parameter:
% count -h
USAGE: count --input <input> --output <output> [--verbose]
OPTIONS:
-i, --input <input> A file to read.
-o, --output <output> A file to save word counts to.
-v, --verbose Print status updates while counting.
-h, --help Show help information.
The Complete Utility
As promised, here’s the complete count
command, for your experimentation:
import ArgumentParser
import Foundation
@main
struct Count: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Word counter.")
@Option(name: [.short, .customLong("input")], help: "A file to read.")
var inputFile: String
@Option(name: [.short, .customLong("output")], help: "A file to save word counts to.")
var outputFile: String
@Flag(name: .shortAndLong, help: "Print status updates while counting.")
var verbose = false
mutating func run() throws {
if verbose {
print("""
Counting words in '\(inputFile)' \
and writing the result into '\(outputFile)'.
""")
}
guard let input = try? String(contentsOfFile: inputFile) else {
throw RuntimeError("Couldn't read from '\(inputFile)'!")
}
let words = input.components(separatedBy: .whitespacesAndNewlines)
.map { word in
word.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
.lowercased()
}
.compactMap { word in word.isEmpty ? nil : word }
let counts = Dictionary(grouping: words, by: { $0 })
.mapValues { $0.count }
.sorted(by: { $0.value > $1.value })
if verbose {
print("Found \(counts.count) words.")
}
let output = counts.map { word, count in "\(word): \(count)" }
.joined(separator: "\n")
guard let _ = try? output.write(toFile: outputFile, atomically: true, encoding: .utf8) else {
throw RuntimeError("Couldn't write to '\(outputFile)'!")
}
}
}
struct RuntimeError: Error, CustomStringConvertible {
var description: String
init(_ description: String) {
self.description = description
}
}
Next Steps … Swift concurrency
ArgumentParser
supports Swift concurrency, notably async
renditions of run
. If you use async
rendition of run
, conform to AsyncParsableCommand
instead of ParsableCommand
.
@main
struct FileUtility: AsyncParsableCommand {
@Argument(
help: "File to be parsed.",
transform: URL.init(fileURLWithPath:)
)
var file: URL
mutating func run() async throws {
let handle = try FileHandle(forReadingFrom: file)
for try await line in handle.bytes.lines {
// do something with each line
}
try handle.close()
}
}