//===--- 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 // //===----------------------------------------------------------------------===// #if canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #elseif os(Windows) import MSVCRT #else import Darwin #endif 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( _ 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.firstIndex(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 { 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.firstIndex(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 /// description and a rule that's applied to process it. /// /// 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;\n" + "use +/- prefix to filter by substring match") 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] /// /// 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..( _ name: String?, _ property: WritableKeyPath, 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( _ name: String?, _ property: WritableKeyPath, _ 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 } } }