From 9eeedff078720f82b5482b27b226a30f7e119909 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 13 May 2025 13:02:44 -0400 Subject: [PATCH 1/8] Nonexclusive flags implemented via an array of enum cases are now separate ArgumentInfoV0 instances, instead of different names for the same ArgumentInfoV0. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Usage/DumpHelpGenerator.swift | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 487d372c..70295f3b 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -52,41 +52,6 @@ extension BidirectionalCollection where Element == ParsableCommand.Type { } } -extension ArgumentSet { - fileprivate func mergingCompositeArguments() -> ArgumentSet { - var arguments = ArgumentSet() - var slice = self[...] - while var argument = slice.popFirst() { - if argument.help.isComposite { - // If this argument is composite, we have a group of arguments to - // merge together. - let groupEnd = - slice - .firstIndex { $0.help.keys != argument.help.keys } - ?? slice.endIndex - let group = [argument] + slice[.. Date: Tue, 13 May 2025 13:19:09 -0400 Subject: [PATCH 2/8] Improve ToolInfoV0 HelpCommand injection. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Usage/DumpHelpGenerator.swift | 7 +++---- .../Snapshots/testMathDoccReference().md | 7 ++++++- .../Snapshots/testMathMarkdownReference().md | 7 ++++++- .../Snapshots/testMathMultiPageManual().mdoc | 3 +++ .../Snapshots/testMathSinglePageManual().mdoc | 2 ++ .../Snapshots/testMathAddDumpHelp().json | 18 ++++++++++++++++++ .../Snapshots/testMathDumpHelp().json | 18 ++++++++++++++++++ .../Snapshots/testMathMultiplyDumpHelp().json | 18 ++++++++++++++++++ .../Snapshots/testMathStatsDumpHelp().json | 18 ++++++++++++++++++ 9 files changed, 92 insertions(+), 6 deletions(-) diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 70295f3b..097a55f9 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -57,11 +57,10 @@ extension ToolInfoV0 { self.init(command: CommandInfoV0(commandStack: commandStack)) // FIXME: This is a hack to inject the help command into the tool info // instead we should try to lift this into the parseable command tree - var helpCommandInfo = CommandInfoV0(commandStack: [HelpCommand.self]) - helpCommandInfo.superCommands = - (self.command.superCommands ?? []) + [self.command.commandName] self.command.subcommands = - (self.command.subcommands ?? []) + [helpCommandInfo] + (self.command.subcommands ?? []) + [ + CommandInfoV0(commandStack: commandStack + [HelpCommand.self]) + ] } } diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md index 3088879f..79cb53e5 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md @@ -207,12 +207,17 @@ math stats quantiles [] [] [] [< Show subcommand help information. ``` -math help [...] +math help [...] [--version] ``` - term **subcommands**: +- term **--version**: + +*Show the version.* + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md index 3578c578..78419e67 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md @@ -207,12 +207,17 @@ math stats quantiles [] [] [] [< Show subcommand help information. ``` -math help [...] +math help [...] [--version] ``` **subcommands:** +**--version:** + +*Show the version.* + + diff --git a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc index c8d90da5..99288bd7 100644 --- a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc +++ b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc @@ -277,9 +277,12 @@ and .Sh SYNOPSIS .Nm .Op Ar subcommands... +.Op Fl -version .Sh DESCRIPTION .Bl -tag -width 6n .It Ar subcommands... +.It Fl -version +Show the version. .El .Sh AUTHORS The diff --git a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc index d36005cd..1aa38453 100644 --- a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc +++ b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc @@ -93,6 +93,8 @@ Show help information. Show subcommand help information. .Bl -tag -width 6n .It Ar subcommands... +.It Fl -version +Show the version. .El .El .Sh AUTHORS diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json index 099f7109..5f5f5d8c 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json @@ -110,6 +110,24 @@ }, "shouldDisplay" : false, "valueName" : "help" + }, + { + "abstract" : "Show the version.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" } ], "commandName" : "help", diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json index f2f2e755..5168b551 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json @@ -724,6 +724,24 @@ }, "shouldDisplay" : false, "valueName" : "help" + }, + { + "abstract" : "Show the version.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" } ], "commandName" : "help", diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json index dd1f9672..0232ecf8 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json @@ -110,6 +110,24 @@ }, "shouldDisplay" : false, "valueName" : "help" + }, + { + "abstract" : "Show the version.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" } ], "commandName" : "help", diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json index 5a56bfc3..75668477 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json @@ -512,6 +512,24 @@ }, "shouldDisplay" : false, "valueName" : "help" + }, + { + "abstract" : "Show the version.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" } ], "commandName" : "help", From 644c3a703efe2516aed818edc37ca4bfb9772e68 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 13 May 2025 14:09:30 -0400 Subject: [PATCH 3/8] Add ArgumentInfoV0.ParsingStrategyV0 enum. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Usage/DumpHelpGenerator.swift | 22 +++++++++++ Sources/ArgumentParserToolInfo/ToolInfo.swift | 26 +++++++++++++ .../Examples/example1.json | 3 ++ .../Snapshots/testADumpHelp().json | 11 ++++++ .../Snapshots/testBDumpHelp().json | 5 +++ .../Snapshots/testCDumpHelp().json | 8 ++++ .../Snapshots/testMathAddDumpHelp().json | 7 ++++ .../Snapshots/testMathDumpHelp().json | 37 +++++++++++++++++++ .../Snapshots/testMathMultiplyDumpHelp().json | 7 ++++ .../Snapshots/testMathStatsDumpHelp().json | 27 ++++++++++++++ 10 files changed, 153 insertions(+) diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 097a55f9..a3a1e0e0 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -127,6 +127,7 @@ extension ArgumentInfoV0 { sectionTitle: argument.help.parentTitle.nonEmpty, isOptional: argument.help.options.contains(.isOptional), isRepeating: argument.help.options.contains(.isRepeating), + parsingStrategy: ArgumentInfoV0.ParsingStrategyV0(argument: argument), names: argument.names.map(ArgumentInfoV0.NameInfoV0.init), preferredName: argument.names.preferredName.map( ArgumentInfoV0.NameInfoV0.init), @@ -159,6 +160,27 @@ extension ArgumentInfoV0.KindV0 { } } +extension ArgumentInfoV0.ParsingStrategyV0 { + fileprivate init(argument: ArgumentDefinition) { + switch argument.parsingStrategy { + case .`default`: + self = .default + case .scanningForValue: + self = .scanningForValue + case .unconditional: + self = .unconditional + case .upToNextOption: + self = .upToNextOption + case .allRemainingInput: + self = .allRemainingInput + case .postTerminator: + self = .postTerminator + case .allUnrecognized: + self = .allUnrecognized + } + } +} + extension ArgumentInfoV0.NameInfoV0 { fileprivate init(name: Name) { switch name { diff --git a/Sources/ArgumentParserToolInfo/ToolInfo.swift b/Sources/ArgumentParserToolInfo/ToolInfo.swift index 3fe9014f..3dac9188 100644 --- a/Sources/ArgumentParserToolInfo/ToolInfo.swift +++ b/Sources/ArgumentParserToolInfo/ToolInfo.swift @@ -140,6 +140,26 @@ public struct ArgumentInfoV0: Codable, Hashable { case flag } + public enum ParsingStrategyV0: String, Codable, Hashable { + /// Expect the next `SplitArguments.Element` to be a value and parse it. + /// Will fail if the next input is an option. + case `default` + /// Parse the next `SplitArguments.Element.value` + case scanningForValue + /// Parse the next `SplitArguments.Element` as a value, regardless of its type. + case unconditional + /// Parse multiple `SplitArguments.Element.value` up to the next non-`.value` + case upToNextOption + /// Parse all remaining `SplitArguments.Element` as values, regardless of its type. + case allRemainingInput + /// Collect all the elements after the terminator, preventing them from + /// appearing in any other position. + case postTerminator + /// Collect all unused inputs once recognized arguments/options/flags have + /// been parsed. + case allUnrecognized + } + public enum CompletionKindV0: Codable, Hashable { /// Use the specified list of completion strings. case list(values: [String]) @@ -171,6 +191,9 @@ public struct ArgumentInfoV0: Codable, Hashable { /// Argument can be specified multiple times. public var isRepeating: Bool + /// Parsing strategy of the ArgumentInfo. + public var parsingStrategy: ParsingStrategyV0 + /// All names of the argument. public var names: [NameInfoV0]? /// The best name to use when referring to the argument in help displays. @@ -210,6 +233,7 @@ public struct ArgumentInfoV0: Codable, Hashable { sectionTitle: String?, isOptional: Bool, isRepeating: Bool, + parsingStrategy: ParsingStrategyV0, names: [NameInfoV0]?, preferredName: NameInfoV0?, valueName: String?, @@ -228,6 +252,8 @@ public struct ArgumentInfoV0: Codable, Hashable { self.isOptional = isOptional self.isRepeating = isRepeating + self.parsingStrategy = parsingStrategy + self.names = names?.nonEmpty self.preferredName = preferredName diff --git a/Tests/ArgumentParserToolInfoTests/Examples/example1.json b/Tests/ArgumentParserToolInfoTests/Examples/example1.json index f369b26c..43ae9b1e 100644 --- a/Tests/ArgumentParserToolInfoTests/Examples/example1.json +++ b/Tests/ArgumentParserToolInfoTests/Examples/example1.json @@ -20,6 +20,7 @@ "shouldDisplay": true, "isOptional": true, "isRepeating": true, + "parsingStrategy" : "default", "names": [ { "kind": "long", @@ -53,12 +54,14 @@ "shouldDisplay": false, "isOptional": false, "isRepeating": false, + "parsingStrategy" : "default", }, { "kind": "flag", "shouldDisplay": false, "isOptional": false, "isRepeating": false, + "parsingStrategy" : "default", } ] } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json index 7e180a26..cb981545 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json @@ -25,6 +25,7 @@ "name" : "enumerated-option" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "enumerated-option" @@ -57,6 +58,7 @@ "name" : "enumerated-option-with-default-value" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "enumerated-option-with-default-value" @@ -74,6 +76,7 @@ "name" : "no-help-option" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "no-help-option" @@ -92,6 +95,7 @@ "name" : "int-option" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "int-option" @@ -111,6 +115,7 @@ "name" : "int-option-with-default-value" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "int-option-with-default-value" @@ -122,6 +127,7 @@ "isOptional" : false, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "arg" }, @@ -130,6 +136,7 @@ "isOptional" : false, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "arg-with-help" }, @@ -139,6 +146,7 @@ "isOptional" : true, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "arg-with-default-value" }, @@ -157,6 +165,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -175,6 +184,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "subcommands" }, @@ -196,6 +206,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json index 1fe07a4e..5b584df6 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json @@ -11,6 +11,7 @@ "name" : "verbose" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "verbose" @@ -29,6 +30,7 @@ "name" : "name" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "name" @@ -52,6 +54,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -70,6 +73,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "subcommands" }, @@ -91,6 +95,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json index 2e115c01..7cbcafee 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json @@ -31,6 +31,7 @@ "name" : "color" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "color" @@ -69,6 +70,7 @@ "name" : "default-color" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "default-color" @@ -106,6 +108,7 @@ "name" : "opt" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "opt" @@ -143,6 +146,7 @@ "name" : "extra" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "extra" @@ -161,6 +165,7 @@ "name" : "discussion" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "discussion" @@ -183,6 +188,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -201,6 +207,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "subcommands" }, @@ -222,6 +229,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json index 5f5f5d8c..51568de7 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathAddDumpHelp().json @@ -17,6 +17,7 @@ "name" : "x" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "hex-output" @@ -29,6 +30,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -43,6 +45,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -65,6 +68,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -83,6 +87,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "subcommands" }, @@ -104,6 +109,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -122,6 +128,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json index 5168b551..01e3b89e 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json @@ -13,6 +13,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -35,6 +36,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -64,6 +66,7 @@ "name" : "x" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "hex-output" @@ -76,6 +79,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -90,6 +94,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -112,6 +117,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -144,6 +150,7 @@ "name" : "x" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "hex-output" @@ -156,6 +163,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -170,6 +178,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -192,6 +201,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -220,6 +230,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -242,6 +253,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -282,6 +294,7 @@ "name" : "kind" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "kind" @@ -294,6 +307,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -308,6 +322,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -330,6 +345,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -353,6 +369,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -367,6 +384,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -389,6 +407,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -421,6 +440,7 @@ "isOptional" : true, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "one-of-four" }, @@ -433,6 +453,7 @@ "isOptional" : true, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "custom-arg" }, @@ -445,6 +466,7 @@ "isOptional" : true, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "custom-deprecated-arg" }, @@ -453,6 +475,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -466,6 +489,7 @@ "name" : "test-success-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-success-exit-code" @@ -483,6 +507,7 @@ "name" : "test-failure-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-failure-exit-code" @@ -500,6 +525,7 @@ "name" : "test-validation-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-validation-exit-code" @@ -517,6 +543,7 @@ "name" : "test-custom-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-custom-exit-code" @@ -542,6 +569,7 @@ "name" : "file" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "file" @@ -564,6 +592,7 @@ "name" : "directory" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "directory" @@ -586,6 +615,7 @@ "name" : "shell" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "shell" @@ -608,6 +638,7 @@ "name" : "custom" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "custom" @@ -630,6 +661,7 @@ "name" : "custom-deprecated" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "custom-deprecated" @@ -648,6 +680,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -670,6 +703,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -697,6 +731,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "subcommands" }, @@ -718,6 +753,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -736,6 +772,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json index 0232ecf8..7494decf 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathMultiplyDumpHelp().json @@ -17,6 +17,7 @@ "name" : "x" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "hex-output" @@ -29,6 +30,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -43,6 +45,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -65,6 +68,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -83,6 +87,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "subcommands" }, @@ -104,6 +109,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -122,6 +128,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json index 75668477..725c51f6 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json @@ -13,6 +13,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -35,6 +36,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -75,6 +77,7 @@ "name" : "kind" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "kind" @@ -87,6 +90,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -101,6 +105,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -123,6 +128,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -146,6 +152,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -160,6 +167,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -182,6 +190,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -214,6 +223,7 @@ "isOptional" : true, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "one-of-four" }, @@ -226,6 +236,7 @@ "isOptional" : true, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "custom-arg" }, @@ -238,6 +249,7 @@ "isOptional" : true, "isRepeating" : false, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "custom-deprecated-arg" }, @@ -246,6 +258,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "values" }, @@ -259,6 +272,7 @@ "name" : "test-success-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-success-exit-code" @@ -276,6 +290,7 @@ "name" : "test-failure-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-failure-exit-code" @@ -293,6 +308,7 @@ "name" : "test-validation-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-validation-exit-code" @@ -310,6 +326,7 @@ "name" : "test-custom-exit-code" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "test-custom-exit-code" @@ -335,6 +352,7 @@ "name" : "file" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "file" @@ -357,6 +375,7 @@ "name" : "directory" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "directory" @@ -379,6 +398,7 @@ "name" : "shell" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "shell" @@ -401,6 +421,7 @@ "name" : "custom" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "custom" @@ -423,6 +444,7 @@ "name" : "custom-deprecated" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "custom-deprecated" @@ -441,6 +463,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" @@ -463,6 +486,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -485,6 +509,7 @@ "isOptional" : true, "isRepeating" : true, "kind" : "positional", + "parsingStrategy" : "default", "shouldDisplay" : true, "valueName" : "subcommands" }, @@ -506,6 +531,7 @@ "name" : "help" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "help" @@ -524,6 +550,7 @@ "name" : "version" } ], + "parsingStrategy" : "default", "preferredName" : { "kind" : "long", "name" : "version" From 3fd8d1975e9cb6bace77c2d6ca0d608756ab1d00 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 20 Feb 2025 09:48:25 -0500 Subject: [PATCH 4/8] Refactor bash completions to use ToolInfoV0. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 394 +++++++++--------- .../Completions/CompletionsGenerator.swift | 63 ++- .../ArgumentParser/Parsing/ArgumentSet.swift | 8 + .../Parsing/CommandParser.swift | 29 +- .../Usage/DumpHelpGenerator.swift | 2 +- Sources/ArgumentParserToolInfo/ToolInfo.swift | 4 +- .../testMathBashCompletionScript().bash | 4 +- .../Snapshots/testBase_Bash().bash | 42 +- 8 files changed, 312 insertions(+), 234 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index af6407e2..4f7bd842 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -9,184 +9,182 @@ // //===----------------------------------------------------------------------===// -extension [ParsableCommand.Type] { - /// Generates a Bash completion script for the given command. +#if swift(>=6.0) +internal import ArgumentParserToolInfo +#else +import ArgumentParserToolInfo +#endif + +extension ToolInfoV0 { var bashCompletionScript: String { - // TODO: Add a check to see if the command is installed where we expect? - // swift-format-ignore: NeverForceUnwrap - // Preconditions: - // - first must be non-empty for a bash completion script to be of use. - // - first is guaranteed non-empty in the one place where this computed var is used. - let commandName = first!._commandName - return """ - #!/bin/bash - - \(cursorIndexInCurrentWordFunctionName)() { - local remaining="${COMP_LINE}" - - local word - for word in "${COMP_WORDS[@]::COMP_CWORD}"; do - remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}" - done - - local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))" - if [[ "${index}" -le 0 ]]; then - printf 0 - else - printf %s "${index}" - fi - } + command.bashCompletionScript + } +} - # positional arguments: - # - # - 1: the current (sub)command's count of positional arguments - # - # required variables: - # - # - flags: the flags that the current (sub)command can accept - # - options: the options that the current (sub)command can accept - # - positional_number: value ignored - # - unparsed_words: unparsed words from the current command line - # - # modified variables: - # - # - flags: remove flags for this (sub)command that are already on the command line - # - options: remove options for this (sub)command that are already on the command line - # - positional_number: set to the current positional number - # - unparsed_words: remove all flags, options, and option values for this (sub)command - \(offerFlagsOptionsFunctionName)() { - local -ir positional_count="${1}" - positional_number=0 - - local was_flag_option_terminator_seen=false - local is_parsing_option_value=false - - local -ar unparsed_word_indices=("${!unparsed_words[@]}") - local -i word_index - for word_index in "${unparsed_word_indices[@]}"; do - if "${is_parsing_option_value}"; then - # This word is an option value: - # Reset marker for next word iff not currently the last word - [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false - unset "unparsed_words[${word_index}]" - # Do not process this word as a flag or an option - continue - fi - - local word="${unparsed_words["${word_index}"]}" - if ! "${was_flag_option_terminator_seen}"; then - case "${word}" in - --) - unset "unparsed_words[${word_index}]" - # by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion - if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then - was_flag_option_terminator_seen=true - fi - continue - ;; - -*) - # ${word} is a flag or an option - # If ${word} is an option, mark that the next word to be parsed is an option value - local option - for option in "${options[@]}"; do - [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break - done - - # Remove ${word} from ${flags} or ${options} so it isn't offered again - local not_found=true - local -i index - for index in "${!flags[@]}"; do - if [[ "${flags[${index}]}" = "${word}" ]]; then - unset "flags[${index}]" - flags=("${flags[@]}") - not_found=false - break - fi - done - if "${not_found}"; then - for index in "${!options[@]}"; do - if [[ "${options[${index}]}" = "${word}" ]]; then - unset "options[${index}]" - options=("${options[@]}") - break - fi - done - fi - unset "unparsed_words[${word_index}]" - continue - ;; - esac - fi - - # ${word} is neither a flag, nor an option, nor an option value - if [[ "${positional_number}" -lt "${positional_count}" ]]; then - # ${word} is a positional - ((positional_number++)) - unset "unparsed_words[${word_index}]" - else - if [[ -z "${word}" ]]; then - # Could be completing a flag, option, or subcommand - positional_number=-1 - else - # ${word} is a subcommand or invalid, so stop processing this (sub)command - positional_number=-2 - fi - break - fi - done - - unparsed_words=("${unparsed_words[@]}") - - if\\ - ! "${was_flag_option_terminator_seen}"\\ - && ! "${is_parsing_option_value}"\\ - && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] - then - COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) - fi - } +extension CommandInfoV0 { + fileprivate var bashCompletionScript: String { + """ + #!/bin/bash + + \(cursorIndexInCurrentWordFunctionName)() { + local remaining="${COMP_LINE}" + + local word + for word in "${COMP_WORDS[@]::COMP_CWORD}"; do + remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}" + done + + local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))" + if [[ "${index}" -le 0 ]]; then + printf 0 + else + printf %s "${index}" + fi + } - \(addCompletionsFunctionName)() { - local completion - while IFS='' read -r completion; do - COMPREPLY+=("${completion}") - done < <(IFS=$'\\n' compgen "${@}" -- "${cur}") - } + # positional arguments: + # + # - 1: the current (sub)command's count of positional arguments + # + # required variables: + # + # - flags: the flags that the current (sub)command can accept + # - options: the options that the current (sub)command can accept + # - positional_number: value ignored + # - unparsed_words: unparsed words from the current command line + # + # modified variables: + # + # - flags: remove flags for this (sub)command that are already on the command line + # - options: remove options for this (sub)command that are already on the command line + # - positional_number: set to the current positional number + # - unparsed_words: remove all flags, options, and option values for this (sub)command + \(offerFlagsOptionsFunctionName)() { + local -ir positional_count="${1}" + positional_number=0 + + local was_flag_option_terminator_seen=false + local is_parsing_option_value=false + + local -ar unparsed_word_indices=("${!unparsed_words[@]}") + local -i word_index + for word_index in "${unparsed_word_indices[@]}"; do + if "${is_parsing_option_value}"; then + # This word is an option value: + # Reset marker for next word iff not currently the last word + [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false + unset "unparsed_words[${word_index}]" + # Do not process this word as a flag or an option + continue + fi + + local word="${unparsed_words["${word_index}"]}" + if ! "${was_flag_option_terminator_seen}"; then + case "${word}" in + --) + unset "unparsed_words[${word_index}]" + # by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion + if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then + was_flag_option_terminator_seen=true + fi + continue + ;; + -*) + # ${word} is a flag or an option + # If ${word} is an option, mark that the next word to be parsed is an option value + local option + for option in "${options[@]}"; do + [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break + done + + # Remove ${word} from ${flags} or ${options} so it isn't offered again + local not_found=true + local -i index + for index in "${!flags[@]}"; do + if [[ "${flags[${index}]}" = "${word}" ]]; then + unset "flags[${index}]" + flags=("${flags[@]}") + not_found=false + break + fi + done + if "${not_found}"; then + for index in "${!options[@]}"; do + if [[ "${options[${index}]}" = "${word}" ]]; then + unset "options[${index}]" + options=("${options[@]}") + break + fi + done + fi + unset "unparsed_words[${word_index}]" + continue + ;; + esac + fi + + # ${word} is neither a flag, nor an option, nor an option value + if [[ "${positional_number}" -lt "${positional_count}" ]]; then + # ${word} is a positional + ((positional_number++)) + unset "unparsed_words[${word_index}]" + else + if [[ -z "${word}" ]]; then + # Could be completing a flag, option, or subcommand + positional_number=-1 + else + # ${word} is a subcommand or invalid, so stop processing this (sub)command + positional_number=-2 + fi + break + fi + done + + unparsed_words=("${unparsed_words[@]}") + + if\\ + ! "${was_flag_option_terminator_seen}"\\ + && ! "${is_parsing_option_value}"\\ + && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] + then + COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + fi + } - \(customCompleteFunctionName)() { - if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then - local -ar words=("${COMP_WORDS[@]}") - else - local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}") - fi + \(addCompletionsFunctionName)() { + local completion + while IFS='' read -r completion; do + COMPREPLY+=("${completion}") + done < <(IFS=$'\\n' compgen "${@}" -- "${cur}") + } - "${COMP_WORDS[0]}" "${@}" "${words[@]}" - } + \(customCompleteFunctionName)() { + if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then + local -ar words=("${COMP_WORDS[@]}") + else + local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}") + fi - \(completionFunctions)\ - complete -o filenames -F \(completionFunctionName().shellEscapeForVariableName()) \(commandName) - """ + "${COMP_WORDS[0]}" "${@}" "${words[@]}" + } + + \(completionFunctions)\ + complete -o filenames -F \(completionFunctionName) \(commandName) + """ } - /// Generates a Bash completion function for the last command in the given list. + /// Generates a Bash completion function. private var completionFunctions: String { - guard let type = last else { - fatalError() - } - let functionName = - completionFunctionName().shellEscapeForVariableName() + let functionName = completionFunctionName - var subcommands = - type.configuration.subcommands.filter { $0.configuration.shouldDisplay } + let subcommands = (subcommands ?? []).filter(\.shouldDisplay) // Start building the resulting function code. var result = "" // Include initial setup iff the root command. let declareTopLevelArray: String - if count == 1 { - subcommands.addHelpSubcommandIfMissing() - + if (superCommands ?? []).isEmpty { result += """ trap "$(shopt -p);$(shopt -po)" RETURN shopt -s extglob @@ -227,17 +225,13 @@ extension [ParsableCommand.Type] { // Generate the case pattern-matching statements for option values. // If there aren't any, skip the case block altogether. let optionHandlers = - ArgumentSet(type, visibility: .default, parent: nil) - .compactMap { arg -> String? in - let words = arg.bashCompletionWords - if words.isEmpty { return nil } - - // Flags don't take a value, so we don't provide follow-on completions. - if arg.isNullary { return nil } - + (arguments ?? []).compactMap { arg in + guard arg.kind != .flag else { return nil } + let words = arg.completionWords + guard !words.isEmpty else { return nil } return """ - \(arg.bashCompletionWords.map { "'\($0.shellEscapeForSingleQuotedString())'" }.joined(separator: "|"))) - \(bashValueCompletion(arg).indentingEachLine(by: 8))\ + \(arg.completionWords.map { "'\($0.shellEscapeForSingleQuotedString())'" }.joined(separator: "|"))) + \(valueCompletion(arg).indentingEachLine(by: 8))\ return ;; """ @@ -257,7 +251,7 @@ extension [ParsableCommand.Type] { let positionalCases = zip(1..., positionalArguments) .compactMap { position, arg in - let completion = bashValueCompletion(arg) + let completion = valueCompletion(arg) return completion.isEmpty ? nil : """ @@ -288,14 +282,14 @@ extension [ParsableCommand.Type] { unset 'unparsed_words[0]' unparsed_words=("${unparsed_words[@]}") case "${subcommand}" in - \(subcommands.map { $0._commandName }.joined(separator: "|"))) + \(subcommands.map(\.commandName).joined(separator: "|"))) # Offer subcommand argument completions "\(functionName)_${subcommand}" ;; *) # Offer subcommand completions COMPREPLY+=($(compgen -W '\( - subcommands.map { $0._commandName.shellEscapeForSingleQuotedString() }.joined(separator: " ") + subcommands.map { $0.commandName.shellEscapeForSingleQuotedString() }.joined(separator: " ") )' -- "${cur}")) ;; esac @@ -312,38 +306,38 @@ extension [ParsableCommand.Type] { \(result)\ } - \(subcommands.map { (self + [$0]).completionFunctions }.joined()) + \(subcommands.map(\.completionFunctions).joined()) """ } - /// Returns flag completions for the last command of the given array. + /// Returns flag completions. private var flagCompletions: [String] { - argumentsForHelp(visibility: .default).flatMap { - switch ($0.kind, $0.update) { - case (.named, .nullary): - return $0.bashCompletionWords + (arguments ?? []).flatMap { + switch $0.kind { + case .flag: + return $0.completionWords default: return [] } } } - /// Returns option completions for the last command of the given array. + /// Returns option completions. private var optionCompletions: [String] { - argumentsForHelp(visibility: .default).flatMap { - switch ($0.kind, $0.update) { - case (.named, .unary): - return $0.bashCompletionWords + (arguments ?? []).flatMap { + switch $0.kind { + case .option: + return $0.completionWords default: return [] } } } - /// Returns the bash completions that can follow the given argument's `--name`. - private func bashValueCompletion(_ arg: ArgumentDefinition) -> String { - switch arg.completion.kind { - case .default: + /// Returns the completions that can follow the given argument's `--name`. + private func valueCompletion(_ arg: ArgumentInfoV0) -> String { + switch arg.completionKind { + case .none: return "" case .file(let extensions) where extensions.isEmpty: @@ -384,7 +378,7 @@ extension [ParsableCommand.Type] { // Generate a call back into the command to retrieve a completions list return """ \(addCompletionsFunctionName) -W\ - "$(\(customCompleteFunctionName) \(arg.customCompletionCall(self))\ + "$(\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self))\ "${COMP_CWORD}"\ "$(\(cursorIndexInCurrentWordFunctionName))")" @@ -394,34 +388,34 @@ extension [ParsableCommand.Type] { // Generate a call back into the command to retrieve a completions list return """ \(addCompletionsFunctionName) -W\ - "$(\(customCompleteFunctionName) \(arg.customCompletionCall(self)))" + "$(\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self)))" """ } } private var cursorIndexInCurrentWordFunctionName: String { - "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_cursor_index_in_current_word" + "\(completionFunctionPrefix)_cursor_index_in_current_word" } private var offerFlagsOptionsFunctionName: String { - "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_offer_flags_options" + "\(completionFunctionPrefix)_offer_flags_options" } private var addCompletionsFunctionName: String { - "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_add_completions" + "\(completionFunctionPrefix)_add_completions" } private var customCompleteFunctionName: String { - "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_custom_complete" + "\(completionFunctionPrefix)_custom_complete" } } -extension ArgumentDefinition { +extension ArgumentInfoV0 { /// Returns the different completion names for this argument. - fileprivate var bashCompletionWords: [String] { - help.visibility.base == .default - ? names.map(\.synopsisString) + fileprivate var completionWords: [String] { + shouldDisplay + ? (names ?? []).map { $0.commonCompletionSynopsisString() } : [] } } diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 213717b5..1db5f53a 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -9,6 +9,12 @@ // //===----------------------------------------------------------------------===// +#if swift(>=6.0) +internal import ArgumentParserToolInfo +#else +import ArgumentParserToolInfo +#endif + /// A shell for which the parser can generate a completion script. public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { public var rawValue: String @@ -136,7 +142,7 @@ struct CompletionsGenerator { case .zsh: return [command].zshCompletionScript case .bash: - return [command].bashCompletionScript + return ToolInfoV0(commandStack: [command]).bashCompletionScript case .fish: return [command].fishCompletionScript default: @@ -215,3 +221,58 @@ extension String { replacingOccurrences(of: "-", with: "_") } } + +extension CommandInfoV0 { + var commandContext: [String] { + (superCommands ?? []) + [commandName] + } + + var initialCommand: String { + superCommands?.first ?? commandName + } + + var positionalArguments: [ArgumentInfoV0] { + (arguments ?? []).filter { $0.kind == .positional } + } + + var completionFunctionName: String { + "_" + commandContext.joined(separator: "_") + } + + var completionFunctionPrefix: String { + "__\(initialCommand)" + } +} + +extension ArgumentInfoV0 { + /// Returns a string with the arguments for the callback to generate custom + /// completions for this argument. + func commonCustomCompletionCall(command: CommandInfoV0) -> String { + let subcommandNames = + command.commandContext.dropFirst().map { "\($0) " }.joined() + + let argumentName: String + switch kind { + case .positional: + if let index = command.positionalArguments.firstIndex(of: self) { + argumentName = "positional@\(index)" + } else { + argumentName = "---" + } + default: + argumentName = preferredName?.commonCompletionSynopsisString() ?? "---" + } + return "---completion \(subcommandNames)-- \(argumentName)" + } +} + +extension ArgumentInfoV0.NameInfoV0 { + func commonCompletionSynopsisString() -> String { + switch kind { + case .long: + return "--\(name)" + case .short, .longWithSingleDash: + return "-\(name)" + } + } +} diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index b2e87387..5f2728de 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -235,6 +235,14 @@ extension ArgumentSet { ) -> 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 diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 97402bef..b8d9b972 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -418,14 +418,29 @@ extension CommandParser { } try customComplete(matchedArgument, forArguments: Array(args)) - case .value(let str): - guard - let key = InputKey(fullPathString: str), - let matchedArgument = argset.firstPositional(withKey: key) - else { - throw ParserError.invalidState + case .value(let value): + // Legacy completion script generators use internal key paths to identify + // positional args, e.g. optionGroupA.optionGroupB.property. Newer + // generators based on ToolInfo use the `positional@` syntax which + // avoids leaking implementation details of the tool. + let toolInfoPrefix = "positional@" + if value.hasPrefix(toolInfoPrefix) { + guard + let index = Int(value.dropFirst(toolInfoPrefix.count)), + let matchedArgument = argset.positional(at: index) + else { + throw ParserError.invalidState + } + try customComplete(matchedArgument, forArguments: Array(args)) + } else { + guard + let key = InputKey(fullPathString: value), + let matchedArgument = argset.firstPositional(withKey: key) + else { + throw ParserError.invalidState + } + try customComplete(matchedArgument, forArguments: Array(args)) } - try customComplete(matchedArgument, forArguments: Array(args)) case .terminator: throw ParserError.invalidState diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index a3a1e0e0..1dbd28fd 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -53,7 +53,7 @@ extension BidirectionalCollection where Element == ParsableCommand.Type { } extension ToolInfoV0 { - fileprivate init(commandStack: [ParsableCommand.Type]) { + init(commandStack: [ParsableCommand.Type]) { self.init(command: CommandInfoV0(commandStack: commandStack)) // FIXME: This is a hack to inject the help command into the tool info // instead we should try to lift this into the parseable command tree diff --git a/Sources/ArgumentParserToolInfo/ToolInfo.swift b/Sources/ArgumentParserToolInfo/ToolInfo.swift index 3dac9188..d4b8efaf 100644 --- a/Sources/ArgumentParserToolInfo/ToolInfo.swift +++ b/Sources/ArgumentParserToolInfo/ToolInfo.swift @@ -217,9 +217,9 @@ public struct ArgumentInfoV0: Codable, Hashable { /// Mapping of valid values to descriptions of the value. public var allValueDescriptions: [String: String]? - /// The type of completion to use for an argument or option. + /// The type of completion to use for an argument or an option value. /// - /// `nil` if the tool use use the default completion kind. + /// `nil` if the tool uses the default completion kind. public var completionKind: CompletionKindV0? /// Short description of the argument's functionality. diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 63d7857b..2993a515 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -267,11 +267,11 @@ _math_stats_quantiles() { return ;; 2) - __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customArg "${COMP_CWORD}" "$(__math_cursor_index_in_current_word)")" + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- positional@1 "${COMP_CWORD}" "$(__math_cursor_index_in_current_word)")" return ;; 3) - __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customDeprecatedArg)" + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- positional@2)" return ;; esac diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 3fd1a1fd..c07725cb 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -1,6 +1,6 @@ #!/bin/bash -__base_test_cursor_index_in_current_word() { +__base-test_cursor_index_in_current_word() { local remaining="${COMP_LINE}" local word @@ -33,7 +33,7 @@ __base_test_cursor_index_in_current_word() { # - options: remove options for this (sub)command that are already on the command line # - positional_number: set to the current positional number # - unparsed_words: remove all flags, options, and option values for this (sub)command -__base_test_offer_flags_options() { +__base-test_offer_flags_options() { local -ir positional_count="${1}" positional_number=0 @@ -125,14 +125,14 @@ __base_test_offer_flags_options() { fi } -__base_test_add_completions() { +__base-test_add_completions() { local completion while IFS='' read -r completion; do COMPREPLY+=("${completion}") done < <(IFS=$'\n' compgen "${@}" -- "${cur}") } -__base_test_custom_complete() { +__base-test_custom_complete() { if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then local -ar words=("${COMP_WORDS[@]}") else @@ -142,7 +142,7 @@ __base_test_custom_complete() { "${COMP_WORDS[0]}" "${@}" "${words[@]}" } -_base_test() { +_base-test() { trap "$(shopt -p);$(shopt -po)" RETURN shopt -s extglob set +o history +o posix @@ -160,7 +160,7 @@ _base_test() { local -a flags=(--one --two --three --kind-counter -h --help) local -a options=(--name --kind --other-kind --path1 --path2 --path3 --rep1 -r --rep2) - __base_test_offer_flags_options 2 + __base-test_offer_flags_options 2 # Offer option value completions case "${prev}" in @@ -168,23 +168,23 @@ _base_test() { return ;; '--kind') - __base_test_add_completions -W 'one'$'\n''two'$'\n''custom-three' + __base-test_add_completions -W 'one'$'\n''two'$'\n''custom-three' return ;; '--other-kind') - __base_test_add_completions -W 'b1_bash'$'\n''b2_bash'$'\n''b3_bash' + __base-test_add_completions -W 'b1_bash'$'\n''b2_bash'$'\n''b3_bash' return ;; '--path1') - __base_test_add_completions -f + __base-test_add_completions -f return ;; '--path2') - __base_test_add_completions -f + __base-test_add_completions -f return ;; '--path3') - __base_test_add_completions -W 'c1_bash'$'\n''c2_bash'$'\n''c3_bash' + __base-test_add_completions -W 'c1_bash'$'\n''c2_bash'$'\n''c3_bash' return ;; '--rep1') @@ -198,11 +198,11 @@ _base_test() { # Offer positional completions case "${positional_number}" in 1) - __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- argument "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")" + __base-test_add_completions -W "$(__base-test_custom_complete ---completion -- positional@0 "${COMP_CWORD}" "$(__base-test_cursor_index_in_current_word)")" return ;; 2) - __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- nested.nestedArgument "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")" + __base-test_add_completions -W "$(__base-test_custom_complete ---completion -- positional@1 "${COMP_CWORD}" "$(__base-test_cursor_index_in_current_word)")" return ;; esac @@ -214,7 +214,7 @@ _base_test() { case "${subcommand}" in sub-command|escaped-command|help) # Offer subcommand argument completions - "_base_test_${subcommand}" + "_base-test_${subcommand}" ;; *) # Offer subcommand completions @@ -223,16 +223,16 @@ _base_test() { esac } -_base_test_sub_command() { +_base-test_sub-command() { flags=(-h --help) options=() - __base_test_offer_flags_options 0 + __base-test_offer_flags_options 0 } -_base_test_escaped_command() { +_base-test_escaped-command() { flags=(-h --help) options=(--o:n[e) - __base_test_offer_flags_options 1 + __base-test_offer_flags_options 1 # Offer option value completions case "${prev}" in @@ -244,14 +244,14 @@ _base_test_escaped_command() { # Offer positional completions case "${positional_number}" in 1) - __base_test_add_completions -W "$(__base_test_custom_complete ---completion escaped-command -- two "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")" + __base-test_add_completions -W "$(__base-test_custom_complete ---completion escaped-command -- positional@0 "${COMP_CWORD}" "$(__base-test_cursor_index_in_current_word)")" return ;; esac } -_base_test_help() { +_base-test_help() { : } -complete -o filenames -F _base_test base-test \ No newline at end of file +complete -o filenames -F _base-test base-test \ No newline at end of file From d19fcf5a79e900331f46d7dbf4f14584a345cac0 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 25 Feb 2025 03:00:47 -0500 Subject: [PATCH 5/8] Refactor fish completions to use ToolInfoV0. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 10 +- .../FishCompletionsGenerator.swift | 276 ++++++++---------- .../testMathFishCompletionScript().fish | 4 +- .../Snapshots/testBase_Fish().fish | 6 +- 4 files changed, 135 insertions(+), 161 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 1db5f53a..2d145f15 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -144,7 +144,7 @@ struct CompletionsGenerator { case .bash: return ToolInfoV0(commandStack: [command]).bashCompletionScript case .fish: - return [command].fishCompletionScript + return ToolInfoV0(commandStack: [command]).fishCompletionScript default: fatalError("Invalid CompletionShell: \(shell)") } @@ -177,14 +177,6 @@ extension ParsableCommand { } extension [ParsableCommand.Type] { - var positionalArguments: [ArgumentDefinition] { - guard let command = last else { - return [] - } - return ArgumentSet(command, visibility: .default, parent: nil) - .filter(\.isPositional) - } - /// Include default 'help' subcommand in nonempty subcommand list if & only if /// no help subcommand already exists. mutating func addHelpSubcommandIfMissing() { diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index 525a14fd..3a722a8e 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -9,85 +9,90 @@ // //===----------------------------------------------------------------------===// -extension [ParsableCommand.Type] { +#if swift(>=6.0) +internal import ArgumentParserToolInfo +#else +import ArgumentParserToolInfo +#endif + +extension ToolInfoV0 { var fishCompletionScript: String { - // swift-format-ignore: NeverForceUnwrap - // Preconditions: - // - first must be non-empty for a fish completion script to be of use. - // - first is guaranteed non-empty in the one place where this computed var is used. - let commandName = first!._commandName - return """ - function \(shouldOfferCompletionsForFunctionName) -a expected_commands -a expected_positional_index - set -l unparsed_tokens (\(tokensFunctionName) -pc) - set -l positional_index 0 - set -l commands - - switch $unparsed_tokens[1] - \(commandCases) - end - - test "$commands" = "$expected_commands" -a \\( -z "$expected_positional_index" -o "$expected_positional_index" -eq "$positional_index" \\) - end - - function \(tokensFunctionName) - if test (string split -m 1 -f 1 -- . "$FISH_VERSION") -gt 3 - commandline --tokens-raw $argv - else - commandline -o $argv - end - end - - function \(parseSubcommandFunctionName) -S - argparse -s r -- $argv - set -l positional_count $argv[1] - set -l option_specs $argv[2..] - - set -a commands $unparsed_tokens[1] - set -e unparsed_tokens[1] - - set positional_index 0 - - while true - argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null - set unparsed_tokens $argv - set positional_index (math $positional_index + 1) - if test (count $unparsed_tokens) -eq 0 -o \\( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \\) - return 0 - end - set -e unparsed_tokens[1] - end - end - - function \(completeDirectoriesFunctionName) - set -l token (commandline -t) - string match -- '*/' $token - set -l subdirs $token*/ - printf '%s\\n' $subdirs - end - - function \(customCompletionFunctionName) - set -x \(CompletionShell.shellEnvironmentVariableName) fish - set -x \(CompletionShell.shellVersionEnvironmentVariableName) $FISH_VERSION - - set -l tokens (\(tokensFunctionName) -p) - if test -z (\(tokensFunctionName) -t) - set -l index (count (\(tokensFunctionName) -pc)) - set tokens $tokens[..$index] \\'\\' $tokens[(math $index + 1)..] - end - command $tokens[1] $argv $tokens - end - - complete -c '\(commandName)' -f - \(completions.joined(separator: "\n")) - """ + command.fishCompletionScript + } +} + +extension CommandInfoV0 { + fileprivate var fishCompletionScript: String { + """ + function \(shouldOfferCompletionsForFunctionName) -a expected_commands -a expected_positional_index + set -l unparsed_tokens (\(tokensFunctionName) -pc) + set -l positional_index 0 + set -l commands + + switch $unparsed_tokens[1] + \(commandCases) + end + + test "$commands" = "$expected_commands" -a \\( -z "$expected_positional_index" -o "$expected_positional_index" -eq "$positional_index" \\) + end + + function \(tokensFunctionName) + if test (string split -m 1 -f 1 -- . "$FISH_VERSION") -gt 3 + commandline --tokens-raw $argv + else + commandline -o $argv + end + end + + function \(parseSubcommandFunctionName) -S + argparse -s r -- $argv + set -l positional_count $argv[1] + set -l option_specs $argv[2..] + + set -a commands $unparsed_tokens[1] + set -e unparsed_tokens[1] + + set positional_index 0 + + while true + argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null + set unparsed_tokens $argv + set positional_index (math $positional_index + 1) + if test (count $unparsed_tokens) -eq 0 -o \\( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \\) + return 0 + end + set -e unparsed_tokens[1] + end + end + + function \(completeDirectoriesFunctionName) + set -l token (commandline -t) + string match -- '*/' $token + set -l subdirs $token*/ + printf '%s\\n' $subdirs + end + + function \(customCompletionFunctionName) + set -x \(CompletionShell.shellEnvironmentVariableName) fish + set -x \(CompletionShell.shellVersionEnvironmentVariableName) $FISH_VERSION + + set -l tokens (\(tokensFunctionName) -p) + if test -z (\(tokensFunctionName) -t) + set -l index (count (\(tokensFunctionName) -pc)) + set tokens $tokens[..$index] \\'\\' $tokens[(math $index + 1)..] + end + command $tokens[1] $argv $tokens + end + + complete -c '\(commandName)' -f + \(completions.joined(separator: "\n")) + """ } private var commandCases: String { - let subcommands = subcommands - // swift-format-ignore: NeverForceUnwrap - // Precondition: last is guaranteed to be non-empty + let subcommands = (subcommands ?? []).filter(\.shouldDisplay) return """ - case '\(last!._commandName)' + case '\(commandName)' \(parseSubcommandFunctionName) \(positionalArgumentCountArguments) \( completableArguments .compactMap(\.optionSpec) @@ -99,7 +104,7 @@ extension [ParsableCommand.Type] { : """ switch $unparsed_tokens[1] - \(subcommands.map { (self + [$0]).commandCases }.joined(separator: "\n")) + \(subcommands.map(\.commandCases).joined(separator: "\n")) end """ ) @@ -108,23 +113,22 @@ extension [ParsableCommand.Type] { } private var completions: [String] { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty let prefix = """ - complete -c '\(first!._commandName)'\ + complete -c '\(initialCommand)'\ -n '\(shouldOfferCompletionsForFunctionName)\ - "\(map { $0._commandName }.joined(separator: separator))" + "\(commandContext.joined(separator: separator))" """ - let subcommands = subcommands + let subcommands = (subcommands ?? []).filter(\.shouldDisplay) var positionalIndex = 0 let argumentCompletions = completableArguments - .map { (arg: ArgumentDefinition) in + .map { arg in """ - \(prefix)\(arg.isPositional + \(prefix)\( + arg.kind == .positional ? """ \({ positionalIndex += 1 @@ -140,57 +144,48 @@ extension [ParsableCommand.Type] { return argumentCompletions - + subcommands.map { subcommand in - "\(prefix) \(positionalIndex)' -fa '\(subcommand._commandName)' -d '\(subcommand.configuration.abstract.fishEscapeForSingleQuotedString())'" - } - + subcommands.flatMap { subcommand in - (self + [subcommand]).completions + + subcommands.map { + "\(prefix) \(positionalIndex)' -fa '\($0.commandName)' -d '\($0.abstract?.fishEscapeForSingleQuotedString() ?? "")'" } + + subcommands.flatMap(\.completions) } - private var subcommands: Self { - // swift-format-ignore: NeverForceUnwrap - // Precondition: last is guaranteed to be non-empty - var subcommands = last!.configuration.subcommands - .filter { $0.configuration.shouldDisplay } - if count == 1 { - subcommands.addHelpSubcommandIfMissing() - } - return subcommands - } - - private var completableArguments: [ArgumentDefinition] { - argumentsForHelp(visibility: .default).compactMap { arg in - switch arg.completion.kind { - case .default where arg.names.isEmpty: + private var completableArguments: [ArgumentInfoV0] { + (arguments ?? []).compactMap { arg in + switch arg.completionKind { + case .none where arg.names?.isEmpty ?? true: return nil default: return - arg.help.visibility.base == .default + arg.shouldDisplay ? arg : nil } } } - private func argumentSegments(_ arg: ArgumentDefinition) -> [String] { + private func argumentSegments(_ arg: ArgumentInfoV0) -> [String] { var results: [String] = [] - if !arg.names.isEmpty { - results += arg.names.map { $0.asFishSuggestion } - if !arg.help.abstract.isEmpty { + if let names = arg.names, !names.isEmpty { + results += names.map(\.asCompleteArgument) + if let abstract = arg.abstract, !abstract.isEmpty { results += [ - "-d '\(arg.help.abstract.fishEscapeForSingleQuotedString())'" + "-d '\(abstract.fishEscapeForSingleQuotedString())'" ] } } - let r = arg.isPositional ? "" : "r" + let r = arg.kind == .positional ? "" : "r" - switch arg.completion.kind { - case .default: - if case .unary = arg.update { + switch arg.completionKind { + case .none: + switch arg.kind { + case .positional, + .option: results += ["-\(r)fka ''"] + default: + break } break case .list(let list): @@ -227,7 +222,7 @@ extension [ParsableCommand.Type] { results += [ """ -\(r)fka '(\ - \(customCompletionFunctionName) \(arg.customCompletionCall(self)) \ + \(customCompletionFunctionName) \(arg.commonCustomCompletionCall(command: self)) \ (count (\(tokensFunctionName) -pc)) (\(tokensFunctionName) -tC)\ )' """ @@ -235,7 +230,7 @@ extension [ParsableCommand.Type] { case .customDeprecated: results += [ """ - -\(r)fka '(\(customCompletionFunctionName) \(arg.customCompletionCall(self)))' + -\(r)fka '(\(customCompletionFunctionName) \(arg.commonCustomCompletionCall(command: self)))' """ ] } @@ -246,42 +241,32 @@ extension [ParsableCommand.Type] { var positionalArgumentCountArguments: String { let positionalArguments = positionalArguments return """ - \(positionalArguments.contains(where: { $0.isRepeatingPositional }) ? "-r " : "")\(positionalArguments.count) + \(positionalArguments.contains(where: { $0.isRepeating }) ? "-r " : "")\(positionalArguments.count) """ } private var shouldOfferCompletionsForFunctionName: String { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty - "__\(first!._commandName)_should_offer_completions_for" + "\(completionFunctionPrefix)_should_offer_completions_for" } private var tokensFunctionName: String { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty - "__\(first!._commandName)_tokens" + "\(completionFunctionPrefix)_tokens" } private var parseSubcommandFunctionName: String { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty - "__\(first!._commandName)_parse_subcommand" + "\(completionFunctionPrefix)_parse_subcommand" } private var completeDirectoriesFunctionName: String { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty - "__\(first!._commandName)_complete_directories" + "\(completionFunctionPrefix)_complete_directories" } private var customCompletionFunctionName: String { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty - "__\(first!._commandName)_custom_completion" + "\(completionFunctionPrefix)_custom_completion" } } -extension ArgumentDefinition { +extension ArgumentInfoV0 { fileprivate var optionSpec: String? { guard let shortName = name(.short) else { guard let longName = name(.long) else { @@ -295,16 +280,13 @@ extension ArgumentDefinition { return optionSpecRequiresValue("\(shortName)/\(longName)") } - private func name(_ nameType: Name.Case) -> String? { - names.first(where: { - $0.case == nameType - })? - .valueString + private func name(_ nameKind: NameInfoV0.KindV0) -> String? { + (names ?? []).first(where: { $0.kind == nameKind })?.name } private func optionSpecRequiresValue(_ optionSpec: String) -> String { - switch update { - case .unary: + switch kind { + case .option: return "\(optionSpec)=" default: return optionSpec @@ -312,15 +294,15 @@ extension ArgumentDefinition { } } -extension Name { - fileprivate var asFishSuggestion: String { - switch self { - case .long(let longName): - return "-l '\(longName.fishEscapeForSingleQuotedString())'" - case .short(let shortName, _): - return "-s '\(String(shortName).fishEscapeForSingleQuotedString())'" - case .longWithSingleDash(let dashedName): - return "-o '\(dashedName.fishEscapeForSingleQuotedString())'" +extension ArgumentInfoV0.NameInfoV0 { + fileprivate var asCompleteArgument: String { + switch kind { + case .long: + return "-l '\(name.fishEscapeForSingleQuotedString())'" + case .short: + return "-s '\(name.fishEscapeForSingleQuotedString())'" + case .longWithSingleDash: + return "-o '\(name.fishEscapeForSingleQuotedString())'" } } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish index d2e1ee73..ee346a9d 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish @@ -101,8 +101,8 @@ complete -c 'math' -n '__math_should_offer_completions_for "math stats average"' complete -c 'math' -n '__math_should_offer_completions_for "math stats stdev"' -l 'version' -d 'Show the version.' complete -c 'math' -n '__math_should_offer_completions_for "math stats stdev"' -s 'h' -l 'help' -d 'Show help information.' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 1' -fka 'alphabet alligator branch braggart' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 2' -fka '(__math_custom_completion ---completion stats quantiles -- customArg (count (__math_tokens -pc)) (__math_tokens -tC))' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 3' -fka '(__math_custom_completion ---completion stats quantiles -- customDeprecatedArg)' +complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 2' -fka '(__math_custom_completion ---completion stats quantiles -- positional@1 (count (__math_tokens -pc)) (__math_tokens -tC))' +complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 3' -fka '(__math_custom_completion ---completion stats quantiles -- positional@2)' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'file' -rfa '(set -l exts \'txt\' \'md\';for p in (string match -e -- \'*/\' (commandline -t);or printf \n)*.{$exts};printf %s\n $p;end;__fish_complete_directories (commandline -t) \'\')' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'directory' -rfa '(__math_complete_directories)' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'shell' -rfka '(head -100 /usr/share/dict/words | tail -50)' diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish index a28ba18e..b64026a9 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish @@ -80,13 +80,13 @@ complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'kind-counter' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'rep1' -rfka '' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -s 'r' -l 'rep2' -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 1' -fka '(__base-test_custom_completion ---completion -- argument (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 2' -fka '(__base-test_custom_completion ---completion -- nested.nestedArgument (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' +complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 1' -fka '(__base-test_custom_completion ---completion -- positional@0 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' +complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 2' -fka '(__base-test_custom_completion ---completion -- positional@1 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -s 'h' -l 'help' -d 'Show help information.' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'sub-command' -d '' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'escaped-command' -d '' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'help' -d 'Show subcommand help information.' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test sub-command"' -s 'h' -l 'help' -d 'Show help information.' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command"' -l 'o:n[e' -d 'Escaped chars: \'[]\\.' -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command" 1' -fka '(__base-test_custom_completion ---completion escaped-command -- two (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' +complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command" 1' -fka '(__base-test_custom_completion ---completion escaped-command -- positional@0 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command"' -s 'h' -l 'help' -d 'Show help information.' \ No newline at end of file From e1cb25ee251c5392987686e6d0c7d3e03493eae0 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Apr 2025 23:35:36 -0400 Subject: [PATCH 6/8] Refactor zsh completions to use ToolInfoV0. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 56 +------ .../Completions/ZshCompletionsGenerator.swift | 157 +++++++++--------- .../testMathZshCompletionScript().zsh | 12 +- .../Snapshots/testBase_Zsh().zsh | 18 +- 4 files changed, 99 insertions(+), 144 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 2d145f15..837e3fac 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -140,7 +140,7 @@ struct CompletionsGenerator { CompletionShell._requesting.withLock { $0 = shell } switch shell { case .zsh: - return [command].zshCompletionScript + return ToolInfoV0(commandStack: [command]).zshCompletionScript case .bash: return ToolInfoV0(commandStack: [command]).bashCompletionScript case .fish: @@ -151,56 +151,6 @@ struct CompletionsGenerator { } } -extension ArgumentDefinition { - /// Returns a string with the arguments for the callback to generate custom completions for - /// this argument. - func customCompletionCall(_ commands: [ParsableCommand.Type]) -> String { - let subcommandNames = - commands.dropFirst().map { "\($0._commandName) " }.joined() - let argumentName = - names.preferredName?.synopsisString - ?? self.help.keys.first?.fullPathString - ?? "---" - return "---completion \(subcommandNames)-- \(argumentName)" - } -} - -extension ParsableCommand { - fileprivate static var compositeCommandName: [String] { - if let superCommandName = configuration._superCommandName { - return [superCommandName] - + _commandName.split(separator: " ").map(String.init) - } else { - return _commandName.split(separator: " ").map(String.init) - } - } -} - -extension [ParsableCommand.Type] { - /// Include default 'help' subcommand in nonempty subcommand list if & only if - /// no help subcommand already exists. - mutating func addHelpSubcommandIfMissing() { - if !isEmpty && !contains(where: { $0._commandName == "help" }) { - append(HelpCommand.self) - } - } -} - -extension Sequence where Element == ParsableCommand.Type { - func completionFunctionName() -> String { - "_" - + self.flatMap { $0.compositeCommandName } - .uniquingAdjacentElements() - .joined(separator: "_") - } - - var shellVariableNamePrefix: String { - flatMap { $0.compositeCommandName } - .joined(separator: "_") - .shellEscapeForVariableName() - } -} - extension String { func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { iterationCount == 0 @@ -234,6 +184,10 @@ extension CommandInfoV0 { var completionFunctionPrefix: String { "__\(initialCommand)" } + + var shellVariableNamePrefix: String { + commandContext.joined(separator: "_").shellEscapeForVariableName() + } } extension ArgumentInfoV0 { diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 5e2001ca..60296aa2 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -9,56 +9,64 @@ // //===----------------------------------------------------------------------===// -extension [ParsableCommand.Type] { - /// Generates a Zsh completion script for the given command. +#if swift(>=6.0) +internal import ArgumentParserToolInfo +#else +import ArgumentParserToolInfo +#endif + +extension ToolInfoV0 { var zshCompletionScript: String { + command.zshCompletionScript + } +} + +extension CommandInfoV0 { + fileprivate var zshCompletionScript: String { // swift-format-ignore: NeverForceUnwrap // Preconditions: // - first must be non-empty for a zsh completion script to be of use. // - first is guaranteed non-empty in the one place where this computed var is used. - let commandName = first!._commandName - return """ - #compdef \(commandName) + """ + #compdef \(commandName) - \(completeFunctionName)() { - local -ar non_empty_completions=("${@:#(|:*)}") - local -ar empty_completions=("${(M)@:#(|:*)}") - _describe -V '' non_empty_completions -- empty_completions -P $'\\'\\'' - } + \(completeFunctionName)() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe -V '' non_empty_completions -- empty_completions -P $'\\'\\'' + } - \(customCompleteFunctionName)() { - local -a completions - completions=("${(@f)"$("${command_name}" "${@}" "${command_line[@]}")"}") - if [[ "${#completions[@]}" -gt 1 ]]; then - \(completeFunctionName) "${completions[@]:0:-1}" - fi - } + \(customCompleteFunctionName)() { + local -a completions + completions=("${(@f)"$("${command_name}" "${@}" "${command_line[@]}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + \(completeFunctionName) "${completions[@]:0:-1}" + fi + } - \(cursorIndexInCurrentWordFunctionName)() { - if [[ -z "${QIPREFIX}${IPREFIX}${PREFIX}" ]]; then - printf 0 - else - printf %s "${#${(z)LBUFFER}[-1]}" - fi - } + \(cursorIndexInCurrentWordFunctionName)() { + if [[ -z "${QIPREFIX}${IPREFIX}${PREFIX}" ]]; then + printf 0 + else + printf %s "${#${(z)LBUFFER}[-1]}" + fi + } - \(completionFunctions)\ - \(completionFunctionName()) - """ + \(completionFunctions)\ + \(completionFunctionName) + """ } private var completionFunctions: String { - guard let type = last else { return "" } - let functionName = completionFunctionName() - let isRootCommand = count == 1 + let functionName = completionFunctionName - let argumentSpecsAndSetupScripts = argumentsForHelp(visibility: .default) - .compactMap { argumentSpecAndSetupScript($0) } + let argumentSpecsAndSetupScripts = (arguments ?? []).compactMap { + argumentSpecAndSetupScript($0) + } var argumentSpecs = argumentSpecsAndSetupScripts.map(\.argumentSpec) let setupScripts = argumentSpecsAndSetupScripts.compactMap(\.setupScript) - var subcommands = type.configuration.subcommands - .filter { $0.configuration.shouldDisplay } + let subcommands = (subcommands ?? []).filter(\.shouldDisplay) let subcommandHandler: String if subcommands.isEmpty { @@ -67,17 +75,13 @@ extension [ParsableCommand.Type] { argumentSpecs.append("'(-): :->command'") argumentSpecs.append("'(-)*:: :->arg'") - if isRootCommand { - subcommands.addHelpSubcommandIfMissing() - } - subcommandHandler = """ case "${state}" in command) local -ar subcommands=( \( subcommands.map { """ - '\($0._commandName.zshEscapeForSingleQuotedDescribeCompletion()):\($0.configuration.abstract.shellEscapeForSingleQuotedString())' + '\($0.commandName.zshEscapeForSingleQuotedDescribeCompletion()):\($0.abstract?.shellEscapeForSingleQuotedString() ?? "")' """ } .joined(separator: "\n") @@ -87,7 +91,7 @@ extension [ParsableCommand.Type] { ;; arg) case "${words[1]}" in - \(subcommands.map { $0._commandName }.joined(separator: "|"))) + \(subcommands.map(\.commandName).joined(separator: "|"))) "\(functionName)_${words[1]}" ;; esac @@ -99,7 +103,7 @@ extension [ParsableCommand.Type] { return """ \(functionName)() { - \(isRootCommand + \((superCommands ?? []).isEmpty ? """ emulate -RL zsh -G setopt extendedglob nullglob numericglobsort @@ -131,52 +135,55 @@ extension [ParsableCommand.Type] { return "${ret}" } - \(subcommands.map { (self + [$0]).completionFunctions }.joined()) + \(subcommands.map(\.completionFunctions).joined()) """ } private func argumentSpecAndSetupScript( - _ arg: ArgumentDefinition + _ arg: ArgumentInfoV0 ) -> (argumentSpec: String, setupScript: String?)? { - guard arg.help.visibility.base == .default else { return nil } + guard arg.shouldDisplay else { return nil } let line: String - switch arg.names.count { + let names = arg.names ?? [] + switch names.count { case 0: - line = arg.help.options.contains(.isRepeating) ? "*" : "" + line = arg.isRepeating ? "*" : "" case 1: + // swift-format-ignore: NeverForceUnwrap + // Preconditions: names has exactly one element. line = """ - \(arg.isRepeatingOption ? "*" : "")\(arg.names[0].synopsisString.zshEscapeForSingleQuotedOptionSpec())\(arg.zshCompletionAbstract) + \(arg.isRepeatingOption ? "*" : "")\(names.first!.commonCompletionSynopsisString().zshEscapeForSingleQuotedOptionSpec())\(arg.completionAbstract) """ default: - let synopses = arg.names.map { - $0.synopsisString.zshEscapeForSingleQuotedOptionSpec() + let synopses = names.map { + $0.commonCompletionSynopsisString().zshEscapeForSingleQuotedOptionSpec() } line = """ \(arg.isRepeatingOption ? "*" : "(\(synopses.joined(separator: " ")))")'\ {\(synopses.joined(separator: ","))}\ - '\(arg.zshCompletionAbstract) + '\(arg.completionAbstract) """ } - switch arg.update { - case .unary: + switch arg.kind { + case .option, .positional: let (argumentAction, setupScript) = argumentActionAndSetupScript(arg) return ( - "'\(line):\(arg.valueName.zshEscapeForSingleQuotedOptionSpec()):\(argumentAction)'", + "'\(line):\(arg.valueName?.zshEscapeForSingleQuotedOptionSpec() ?? ""):\(argumentAction)'", setupScript ) - case .nullary: + case .flag: return ("'\(line)'", nil) } } /// Returns the zsh "action" for an argument completion string. private func argumentActionAndSetupScript( - _ arg: ArgumentDefinition + _ arg: ArgumentInfoV0 ) -> (argumentAction: String, setupScript: String?) { - switch arg.completion.kind { - case .default: + switch arg.completionKind { + case .none: return ("", nil) case .file(let extensions): @@ -206,51 +213,46 @@ extension [ParsableCommand.Type] { case .custom, .customAsync: return ( - "{\(customCompleteFunctionName) \(arg.customCompletionCall(self)) \"${current_word_index}\" \"$(\(cursorIndexInCurrentWordFunctionName))\"}", + "{\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self)) \"${current_word_index}\" \"$(\(cursorIndexInCurrentWordFunctionName))\"}", nil ) case .customDeprecated: return ( - "{\(customCompleteFunctionName) \(arg.customCompletionCall(self))}", + "{\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self))}", nil ) } } - private func variableName(_ arg: ArgumentDefinition) -> String { - guard let argName = arg.names.preferredName else { - return - "\(shellVariableNamePrefix)_\(arg.valueName.shellEscapeForVariableName())" + private func variableName(_ arg: ArgumentInfoV0) -> String { + guard let argName = arg.preferredName else { + return "_\(arg.valueName?.shellEscapeForVariableName() ?? "")" } return - "\(argName.case == .long ? "__" : "_")\(shellVariableNamePrefix)_\(argName.valueString.shellEscapeForVariableName())" + "\(argName.kind == .long ? "___" : "__")\(argName.name.shellEscapeForVariableName())" } private var completeFunctionName: String { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty - "__\(first!._commandName)_complete" + "\(completionFunctionPrefix)_complete" } private var customCompleteFunctionName: String { - // swift-format-ignore: NeverForceUnwrap - // Precondition: first is guaranteed to be non-empty - "__\(first!._commandName)_custom_complete" + "\(completionFunctionPrefix)_custom_complete" } private var cursorIndexInCurrentWordFunctionName: String { - "__\(first?._commandName ?? "")_cursor_index_in_current_word" + "\(completionFunctionPrefix)_cursor_index_in_current_word" } } -extension ArgumentDefinition { +extension ArgumentInfoV0 { /// - returns: `true` if `self` is a flag or an option and can be tab-completed multiple times in one command line. /// For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings. fileprivate var isRepeatingOption: Bool { guard - case .named(_) = kind, - help.options.contains(.isRepeating) + [.flag, .option].contains(kind), + isRepeating else { return false } switch parsingStrategy { @@ -259,10 +261,9 @@ extension ArgumentDefinition { } } - fileprivate var zshCompletionAbstract: String { - help.abstract.isEmpty - ? "" - : "[\(help.abstract.zshEscapeForSingleQuotedOptionSpec())]" + fileprivate var completionAbstract: String { + guard let abstract, !abstract.isEmpty else { return "" } + return "[\(abstract.zshEscapeForSingleQuotedOptionSpec())]" } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 07abcf40..2e47fa2c 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -127,9 +127,9 @@ _math_stats() { _math_stats_average() { local -i ret=1 - local -ar __math_stats_average_kind=('mean' 'median' 'mode') + local -ar ___kind=('mean' 'median' 'mode') local -ar arg_specs=( - '--kind[The kind of average to provide.]:kind:{__math_complete "${__math_stats_average_kind[@]}"}' + '--kind[The kind of average to provide.]:kind:{__math_complete "${___kind[@]}"}' '*:values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' @@ -153,11 +153,11 @@ _math_stats_stdev() { _math_stats_quantiles() { local -i ret=1 - local -ar math_stats_quantiles_one_of_four=('alphabet' 'alligator' 'branch' 'braggart') + local -ar _one_of_four=('alphabet' 'alligator' 'branch' 'braggart') local -ar arg_specs=( - ':one-of-four:{__math_complete "${math_stats_quantiles_one_of_four[@]}"}' - ':custom-arg:{__math_custom_complete ---completion stats quantiles -- customArg "${current_word_index}" "$(__math_cursor_index_in_current_word)"}' - ':custom-deprecated-arg:{__math_custom_complete ---completion stats quantiles -- customDeprecatedArg}' + ':one-of-four:{__math_complete "${_one_of_four[@]}"}' + ':custom-arg:{__math_custom_complete ---completion stats quantiles -- positional@1 "${current_word_index}" "$(__math_cursor_index_in_current_word)"}' + ':custom-deprecated-arg:{__math_custom_complete ---completion stats quantiles -- positional@2}' '*:values:' '--file:file:_files -g '\''*.txt *.md'\''' '--directory:directory:_files -/' diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 5162643b..a238886f 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -40,24 +40,24 @@ _base-test() { local -ir current_word_index="$((CURRENT - 1))" local -i ret=1 - local -ar __base_test_kind=('one' 'two' 'custom-three') - local -ar __base_test_other_kind=('b1_zsh' 'b2_zsh' 'b3_zsh') - local -ar __base_test_path3=('c1_zsh' 'c2_zsh' 'c3_zsh') + local -ar ___kind=('one' 'two' 'custom-three') + local -ar ___other_kind=('b1_zsh' 'b2_zsh' 'b3_zsh') + local -ar ___path3=('c1_zsh' 'c2_zsh' 'c3_zsh') local -ar arg_specs=( '--name[The user'\''s name.]:name:' - '--kind:kind:{__base-test_complete "${__base_test_kind[@]}"}' - '--other-kind:other-kind:{__base-test_complete "${__base_test_other_kind[@]}"}' + '--kind:kind:{__base-test_complete "${___kind[@]}"}' + '--other-kind:other-kind:{__base-test_complete "${___other_kind[@]}"}' '--path1:path1:_files' '--path2:path2:_files' - '--path3:path3:{__base-test_complete "${__base_test_path3[@]}"}' + '--path3:path3:{__base-test_complete "${___path3[@]}"}' '--one' '--two' '--three' '*--kind-counter' '*--rep1:rep1:' '*'{-r,--rep2}':rep2:' - ':argument:{__base-test_custom_complete ---completion -- argument "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' - ':nested-argument:{__base-test_custom_complete ---completion -- nested.nestedArgument "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' + ':argument:{__base-test_custom_complete ---completion -- positional@0 "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' + ':nested-argument:{__base-test_custom_complete ---completion -- positional@1 "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' @@ -98,7 +98,7 @@ _base-test_escaped-command() { local -i ret=1 local -ar arg_specs=( '--o\:n\[e[Escaped chars\: '\''\[\]\\.]:path\[\:options\]:' - ':two:{__base-test_custom_complete ---completion escaped-command -- two "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' + ':two:{__base-test_custom_complete ---completion escaped-command -- positional@0 "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S : "${arg_specs[@]}" && ret=0 From 9dc5b3ab81dcf3c5fded7c41b15b6b7388f0b90d Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 17 May 2025 17:42:44 -0400 Subject: [PATCH 7/8] Remove vestigial shellVariableNamePrefix. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/ArgumentParser/Completions/CompletionsGenerator.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 837e3fac..d196d00b 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -184,10 +184,6 @@ extension CommandInfoV0 { var completionFunctionPrefix: String { "__\(initialCommand)" } - - var shellVariableNamePrefix: String { - commandContext.joined(separator: "_").shellEscapeForVariableName() - } } extension ArgumentInfoV0 { From 246863b84d0fb7db0045ebd3f27aa4166d7bfa2c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 19 May 2025 16:08:59 -0400 Subject: [PATCH 8/8] Add .editorconfig files to prevent automatic whitespace changes to test snapshots. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/ArgumentParserExampleTests/Snapshots/.editorconfig | 3 +++ .../Snapshots/.editorconfig | 3 +++ .../ArgumentParserGenerateManualTests/Snapshots/.editorconfig | 3 +++ Tests/ArgumentParserUnitTests/Snapshots/.editorconfig | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 Tests/ArgumentParserExampleTests/Snapshots/.editorconfig create mode 100644 Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/.editorconfig create mode 100644 Tests/ArgumentParserGenerateManualTests/Snapshots/.editorconfig create mode 100644 Tests/ArgumentParserUnitTests/Snapshots/.editorconfig diff --git a/Tests/ArgumentParserExampleTests/Snapshots/.editorconfig b/Tests/ArgumentParserExampleTests/Snapshots/.editorconfig new file mode 100644 index 00000000..059c2d6d --- /dev/null +++ b/Tests/ArgumentParserExampleTests/Snapshots/.editorconfig @@ -0,0 +1,3 @@ +[*] +trim_trailing_whitespace = false +insert_final_newline = false diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/.editorconfig b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/.editorconfig new file mode 100644 index 00000000..059c2d6d --- /dev/null +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/.editorconfig @@ -0,0 +1,3 @@ +[*] +trim_trailing_whitespace = false +insert_final_newline = false diff --git a/Tests/ArgumentParserGenerateManualTests/Snapshots/.editorconfig b/Tests/ArgumentParserGenerateManualTests/Snapshots/.editorconfig new file mode 100644 index 00000000..059c2d6d --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/Snapshots/.editorconfig @@ -0,0 +1,3 @@ +[*] +trim_trailing_whitespace = false +insert_final_newline = false diff --git a/Tests/ArgumentParserUnitTests/Snapshots/.editorconfig b/Tests/ArgumentParserUnitTests/Snapshots/.editorconfig new file mode 100644 index 00000000..059c2d6d --- /dev/null +++ b/Tests/ArgumentParserUnitTests/Snapshots/.editorconfig @@ -0,0 +1,3 @@ +[*] +trim_trailing_whitespace = false +insert_final_newline = false