blob: 8796e02d6871b41779174e0717b4aec7a0aebf1d [file] [log] [blame]
//===--- ArgParse.swift ---------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Foundation
enum ArgumentError: Error {
case missingValue(String)
case invalidType(value: String, type: String, argument: String?)
case unsupportedArgument(String)
}
extension ArgumentError: CustomStringConvertible {
public var description: String {
switch self {
case let .missingValue(key):
return "missing value for '\(key)'"
case let .invalidType(value, type, argument):
return (argument == nil)
? "'\(value)' is not a valid '\(type)'"
: "'\(value)' is not a valid '\(type)' for '\(argument!)'"
case let .unsupportedArgument(argument):
return "unsupported argument '\(argument)'"
}
}
}
/// Type-checked parsing of the argument value.
///
/// - Returns: Typed value of the argument converted using the `parse` function.
///
/// - Throws: `ArgumentError.invalidType` when the conversion fails.
func checked<T>(
_ parse: (String) throws -> T?,
_ value: String,
argument: String? = nil
) throws -> T {
if let t = try parse(value) { return t }
var type = "\(T.self)"
if type.starts(with: "Optional<") {
let s = type.index(after: type.index(of:"<")!)
let e = type.index(before: type.endIndex) // ">"
type = String(type[s ..< e]) // strip Optional< >
}
throw ArgumentError.invalidType(
value: value, type: type, argument: argument)
}
/// Parser that converts the program's command line arguments to typed values
/// according to the parser's configuration, storing them in the provided
/// instance of a value-holding type.
class ArgumentParser<U> {
private var result: U
private var validOptions: [String] {
return arguments.compactMap { $0.name }
}
private var arguments: [Argument] = []
private let programName: String = {
// Strip full path from the program name.
let r = CommandLine.arguments[0].reversed()
let ss = r[r.startIndex ..< (r.index(of:"/") ?? r.endIndex)]
return String(ss.reversed())
}()
private var positionalArgs = [String]()
private var optionalArgsMap = [String : String]()
/// Argument holds the name of the command line parameter, its help
/// desciption and a rule that's applied to process it.
///
/// The the rule is typically a value processing closure used to convert it
/// into given type and storing it in the parsing result.
///
/// See also: addArgument, parseArgument
struct Argument {
let name: String?
let help: String?
let apply: () throws -> ()
}
/// ArgumentParser is initialized with an instance of a type that holds
/// the results of the parsing of the individual command line arguments.
init(into result: U) {
self.result = result
self.arguments += [
Argument(name: "--help", help: "show this help message and exit",
apply: printUsage)
]
}
private func printUsage() {
guard let _ = optionalArgsMap["--help"] else { return }
let space = " "
let maxLength = arguments.compactMap({ $0.name?.count }).max()!
let padded = { (s: String) in
" \(s)\(String(repeating:space, count: maxLength - s.count)) " }
let f: (String, String) -> String = {
"\(padded($0))\($1)"
.split(separator: "\n")
.joined(separator: "\n" + padded(""))
}
let positional = f("TEST", "name or number of the benchmark to measure")
let optional = arguments.filter { $0.name != nil }
.map { f($0.name!, $0.help ?? "") }
.joined(separator: "\n")
print(
"""
usage: \(programName) [--argument=VALUE] [TEST [TEST ...]]
positional arguments:
\(positional)
optional arguments:
\(optional)
""")
exit(0)
}
/// Parses the command line arguments, returning the result filled with
/// specified argument values or report errors and exit the program if
/// the parsing fails.
public func parse() -> U {
do {
try parseArgs() // parse the argument syntax
try arguments.forEach { try $0.apply() } // type-check and store values
return result
} catch let error as ArgumentError {
fputs("error: \(error)\n", stderr)
exit(1)
} catch {
fflush(stdout)
fatalError("\(error)")
}
}
/// Using CommandLine.arguments, parses the structure of optional and
/// positional arguments of this program.
///
/// We assume that optional switch args are of the form:
///
/// --opt-name[=opt-value]
/// -opt-name[=opt-value]
///
/// with `opt-name` and `opt-value` not containing any '=' signs. Any
/// other option passed in is assumed to be a positional argument.
///
/// - Throws: `ArgumentError.unsupportedArgument` on failure to parse
/// the supported argument syntax.
private func parseArgs() throws {
// For each argument we are passed...
for arg in CommandLine.arguments[1..<CommandLine.arguments.count] {
// If the argument doesn't match the optional argument pattern. Add
// it to the positional argument list and continue...
if !arg.starts(with: "-") {
positionalArgs.append(arg)
continue
}
// Attempt to split it into two components separated by an equals sign.
let components = arg.split(separator: "=")
let optionName = String(components[0])
guard validOptions.contains(optionName) else {
throw ArgumentError.unsupportedArgument(arg)
}
var optionVal : String
switch components.count {
case 1: optionVal = ""
case 2: optionVal = String(components[1])
default:
// If we do not have two components at this point, we can not have
// an option switch. This is an invalid argument. Bail!
throw ArgumentError.unsupportedArgument(arg)
}
optionalArgsMap[optionName] = optionVal
}
}
/// Add a rule for parsing the specified argument.
///
/// Stores the type-erased invocation of the `parseArgument` in `Argument`.
///
/// Parameters:
/// - name: Name of the command line argument. E.g.: `--opt-arg`.
/// `nil` denotes positional arguments.
/// - property: Property on the `result`, to store the value into.
/// - defaultValue: Value used when the command line argument doesn't
/// provide one.
/// - help: Argument's description used when printing usage with `--help`.
/// - parser: Function that converts the argument value to given type `T`.
public func addArgument<T>(
_ name: String?,
_ property: WritableKeyPath<U, T>,
defaultValue: T? = nil,
help: String? = nil,
parser: @escaping (String) throws -> T? = { _ in nil }
) {
arguments.append(Argument(name: name, help: help)
{ try self.parseArgument(name, property, defaultValue, parser) })
}
/// Process the specified command line argument.
///
/// For optional arguments that have a value we attempt to convert it into
/// given type using the supplied parser, performing the type-checking with
/// the `checked` function.
/// If the value is empty the `defaultValue` is used instead.
/// The typed value is finally stored in the `result` into the specified
/// `property`.
///
/// For the optional positional arguments, the [String] is simply assigned
/// to the specified property without any conversion.
///
/// See `addArgument` for detailed parameter descriptions.
private func parseArgument<T>(
_ name: String?,
_ property: WritableKeyPath<U, T>,
_ defaultValue: T?,
_ parse: (String) throws -> T?
) throws {
if let name = name, let value = optionalArgsMap[name] {
guard !value.isEmpty || defaultValue != nil
else { throw ArgumentError.missingValue(name) }
result[keyPath: property] = (value.isEmpty)
? defaultValue!
: try checked(parse, value, argument: name)
} else if name == nil {
result[keyPath: property] = positionalArgs as! T
}
}
}