|
| 1 | +# Getting Started with `ArgumentParser` |
| 2 | + |
| 3 | +Learn to set up and customize a simple command-line tool. |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +## Adding `ArgumentParser` as a Dependency |
| 8 | + |
| 9 | +First, we need to add `swift-argument-parser` as a dependency to our package, |
| 10 | +and then include `"ArgumentParser"` as a dependency for our executable target. |
| 11 | +Our "Package.swift" file ends up looking like this: |
| 12 | + |
| 13 | +```swift |
| 14 | +// swift-tools-version:5.2 |
| 15 | +import PackageDescription |
| 16 | + |
| 17 | +let package = Package( |
| 18 | + name: "random", |
| 19 | + dependencies: [ |
| 20 | + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.0.1"), |
| 21 | + ], |
| 22 | + targets: [ |
| 23 | + .target( |
| 24 | + name: "count", |
| 25 | + dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]), |
| 26 | + ] |
| 27 | +) |
| 28 | +``` |
| 29 | + |
| 30 | +## Building Our First Command |
| 31 | + |
| 32 | +Let's write a tool called `count` that reads an input file, counts the words, and writes the result to an output file. |
| 33 | + |
| 34 | +We can run our `count` tool like this: |
| 35 | + |
| 36 | +``` |
| 37 | +% count readme.md readme.counts |
| 38 | +Counting words in 'readme.md' and writing the result into 'readme.counts'. |
| 39 | +``` |
| 40 | + |
| 41 | +We'll define the initial version of the command as a type that conforms to the `ParsableCommand` protocol: |
| 42 | + |
| 43 | +```swift |
| 44 | +import ArgumentParser |
| 45 | + |
| 46 | +struct Count: ParsableCommand { |
| 47 | + @Argument() |
| 48 | + var inputFile: String |
| 49 | + |
| 50 | + @Argument() |
| 51 | + var outputFile: String |
| 52 | + |
| 53 | + func run() throws { |
| 54 | + print(""" |
| 55 | + Counting words in '\(inputFile)' \ |
| 56 | + and writing the result into '\(outputFile)'. |
| 57 | + """) |
| 58 | + |
| 59 | + // Read 'inputFile', count the words, and save to 'outputFile'. |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +Count.main() |
| 64 | +``` |
| 65 | + |
| 66 | +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 read second. |
| 67 | + |
| 68 | +We've implemented the command's logic in its `run()` method. Here, we're printing 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.) |
| 69 | + |
| 70 | +Finally, you tell the parser to execute the `Count` command by calling its static `main()` method. This method 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. |
| 71 | + |
| 72 | + |
| 73 | +## Working with Named Options |
| 74 | + |
| 75 | +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: |
| 76 | + |
| 77 | +``` |
| 78 | +% count --input-file readme.md --output-file readme.counts |
| 79 | +Counting words in 'readme.md' and writing the result into 'readme.counts'. |
| 80 | +``` |
| 81 | + |
| 82 | +We do this by using the `@Option` property wrapper instead of `@Argument`: |
| 83 | + |
| 84 | +```swift |
| 85 | +struct Count: ParsableCommand { |
| 86 | + @Option() |
| 87 | + var inputFile: String |
| 88 | + |
| 89 | + @Option() |
| 90 | + var outputFile: String |
| 91 | + |
| 92 | + func run() throws { |
| 93 | + print(""" |
| 94 | + Counting words in '\(inputFile)' \ |
| 95 | + and writing the result into '\(outputFile)'. |
| 96 | + """) |
| 97 | + |
| 98 | + // Read 'inputFile', count the words, and save to 'outputFile'. |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +The `@Option` property wrapper denotes a command-line input that looks like `--name value`, deriving its name from the name of your property. |
| 104 | + |
| 105 | +This interface has a trade-off for the users of our `count` tool: With `@Argument`, users don't need to type as much, but have to remember whether the input file or the output file needs to be given 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: |
| 106 | + |
| 107 | +``` |
| 108 | +% count --output-file readme.counts --input-file readme.md |
| 109 | +Counting words in 'readme.md' and writing the result into 'readme.counts'. |
| 110 | +``` |
| 111 | + |
| 112 | +## Adding a Flag |
| 113 | + |
| 114 | +Next, we want to add a `--verbose` flag to our tool, and only print the message if the user specifies that option: |
| 115 | + |
| 116 | +``` |
| 117 | +% count --input-file readme.md --output-file readme.counts |
| 118 | +(no output) |
| 119 | +% count --verbose --input-file readme.md --output-file readme.counts |
| 120 | +Counting words in 'readme.md' and writing the result into 'readme.counts'. |
| 121 | +``` |
| 122 | + |
| 123 | +Let's change our `Count` type to look like this: |
| 124 | + |
| 125 | +```swift |
| 126 | +struct Count: ParsableCommand { |
| 127 | + @Option() |
| 128 | + var inputFile: String |
| 129 | + |
| 130 | + @Option() |
| 131 | + var outputFile: String |
| 132 | + |
| 133 | + @Flag() |
| 134 | + var verbose: Bool |
| 135 | + |
| 136 | + func run() throws { |
| 137 | + if verbose { |
| 138 | + print(""" |
| 139 | + Counting words in '\(inputFile)' \ |
| 140 | + and writing the result into '\(outputFile)'. |
| 141 | + """) |
| 142 | + } |
| 143 | + |
| 144 | + // Read 'inputFile', count the words, and save to 'outputFile'. |
| 145 | + } |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +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. |
| 150 | + |
| 151 | + |
| 152 | +## Using Custom Names |
| 153 | + |
| 154 | +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: |
| 155 | + |
| 156 | +``` |
| 157 | +% count -v -i readme.md -o readme.counts |
| 158 | +Counting words in 'readme.md' and writing the result into 'readme.counts'. |
| 159 | +% count --input readme.md --output readme.counts -v |
| 160 | +Counting words in 'readme.md' and writing the result into 'readme.counts'. |
| 161 | +% count -o readme.counts -i readme.md --verbose |
| 162 | +Counting words in 'readme.md' and writing the result into 'readme.counts'. |
| 163 | +``` |
| 164 | + |
| 165 | +Customize the input names by passing `name` parameters to the `@Option` and `@Flag` initializers: |
| 166 | + |
| 167 | +```swift |
| 168 | +struct Count: ParsableCommand { |
| 169 | + @Option(name: [.short, .customLong("input")]) |
| 170 | + var inputFile: String |
| 171 | + |
| 172 | + @Option(name: [.short, .customLong("output")]) |
| 173 | + var outputFile: String |
| 174 | + |
| 175 | + @Flag(name: .shortAndLong) |
| 176 | + var verbose: Bool |
| 177 | + |
| 178 | + func run() throws { ... } |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +The default name specification is `.long`, which uses a property's 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. |
| 183 | + |
| 184 | +## Providing Help |
| 185 | + |
| 186 | +`ArgumentParser` automatically generates help for any command when a user provides the `-h` or `--help` flags: |
| 187 | + |
| 188 | +``` |
| 189 | +% count --help |
| 190 | +USAGE: count --input <input> --output <output> [--verbose] |
| 191 | +
|
| 192 | +OPTIONS: |
| 193 | + -i, --input <input> |
| 194 | + -o, --output <output> |
| 195 | + -v, --verbose |
| 196 | + -h, --help Show help information. |
| 197 | +``` |
| 198 | + |
| 199 | +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: |
| 200 | + |
| 201 | +```swift |
| 202 | +struct Count: ParsableCommand { |
| 203 | + @Option(name: [.short, .customLong("input")], help: "A file to read.") |
| 204 | + var inputFile: String |
| 205 | + |
| 206 | + @Option(name: [.short, .customLong("output")], help: "A file to save word counts to.") |
| 207 | + var outputFile: String |
| 208 | + |
| 209 | + @Flag(name: .shortAndLong, help: "Print status updates while counting.") |
| 210 | + var verbose: Bool |
| 211 | + |
| 212 | + func run() throws { ... } |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | +The help screen now includes descriptions for each parameter: |
| 217 | + |
| 218 | +``` |
| 219 | +% count -h |
| 220 | +USAGE: count --input <input> --output <output> [--verbose] |
| 221 | +
|
| 222 | +OPTIONS: |
| 223 | + -i, --input <input> A file to read. |
| 224 | + -o, --output <output> A file to save word counts to. |
| 225 | + -v, --verbose Print status updates while counting. |
| 226 | + -h, --help Show help information. |
| 227 | +
|
| 228 | +``` |
| 229 | + |
| 230 | +## The Complete Utility |
| 231 | + |
| 232 | +As promised, here's the complete `count` command, for your experimentation: |
| 233 | + |
| 234 | +```swift |
| 235 | +struct Count: ParsableCommand { |
| 236 | + static let configuration = CommandConfiguration(abstract: "Word counter.") |
| 237 | + |
| 238 | + @Option(name: [.short, .customLong("input")], help: "A file to read.") |
| 239 | + var inputFile: String |
| 240 | + |
| 241 | + @Option(name: [.short, .customLong("output")], help: "A file to save word counts to.") |
| 242 | + var outputFile: String |
| 243 | + |
| 244 | + @Flag(name: .shortAndLong, help: "Print status updates while counting.") |
| 245 | + var verbose: Bool |
| 246 | + |
| 247 | + func run() throws { |
| 248 | + if verbose { |
| 249 | + print(""" |
| 250 | + Counting words in '\(inputFile)' \ |
| 251 | + and writing the result into '\(outputFile)'. |
| 252 | + """) |
| 253 | + } |
| 254 | + |
| 255 | + guard let input = try? String(contentsOfFile: inputFile) else { |
| 256 | + throw RuntimeError("Couldn't read from '\(inputFile)'!") |
| 257 | + } |
| 258 | + |
| 259 | + let words = input.components(separatedBy: .whitespacesAndNewlines) |
| 260 | + .map { word in |
| 261 | + word.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) |
| 262 | + .lowercased() |
| 263 | + } |
| 264 | + .compactMap { word in word.isEmpty ? nil : word } |
| 265 | + |
| 266 | + let counts = Dictionary(grouping: words, by: { $0 }) |
| 267 | + .mapValues { $0.count } |
| 268 | + .sorted(by: { $0.value > $1.value }) |
| 269 | + |
| 270 | + if verbose { |
| 271 | + print("Found \(counts.count) words.") |
| 272 | + } |
| 273 | + |
| 274 | + let output = counts.map { word, count in "\(word): \(count)" } |
| 275 | + .joined(separator: "\n") |
| 276 | + |
| 277 | + guard let _ = try? output.write(toFile: outputFile, atomically: true, encoding: .utf8) else { |
| 278 | + throw RuntimeError("Couldn't write to '\(outputFile)'!") |
| 279 | + } |
| 280 | + } |
| 281 | +} |
| 282 | + |
| 283 | +struct RuntimeError: Error, CustomStringConvertible { |
| 284 | + var description: String |
| 285 | + |
| 286 | + init(_ description: String) { |
| 287 | + self.description = description |
| 288 | + } |
| 289 | +} |
| 290 | + |
| 291 | +Count.main() |
| 292 | +``` |
0 commit comments