1
0

CommandParser.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. //===----------------------------------------------------------------------===//
  2. //
  3. // This source file is part of the Swift Argument Parser open source project
  4. //
  5. // Copyright (c) 2020 Apple Inc. and the Swift project authors
  6. // Licensed under Apache License v2.0 with Runtime Library Exception
  7. //
  8. // See https://swift.org/LICENSE.txt for license information
  9. //
  10. //===----------------------------------------------------------------------===//
  11. #if compiler(>=6.0)
  12. #if canImport(Dispatch)
  13. @preconcurrency import class Dispatch.DispatchSemaphore
  14. #endif
  15. #else
  16. #if canImport(Dispatch)
  17. @preconcurrency import class Dispatch.DispatchSemaphore
  18. #endif
  19. #endif
  20. struct CommandError: Error {
  21. var commandStack: [ParsableCommand.Type]
  22. var parserError: ParserError
  23. }
  24. struct HelpRequested: Error {
  25. var visibility: ArgumentVisibility
  26. }
  27. struct CommandParser {
  28. let commandTree: Tree<ParsableCommand.Type>
  29. var currentNode: Tree<ParsableCommand.Type>
  30. var decodedArguments: [DecodedArguments] = []
  31. var rootCommand: ParsableCommand.Type {
  32. commandTree.element
  33. }
  34. var commandStack: [ParsableCommand.Type] {
  35. // Filter to only include types that exist in the command tree.
  36. // This prevents @OptionGroup types that happen to conform to
  37. // ParsableCommand from being included in the command stack (#578).
  38. let result =
  39. decodedArguments
  40. .compactMap { $0.commandType }
  41. .filter { !commandTree.path(to: $0).isEmpty }
  42. if currentNode.element == result.last {
  43. return result
  44. } else {
  45. return result + [currentNode.element]
  46. }
  47. }
  48. init(_ rootCommand: ParsableCommand.Type) {
  49. do {
  50. self.commandTree = try Tree(root: rootCommand)
  51. } catch Tree<ParsableCommand.Type>.InitializationError.recursiveSubcommand(
  52. let command)
  53. {
  54. configurationFailure(
  55. """
  56. The command \"\(command)\" can't have itself as its own subcommand.
  57. """.wrapped(to: 70))
  58. } catch Tree<ParsableCommand.Type>
  59. .InitializationError.aliasMatchingCommand(let command)
  60. {
  61. configurationFailure(
  62. """
  63. The command \"\(command)\" can't have an alias with the same name \
  64. as the command itself.
  65. """.wrapped(to: 70))
  66. } catch {
  67. fatalError("Unexpected error: \(error).")
  68. }
  69. self.currentNode = commandTree
  70. // A command tree that has a depth greater than zero gets a `help`
  71. // subcommand.
  72. if !commandTree.isLeaf {
  73. commandTree.addChild(Tree(HelpCommand.self))
  74. }
  75. }
  76. }
  77. extension CommandParser {
  78. /// Consumes the next argument in `split` if it matches a subcommand at the
  79. /// current node of the command tree.
  80. ///
  81. /// If a matching subcommand is found, the subcommand argument is consumed
  82. /// in `split`.
  83. ///
  84. /// - Returns: A node for the matched subcommand if one was found;
  85. /// otherwise, `nil`.
  86. fileprivate func consumeNextCommand(split: inout SplitArguments) -> Tree<
  87. ParsableCommand.Type
  88. >? {
  89. guard let (origin, element) = split.peekNext(),
  90. element.isValue,
  91. let value = split.originalInput(at: origin),
  92. let subcommandNode = currentNode.firstChild(withName: value)
  93. else { return nil }
  94. _ = split.popNextValue()
  95. return subcommandNode
  96. }
  97. /// Throws a `HelpRequested` error if the user has specified any of the
  98. /// built-in flags.
  99. ///
  100. /// - Parameters:
  101. /// - split: The remaining arguments to examine.
  102. /// - requireSoloArgument: `true` if the built-in flag must be the only
  103. /// input argument remaining for this to catch it.
  104. ///
  105. /// - Throws: If a built-in flag is found.
  106. func checkForBuiltInFlags(
  107. _ split: SplitArguments,
  108. requireSoloArgument: Bool = false
  109. ) throws {
  110. if requireSoloArgument {
  111. // If we require exactly one input argument, then we require at least one
  112. // parsed argument. But we also allow more than one parsed argument
  113. // because certain arguments (such `-help`) get parsed as multiple
  114. // arguments (in this case [-help, -h, -e, -l, -p]).
  115. guard split.count >= 1 else { return }
  116. // Require that all remaining parsed arguments came from the same input
  117. // argument.
  118. let originIndex = split.elements[split.elements.startIndex].index
  119. .inputIndex
  120. for element in split.elements {
  121. guard element.index.inputIndex == originIndex else {
  122. return
  123. }
  124. }
  125. }
  126. // Look for help flags
  127. guard
  128. !split.contains(
  129. anyOf: self.commandStack.getHelpNames(visibility: .default))
  130. else {
  131. throw HelpRequested(visibility: .default)
  132. }
  133. // Look for help-hidden flags
  134. guard
  135. !split.contains(
  136. anyOf: self.commandStack.getHelpNames(visibility: .hidden))
  137. else {
  138. throw HelpRequested(visibility: .hidden)
  139. }
  140. // Look for dump-help flag
  141. guard !split.contains(Name.long("experimental-dump-help")) else {
  142. throw CommandError(
  143. commandStack: commandStack, parserError: .dumpHelpRequested)
  144. }
  145. // Look for a version flag if any commands in the stack define a version
  146. if commandStack.contains(where: { !$0.configuration.version.isEmpty }) {
  147. guard !split.contains(Name.long("version")) else {
  148. throw CommandError(
  149. commandStack: commandStack, parserError: .versionRequested)
  150. }
  151. }
  152. }
  153. /// Returns the last parsed value if there are no remaining unused arguments.
  154. ///
  155. /// If there are remaining arguments or if no commands have been parsed,
  156. /// this throws an error.
  157. fileprivate func extractLastParsedValue(_ split: SplitArguments) throws
  158. -> ParsableCommand
  159. {
  160. try checkForBuiltInFlags(split)
  161. // We should have used up all arguments at this point:
  162. guard !split.containsNonTerminatorArguments else {
  163. // Check if one of the arguments is an unknown option
  164. for element in split.elements {
  165. if case .option(let argument) = element.value {
  166. throw ParserError.unknownOption(
  167. InputOrigin.Element.argumentIndex(element.index), argument.name)
  168. }
  169. }
  170. let extra = split.coalescedExtraElements()
  171. throw ParserError.unexpectedExtraValues(extra)
  172. }
  173. guard
  174. let lastCommand = decodedArguments.lazy.compactMap({ $0.command }).last
  175. else {
  176. throw ParserError.invalidState
  177. }
  178. return lastCommand
  179. }
  180. /// Extracts the current command from `split`, throwing if decoding isn't
  181. /// possible.
  182. fileprivate mutating func parseCurrent(_ split: inout SplitArguments) throws
  183. -> ParsableCommand
  184. {
  185. // Parse the arguments, ignoring anything unexpected
  186. var parser = LenientParser(currentNode.element, split)
  187. let values = try parser.parse()
  188. if currentNode.element.includesAllUnrecognizedArgument {
  189. // If this command includes an all-unrecognized argument, any built-in
  190. // flags will have been parsed into that argument. Check for flags
  191. // before decoding.
  192. try checkForBuiltInFlags(values.capturedUnrecognizedArguments)
  193. }
  194. // Decode the values from ParsedValues into the ParsableCommand:
  195. let decoder = ArgumentDecoder(
  196. values: values, previouslyDecoded: decodedArguments)
  197. var decodedResult: ParsableCommand
  198. do {
  199. decodedResult = try currentNode.element.init(from: decoder)
  200. } catch let error {
  201. // If decoding this command failed, see if they were asking for
  202. // help before propagating that parsing failure.
  203. try checkForBuiltInFlags(split)
  204. throw error
  205. }
  206. // Decoding was successful, so remove the arguments that were used
  207. // by the decoder.
  208. split.removeAll(in: decoder.usedOrigins)
  209. // Save the decoded results to add to the next command.
  210. let newDecodedValues = decoder.previouslyDecoded
  211. .filter { prev in
  212. !decodedArguments.contains(where: { $0.type == prev.type })
  213. }
  214. decodedArguments.append(contentsOf: newDecodedValues)
  215. decodedArguments.append(
  216. DecodedArguments(type: currentNode.element, value: decodedResult))
  217. return decodedResult
  218. }
  219. /// Starting with the current node, extracts commands out of `split` and
  220. /// descends into subcommands as far as possible.
  221. internal mutating func descendingParse(_ split: inout SplitArguments) throws {
  222. while true {
  223. var parsedCommand = try parseCurrent(&split)
  224. // after decoding a command, make sure to validate it
  225. do {
  226. try parsedCommand.validate()
  227. var lastArgument = decodedArguments.removeLast()
  228. lastArgument.value = parsedCommand
  229. decodedArguments.append(lastArgument)
  230. } catch {
  231. try checkForBuiltInFlags(split)
  232. throw CommandError(
  233. commandStack: commandStack,
  234. parserError: ParserError.userValidationError(error))
  235. }
  236. // Look for next command in the argument list.
  237. if let nextCommand = consumeNextCommand(split: &split) {
  238. currentNode = nextCommand
  239. continue
  240. }
  241. // Look for the help flag before falling back to a default command.
  242. try checkForBuiltInFlags(split, requireSoloArgument: true)
  243. // No command was found, so fall back to the default subcommand.
  244. if let defaultSubcommand = currentNode.element.configuration
  245. .defaultSubcommand
  246. {
  247. guard
  248. let subcommandNode = currentNode.firstChild(
  249. equalTo: defaultSubcommand)
  250. else {
  251. throw ParserError.invalidState
  252. }
  253. currentNode = subcommandNode
  254. continue
  255. }
  256. // No more subcommands to parse.
  257. return
  258. }
  259. }
  260. /// Returns the fully-parsed matching command for `arguments`, or an
  261. /// appropriate error.
  262. ///
  263. /// - Parameter arguments: The array of arguments to parse. This should not
  264. /// include the command name as the first argument.
  265. ///
  266. /// - Returns: The parsed command or error.
  267. mutating func parse(
  268. arguments: [String]
  269. ) -> Result<ParsableCommand, CommandError> {
  270. do {
  271. try handleCustomCompletion(arguments)
  272. } catch let error as ParserError {
  273. return .failure(
  274. CommandError(
  275. commandStack: [commandTree.element],
  276. parserError: error))
  277. } catch {
  278. fatalError("Internal error: \(error)")
  279. }
  280. var split: SplitArguments
  281. do {
  282. split = try SplitArguments(arguments: arguments)
  283. } catch let error as ParserError {
  284. return .failure(
  285. CommandError(commandStack: [commandTree.element], parserError: error))
  286. } catch {
  287. return .failure(
  288. CommandError(
  289. commandStack: [commandTree.element], parserError: .invalidState))
  290. }
  291. do {
  292. try checkForCompletionScriptRequest(&split)
  293. try descendingParse(&split)
  294. let result = try extractLastParsedValue(split)
  295. // HelpCommand is a valid result, but needs extra information about
  296. // the tree from the parser to build its stack of commands.
  297. if var helpResult = result as? HelpCommand {
  298. try helpResult.buildCommandStack(with: self)
  299. return .success(helpResult)
  300. }
  301. return .success(result)
  302. } catch let error as CommandError {
  303. return .failure(error)
  304. } catch let error as ParserError {
  305. let error = arguments.isEmpty ? ParserError.noArguments(error) : error
  306. return .failure(
  307. CommandError(commandStack: commandStack, parserError: error))
  308. } catch let helpRequest as HelpRequested {
  309. return .success(
  310. HelpCommand(
  311. commandStack: commandStack,
  312. visibility: helpRequest.visibility))
  313. } catch {
  314. return .failure(
  315. CommandError(commandStack: commandStack, parserError: .invalidState))
  316. }
  317. }
  318. }
  319. // MARK: Completion Script Support
  320. struct GenerateCompletions: ParsableCommand {
  321. @Option() var generateCompletionScript: String
  322. }
  323. struct AutodetectedGenerateCompletions: ParsableCommand {
  324. @Flag() var generateCompletionScript = false
  325. }
  326. extension CommandParser {
  327. func checkForCompletionScriptRequest(_ split: inout SplitArguments) throws {
  328. // Pseudo-commands don't support `--generate-completion-script` flag
  329. guard rootCommand.configuration._superCommandName == nil else {
  330. return
  331. }
  332. // We don't have the ability to check for `--name [value]`-style args yet,
  333. // so we need to try parsing two different commands.
  334. // First look for `--generate-completion-script <shell>`
  335. var completionsParser = CommandParser(GenerateCompletions.self)
  336. if let result = try? completionsParser.parseCurrent(&split)
  337. as? GenerateCompletions
  338. {
  339. throw CommandError(
  340. commandStack: commandStack,
  341. parserError: .completionScriptRequested(
  342. shell: result.generateCompletionScript))
  343. }
  344. // Check for for `--generate-completion-script` without a value
  345. var autodetectedParser = CommandParser(AutodetectedGenerateCompletions.self)
  346. if let result = try? autodetectedParser.parseCurrent(&split)
  347. as? AutodetectedGenerateCompletions,
  348. result.generateCompletionScript
  349. {
  350. throw CommandError(
  351. commandStack: commandStack,
  352. parserError: .completionScriptRequested(shell: nil))
  353. }
  354. }
  355. func handleCustomCompletion(_ arguments: [String]) throws {
  356. // Completion functions use a custom format:
  357. //
  358. // <command> ---completion [<subcommand> ...] -- <argument-name> <argument-index> <cursor-index> [<argument> ...]
  359. //
  360. // <argument-index> is the 0-based index of the <argument> for which completions are being requested.
  361. //
  362. // <cursor-index> is the 0-based index of the character within the <argument> before which the cursor is located.
  363. // For an <argument> whose length is n, if the cursor is after the last element, <cursor-index> will be set to n.
  364. //
  365. // The triple-dash prefix makes '---completion' invalid syntax for regular
  366. // arguments, so it's safe to use for this internal purpose.
  367. guard arguments.first == "---completion"
  368. else { return }
  369. var args = arguments.dropFirst()
  370. var current = commandTree
  371. while let subcommandName = args.popFirst() {
  372. // A double dash separates the subcommands from the argument information
  373. if subcommandName == "--" { break }
  374. guard let nextCommandNode = current.firstChild(withName: subcommandName)
  375. else { throw ParserError.invalidState }
  376. current = nextCommandNode
  377. }
  378. // Some kind of argument name is the next required element
  379. guard let argToMatch = args.popFirst() else {
  380. throw ParserError.invalidState
  381. }
  382. // Generate the argument set and parse the argument to find in the set
  383. let argset = ArgumentSet(current.element, visibility: .private, parent: nil)
  384. guard let parsedArgument = try parseIndividualArg(argToMatch, at: 0).first
  385. else { throw ParserError.invalidState }
  386. // Look up the specified argument, then retrieve & run its custom completion function
  387. switch parsedArgument.value {
  388. case .option(let parsed):
  389. guard let matchedArgument = argset.first(matching: parsed) else {
  390. throw ParserError.invalidState
  391. }
  392. try customComplete(matchedArgument, forArguments: Array(args))
  393. case .value(let value):
  394. // Legacy completion script generators use internal key paths to identify
  395. // positional args, e.g. optionGroupA.optionGroupB.property. Newer
  396. // generators based on ToolInfo use the `positional@<index>` syntax which
  397. // avoids leaking implementation details of the tool.
  398. let toolInfoPrefix = "positional@"
  399. if value.hasPrefix(toolInfoPrefix) {
  400. guard
  401. let index = Int(value.dropFirst(toolInfoPrefix.count)),
  402. let matchedArgument = argset.positional(at: index)
  403. else {
  404. throw ParserError.invalidState
  405. }
  406. try customComplete(matchedArgument, forArguments: Array(args))
  407. } else {
  408. guard
  409. let key = InputKey(fullPathString: value),
  410. let matchedArgument = argset.firstPositional(withKey: key)
  411. else {
  412. throw ParserError.invalidState
  413. }
  414. try customComplete(matchedArgument, forArguments: Array(args))
  415. }
  416. case .terminator:
  417. throw ParserError.invalidState
  418. }
  419. }
  420. private func customComplete(
  421. _ argument: ArgumentDefinition,
  422. forArguments args: [String]
  423. ) throws {
  424. if let completionShellName = Platform.Environment[.shellName] {
  425. let shell = CompletionShell(rawValue: completionShellName)
  426. CompletionShell._requesting.withLock { $0 = shell }
  427. }
  428. CompletionShell._requestingVersion.withLock {
  429. $0 = Platform.Environment[.shellVersion]
  430. }
  431. let completions: [String]
  432. switch argument.completion.kind {
  433. case .custom(let complete):
  434. let (args, completingArgumentIndex, completingPrefix) =
  435. try parseCustomCompletionArguments(from: args)
  436. completions = complete(
  437. args,
  438. completingArgumentIndex,
  439. completingPrefix
  440. )
  441. case .customAsync(let complete):
  442. #if canImport(Dispatch)
  443. if #available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
  444. {
  445. completions = try asyncCustomCompletions(from: args, complete: complete)
  446. } else {
  447. throw ParserError.invalidState
  448. }
  449. #else
  450. throw ParserError.invalidState
  451. #endif
  452. case .customDeprecated(let complete):
  453. completions = complete(args)
  454. default:
  455. throw ParserError.invalidState
  456. }
  457. // Parsing and retrieval successful! We don't want to continue with any
  458. // other parsing here, so after printing the result of the completion
  459. // function, exit with a success code.
  460. throw ParserError.completionScriptCustomResponse(
  461. CompletionShell.requesting?.format(completions: completions)
  462. ?? completions.joined(separator: "\n")
  463. )
  464. }
  465. }
  466. private func parseCustomCompletionArguments(
  467. from args: [String]
  468. ) throws -> ([String], Int, String) {
  469. var args = args.dropFirst(0)
  470. guard
  471. let s = args.popFirst(),
  472. let completingArgumentIndex = Int(s)
  473. else {
  474. throw ParserError.invalidState
  475. }
  476. guard
  477. let arg = args.popFirst(),
  478. let cursorIndexWithinCompletingArgument = Int(arg)
  479. else {
  480. throw ParserError.invalidState
  481. }
  482. let completingPrefix: String
  483. if let completingArgument = args.last {
  484. completingPrefix = String(
  485. completingArgument.prefix(cursorIndexWithinCompletingArgument)
  486. )
  487. } else if cursorIndexWithinCompletingArgument == 0 {
  488. completingPrefix = ""
  489. } else {
  490. throw ParserError.invalidState
  491. }
  492. return (Array(args), completingArgumentIndex, completingPrefix)
  493. }
  494. #if !canImport(Dispatch)
  495. @available(*, unavailable, message: "DispatchSemaphore is unavailable")
  496. @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
  497. private func asyncCustomCompletions(
  498. from args: [String],
  499. complete: @escaping @Sendable ([String], Int, String) async -> [String]
  500. ) throws -> [String] {
  501. throw ParserError.invalidState
  502. }
  503. #else
  504. @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
  505. private func asyncCustomCompletions(
  506. from args: [String],
  507. complete: @escaping @Sendable ([String], Int, String) async -> [String]
  508. ) throws -> [String] {
  509. let (args, completingArgumentIndex, completingPrefix) =
  510. try parseCustomCompletionArguments(from: args)
  511. let completionsBox = ArgParserMutex<[String]>([])
  512. let semaphore = DispatchSemaphore(value: 0)
  513. Task {
  514. let completion = await complete(
  515. args,
  516. completingArgumentIndex,
  517. completingPrefix)
  518. completionsBox.withLock { $0 = completion }
  519. semaphore.signal()
  520. }
  521. semaphore.wait()
  522. return completionsBox.withLock { $0 }
  523. }
  524. #endif
  525. // MARK: Building Command Stacks
  526. extension CommandParser {
  527. /// Builds an array of commands that matches the given command names.
  528. ///
  529. /// This stops building the stack if it encounters any command names that
  530. /// aren't in the command tree, so it's okay to pass a list of arbitrary
  531. /// commands. Will always return at least the root of the command tree.
  532. func commandStack(for commandNames: [String]) -> [ParsableCommand.Type] {
  533. var node = commandTree
  534. var result = [node.element]
  535. for name in commandNames {
  536. guard let nextNode = node.firstChild(withName: name) else {
  537. // Reached a non-command argument.
  538. // Ignore anything after this point
  539. return result
  540. }
  541. result.append(nextNode.element)
  542. node = nextNode
  543. }
  544. return result
  545. }
  546. func commandStack(
  547. for subcommand: ParsableCommand.Type
  548. ) -> [ParsableCommand.Type] {
  549. let path = commandTree.path(to: subcommand)
  550. return path.isEmpty
  551. ? [commandTree.element]
  552. : path
  553. }
  554. }
  555. extension SplitArguments {
  556. func contains(_ needle: Name) -> Bool {
  557. self.elements.contains {
  558. switch $0.value {
  559. case .option(.name(let name)),
  560. .option(.nameWithValue(let name, _)):
  561. return name == needle
  562. default:
  563. return false
  564. }
  565. }
  566. }
  567. func contains(anyOf names: [Name]) -> Bool {
  568. self.elements.contains {
  569. switch $0.value {
  570. case .option(.name(let name)),
  571. .option(.nameWithValue(let name, _)):
  572. return names.contains(name)
  573. default:
  574. return false
  575. }
  576. }
  577. }
  578. }