// from https://github.com/apple/swift-package-manager/blob/cce860c70c3d96d6b21d5f56c06a7edb53996d90/Sources/TSCUtility/ArgumentParser.swift#L1-L1137 /* 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 http://swift.org/LICENSE.txt for license information See http://swift.org/CONTRIBUTORS.txt for Swift project authors */ import TSCBasic import Foundation import func TSCLibc.exit /// Errors which may be encountered when running argument parser. public enum ArgumentParserError: Swift.Error { /// An unknown option is encountered. case unknownOption(String, suggestion: String?) /// The value of an argument is invalid. case invalidValue(argument: String, error: ArgumentConversionError) /// Expected a value from the option. case expectedValue(option: String) /// An unexpected positional argument encountered. case unexpectedArgument(String) /// Expected these positional arguments but not found. case expectedArguments(ArgumentParser, [String]) /// Expected a single argument but got multiple ones. case duplicateArgument(String) } extension ArgumentParserError: LocalizedError { public var errorDescription: String? { return description } } extension ArgumentParserError: CustomStringConvertible { public var description: String { switch self { case .unknownOption(let option, let suggestion): var desc = "unknown option \(option); use --help to list available options" if let suggestion = suggestion { desc += "\nDid you mean \(suggestion)?" } return desc case .invalidValue(let argument, let error): return "\(error) for argument \(argument); use --help to print usage" case .expectedValue(let option): return "option \(option) requires a value; provide a value using '\(option) ' or '\(option)='" case .unexpectedArgument(let argument): return "unexpected argument \(argument); use --help to list available arguments" case .expectedArguments(_, let arguments): return "expected arguments: \(arguments.joined(separator: ", "))" case .duplicateArgument(let option): return "expected single value for argument: \(option)" } } } /// Conversion errors that can be returned from `ArgumentKind`'s failable /// initializer. public enum ArgumentConversionError: Swift.Error { /// The value is unknown. case unknown(value: String) /// The value could not be converted to the target type. case typeMismatch(value: String, expectedType: Any.Type) /// Custom reason for conversion failure. case custom(String) } extension ArgumentConversionError: LocalizedError { public var errorDescription: String? { return description } } extension ArgumentConversionError: CustomStringConvertible { public var description: String { switch self { case .unknown(let value): return "unknown value '\(value)'" case .typeMismatch(let value, let expectedType): return "'\(value)' is not convertible to \(expectedType)" case .custom(let reason): return reason } } } extension ArgumentConversionError: Equatable { public static func ==(lhs: ArgumentConversionError, rhs: ArgumentConversionError) -> Bool { switch (lhs, rhs) { case (.unknown(let lhsValue), .unknown(let rhsValue)): return lhsValue == rhsValue case (.unknown, _): return false case (.typeMismatch(let lhsValue, let lhsType), .typeMismatch(let rhsValue, let rhsType)): return lhsValue == rhsValue && lhsType == rhsType case (.typeMismatch, _): return false case (.custom(let lhsReason), .custom(let rhsReason)): return lhsReason == rhsReason case (.custom, _): return false } } } /// Different shells for which we can generate shell scripts. public enum Shell: String, StringEnumArgument { case bash case zsh public static var completion: ShellCompletion = .values([ (bash.rawValue, "generate completion script for Bourne-again shell"), (zsh.rawValue, "generate completion script for Z shell"), ]) } /// Various shell completions modes supplied by ArgumentKind. 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)]) } /// A protocol representing the possible types of arguments. /// /// Conforming to this protocol will qualify the type to act as /// positional and option arguments in the argument parser. public protocol ArgumentKind { /// Throwable convertion initializer. init(argument: String) throws /// Type of shell completion to provide for this argument. static var completion: ShellCompletion { get } } // MARK: - ArgumentKind conformance for common types 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 { switch argument { case "true": self = true case "false": self = false default: throw ArgumentConversionError.unknown(value: argument) } } public static var completion: ShellCompletion = .unspecified } /// A protocol which implements ArgumentKind for string initializable enums. /// /// Conforming to this protocol will automatically make an enum with is /// String initializable conform to ArgumentKind. public protocol StringEnumArgument: ArgumentKind { init?(rawValue: String) } extension StringEnumArgument { public init(argument: String) throws { guard let value = Self.init(rawValue: argument) else { throw ArgumentConversionError.unknown(value: argument) } self = value } } /// An argument representing a path (file / directory). /// /// The path is resolved in the current working directory. public struct PathArgument: ArgumentKind { public let path: AbsolutePath public init(argument: String) throws { // FIXME: This should check for invalid paths. if let cwd = localFileSystem.currentWorkingDirectory { path = AbsolutePath(argument, relativeTo: cwd) } else { path = try AbsolutePath(validating: argument) } } public static var completion: ShellCompletion = .filename } /// An enum representing the strategy to parse argument values. public enum ArrayParsingStrategy { /// Will parse only the next argument and append all values together: `-Xcc -Lfoo -Xcc -Lbar`. case oneByOne /// Will parse all values up to the next option argument: `--files file1 file2 --verbosity 1`. case upToNextOption /// Will parse all remaining arguments, usually for executable commands: `swift run exe --option 1`. case remaining /// Function that parses the current arguments iterator based on the strategy /// and returns the parsed values. func parse(_ kind: ArgumentKind.Type, with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] { var values: [ArgumentKind] = [] switch self { case .oneByOne: guard let nextArgument = parser.next() else { throw ArgumentParserError.expectedValue(option: parser.currentArgument) } try values.append(kind.init(argument: nextArgument)) case .upToNextOption: /// Iterate over arguments until the end or an optional argument while let nextArgument = parser.peek(), isPositional(argument: nextArgument) { /// We need to call next to consume the argument. The peek above did not. _ = parser.next() try values.append(kind.init(argument: nextArgument)) } case .remaining: while let nextArgument = parser.next() { try values.append(kind.init(argument: nextArgument)) } } return values } } /// A protocol representing positional or options argument. protocol ArgumentProtocol: Hashable { // FIXME: This should be constrained to ArgumentKind but Array can't conform // to it: `extension of type 'Array' with constraints cannot have an // inheritance clause`. // /// The argument kind of this argument for eg String, Bool etc. associatedtype ArgumentKindTy /// Name of the argument which will be parsed by the parser. var name: String { get } /// Short name of the argument, this is usually used in options arguments /// for a short names for e.g: `--help` -> `-h`. var shortName: String? { get } /// The parsing strategy to adopt when parsing values. var strategy: ArrayParsingStrategy { get } /// Defines is the argument is optional var isOptional: Bool { get } /// The usage text associated with this argument. Used to generate complete help string. var usage: String? { get } /// The shell completions to offer as values for this argument. var completion: ShellCompletion { get } // FIXME: Because `ArgumentKindTy`` can't conform to `ArgumentKind`, this // function has to be provided a kind (which will be different from // ArgumentKindTy for arrays). Once the generics feature exists we can // improve this API. // /// Parses and returns the argument values from the parser. func parse(_ kind: ArgumentKind.Type, with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] } extension ArgumentProtocol { // MARK: - Conformance for Hashable public func hash(into hasher: inout Hasher) { return hasher.combine(name) } public static func == (_ lhs: Self, _ rhs: Self) -> Bool { return lhs.name == rhs.name && lhs.usage == rhs.usage } } /// Returns true if the given argument does not starts with '-' i.e. it is /// a positional argument, otherwise it is an options argument. fileprivate func isPositional(argument: String) -> Bool { return !argument.hasPrefix("-") } /// A class representing option arguments. These are optional arguments which may /// or may not be provided in the command line. They are always prefixed by their /// name. For e.g. --verbosity true. public final class OptionArgument: ArgumentProtocol { typealias ArgumentKindTy = Kind let name: String let shortName: String? // Option arguments are always optional. var isOptional: Bool { return true } let strategy: ArrayParsingStrategy let usage: String? let completion: ShellCompletion init(name: String, shortName: String?, strategy: ArrayParsingStrategy, usage: String?, completion: ShellCompletion) { precondition(!isPositional(argument: name)) self.name = name self.shortName = shortName self.strategy = strategy self.usage = usage self.completion = completion } func parse(_ kind: ArgumentKind.Type, with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] { do { return try _parse(kind, with: &parser) } catch let conversionError as ArgumentConversionError { throw ArgumentParserError.invalidValue(argument: name, error: conversionError) } } func _parse(_ kind: ArgumentKind.Type, with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] { // When we have an associated value, we ignore the strategy and only // parse that value. if let associatedArgument = parser.associatedArgumentValue { return try [kind.init(argument: associatedArgument)] } // As a special case, Bool options don't consume arguments. if kind == Bool.self && strategy == .oneByOne { return [true] } let values = try strategy.parse(kind, with: &parser) guard !values.isEmpty else { throw ArgumentParserError.expectedValue(option: name) } return values } } /// A class representing positional arguments. These arguments must be present /// and in the same order as they are added in the parser. public final class PositionalArgument: ArgumentProtocol { typealias ArgumentKindTy = Kind let name: String // Postional arguments don't need short names. var shortName: String? { return nil } let strategy: ArrayParsingStrategy let isOptional: Bool let usage: String? let completion: ShellCompletion init(name: String, strategy: ArrayParsingStrategy, optional: Bool, usage: String?, completion: ShellCompletion) { precondition(isPositional(argument: name)) self.name = name self.strategy = strategy self.isOptional = optional self.usage = usage self.completion = completion } func parse(_ kind: ArgumentKind.Type, with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] { do { return try _parse(kind, with: &parser) } catch let conversionError as ArgumentConversionError { throw ArgumentParserError.invalidValue(argument: name, error: conversionError) } } func _parse(_ kind: ArgumentKind.Type, with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] { let value = try kind.init(argument: parser.currentArgument) var values = [value] switch strategy { case .oneByOne: // We shouldn't apply the strategy with `.oneByOne` because we // already have one, the parsed `parser.currentArgument`. break case .upToNextOption, .remaining: try values.append(contentsOf: strategy.parse(kind, with: &parser)) } return values } } /// A type-erased argument. /// /// Note: Only used for argument parsing purpose. final class AnyArgument: ArgumentProtocol, CustomStringConvertible { typealias ArgumentKindTy = Any let name: String let shortName: String? let strategy: ArrayParsingStrategy let isOptional: Bool let usage: String? let completion: ShellCompletion /// The argument kind this holds, used while initializing that argument. let kind: ArgumentKind.Type /// True if the argument kind is of array type. let isArray: Bool /// A type-erased wrapper around the argument's `parse` function. private let parseClosure: (ArgumentKind.Type, inout ArgumentParserProtocol) throws -> [ArgumentKind] init(_ argument: T) { self.kind = T.ArgumentKindTy.self as! ArgumentKind.Type self.name = argument.name self.shortName = argument.shortName self.strategy = argument.strategy self.isOptional = argument.isOptional self.usage = argument.usage self.completion = argument.completion self.parseClosure = argument.parse(_:with:) isArray = false } /// Initializer for array arguments. init(_ argument: T) where T.ArgumentKindTy: Sequence { self.kind = T.ArgumentKindTy.Element.self as! ArgumentKind.Type self.name = argument.name self.shortName = argument.shortName self.strategy = argument.strategy self.isOptional = argument.isOptional self.usage = argument.usage self.completion = argument.completion self.parseClosure = argument.parse(_:with:) isArray = true } var description: String { return "Argument(\(name))" } func parse(_ kind: ArgumentKind.Type, with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] { return try self.parseClosure(kind, &parser) } func parse(with parser: inout ArgumentParserProtocol) throws -> [ArgumentKind] { return try self.parseClosure(self.kind, &parser) } } // FIXME: We probably don't need this protocol anymore and should convert this to a class. // /// Argument parser protocol passed in initializers of ArgumentKind to manipulate /// parser as needed by the argument. public protocol ArgumentParserProtocol { /// The current argument being parsed. var currentArgument: String { get } /// The associated value in a `--foo=bar` style argument. var associatedArgumentValue: String? { get } /// Provides (consumes) and returns the next argument. Returns `nil` if there are not arguments left. mutating func next() -> String? /// Peek at the next argument without consuming it. func peek() -> String? } /// Argument parser struct responsible to parse the provided array of arguments /// and return the parsed result. public final class ArgumentParser { /// A class representing result of the parsed arguments. public class Result: CustomStringConvertible { /// Internal representation of arguments mapped to their values. private var results = [String: Any]() /// Result of the parent parent parser, if any. private var parentResult: Result? /// Reference to the parser this result belongs to. private let parser: ArgumentParser /// The subparser command chosen. fileprivate var subparser: String? /// Create a result with a parser and parent result. init(parser: ArgumentParser, parent: Result?) { self.parser = parser self.parentResult = parent } /// Adds a result. /// /// - Parameters: /// - values: The associated values of the argument. /// - argument: The argument for which this result is being added. /// - Note: /// While it may seem more fragile to use an array as input in the /// case of single-value arguments, this design choice allows major /// simplifications in the parsing code. fileprivate func add(_ values: [ArgumentKind], for argument: AnyArgument) throws { if argument.isArray { var array = results[argument.name] as? [ArgumentKind] ?? [] array.append(contentsOf: values) results[argument.name] = array } else { // We expect only one value for non-array arguments. guard let value = values.spm_only else { assertionFailure() return } guard results[argument.name] == nil else { throw ArgumentParserError.duplicateArgument(argument.name) } results[argument.name] = value } } /// Get an option argument's value from the results. /// /// Since the options are optional, their result may or may not be present. public func get(_ argument: OptionArgument) -> T? { return (results[argument.name] as? T) ?? parentResult?.get(argument) } /// Array variant for option argument's get(_:). public func get(_ argument: OptionArgument<[T]>) -> [T]? { return (results[argument.name] as? [T]) ?? parentResult?.get(argument) } /// Get a positional argument's value. public func get(_ argument: PositionalArgument) -> T? { return results[argument.name] as? T } /// Array variant for positional argument's get(_:). public func get(_ argument: PositionalArgument<[T]>) -> [T]? { return results[argument.name] as? [T] } /// Get an argument's value using its name. /// - throws: An ArgumentParserError.invalidValue error if the parsed argument does not match the expected type. public func get(_ name: String) throws -> T? { guard let value = results[name] else { // if we have a parent and this is an option argument, look in the parent result if let parentResult = parentResult, name.hasPrefix("-") { return try parentResult.get(name) } else { return nil } } guard let typedValue = value as? T else { throw ArgumentParserError.invalidValue(argument: name, error: .typeMismatch(value: String(describing: value), expectedType: T.self)) } return typedValue } /// Returns true if the given arg is present in the results. public func exists(arg: String) -> Bool { return results[arg] != nil } /// Get the subparser which was chosen for the given parser. public func subparser(_ parser: ArgumentParser) -> String? { if parser === self.parser { return subparser } return parentResult?.subparser(parser) } public var description: String { var description = "ArgParseResult(\(results))" if let parent = parentResult { description += " -> " + parent.description } return description } } /// The mapping of subparsers to their subcommand. private(set) var subparsers: [String: ArgumentParser] = [:] /// List of arguments added to this parser. private(set) var optionArguments: [AnyArgument] = [] private(set) var positionalArguments: [AnyArgument] = [] // If provided, will be substituted instead of arg0 in usage text. let commandName: String? /// Usage string of this parser. let usage: String /// Overview text of this parser. let overview: String /// See more text of this parser. let seeAlso: String? /// If this parser is a subparser. private let isSubparser: Bool /// Boolean specifying if the parser can accept further positional /// arguments (false if it already has a positional argument with /// `isOptional` set to `true` or strategy set to `.remaining`). private var canAcceptPositionalArguments: Bool = true /// Create an argument parser. /// /// - Parameters: /// - commandName: If provided, this will be substitued in "usage" line of the generated usage text. /// Otherwise, first command line argument will be used. /// - usage: The "usage" line of the generated usage text. /// - overview: The "overview" line of the generated usage text. /// - seeAlso: The "see also" line of generated usage text. public init(commandName: String? = nil, usage: String, overview: String, seeAlso: String? = nil) { self.isSubparser = false self.commandName = commandName self.usage = usage self.overview = overview self.seeAlso = seeAlso } /// Create a subparser with its help text. private init(subparser commandName: String, usage: String, overview: String) { self.isSubparser = true self.commandName = commandName self.usage = usage self.overview = overview self.seeAlso = nil } /// Adds an option to the parser. public func add( option: String, shortName: String? = nil, kind: T.Type = T.self, usage: String? = nil, completion: ShellCompletion? = nil ) -> OptionArgument { assert(!optionArguments.contains(where: { $0.name == option }), "Can not define an option twice") let argument = OptionArgument(name: option, shortName: shortName, strategy: .oneByOne, usage: usage, completion: completion ?? T.completion) optionArguments.append(AnyArgument(argument)) return argument } /// Adds an array argument type. public func add( option: String, shortName: String? = nil, kind: [T].Type = [T].self, strategy: ArrayParsingStrategy = .upToNextOption, usage: String? = nil, completion: ShellCompletion? = nil ) -> OptionArgument<[T]> { assert(!optionArguments.contains(where: { $0.name == option }), "Can not define an option twice") let argument = OptionArgument<[T]>(name: option, shortName: shortName, strategy: strategy, usage: usage, completion: completion ?? T.completion) optionArguments.append(AnyArgument(argument)) return argument } /// Adds an argument to the parser. /// /// Note: Only one positional argument is allowed if optional setting is enabled. public func add( positional: String, kind: T.Type = T.self, optional: Bool = false, usage: String? = nil, completion: ShellCompletion? = nil ) -> PositionalArgument { precondition(subparsers.isEmpty, "Positional arguments are not supported with subparsers") precondition(canAcceptPositionalArguments, "Can not accept more positional arguments") if optional { canAcceptPositionalArguments = false } let argument = PositionalArgument(name: positional, strategy: .oneByOne, optional: optional, usage: usage, completion: completion ?? T.completion) positionalArguments.append(AnyArgument(argument)) return argument } /// Adds an argument to the parser. /// /// Note: Only one multiple-value positional argument is allowed. public func add( positional: String, kind: [T].Type = [T].self, optional: Bool = false, strategy: ArrayParsingStrategy = .upToNextOption, usage: String? = nil, completion: ShellCompletion? = nil ) -> PositionalArgument<[T]> { precondition(subparsers.isEmpty, "Positional arguments are not supported with subparsers") precondition(canAcceptPositionalArguments, "Can not accept more positional arguments") if optional || strategy == .remaining { canAcceptPositionalArguments = false } let argument = PositionalArgument<[T]>(name: positional, strategy: strategy, optional: optional, usage: usage, completion: completion ?? T.completion) positionalArguments.append(AnyArgument(argument)) return argument } /// Add a parser with a subcommand name and its corresponding overview. @discardableResult public func add(subparser command: String, usage: String = "", overview: String) -> ArgumentParser { precondition(positionalArguments.isEmpty, "Subparsers are not supported with positional arguments") let parser = ArgumentParser(subparser: command, usage: usage, overview: overview) subparsers[command] = parser return parser } // MARK: - Parsing /// A wrapper struct to pass to the ArgumentKind initializers. struct Parser: ArgumentParserProtocol { let currentArgument: String private(set) var associatedArgumentValue: String? /// The iterator used to iterate arguments. fileprivate var argumentsIterator: IndexingIterator<[String]> init(associatedArgumentValue: String?, argumentsIterator: IndexingIterator<[String]>, currentArgument: String) { self.associatedArgumentValue = associatedArgumentValue self.argumentsIterator = argumentsIterator self.currentArgument = currentArgument } mutating func next() -> String? { return argumentsIterator.next() } func peek() -> String? { var iteratorCopy = argumentsIterator let nextArgument = iteratorCopy.next() return nextArgument } } /// Parses the provided array and return the result. public func parse(_ arguments: [String] = []) throws -> Result { return try parse(arguments, parent: nil) } private func parse(_ arguments: [String] = [], parent: Result?) throws -> Result { let result = Result(parser: self, parent: parent) // Create options map to quickly look up the arguments. let optionsTuple = optionArguments.flatMap({ option -> [(String, AnyArgument)] in var result = [(option.name, option)] // Add the short names too, if we have them. if let shortName = option.shortName { result += [(shortName, option)] } return result }) let optionsMap = Dictionary(uniqueKeysWithValues: optionsTuple) // Create iterators. var positionalArgumentIterator = positionalArguments.makeIterator() var argumentsIterator = arguments.makeIterator() while let argumentString = argumentsIterator.next() { let argument: AnyArgument let parser: Parser // If argument is help then just print usage and exit. if argumentString == "-h" || argumentString == "-help" || argumentString == "--help" { printUsage(on: stdoutStream) exit(0) } else if isPositional(argument: argumentString) { /// If this parser has subparsers, we allow only one positional argument which is the subparser command. if !subparsers.isEmpty { // Make sure this argument has a subparser. guard let subparser = subparsers[argumentString] else { throw ArgumentParserError.expectedArguments(self, Array(subparsers.keys)) } // Save which subparser was chosen. result.subparser = argumentString // Parse reset of the arguments with the subparser. return try subparser.parse(Array(argumentsIterator), parent: result) } // Get the next positional argument we are expecting. guard let positionalArgument = positionalArgumentIterator.next() else { throw ArgumentParserError.unexpectedArgument(argumentString) } argument = positionalArgument parser = Parser( associatedArgumentValue: nil, argumentsIterator: argumentsIterator, currentArgument: argumentString) } else { let (argumentString, value) = argumentString.spm_split(around: "=") // Get the corresponding option for the option argument. guard let optionArgument = optionsMap[argumentString] else { let suggestion = bestMatch(for: argumentString, from: Array(optionsMap.keys)) throw ArgumentParserError.unknownOption(argumentString, suggestion: suggestion) } argument = optionArgument parser = Parser( associatedArgumentValue: value, argumentsIterator: argumentsIterator, currentArgument: argumentString) } // Update results. var parserProtocol = parser as ArgumentParserProtocol let values = try argument.parse(with: &parserProtocol) try result.add(values, for: argument) // Restore the argument iterator state. argumentsIterator = (parserProtocol as! Parser).argumentsIterator } // Report if there are any non-optional positional arguments left which were not present in the arguments. let leftOverArguments = Array(positionalArgumentIterator) if leftOverArguments.contains(where: { !$0.isOptional }) { throw ArgumentParserError.expectedArguments(self, leftOverArguments.map({ $0.name })) } return result } /// Prints usage text for this parser on the provided stream. public func printUsage(on stream: OutputByteStream) { /// Space settings. let maxWidthDefault = 24 let padding = 2 let maxWidth: Int // Determine the max width based on argument length or choose the // default width if max width is longer than the default width. if let maxArgument = (positionalArguments + optionArguments).map({ [$0.name, $0.shortName].compactMap({ $0 }).joined(separator: ", ").count }).max(), maxArgument < maxWidthDefault { maxWidth = maxArgument + padding + 1 } else { maxWidth = maxWidthDefault } /// Prints an argument on a stream if it has usage. func print(formatted argument: String, usage: String, on stream: OutputByteStream) { // Start with a new line and add some padding. stream <<< "\n" <<< Format.asRepeating(string: " ", count: padding) let count = argument.count // If the argument is longer than the max width, print the usage // on a new line. Otherwise, print the usage on the same line. if count >= maxWidth - padding { stream <<< argument <<< "\n" // Align full width because usage is to be printed on a new line. stream <<< Format.asRepeating(string: " ", count: maxWidth + padding) } else { stream <<< argument // Align to the remaining empty space on the line. stream <<< Format.asRepeating(string: " ", count: maxWidth - count) } stream <<< usage } stream <<< "OVERVIEW: " <<< overview if !usage.isEmpty { stream <<< "\n\n" // Get the binary name from command line arguments. let defaultCommandName = CommandLine.arguments[0].components(separatedBy: "/").last! stream <<< "USAGE: " <<< (commandName ?? defaultCommandName) <<< " " <<< usage } if optionArguments.count > 0 { stream <<< "\n\n" stream <<< "OPTIONS:" for argument in optionArguments.lazy.sorted(by: { $0.name < $1.name }) { guard let usage = argument.usage else { continue } // Create name with its shortname, if available. let name = [argument.name, argument.shortName].compactMap({ $0 }).joined(separator: ", ") print(formatted: name, usage: usage, on: stream) } // Print help option, if this is a top level command. if !isSubparser { print(formatted: "--help", usage: "Display available options", on: stream) } } if subparsers.keys.count > 0 { stream <<< "\n\n" stream <<< "SUBCOMMANDS:" for (command, parser) in subparsers.sorted(by: { $0.key < $1.key }) { // Special case for hidden subcommands. guard !parser.overview.isEmpty else { continue } print(formatted: command, usage: parser.overview, on: stream) } } if positionalArguments.count > 0 { stream <<< "\n\n" stream <<< "POSITIONAL ARGUMENTS:" for argument in positionalArguments { guard let usage = argument.usage else { continue } print(formatted: argument.name, usage: usage, on: stream) } } if let seeAlso = seeAlso { stream <<< "\n\n" stream <<< "SEE ALSO: \(seeAlso)" } stream <<< "\n" stream.flush() } } /// A class to bind ArgumentParser's arguments to an option structure. public final class ArgumentBinder { /// The signature of body closure. private typealias BodyClosure = (inout Options, ArgumentParser.Result) throws -> Void /// This array holds the closures which should be executed to fill the options structure. private var bodies = [BodyClosure]() /// Create a binder. public init() { } /// Bind an option argument. public func bind( option: OptionArgument, to body: @escaping (inout Options, T) throws -> Void ) { addBody { guard let result = $1.get(option) else { return } try body(&$0, result) } } /// Bind an array option argument. public func bindArray( option: OptionArgument<[T]>, to body: @escaping (inout Options, [T]) throws -> Void ) { addBody { guard let result = $1.get(option) else { return } try body(&$0, result) } } /// Bind a positional argument. public func bind( positional: PositionalArgument, to body: @escaping (inout Options, T) throws -> Void ) { addBody { // All the positional argument will always be present. guard let result = $1.get(positional) else { return } try body(&$0, result) } } /// Bind an array positional argument. public func bindArray( positional: PositionalArgument<[T]>, to body: @escaping (inout Options, [T]) throws -> Void ) { addBody { // All the positional argument will always be present. guard let result = $1.get(positional) else { return } try body(&$0, result) } } /// Bind two positional arguments. public func bindPositional( _ first: PositionalArgument, _ second: PositionalArgument, to body: @escaping (inout Options, T, U) throws -> Void ) { addBody { // All the positional arguments will always be present. guard let first = $1.get(first) else { return } guard let second = $1.get(second) else { return } try body(&$0, first, second) } } /// Bind three positional arguments. public func bindPositional( _ first: PositionalArgument, _ second: PositionalArgument, _ third: PositionalArgument, to body: @escaping (inout Options, T, U, V) throws -> Void ) { addBody { // All the positional arguments will always be present. guard let first = $1.get(first) else { return } guard let second = $1.get(second) else { return } guard let third = $1.get(third) else { return } try body(&$0, first, second, third) } } /// Bind two options. public func bind( _ first: OptionArgument, _ second: OptionArgument, to body: @escaping (inout Options, T?, U?) throws -> Void ) { addBody { try body(&$0, $1.get(first), $1.get(second)) } } /// Bind three options. public func bind( _ first: OptionArgument, _ second: OptionArgument, _ third: OptionArgument, to body: @escaping (inout Options, T?, U?, V?) throws -> Void ) { addBody { try body(&$0, $1.get(first), $1.get(second), $1.get(third)) } } /// Bind two array options. public func bindArray( _ first: OptionArgument<[T]>, _ second: OptionArgument<[U]>, to body: @escaping (inout Options, [T], [U]) throws -> Void ) { addBody { try body(&$0, $1.get(first) ?? [], $1.get(second) ?? []) } } /// Add three array option and call the final closure with their values. public func bindArray( _ first: OptionArgument<[T]>, _ second: OptionArgument<[U]>, _ third: OptionArgument<[V]>, to body: @escaping (inout Options, [T], [U], [V]) throws -> Void ) { addBody { try body(&$0, $1.get(first) ?? [], $1.get(second) ?? [], $1.get(third) ?? []) } } /// Bind a subparser. public func bind( parser: ArgumentParser, to body: @escaping (inout Options, String) throws -> Void ) { addBody { guard let result = $1.subparser(parser) else { return } try body(&$0, result) } } /// Appends a closure to bodies array. private func addBody(_ body: @escaping BodyClosure) { bodies.append(body) } /// Fill the result into the options structure, /// throwing if one of the user-provided binder function throws. public func fill(parseResult result: ArgumentParser.Result, into options: inout Options) throws { try bodies.forEach { try $0(&options, result) } } /// Fill the result into the options structure. @available(*, deprecated, renamed: "fill(parseResult:into:)") public func fill(_ result: ArgumentParser.Result, into options: inout Options) { try! fill(parseResult: result, into: &options) } }