diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 5328e798c..d1370b6ce 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -124,11 +124,14 @@ struct CompletionsGenerator { CompletionShell._requesting.withLock { $0 = shell } switch shell { case .zsh: - return ToolInfoV0(commandStack: [command]).zshCompletionScript + return ToolInfoV0(commandStack: [command], includeHiddenArguments: true) + .zshCompletionScript case .bash: - return ToolInfoV0(commandStack: [command]).bashCompletionScript + return ToolInfoV0(commandStack: [command], includeHiddenArguments: true) + .bashCompletionScript case .fish: - return ToolInfoV0(commandStack: [command]).fishCompletionScript + return ToolInfoV0(commandStack: [command], includeHiddenArguments: true) + .fishCompletionScript default: fatalError("Invalid CompletionShell: \(shell)") } diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 002d564f5..d01e94d97 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -43,19 +43,28 @@ extension BidirectionalCollection where Element == ParsableCommand.Type { } extension ToolInfoV0 { - init(commandStack: [ParsableCommand.Type]) { - self.init(command: CommandInfoV0(commandStack: commandStack)) + init( + commandStack: [ParsableCommand.Type], includeHiddenArguments: Bool = false + ) { + self.init( + command: CommandInfoV0( + commandStack: commandStack, + includeHiddenArguments: includeHiddenArguments)) // 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 self.command.subcommands = (self.command.subcommands ?? []) + [ - CommandInfoV0(commandStack: commandStack + [HelpCommand.self]) + CommandInfoV0( + commandStack: commandStack + [HelpCommand.self], + includeHiddenArguments: includeHiddenArguments) ] } } extension CommandInfoV0 { - fileprivate init(commandStack: [ParsableCommand.Type]) { + fileprivate init( + commandStack: [ParsableCommand.Type], includeHiddenArguments: Bool + ) { guard let command = commandStack.last else { preconditionFailure("commandStack must not be empty") } @@ -72,12 +81,17 @@ extension CommandInfoV0 { .map { subcommand -> CommandInfoV0 in var commandStack = commandStack commandStack.append(subcommand) - return CommandInfoV0(commandStack: commandStack) + return CommandInfoV0( + commandStack: commandStack, + includeHiddenArguments: includeHiddenArguments) } let arguments = commandStack .allArguments() - .compactMap(ArgumentInfoV0.init) + .compactMap { + ArgumentInfoV0( + argument: $0, includeHiddenArguments: includeHiddenArguments) + } self = CommandInfoV0( superCommands: superCommands, @@ -93,7 +107,8 @@ extension CommandInfoV0 { } extension ArgumentInfoV0 { - fileprivate init?(argument: ArgumentDefinition) { + fileprivate init?(argument: ArgumentDefinition, includeHiddenArguments: Bool) + { guard let kind = ArgumentInfoV0.KindV0(argument: argument) else { return nil } @@ -112,9 +127,15 @@ extension ArgumentInfoV0 { allValueDescriptions = options.allValueDescriptions } + // When building for completions (includeHiddenArguments == true), mark + // hidden arguments as displayable so completion scripts include them. + // Private arguments remain non-displayable in both cases. + let shouldDisplay = argument.help.visibility.isAtLeastAsVisible( + as: includeHiddenArguments ? .hidden : .default) + self.init( kind: kind, - shouldDisplay: argument.help.visibility.base == .default, + shouldDisplay: shouldDisplay, sectionTitle: argument.help.parentTitle.nonEmpty, isOptional: argument.help.options.contains(.isOptional), isRepeating: argument.help.options.contains(.isRepeating), diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index b96d7ace9..ed358a365 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -249,13 +249,16 @@ _math_stats_stdev() { _math_stats_quantiles() { repeating_flags=() - non_repeating_flags=(--version -h --help) + non_repeating_flags=(--test-success-exit-code --test-failure-exit-code --test-validation-exit-code --version -h --help) repeating_options=() - non_repeating_options=(--file --directory --shell --custom --custom-deprecated) + non_repeating_options=(--test-custom-exit-code --file --directory --shell --custom --custom-deprecated) __math_offer_flags_options -1 # Offer option value completions case "${prev}" in + '--test-custom-exit-code') + return + ;; '--file') __math_add_completions -o plusdirs -fX '!*.@(txt|md)' return diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish index 0fd19c10d..1d92835b8 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish @@ -41,7 +41,7 @@ function __math_parse_tokens -S case 'stdev' __math_parse_subcommand -r 1 'version' 'h/help' case 'quantiles' - __math_parse_subcommand -r 4 'file=' 'directory=' 'shell=' 'custom=' 'custom-deprecated=' 'version' 'h/help' + __math_parse_subcommand -r 4 'test-success-exit-code' 'test-failure-exit-code' 'test-validation-exit-code' 'test-custom-exit-code=' 'file=' 'directory=' 'shell=' 'custom=' 'custom-deprecated=' 'version' 'h/help' end case 'help' __math_parse_subcommand -r 1 'version' @@ -130,6 +130,10 @@ complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "mat complete -c 'math' -n '__math_should_offer_completions_for_positional "math stats quantiles" 1' -fka 'alphabet alligator branch braggart' complete -c 'math' -n '__math_should_offer_completions_for_positional "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_positional "math stats quantiles" 3' -fka '(__math_custom_completion ---completion stats quantiles -- positional@2)' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" test-success-exit-code' -l 'test-success-exit-code' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" test-failure-exit-code' -l 'test-failure-exit-code' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" test-validation-exit-code' -l 'test-validation-exit-code' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" test-custom-exit-code' -l 'test-custom-exit-code' -rfka '' complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" file' -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_flags_or_options "math stats quantiles" directory' -l 'directory' -rfa '(__math_complete_directories)' complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" shell' -l 'shell' -rfka '(head -100 \'/usr/share/dict/words\' | tail -50)' diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 0f5518ba8..9fc6d6a19 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -159,6 +159,10 @@ _math_stats_quantiles() { ':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:' + '--test-success-exit-code' + '--test-failure-exit-code' + '--test-validation-exit-code' + '--test-custom-exit-code:test-custom-exit-code:' '--file:file:_files -g '\''*.txt *.md'\''' '--directory:directory:_files -/' '--shell:shell:{local -a list;list=(${(f)"$(head -100 '\''/usr/share/dict/words'\'' | tail -50)"});_describe -V "" list}' diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift index d34d71aaf..d395d3d76 100644 --- a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift +++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift @@ -280,3 +280,93 @@ extension CompletionScriptTests { try await assertCustomCompletions(shell: .zsh) } } + +// MARK: - Hidden and private visibility tests + +extension CompletionScriptTests { + /// A command with arguments at each visibility level to verify that + /// completion scripts include hidden arguments but exclude private ones. + struct Visibility: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "visibility-test") + + @Flag(help: ArgumentHelp("A default flag.", visibility: .default)) + var defaultFlag = false + + @Flag(help: ArgumentHelp("A hidden flag.", visibility: .hidden)) + var hiddenFlag = false + + @Flag(help: ArgumentHelp("A private flag.", visibility: .private)) + var privateFlag = false + + @Option( + help: ArgumentHelp("A hidden option.", visibility: .hidden), + completion: .list(["hidden-a", "hidden-b"])) + var hiddenOption: String? + + @Option( + help: ArgumentHelp("A private option.", visibility: .private), + completion: .list(["private-a", "private-b"])) + var privateOption: String? + } + + func testHiddenAndPrivateBash() throws { + let script = try CompletionsGenerator( + command: Visibility.self, shell: .bash + ) + .generateCompletionScript() + // Hidden args should appear in completions. + XCTAssert( + script.contains("--hidden-flag"), + "Expected --hidden-flag in bash completion") + XCTAssert( + script.contains("--hidden-option"), + "Expected --hidden-option in bash completion") + // Private args must not appear. + XCTAssertFalse( + script.contains("--private-flag"), + "Expected --private-flag absent from bash completion") + XCTAssertFalse( + script.contains("--private-option"), + "Expected --private-option absent from bash completion") + } + + func testHiddenAndPrivateFish() throws { + let script = try CompletionsGenerator( + command: Visibility.self, shell: .fish + ) + .generateCompletionScript() + // Hidden args should appear in completions. + XCTAssert( + script.contains("hidden-flag"), "Expected hidden-flag in fish completion") + XCTAssert( + script.contains("hidden-option"), + "Expected hidden-option in fish completion") + // Private args must not appear. + XCTAssertFalse( + script.contains("private-flag"), + "Expected private-flag absent from fish completion") + XCTAssertFalse( + script.contains("private-option"), + "Expected private-option absent from fish completion") + } + + func testHiddenAndPrivateZsh() throws { + let script = try CompletionsGenerator(command: Visibility.self, shell: .zsh) + .generateCompletionScript() + // Hidden args should appear in completions. + XCTAssert( + script.contains("--hidden-flag"), + "Expected --hidden-flag in zsh completion") + XCTAssert( + script.contains("--hidden-option"), + "Expected --hidden-option in zsh completion") + // Private args must not appear. + XCTAssertFalse( + script.contains("--private-flag"), + "Expected --private-flag absent from zsh completion") + XCTAssertFalse( + script.contains("--private-option"), + "Expected --private-option absent from zsh completion") + } +} diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 95b84438d..fb1cdc3ed 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -163,7 +163,7 @@ _base-test() { local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}") local -a repeating_flags=(--kind-counter) - local -a non_repeating_flags=(--one --two --custom-three -h --help) + local -a non_repeating_flags=(--verbose --one --two --custom-three -h --help) local -a repeating_options=(--rep1 -r --rep2) local -a non_repeating_options=(--name --kind --other-kind --path1 --path2 --path3) __base-test_offer_flags_options 2 diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish index df9e309b8..a8a2ab953 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish @@ -27,7 +27,7 @@ function __base-test_parse_tokens -S switch $unparsed_tokens[1] case 'base-test' - __base-test_parse_subcommand 2 'name=' 'kind=' 'other-kind=' 'path1=' 'path2=' 'path3=' 'one' 'two' 'custom-three' 'kind-counter' 'rep1=+' 'r/rep2=+' 'h/help' + __base-test_parse_subcommand 2 'name=' 'kind=' 'other-kind=' 'path1=' 'path2=' 'path3=' 'verbose' 'one' 'two' 'custom-three' 'kind-counter' 'rep1=+' 'r/rep2=+' 'h/help' switch $unparsed_tokens[1] case 'sub-command' __base-test_parse_subcommand 0 'h/help' @@ -101,6 +101,7 @@ complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_op complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" path1' -l 'path1' -rF complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" path2' -l 'path2' -rF complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" path3' -l 'path3' -rfka 'c1_fish c2_fish c3_fish' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" verbose' -l 'verbose' complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" one' -l 'one' complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" two' -l 'two' complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" custom-three' -l 'custom-three' diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 6d8eb1aa8..58f648ab5 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -50,6 +50,7 @@ _base-test() { '--path1:path1:_files' '--path2:path2:_files' '--path3:path3:{__base-test_complete "${___path3[@]}"}' + '--verbose' '--one' '--two' '--custom-three'