|
12 | 12 |
|
13 | 13 | import Foundation
|
14 | 14 |
|
15 |
| -public struct Arguments { |
16 |
| - public var progName: String |
17 |
| - public var positionalArgs: [String] |
18 |
| - public var optionalArgsMap: [String : String] |
19 |
| - |
20 |
| - init(_ pName: String, _ posArgs: [String], _ optArgsMap: [String : String]) { |
21 |
| - progName = pName |
22 |
| - positionalArgs = posArgs |
23 |
| - optionalArgsMap = optArgsMap |
| 15 | +enum ArgumentError: Error { |
| 16 | + case missingValue(String) |
| 17 | + case invalidType(value: String, type: String, argument: String?) |
| 18 | + case unsupportedArgument(String) |
| 19 | +} |
| 20 | + |
| 21 | +extension ArgumentError: CustomStringConvertible { |
| 22 | + public var description: String { |
| 23 | + switch self { |
| 24 | + case let .missingValue(key): |
| 25 | + return "missing value for '\(key)'" |
| 26 | + case let .invalidType(value, type, argument): |
| 27 | + return (argument == nil) |
| 28 | + ? "'\(value)' is not a valid '\(type)'" |
| 29 | + : "'\(value)' is not a valid '\(type)' for '\(argument!)'" |
| 30 | + case let .unsupportedArgument(argument): |
| 31 | + return "unsupported argument '\(argument)'" |
| 32 | + } |
24 | 33 | }
|
25 | 34 | }
|
26 | 35 |
|
27 |
| -/// Using CommandLine.arguments, returns an Arguments struct describing |
28 |
| -/// the arguments to this program. If we fail to parse arguments, we |
29 |
| -/// return nil. |
| 36 | +/// Type-checked parsing of the argument value. |
30 | 37 | ///
|
31 |
| -/// We assume that optional switch args are of the form: |
| 38 | +/// - Returns: Typed value of the argument converted using the `parse` function. |
32 | 39 | ///
|
33 |
| -/// --opt-name[=opt-value] |
34 |
| -/// -opt-name[=opt-value] |
35 |
| -/// |
36 |
| -/// with opt-name and opt-value not containing any '=' signs. Any |
37 |
| -/// other option passed in is assumed to be a positional argument. |
38 |
| -public func parseArgs(_ validOptions: [String]? = nil) |
39 |
| - -> Arguments? { |
40 |
| - let progName = CommandLine.arguments[0] |
41 |
| - var positionalArgs = [String]() |
42 |
| - var optionalArgsMap = [String : String]() |
43 |
| - |
44 |
| - // For each argument we are passed... |
45 |
| - var passThroughArgs = false |
46 |
| - for arg in CommandLine.arguments[1..<CommandLine.arguments.count] { |
47 |
| - // If the argument doesn't match the optional argument pattern. Add |
48 |
| - // it to the positional argument list and continue... |
49 |
| - if passThroughArgs || !arg.starts(with: "-") { |
50 |
| - positionalArgs.append(arg) |
51 |
| - continue |
| 40 | +/// - Throws: `ArgumentError.invalidType` when the conversion fails. |
| 41 | +func checked<T>( |
| 42 | + _ parse: (String) throws -> T?, |
| 43 | + _ value: String, |
| 44 | + argument: String? = nil |
| 45 | +) throws -> T { |
| 46 | + if let t = try parse(value) { return t } |
| 47 | + var type = "\(T.self)" |
| 48 | + if type.starts(with: "Optional<") { |
| 49 | + let s = type.index(after: type.index(of:"<")!) |
| 50 | + let e = type.index(before: type.endIndex) // ">" |
| 51 | + type = String(type[s ..< e]) // strip Optional< > |
| 52 | + } |
| 53 | + throw ArgumentError.invalidType( |
| 54 | + value: value, type: type, argument: argument) |
| 55 | +} |
| 56 | + |
| 57 | +/// Parser that converts the program's command line arguments to typed values |
| 58 | +/// according to the parser's configuration, storing them in the provided |
| 59 | +/// instance of a value-holding type. |
| 60 | +class ArgumentParser<U> { |
| 61 | + private var result: U |
| 62 | + private var validOptions: [String] { |
| 63 | + return arguments.compactMap { $0.name } |
| 64 | + } |
| 65 | + private var arguments: [Argument] = [] |
| 66 | + private let programName: String = { |
| 67 | + // Strip full path from the program name. |
| 68 | + let r = CommandLine.arguments[0].reversed() |
| 69 | + let ss = r[r.startIndex ..< (r.index(of:"/") ?? r.endIndex)] |
| 70 | + return String(ss.reversed()) |
| 71 | + }() |
| 72 | + private var positionalArgs = [String]() |
| 73 | + private var optionalArgsMap = [String : String]() |
| 74 | + |
| 75 | + /// Argument holds the name of the command line parameter, its help |
| 76 | + /// desciption and a rule that's applied to process it. |
| 77 | + /// |
| 78 | + /// The the rule is typically a value processing closure used to convert it |
| 79 | + /// into given type and storing it in the parsing result. |
| 80 | + /// |
| 81 | + /// See also: addArgument, parseArgument |
| 82 | + struct Argument { |
| 83 | + let name: String? |
| 84 | + let help: String? |
| 85 | + let apply: () throws -> () |
| 86 | + } |
| 87 | + |
| 88 | + /// ArgumentParser is initialized with an instance of a type that holds |
| 89 | + /// the results of the parsing of the individual command line arguments. |
| 90 | + init(into result: U) { |
| 91 | + self.result = result |
| 92 | + self.arguments += [ |
| 93 | + Argument(name: "--help", help: "show this help message and exit", |
| 94 | + apply: printUsage) |
| 95 | + ] |
52 | 96 | }
|
53 |
| - if arg == "--" { |
54 |
| - passThroughArgs = true |
55 |
| - continue |
| 97 | + |
| 98 | + private func printUsage() { |
| 99 | + guard let _ = optionalArgsMap["--help"] else { return } |
| 100 | + let space = " " |
| 101 | + let maxLength = arguments.compactMap({ $0.name?.count }).max()! |
| 102 | + let padded = { (s: String) in |
| 103 | + " \(s)\(String(repeating:space, count: maxLength - s.count)) " } |
| 104 | + let f: (String, String) -> String = { |
| 105 | + "\(padded($0))\($1)" |
| 106 | + .split(separator: "\n") |
| 107 | + .joined(separator: "\n" + padded("")) |
| 108 | + } |
| 109 | + let positional = f("TEST", "name or number of the benchmark to measure") |
| 110 | + let optional = arguments.filter { $0.name != nil } |
| 111 | + .map { f($0.name!, $0.help ?? "") } |
| 112 | + .joined(separator: "\n") |
| 113 | + print( |
| 114 | + """ |
| 115 | + usage: \(programName) [--argument=VALUE] [TEST [TEST ...]] |
| 116 | +
|
| 117 | + positional arguments: |
| 118 | + \(positional) |
| 119 | +
|
| 120 | + optional arguments: |
| 121 | + \(optional) |
| 122 | + """) |
| 123 | + exit(0) |
56 | 124 | }
|
57 |
| - // Attempt to split it into two components separated by an equals sign. |
58 |
| - let components = arg.split(separator: "=") |
59 |
| - let optionName = String(components[0]) |
60 |
| - if validOptions != nil && !validOptions!.contains(optionName) { |
61 |
| - print("Invalid option: \(arg)") |
62 |
| - return nil |
| 125 | + |
| 126 | + /// Parses the command line arguments, returning the result filled with |
| 127 | + /// specified argument values or report errors and exit the program if |
| 128 | + /// the parsing fails. |
| 129 | + public func parse() -> U { |
| 130 | + do { |
| 131 | + try parseArgs() // parse the argument syntax |
| 132 | + try arguments.forEach { try $0.apply() } // type-check and store values |
| 133 | + return result |
| 134 | + } catch let error as ArgumentError { |
| 135 | + fputs("error: \(error)\n", stderr) |
| 136 | + exit(1) |
| 137 | + } catch { |
| 138 | + fflush(stdout) |
| 139 | + fatalError("\(error)") |
| 140 | + } |
63 | 141 | }
|
64 |
| - var optionVal : String |
65 |
| - switch components.count { |
66 |
| - case 1: optionVal = "" |
67 |
| - case 2: optionVal = String(components[1]) |
68 |
| - default: |
69 |
| - // If we do not have two components at this point, we can not have |
70 |
| - // an option switch. This is an invalid argument. Bail! |
71 |
| - print("Invalid option: \(arg)") |
72 |
| - return nil |
| 142 | + |
| 143 | + /// Using CommandLine.arguments, parses the structure of optional and |
| 144 | + /// positional arguments of this program. |
| 145 | + /// |
| 146 | + /// We assume that optional switch args are of the form: |
| 147 | + /// |
| 148 | + /// --opt-name[=opt-value] |
| 149 | + /// -opt-name[=opt-value] |
| 150 | + /// |
| 151 | + /// with `opt-name` and `opt-value` not containing any '=' signs. Any |
| 152 | + /// other option passed in is assumed to be a positional argument. |
| 153 | + /// |
| 154 | + /// - Throws: `ArgumentError.unsupportedArgument` on failure to parse |
| 155 | + /// the supported argument syntax. |
| 156 | + private func parseArgs() throws { |
| 157 | + |
| 158 | + // For each argument we are passed... |
| 159 | + for arg in CommandLine.arguments[1..<CommandLine.arguments.count] { |
| 160 | + // If the argument doesn't match the optional argument pattern. Add |
| 161 | + // it to the positional argument list and continue... |
| 162 | + if !arg.starts(with: "-") { |
| 163 | + positionalArgs.append(arg) |
| 164 | + continue |
| 165 | + } |
| 166 | + // Attempt to split it into two components separated by an equals sign. |
| 167 | + let components = arg.split(separator: "=") |
| 168 | + let optionName = String(components[0]) |
| 169 | + guard validOptions.contains(optionName) else { |
| 170 | + throw ArgumentError.unsupportedArgument(arg) |
| 171 | + } |
| 172 | + var optionVal : String |
| 173 | + switch components.count { |
| 174 | + case 1: optionVal = "" |
| 175 | + case 2: optionVal = String(components[1]) |
| 176 | + default: |
| 177 | + // If we do not have two components at this point, we can not have |
| 178 | + // an option switch. This is an invalid argument. Bail! |
| 179 | + throw ArgumentError.unsupportedArgument(arg) |
| 180 | + } |
| 181 | + optionalArgsMap[optionName] = optionVal |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + /// Add a rule for parsing the specified argument. |
| 186 | + /// |
| 187 | + /// Stores the type-erased invocation of the `parseArgument` in `Argument`. |
| 188 | + /// |
| 189 | + /// Parameters: |
| 190 | + /// - name: Name of the command line argument. E.g.: `--opt-arg`. |
| 191 | + /// `nil` denotes positional arguments. |
| 192 | + /// - property: Property on the `result`, to store the value into. |
| 193 | + /// - defaultValue: Value used when the command line argument doesn't |
| 194 | + /// provide one. |
| 195 | + /// - help: Argument's description used when printing usage with `--help`. |
| 196 | + /// - parser: Function that converts the argument value to given type `T`. |
| 197 | + public func addArgument<T>( |
| 198 | + _ name: String?, |
| 199 | + _ property: WritableKeyPath<U, T>, |
| 200 | + defaultValue: T? = nil, |
| 201 | + help: String? = nil, |
| 202 | + parser: @escaping (String) throws -> T? = { _ in nil } |
| 203 | + ) { |
| 204 | + arguments.append(Argument(name: name, help: help) |
| 205 | + { try self.parseArgument(name, property, defaultValue, parser) }) |
73 | 206 | }
|
74 |
| - optionalArgsMap[optionName] = optionVal |
75 |
| - } |
76 | 207 |
|
77 |
| - return Arguments(progName, positionalArgs, optionalArgsMap) |
| 208 | + /// Process the specified command line argument. |
| 209 | + /// |
| 210 | + /// For optional arguments that have a value we attempt to convert it into |
| 211 | + /// given type using the supplied parser, performing the type-checking with |
| 212 | + /// the `checked` function. |
| 213 | + /// If the value is empty the `defaultValue` is used instead. |
| 214 | + /// The typed value is finally stored in the `result` into the specified |
| 215 | + /// `property`. |
| 216 | + /// |
| 217 | + /// For the optional positional arguments, the [String] is simply assigned |
| 218 | + /// to the specified property without any conversion. |
| 219 | + /// |
| 220 | + /// See `addArgument` for detailed parameter descriptions. |
| 221 | + private func parseArgument<T>( |
| 222 | + _ name: String?, |
| 223 | + _ property: WritableKeyPath<U, T>, |
| 224 | + _ defaultValue: T?, |
| 225 | + _ parse: (String) throws -> T? |
| 226 | + ) throws { |
| 227 | + if let name = name, let value = optionalArgsMap[name] { |
| 228 | + guard !value.isEmpty || defaultValue != nil |
| 229 | + else { throw ArgumentError.missingValue(name) } |
| 230 | + |
| 231 | + result[keyPath: property] = (value.isEmpty) |
| 232 | + ? defaultValue! |
| 233 | + : try checked(parse, value, argument: name) |
| 234 | + } else if name == nil { |
| 235 | + result[keyPath: property] = positionalArgs as! T |
| 236 | + } |
| 237 | + } |
78 | 238 | }
|
0 commit comments