TSCUtility's ArgumentParser
While we have a brand new library from the Swift team that specializes in argument parsing, it's good to have a look at how we got there, which is the purpose of this article.
In the Swift Executables Guide we've seen how we can use ArgumentParser to easily read and transform launch arguments. The ArgumentParser package is actually an iteration over TSCUtility
's ArgumentParser
, which we are going to dive into next.
An example
I'm going to make a small example here, however please feel free to bookmark Derik Ramirez's awesome argument parsing article for more.
In this example we will require a flag --name
with an associated value: after implementing it, the scripts will expect to be launched with something like $ swift run hello --name YourName
:
- First, we will define our parser, which we can think of as our command line interface to the user:
let parser = ArgumentParser(
usage: "--name YourName",
overview: "Tell me your name π"
)
- Then we declare an expected argument via the generic
OptionArgument
:
let nameArgument: OptionArgument<String> = parser.add(
option: "--name",
kind: String.self,
usage: "Specify your name"
)
Beside the flag,
OptionArgument
also knows the expected argument value type:kind
is a metatype instance conforming to theArgumentKind
protocol.Exactly as forDecodable
, all Swift primitives natively conform to it, and we can make our own types conform to it as well.
- Lastly, we can do the actual parsing:
let parseResult = try! parser.parse(arguments)
if let name: String = parseResult.get(nameArgument) { ... }
Here's the complete code:
import TSCBasic
import TSCUtility
// Read the arguments.
let arguments: [String] = Array(
CommandLine.arguments.dropFirst()
)
// Define our parser.
let parser = ArgumentParser(
usage: "--name YourName",
overview: "Tell me your name π")
// Declare expected launch argument(s).
let nameArgument: OptionArgument<String> = parser.add(
option: "--name",
kind: String.self,
usage: "Specify your name")
// Do the parsing.
do {
let parseResult = try parser.parse(arguments)
if let name: String = parseResult.get(nameArgument) {
print("Hello \(name)")
} else {
parser.printUsage(on: stdoutStream)
}
} catch {
parser.printUsage(on: stdoutStream)
}
Replace the
main.swift
content with this to try it out!
In case of any failure, we're asking the parser to print the command line usage: this is done for free, we don't have to do anything to get this behavior! π
And here are a few examples of the script in action:
$ swift run hello --name Federico
> Hello Federico
$ swift run hello missingFlag
> OVERVIEW: Tell me your name π
>
> USAGE: hello --name YourName
>
> OPTIONS:
> --name Specify your name
> --help Display available options
$ swift run hello --help
> OVERVIEW: Tell me your name π
>
> USAGE: hello --name YourName
>
> OPTIONS:
> --name Specify your name
> --help Display available options
This is how the
swift
command line tool works as well: if you run$ swift --help
, or$ swift run --help
, etc you will see exactly the same format as our new script.
ArgumentKind
In the example above we declared an expected argument via the generic OptionArgument
:
let nameArgument: OptionArgument<String> = parser.add(
option: "--name",
kind: String.self,
usage: "Specify your name"
)
The kind
parameter expects a type conforming to a protocol called ArgumentKind
:
public protocol ArgumentKind {
/// Throwable convertion initializer.
init(argument: String) throws
/// Type of shell completion to provide for this argument.
static var completion: ShellCompletion { get }
}
ArgumentKind
definition: I like to think of this protocol as the CLI version of Swift'sDecodable
.
All it is requested is an initializer init(argument:)
that parses the given launch argument, and a static completion
property of type ShellCompletion
:
public enum ShellCompletion {
/// Offers no completions at all; e.g. for a string identifier.
case none
/// No specific completions, will offer tool's completions.
case unspecified
/// Offers filename completions.
case filename
/// Custom function for generating completions. Must be provided in the script's scope.
case function(String)
/// Offers completions from predefined list. A description can be provided which is shown in some shells, like zsh.
case values([(value: String, description: String)])
}
ShellCompletion
definition.
This second requirement is to provide shell completion to our users.
Primitives Conformation
TSCUtility
provides ArgumentKind
conformation to the most common primitives:
extension String: ArgumentKind {
public init(argument: String) throws {
self = argument
}
public static let completion: ShellCompletion = .none
}
Int
:
extension Int: ArgumentKind {
public init(argument: String) throws {
guard let int = Int(argument) else {
throw ArgumentConversionError.typeMismatch(value: argument, expectedType: Int.self)
}
self = int
}
public static let completion: ShellCompletion = .none
}
Bool
:
extension Bool: ArgumentKind {
public init(argument: String) throws {
guard let bool = Bool(argument) else {
throw ArgumentConversionError.unknown(value: argument)
}
self = bool
}
public static var completion: ShellCompletion = .unspecified
}
There's a fourth conformation in
TSCUtility
for a custom typePathArgument
, we skip it here as it's off topic.
Conforming to ArgumentKind
is straightforward, let's make a new conformation next.
Custom Conformation
In my app suite I have several scripts where I must specify which city I want to work on, this is a good opportunity to define an enum with String
Raw Values:
enum City: String {
case bangkok
case chongqing
case jakarta
case kualalumpur
}
Then we can define our ArgumentKind
initializer:
public init(argument: String) throws {
guard let city = City(rawValue: argument) else {
throw ArgumentConversionError.unknown(value: argument)
}
self = city
}
We throw
TSCUtility
'sArgumentConversionError
, no need to create custom error types!
Lastly, we add the ArgumentKind
's static completion
property:
public static var completion: ShellCompletion = .values([
("bangkok", "Capital of Thailand"),
("chongqing", "Capital of the Hot Pot!"),
("jakarta", "Capital of Indonesia"),
("kualalumpur", "Capital of Malaysia")
])
Making the enum conform to
CaseIterable
would be more correct, but for brevity's sake ...
And that's it! We can now define a new argument with associated type City
, and use it in our script:
let parser = ArgumentParser(
usage: "My new tool",
overview: "A five stars tool."
)
let cityArgument: OptionArgument<City> = parser.add(
option: "--city",
kind: City.self,
usage: "The city to work on"
)
Here's the complete example:
import TSCBasic
import TSCUtility
enum City: String, ArgumentKind {
case bangkok
case chongqing
case jakarta
case kualaLumpur
public init(argument: String) throws {
guard let city = City(rawValue: argument) else {
throw ArgumentConversionError.unknown(value: argument)
}
self = city
}
public static var completion: ShellCompletion = .values([
("bangkok", "Capital of Thailand"),
("chongqing", "Capital of the Hot Pot!"),
("jakarta", "Capital of Indonesia"),
("kualalumpur", "Capital of Malaysia")
])
}
// Read the arguments.
let arguments: [String] = Array(
CommandLine.arguments.dropFirst()
)
// Define our parser.
let parser = ArgumentParser(
usage: "My new tool",
overview: "A five stars tool."
)
// Declare expected launch argument(s).
let cityArgument: OptionArgument<City> = parser.add(
option: "--city",
kind: City.self,
usage: "The city to work on",
completion: City.completion
)
// Do the parsing.
do {
let parseResult = try parser.parse(arguments)
if let city: City = parseResult.get(cityArgument) {
print("Working on \(city.rawValue) π")
} else {
print("β οΈ Seems like the city is missing!")
}
} catch {
print(error)
}
Place this in a Swift Executable
main.swift
file.
And here we can see the script in action:
$ swift run yourToolName --city bangkok
> Working on bangkok π
$ swift run yourToolName
> β οΈ Seems like the city is missing!
Custom Conformation 2: Extending Types
While it's awesome that we can make our own types conform to ArgumentKind
, nobody stops us to also extend other types as well.
For example, if our script expects an URL
, we can extend Foundation's URL
:
import Foundation
import TSCUtility
extension Foundation.URL: ArgumentKind {
public init(argument: String) throws {
guard let url = URL(string: argument) else {
throw ArgumentConversionError.unknown(value: argument)
}
self = url
}
public static var completion: ShellCompletion = .none
}
And now we can get an URL
instance directly from our parser!
Comparison with ArgumentParser
As of today, the new ArgumentParser library does not support auto-completion, therefore ArgumentParser's ArgumentKind
equivalent, ExpressibleByArgument
, only requires an initializer:
/// A type that can be expressed as a command-line argument.
public protocol ExpressibleByArgument {
/// Creates a new instance of this type from a command-line-specified
/// argument.
init?(argument: String)
}
Snippet from ArgumentParser's
ExpressibleByArgument.swift
.
Having to implement one initializer surely makes adopting the new library faster, however I'm hopeful that auto-completion support will be added in the future.
If you're interested to learn more about the new library: - have a look at the amazing official documentation - maybe subscribe to this blog feed RSS π
Conclusions
In this article we've seen how TSCUtility
's ArgumentParser
and ArgumentKind
are defined and how we can make any type conform to it, we've then compared it with the new ArgumentParser library, noting a little regression. Despite that, both libraries make our scripts argument parsing effortless π.
If you're wondering which one you should pick up today: go with ArgumentParser. TSCUtility's ArgumentParser will be completely removed in the future.
As always you can find me on Twitter for any comment and/or feedback, thank you for reading and stay tuned for more articles!