| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657 |
- //===----------------------------------------------------------------------===//
- //
- // 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] <input> <output>
- ///
- /// The `-v | -f` part is one *set* that’s optional, `<input> <output>` is
- /// another. Both of these can then be combined into a third set.
- struct ArgumentSet {
- var content: [ArgumentDefinition] = []
- var namePositions: [Name: Int] = [:]
- init<S: Sequence>(_ 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<Value: Equatable>(
- 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[..<endOfInput].filter {
- $0.index.subIndex == .complete
- }[...]
- guard !argumentStack.isEmpty else { return }
- /// Pops arguments off the stack until the next valid value.
- ///
- /// Skips over dash-prefixed inputs unless `unconditional` is `true`.
- func next(unconditional: Bool) -> 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
- }
- }
|