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 @@ -263,15 +263,21 @@ extension CommandInfoV0 {
return nil as String?
}

let positionPattern = arg.isRepeating ? "*" : position.description

if arg.isRepeating {
encounteredRepeatingPositional = true
}

guard arg.shouldDisplay else {
return nil
}

let completion = valueCompletion(arg)
return completion.isEmpty
? nil
: """
\(encounteredRepeatingPositional ? "*" : position.description))
\(positionPattern))
\(completion.indentingEachLine(by: 8))\
return
;;
Expand Down
15 changes: 12 additions & 3 deletions Sources/ArgumentParser/Completions/CompletionsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,20 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ extension CommandInfoV0 {
var positionalIndex = 0

var repeatingPositionalComparison = ""
let completableArguments = self.completableArguments
let argumentCompletions =
completableArguments
(arguments ?? [])
.compactMap { arg in
if arg.kind == .positional {
guard repeatingPositionalComparison.isEmpty else {
Expand All @@ -154,16 +155,22 @@ extension CommandInfoV0 {
if arg.isRepeating {
repeatingPositionalComparison = " -ge"
}

positionalIndex += 1
guard arg.shouldDisplay else {
return nil
}
}

guard completableArguments.contains(arg) else {
return nil
}

return """
\(prefix)\(
arg.kind == .positional
? """
\(shouldOfferCompletionsForPositionalFunctionName) "\(commandContext.joined(separator: separator))" \({
positionalIndex += 1
return "\(positionalIndex)\(repeatingPositionalComparison)"
}())
\(shouldOfferCompletionsForPositionalFunctionName) "\(commandContext.joined(separator: separator))" \(positionalIndex)\(repeatingPositionalComparison)
"""
: """
\(shouldOfferCompletionsForFlagsOrOptionsFunctionName) "\(commandContext.joined(separator: separator))"\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ extension CommandInfoV0 {

var repeatingPositionalIndicator = ""
let argumentSpecsAndSetupScripts = (arguments ?? []).compactMap { arg in
guard arg.shouldDisplay else {
guard arg.shouldDisplay || arg.kind == .positional else {
return nil as (argumentSpec: String, setupScript: String?)?
}

Expand All @@ -78,6 +78,9 @@ extension CommandInfoV0 {
repeatingPositionalIndicator = "*"
}
line = repeatingPositionalIndicator
guard arg.shouldDisplay else {
return ("'\(line)::'", nil)
}
case 1:
// swift-format-ignore: NeverForceUnwrap
// Preconditions: names has exactly one element.
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,30 @@ 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 +83,18 @@ 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 +110,10 @@ 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 @@ -114,7 +134,8 @@ extension ArgumentInfoV0 {

self.init(
kind: kind,
shouldDisplay: argument.help.visibility.base == .default,
shouldDisplay: argument.help.visibility.isAtLeastAsVisible(
as: includeHiddenArguments ? .hidden : .default),
sectionTitle: argument.help.parentTitle.nonEmpty,
isOptional: argument.help.options.contains(.isOptional),
isRepeating: argument.help.options.contains(.isRepeating),
Expand Down
108 changes: 108 additions & 0 deletions Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,114 @@ extension CompletionScriptTests {
let script3 = Base.completionScript(for: .fish)
try assertSnapshot(actual: script3, extension: "fish")
}

struct Visibility: ParsableCommand {
@Flag(help: ArgumentHelp("Hidden flag.", visibility: .hidden))
var hiddenFlag = false

@Flag(help: ArgumentHelp("Private flag.", visibility: .private))
var privateFlag = false

@Option(
help: ArgumentHelp("Hidden option.", visibility: .hidden),
completion: .list(["hidden-option-value"]))
var hiddenOption: String?

@Option(
help: ArgumentHelp("Private option.", visibility: .private),
completion: .list(["private-option-value"]))
var privateOption: String?

@Argument(
help: ArgumentHelp("Private argument.", visibility: .private),
completion: .list(["private-argument-value"]))
var privateArgument: String?

@Argument(
help: ArgumentHelp("Hidden argument.", visibility: .hidden),
completion: .list(["hidden-argument-value"]))
var hiddenArgument: String?
}

func testHiddenAndPrivateVisibility_Bash() throws {
try assertHiddenAndPrivateVisibility(in: .bash)
}

func testHiddenAndPrivateVisibility_Fish() throws {
try assertHiddenAndPrivateVisibility(in: .fish)
}

func testHiddenAndPrivateVisibility_Zsh() throws {
try assertHiddenAndPrivateVisibility(in: .zsh)
}

private func assertHiddenAndPrivateVisibility(
in shell: CompletionShell,
file: StaticString = #filePath,
line: UInt = #line
) throws {
let script = try CompletionsGenerator(command: Visibility.self, shell: shell)
.generateCompletionScript()

let (expectedNames, unexpectedNames, expectedHiddenArgument): (
[String],
[String],
String
) =
switch shell {
case .bash:
(
["--hidden-flag", "--hidden-option"],
["--private-flag", "--private-option"],
"""
2)
__visibility_add_completions -W 'hidden-argument-value'
"""
)
case .zsh:
(
["--hidden-flag", "--hidden-option"],
["--private-flag", "--private-option"],
"""
'::'
':hidden-argument:{__visibility_complete "${_hidden_argument[@]}"}'
"""
)
case .fish:
(
["-l 'hidden-flag'", "-l 'hidden-option'"],
["-l 'private-flag'", "-l 'private-option'"],
"""
__visibility_should_offer_completions_for_positional "visibility" 2' -fka 'hidden-argument-value'
"""
)
default:
([], [], "")
}

for expected in [
"hidden-option-value",
"hidden-argument-value",
expectedHiddenArgument,
] + expectedNames {
XCTAssertTrue(
script.contains(expected),
"\(shell.rawValue) completion script is missing \(expected)",
file: file,
line: line)
}

for unexpected in [
"private-option-value",
"private-argument-value",
] + unexpectedNames {
XCTAssertFalse(
script.contains(unexpected),
"\(shell.rawValue) completion script includes \(unexpected)",
file: file,
line: line)
}
}
}

extension CompletionScriptTests {
Expand Down
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