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:

  1. 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 😊"
)
  1. 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 the ArgumentKind protocol.Exactly as for Decodable, all Swift primitives natively conform to it, and we can make our own types conform to it as well.

  1. 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's Decodable.

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
}
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
}
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 type PathArgument, 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's ArgumentConversionError, 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
}

Code snippet from Selenops, A Swift Web Crawler.

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!

β­‘β­‘β­‘β­‘β­‘

Further Reading

Explore Swift

Browse all