Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
Expand Down
37 changes: 29 additions & 8 deletions Sources/ArgumentParser/Usage/DumpHelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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,
Expand All @@ -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
}
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand Down
90 changes: 90 additions & 0 deletions Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ _base-test() {
'--path1:path1:_files'
'--path2:path2:_files'
'--path3:path3:{__base-test_complete "${___path3[@]}"}'
'--verbose'
'--one'
'--two'
'--custom-three'
Expand Down