//===----------------------------------------------------------------------===// // // This source file is part of the Swift Argument Parser open source project // // Copyright (c) 2020 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 // //===----------------------------------------------------------------------===// /// A nested tree of argument definitions. /// /// The main reason for having a nested representation is to build help output. /// For output like: /// /// Usage: mytool [-v | -f] /// /// The `-v | -f` part is one *set* that’s optional, ` ` is /// another. Both of these can then be combined into a third set. struct ArgumentSet { var content: [ArgumentDefinition] = [] var namePositions: [Name: Int] = [:] init(_ arguments: S) where S.Element == ArgumentDefinition { self.content = Array(arguments) self.namePositions = Dictionary( content.enumerated().flatMap { i, arg in arg.names.map { ($0.nameToMatch, i) } }, uniquingKeysWith: { first, _ in first }) } init() {} init(_ arg: ArgumentDefinition) { self.init([arg]) } init(sets: [ArgumentSet]) { self.init(sets.joined()) } mutating func append(_ arg: ArgumentDefinition) { let newPosition = content.count content.append(arg) for name in arg.names where namePositions[name.nameToMatch] == nil { namePositions[name.nameToMatch] = newPosition } } } extension ArgumentSet: CustomDebugStringConvertible { var debugDescription: String { content .map { $0.debugDescription } .joined(separator: " / ") } } extension ArgumentSet: RandomAccessCollection { var startIndex: Int { content.startIndex } var endIndex: Int { content.endIndex } subscript(position: Int) -> ArgumentDefinition { content[position] } } // MARK: Flag extension ArgumentSet { /// Creates an argument set for a single Boolean flag. static func flag( key: InputKey, name: NameSpecification, default initialValue: Bool?, help: ArgumentHelp? ) -> ArgumentSet { // The flag is required if initialValue is `nil`, otherwise it's optional let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : [] let defaultValueString = initialValue == true ? "true" : nil let help = ArgumentDefinition.Help( allValueStrings: [], options: helpOptions, help: help, defaultValue: defaultValueString, key: key, isComposite: false) let arg = ArgumentDefinition( kind: .name(key: key, specification: name), help: help, completion: .default, update: .nullary({ (origin, name, values) in values.set(true, forKey: key, inputOrigin: origin) }), initial: { origin, values in if let initialValue = initialValue { values.set(initialValue, forKey: key, inputOrigin: origin) } }) return ArgumentSet(arg) } static func updateFlag( key: InputKey, value: Value, origin: InputOrigin, values: inout ParsedValues, exclusivity: FlagExclusivity ) throws { let hasUpdated: Bool if let previous = values.element(forKey: key) { hasUpdated = !previous.inputOrigin.elements.isEmpty } else { hasUpdated = false } switch (hasUpdated, exclusivity.base) { case (true, .exclusive): // This value has already been set. if let previous = values.element(forKey: key) { if (previous.value as? Value) == value { // setting the value again will consume the argument values.set(value, forKey: key, inputOrigin: origin) } else { throw ParserError.duplicateExclusiveValues( previous: previous.inputOrigin, duplicate: origin, originalInput: values.originalInput) } } case (true, .chooseFirst): values.update( forKey: key, inputOrigin: origin, initial: value, closure: { _ in }) case (false, _), (_, .chooseLast): values.set(value, forKey: key, inputOrigin: origin) } } /// Creates an argument set for a pair of inverted Boolean flags. static func flag( key: InputKey, name: NameSpecification, default initialValue: Bool?, required: Bool, inversion: FlagInversion, exclusivity: FlagExclusivity, help: ArgumentHelp? ) -> ArgumentSet { let helpOptions: ArgumentDefinition.Help.Options = required ? [] : .isOptional let (enableNames, disableNames) = inversion.enableDisableNamePair( for: key, name: name) let initialValueNames = initialValue.map { $0 ? enableNames : disableNames } let enableHelp = ArgumentDefinition.Help( allValueStrings: [], options: helpOptions, help: help, defaultValue: initialValueNames?.first?.synopsisString, key: key, isComposite: true) let disableHelp = ArgumentDefinition.Help( allValueStrings: [], options: [.isOptional], help: help, defaultValue: nil, key: key, isComposite: false) let enableArg = ArgumentDefinition( kind: .named(enableNames), help: enableHelp, completion: .default, update: .nullary({ (origin, name, values) in try ArgumentSet.updateFlag( key: key, value: true, origin: origin, values: &values, exclusivity: exclusivity) }), initial: { origin, values in if let initialValue = initialValue { values.set(initialValue, forKey: key, inputOrigin: origin) } }) let disableArg = ArgumentDefinition( kind: .named(disableNames), help: disableHelp, completion: .default, update: .nullary({ (origin, name, values) in try ArgumentSet.updateFlag( key: key, value: false, origin: origin, values: &values, exclusivity: exclusivity) }), initial: { _, _ in }) return ArgumentSet([enableArg, disableArg]) } /// Creates an argument set for an incrementing integer flag. static func counter( key: InputKey, name: NameSpecification, help: ArgumentHelp? ) -> ArgumentSet { let help = ArgumentDefinition.Help( allValueStrings: [], options: [.isOptional, .isRepeating], help: help, defaultValue: nil, key: key, isComposite: false) let arg = ArgumentDefinition( kind: .name(key: key, specification: name), help: help, completion: .default, update: .nullary({ (origin, name, values) in guard let a = values.element(forKey: key)?.value, let b = a as? Int else { throw ParserError.invalidState } values.set(b + 1, forKey: key, inputOrigin: origin) }), initial: { origin, values in values.set(0, forKey: key, inputOrigin: origin) }) return ArgumentSet(arg) } } extension ArgumentSet { /// Fills the given `ParsedValues` instance with initial values from this /// argument set. func setInitialValues(into parsed: inout ParsedValues) throws { for arg in self { try arg.initial(InputOrigin(), &parsed) } } } extension ArgumentSet { /// Find an `ArgumentDefinition` that matches the given `ParsedArgument`. /// /// As we iterate over the values from the command line, we try to find a /// definition that matches the particular element. /// /// - Parameter parsed: The argument from the command line /// /// - Returns: The matching definition. func first( matching parsed: ParsedArgument ) -> ArgumentDefinition? { namePositions[parsed.name].map { content[$0] } } func firstPositional( withKey key: InputKey ) -> ArgumentDefinition? { first(where: { $0.help.keys.contains(key) }) } func positional( at index: Int ) -> ArgumentDefinition? { let positionals = content.filter { $0.isPositional } guard positionals.count > index else { return nil } return positionals[index] } } /// A parser for a given input and set of arguments defined by the given /// command. /// /// This parser will consume only the arguments that it understands. If any /// arguments are declared to capture all remaining input, or a subcommand /// is configured as such, parsing stops on the first positional argument or /// unrecognized dash-prefixed argument. struct LenientParser { var command: ParsableCommand.Type var argumentSet: ArgumentSet var inputArguments: SplitArguments init(_ command: ParsableCommand.Type, _ split: SplitArguments) { self.command = command self.argumentSet = ArgumentSet(command, visibility: .private, parent: nil) self.inputArguments = split } var defaultCapturesForPassthrough: Bool { command.defaultIncludesPassthroughArguments } var subcommands: [ParsableCommand.Type] { command.configuration.subcommands } func errorForMissingValue( _ originElement: InputOrigin.Element, _ parsed: ParsedArgument ) -> ParserError { if case .argumentIndex(let index) = originElement, index.subIndex != .complete, let originalInput = inputArguments .originalInput(at: .argumentIndex(index.completeIndex)) { let completeName = Name(originalInput[...]) return ParserError.missingValueOrUnknownCompositeOption( InputOrigin(element: originElement), parsed.name, completeName) } else { return ParserError.missingValueForOption( InputOrigin(element: originElement), parsed.name) } } mutating func parseValue( _ argument: ArgumentDefinition, _ parsed: ParsedArgument, _ originElement: InputOrigin.Element, _ update: ArgumentDefinition.Update.Unary, _ result: inout ParsedValues, _ usedOrigins: inout InputOrigin ) throws { let origin = InputOrigin(elements: [originElement]) switch argument.parsingStrategy { case .default: // We need a value for this option. if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement( at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) } else if let (origin2, value) = inputArguments.popNextElementIfValue( after: originElement) { // Use `popNextElementIfValue(after:)` to handle cases where short option // labels are combined let origins = origin.inserting(origin2) try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } else { throw errorForMissingValue(originElement, parsed) } case .scanningForValue: // We need a value for this option. if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement( at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) } else if let (origin2, value) = inputArguments.popNextValue( after: originElement) { // Use `popNext(after:)` to handle cases where short option // labels are combined let origins = origin.inserting(origin2) try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } else { throw errorForMissingValue(originElement, parsed) } case .unconditional: // Use an attached value if it exists... if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement( at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) } else { guard let (origin2, value) = inputArguments.popNextElementAsValue( after: originElement) else { throw errorForMissingValue(originElement, parsed) } let origins = origin.inserting(origin2) try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } case .allRemainingInput: // Reset initial value with the found input origins: try argument.initial(origin, &result) // Use an attached value if it exists... if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement( at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) inputArguments.removeAll(in: usedOrigins) } // ...and then consume the rest of the arguments while let (origin2, value) = inputArguments.popNextElementAsValue( after: originElement) { let origins = origin.inserting(origin2) try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } case .upToNextOption: // Use an attached value if it exists... var foundAttachedValue = false if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) foundAttachedValue = true } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement( at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) inputArguments.removeAll(in: usedOrigins) foundAttachedValue = true } // Clear out the initial origin first, since it can include // the exploded elements of an options group (see issue #327). usedOrigins.formUnion(origin) inputArguments.removeAll(in: origin) // Fix incorrect error message // for @Option array without values (see issue #434). guard let first = inputArguments.elements.first, first.isValue else { // No independent values to be found, which is an error if there was // no `--foo=bar`-style value already found. if foundAttachedValue { break } else { throw errorForMissingValue(originElement, parsed) } } // ...and then consume the arguments until hitting an option while let (origin2, value) = inputArguments.popNextElementIfValue() { let origins = origin.inserting(origin2) try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } case .postTerminator, .allUnrecognized: // These parsing kinds are for arguments only. throw ParserError.invalidState } } mutating func parsePositionalValues( from unusedInput: SplitArguments, into result: inout ParsedValues ) throws { var endOfInput = unusedInput.elements.endIndex // If this argument set includes a definition that should collect all the // post-terminator inputs, capture them before trying to fill other // `@Argument` definitions. if let postTerminatorArg = argumentSet.first(where: { def in def.isRepeatingPositional && def.parsingStrategy == .postTerminator }), case .unary(let update) = postTerminatorArg.update, let terminatorIndex = unusedInput.elements.firstIndex( where: \.isTerminator) { for input in unusedInput.elements[(terminatorIndex + 1)...] { // swift-format-ignore: NeverForceUnwrap // Everything post-terminator is a value, force-unwrapping here is safe: let value = input.value.valueString! try update([.argumentIndex(input.index)], nil, value, &result) } endOfInput = terminatorIndex } // Create a stack out of the remaining unused inputs that aren't "partial" // arguments (i.e. the individual components of a `-vix` grouped short // option input). var argumentStack = unusedInput.elements[.. SplitArguments.Element? { while let arg = argumentStack.popFirst() { if arg.isValue || unconditional { return arg } } return nil } // For all positional arguments, consume one or more inputs. var usedOrigins = InputOrigin() ArgumentLoop: for argumentDefinition in argumentSet { guard case .positional = argumentDefinition.kind else { continue } switch argumentDefinition.parsingStrategy { case .default, .allRemainingInput: break default: continue ArgumentLoop } guard case .unary(let update) = argumentDefinition.update else { preconditionFailure("Shouldn't see a nullary positional argument.") } let allowOptionsAsInput = argumentDefinition.parsingStrategy == .allRemainingInput repeat { guard let arg = next(unconditional: allowOptionsAsInput) else { break ArgumentLoop } let origin: InputOrigin.Element = .argumentIndex(arg.index) // swift-format-ignore: NeverForceUnwrap // FIXME: I dont actually know why this is safe let value = unusedInput.originalInput(at: origin)! try update([origin], nil, value, &result) usedOrigins.insert(origin) } while argumentDefinition.isRepeatingPositional } // If there's an `.allUnrecognized` argument array, collect leftover args. if let allUnrecognizedArg = argumentSet.first(where: { def in def.isRepeatingPositional && def.parsingStrategy == .allUnrecognized }), case .unary(let update) = allUnrecognizedArg.update { result.capturedUnrecognizedArguments = SplitArguments( _elements: Array(argumentStack), originalInput: []) while let arg = argumentStack.popFirst() { let origin: InputOrigin.Element = .argumentIndex(arg.index) // swift-format-ignore: NeverForceUnwrap // FIXME: I dont actually know why this is safe let value = unusedInput.originalInput(at: origin)! try update([origin], nil, value, &result) } } } mutating func parse() throws -> ParsedValues { let originalInput = inputArguments defer { inputArguments = originalInput } // If this argument set includes a positional argument that unconditionally // captures all remaining input, we use a different behavior, where we // shortcut out at the first sign of a positional argument or unrecognized // option/flag label. let capturesForPassthrough = defaultCapturesForPassthrough || argumentSet.contains(where: { arg in arg.isRepeatingPositional && arg.parsingStrategy == .allRemainingInput }) var result = ParsedValues( elements: [:], originalInput: inputArguments.originalInput) var allUsedOrigins = InputOrigin() try argumentSet.setInitialValues(into: &result) // Loop over all arguments: ArgumentLoop: while let (origin, next) = inputArguments.popNext() { var usedOrigins = InputOrigin() defer { inputArguments.removeAll(in: usedOrigins) allUsedOrigins.formUnion(usedOrigins) } switch next.value { case .value(let argument): // Special handling for matching subcommand names. We generally want // parsing to skip over unrecognized input, but if the current // command or the matched subcommand captures all remaining input, // then we want to break out of parsing at this point. let matchedSubcommand = subcommands.first(where: { $0._commandName == argument || $0.configuration.aliases.contains(argument) }) if let matchedSubcommand { if !matchedSubcommand.includesPassthroughArguments && defaultCapturesForPassthrough { continue ArgumentLoop } else if matchedSubcommand.includesPassthroughArguments { break ArgumentLoop } } // If we're capturing all, the first positional value represents the // start of positional input. if capturesForPassthrough { break ArgumentLoop } // We'll parse positional values later. break case .option(let parsed): // Look for an argument that matches this `--option` or `-o`-style // input. If we can't find one, just move on to the next input. We // defer catching leftover arguments until we've fully extracted all // the information for the selected command. guard let argument = argumentSet.first(matching: parsed) else { // If we're capturing all, an unrecognized option/flag is the start // of positional input. However, the first time we see an option // pack (like `-fi`) it looks like a long name with a single-dash // prefix, which may not match an argument even if its subcomponents // will match. if capturesForPassthrough && parsed.subarguments.isEmpty { break ArgumentLoop } // Otherwise, continue parsing. This option/flag may get picked up // by a child command. continue } switch argument.update { case .nullary(let update): // We don’t expect a value for this option. if let value = parsed.value { throw ParserError.unexpectedValueForOption( origin, parsed.name, value) } _ = try update([origin], parsed.name, &result) usedOrigins.insert(origin) case .unary(let update): try parseValue( argument, parsed, origin, update, &result, &usedOrigins) } case .terminator: // Ignore the terminator, it might get picked up as a positional value later. break } } // We have parsed all non-positional values at this point. // Next: parse / consume the positional values. var unusedArguments = originalInput unusedArguments.removeAll(in: allUsedOrigins) try parsePositionalValues(from: unusedArguments, into: &result) return result } }