Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ extension [ParsableCommand.Type] {
\(isRootCommand
? """
emulate -RL zsh -G
setopt extendedglob
setopt extendedglob nullglob numericglobsort
unsetopt aliases banghist

local -xr \(CompletionShell.shellEnvironmentVariableName)=zsh
Expand Down
131 changes: 113 additions & 18 deletions Sources/ArgumentParser/Parsable Properties/CompletionKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,151 @@
//
//===----------------------------------------------------------------------===//

/// The type of completion to use for an argument or option.
/// The type of completion to use for an argument or option value.
///
/// For all `CompletionKind`s, the completion shell script is configured with
/// the following settings, which will not affect the requesting shell outside
/// the completion script:
///
/// ### bash
///
/// ```shell
/// shopt -s extglob
/// set +o history +o posix
/// ```
///
/// ### fish
///
/// no settings
///
/// ### zsh
///
/// ```shell
/// emulate -RL zsh -G
/// setopt extendedglob nullglob numericglobsort
/// unsetopt aliases banghist
/// ```
public struct CompletionKind {
internal enum Kind {
/// Use the default completion kind for the value's type.
case `default`

/// Use the specified list of completion strings.
case list([String])

/// Complete file names with the specified extensions.
case file(extensions: [String])

/// Complete directory names that match the specified pattern.
case directory

/// Call the given shell command to generate completions.
case shellCommand(String)

/// Generate completions using the given closure.
case custom(@Sendable ([String]) -> [String])
}

internal var kind: Kind

/// Use the default completion kind for the value's type.
/// Use the default completion kind for the argument's or option value's type.
public static var `default`: CompletionKind {
CompletionKind(kind: .default)
}

/// Use the specified list of completion strings.
/// The completion candidates are the strings in the given array.
///
/// Completion candidates are interpreted by the requesting shell as literals.
/// They must be neither escaped nor quoted; Swift Argument Parser escapes or
/// quotes them as necessary for the requesting shell.
///
/// The completion candidates are included in a completion script when it is
/// generated.
public static func list(_ words: [String]) -> CompletionKind {
CompletionKind(kind: .list(words))
}

/// Complete file names.
/// The completion candidates include directory and file names, the latter
/// filtered by the given list of extensions.
///
/// If the given list of extensions is empty, then file names are not
/// filtered.
///
/// Given file extensions must not include the `.` initial extension
/// separator.
///
/// Given file extensions are parsed by the requesting shell as globs; Swift
/// Argument Parser does not perform any escaping or quoting.
///
/// The directory/file filter and the given list of extensions are included in
/// a completion script when it is generated.
public static func file(extensions: [String] = []) -> CompletionKind {
CompletionKind(kind: .file(extensions: extensions))
}

/// Complete directory names.
/// The completion candidates are directory names.
///
/// The directory filter is included in a completion script when it is
/// generated.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these cues about how/where the different completion configurations are used 👍🏻

public static var directory: CompletionKind {
CompletionKind(kind: .directory)
}

/// Call the given shell command to generate completions.
/// The completion candidates are specified by the `stdout` output of the
/// given string run as a shell command when a user requests completions.
///
/// Swift Argument Parser does not perform any escaping or quoting on the
/// given shell command.
///
/// The given shell command is included in a completion script when it is
/// generated.
public static func shellCommand(_ command: String) -> CompletionKind {
CompletionKind(kind: .shellCommand(command))
}

/// Generate completions using the given closure.
/// The completion candidates are the strings in the array returned by the
/// given closure when it is executed in response to a user's request for
/// completions.
///
/// Completion candidates are interpreted by the requesting shell as literals.
/// They must be neither escaped nor quoted; Swift Argument Parser escapes or
/// quotes them as necessary for the requesting shell.
///
/// The given closure is evaluated after a user invokes completion in their
/// shell (normally by pressing TAB); it is not evaluated when a completion
/// script is generated.
///
/// The array of strings passed to the given closure contains all the shell
/// words in the command line for the current command at completion
/// invocation; this is exclusive of words for prior or subsequent commands or
/// pipes, but inclusive of redirects and any other command line elements.
/// Each word is its own element in the argument array; they appear in the
/// same order as in the command line. Note that shell words may contain
/// spaces if they are escaped or quoted.
///
/// Shell words are passed to Swift verbatim, without processing or removing
/// any quotes or escapes. For example, the shell word `"abc\\""def"` would be
/// passed to Swift as `"abc\\""def"` (i.e. the Swift String's contents would
/// include all 4 of the double quotes and the 2 consecutive backslashes).
///
/// ### bash
///
/// In bash 3-, a process substitution (`<(…)`) in the command line prevents
/// Swift custom completion functions from being called.
///
/// In bash 4+, a process substitution (`<(…)`) is split into multiple
/// elements in the argument array: one for the starting `<(`, and one for
/// each unescaped/unquoted-space-separated token through the closing `)`.
///
/// In bash, if the cursor is between the backslash and the single quote for
/// the last escaped single quote in a word, all subsequent pipes or other
/// commands are included in the words passed to Swift. This oddity might
/// occur only when additional constraints are met. This or similar oddities
/// might occur in other circumstances.
///
/// ### fish
///
/// In fish 3-, due to a bug, the argument array includes the fish words only
/// through the word being completed. This is fixed in fish 4+.
///
/// In fish, a redirect's symbol is not included, but its source/target is.
///
/// In fish 3-, due to limitations, words are passed to Swift unquoted. For
/// example, the shell word `"abc\\""def"` would be passed to Swift as
/// `abc\def`. This is fixed in fish 4+.
///
/// ### zsh
///
/// In zsh, redirects (both their symbol and source/target) are omitted.
@preconcurrency
public static func custom(
_ completion: @Sendable @escaping ([String]) -> [String]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ __math_custom_complete() {

_math() {
emulate -RL zsh -G
setopt extendedglob
setopt extendedglob nullglob numericglobsort
unsetopt aliases banghist

local -xr SAP_SHELL=zsh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ __base-test_custom_complete() {

_base-test() {
emulate -RL zsh -G
setopt extendedglob
setopt extendedglob nullglob numericglobsort
unsetopt aliases banghist

local -xr SAP_SHELL=zsh
Expand Down