From 11afee447525339a953776598cf4731d824b846d Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 21 Jul 2025 20:24:45 -0400 Subject: [PATCH 1/3] Add a new parseAllAsRoot() for ParseableCommand to get the full list of parsed commands --- .../Parsable Types/ParsableCommand.swift | 17 ++++++++++ .../Parsing/CommandParser.swift | 34 +++++++++++++------ .../SubcommandEndToEndTests.swift | 13 +++++++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift index 723fd733..485b5eb7 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @@ -68,6 +68,23 @@ extension ParsableCommand { return try parser.parse(arguments: arguments).get() } + /// Parses an instance of this type, from command-line arguments + /// and provide all of the commands in order from this one to possible + /// subcommands to a final leaf command to be run. + /// + /// - Parameter arguments: An array of arguments to use for parsing. If + /// `arguments` is `nil`, this uses the program's command-line arguments. + /// - Returns: A new instance of this type, one of its subcommands, or a + /// command type internal to the `ArgumentParser` library. + /// - Throws: If parsing fails. + public static func parseAllAsRoot( + _ arguments: [String]? = nil + ) throws -> [ParsableCommand] { + var parser = CommandParser(self) + let arguments = arguments ?? Array(CommandLine._staticArguments.dropFirst()) + return try parser.parseAll(arguments: arguments).get() + } + /// Returns the text of the help screen for the given subcommand of this /// command. /// diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 3f2f08cc..8311928b 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -148,12 +148,12 @@ extension CommandParser { } } - /// Returns the last parsed value if there are no remaining unused arguments. + /// Returns the parsed values if there are no remaining unused arguments. /// /// If there are remaining arguments or if no commands have been parsed, /// this throws an error. - fileprivate func extractLastParsedValue(_ split: SplitArguments) throws - -> ParsableCommand + fileprivate func extractParsedValues(_ split: SplitArguments) throws + -> [ParsableCommand] { try checkForBuiltInFlags(split) @@ -172,12 +172,13 @@ extension CommandParser { } guard - let lastCommand = decodedArguments.lazy.compactMap({ $0.command }).last + case let parsedCommands = decodedArguments.lazy.compactMap({ $0.command }), + parsedCommands.count > 0 else { throw ParserError.invalidState } - return lastCommand + return [ParsableCommand](parsedCommands) } /// Extracts the current command from `split`, throwing if decoding isn't @@ -282,6 +283,19 @@ extension CommandParser { mutating func parse( arguments: [String] ) -> Result { + return self.parseAll(arguments: arguments).map { $0.last! } + } + + /// Returns the fully-parsed matching commands for `arguments`, or an + /// appropriate error. + /// + /// - Parameter arguments: The array of arguments to parse. This should not + /// include the command name as the first argument. + /// + /// - Returns: The parsed commands or error. + mutating func parseAll( + arguments: [String] + ) -> Result<[ParsableCommand], CommandError> { do { try handleCustomCompletion(arguments) } catch let error as ParserError { @@ -308,13 +322,13 @@ extension CommandParser { do { try checkForCompletionScriptRequest(&split) try descendingParse(&split) - let result = try extractLastParsedValue(split) + let result = try extractParsedValues(split) // HelpCommand is a valid result, but needs extra information about // the tree from the parser to build its stack of commands. - if var helpResult = result as? HelpCommand { + if var helpResult = result.last as? HelpCommand { try helpResult.buildCommandStack(with: self) - return .success(helpResult) + return .success([helpResult]) } return .success(result) } catch let error as CommandError { @@ -325,9 +339,9 @@ extension CommandParser { CommandError(commandStack: commandStack, parserError: error)) } catch let helpRequest as HelpRequested { return .success( - HelpCommand( + [HelpCommand( commandStack: commandStack, - visibility: helpRequest.visibility)) + visibility: helpRequest.visibility)]) } catch { return .failure( CommandError(commandStack: commandStack, parserError: .invalidState)) diff --git a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift index 1de975b2..6d062108 100644 --- a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift @@ -68,6 +68,19 @@ extension SubcommandEndToEndTests { XCTAssertEqual(a.foo.name, "Foo") } + let allCmds = try Foo.parseAllAsRoot(["--name", "Foo", "a", "--bar", "42"]) + XCTAssertEqual(allCmds.count, 2) + guard let fooCmd = (allCmds[0] as? Foo) else { + XCTFail("") + return + } + XCTAssertEqual(fooCmd.name, "Foo") + guard let aCmd = (allCmds[1] as? CommandA) else { + XCTFail("") + return + } + XCTAssertEqual(aCmd.bar, 42) + AssertParseCommand(Foo.self, Foo.self, ["--name", "Foo"]) { foo in XCTAssertEqual(foo.name, "Foo") } From e03168c896db8ee36bb76aba07d5dc740af6cfc2 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 21 Jul 2025 20:53:32 -0400 Subject: [PATCH 2/3] Fix linter error --- Sources/ArgumentParser/Parsing/CommandParser.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 8311928b..993d0ad2 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -283,7 +283,8 @@ extension CommandParser { mutating func parse( arguments: [String] ) -> Result { - return self.parseAll(arguments: arguments).map { $0.last! } + // swift-format-ignore: NeverForceUnwrap + self.parseAll(arguments: arguments).map { $0.last! } } /// Returns the fully-parsed matching commands for `arguments`, or an From 839ad49e8588b0f0ebbb9a0a569e501fe7724d72 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 21 Jul 2025 20:57:36 -0400 Subject: [PATCH 3/3] Fix formatting error --- Sources/ArgumentParser/Parsing/CommandParser.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 993d0ad2..8c60fa75 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -340,9 +340,11 @@ extension CommandParser { CommandError(commandStack: commandStack, parserError: error)) } catch let helpRequest as HelpRequested { return .success( - [HelpCommand( - commandStack: commandStack, - visibility: helpRequest.visibility)]) + [ + HelpCommand( + commandStack: commandStack, + visibility: helpRequest.visibility) + ]) } catch { return .failure( CommandError(commandStack: commandStack, parserError: .invalidState))