Skip to content
Draft
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ EXAMPLES
$ oclif-example autocomplete --refresh-cache
```

_See code: [src/commands/autocomplete/index.ts](https://github.com/oclif/plugin-autocomplete/blob/3.2.36/src/commands/autocomplete/index.ts)_
_See code: [src/commands/autocomplete/index.ts](https://github.com/oclif/plugin-autocomplete/blob/3.2.37-autocomplete.1/src/commands/autocomplete/index.ts)_

<!-- commandsstop -->

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "@oclif/plugin-autocomplete",
"description": "autocomplete plugin for oclif",
"version": "3.2.36",
"version": "3.2.37-autocomplete.1",
"author": "Salesforce",
"bugs": "https://github.com/oclif/plugin-autocomplete/issues",
"dependencies": {
"@oclif/core": "^4",
"@oclif/core": "autocomplete",
"ansis": "^3.16.0",
"debug": "^4.4.1",
"ejs": "^3.1.10"
Expand Down
41 changes: 35 additions & 6 deletions src/autocomplete/bash-spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,42 @@ _<CLI_BIN>_autocomplete()
else
# Flag

# The full CLI command separated by colons (e.g. "mycli command subcommand --fl" -> "command:subcommand")
# This needs to be defined with $COMP_CWORD-1 as opposed to above because the current "word" on the command line is a flag and the command is everything before the flag
normalizedCommand="$( printf "%s" "$(join_by ":" "\${COMP_WORDS[@]:1:($COMP_CWORD - 1)}")" )"
# Check if we're completing a flag value (previous word is a flag)
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
if [[ "$prev" == --* ]] && [[ "$cur" != "-"* ]]; then
# We're completing a flag value, try dynamic completion
# The full CLI command separated by colons
normalizedCommand="$( printf "%s" "$(join_by ":" "\${COMP_WORDS[@]:1:($COMP_CWORD - 2)}")" )"
local flagName="\${prev#--}"

# Try to get dynamic completions
local dynamicOpts=$(<CLI_BIN> autocomplete:options --command="\${normalizedCommand}" --flag="\${flagName}" 2>/dev/null)

if [[ -n "$dynamicOpts" ]]; then
# Handle dynamic options line-by-line to properly support special characters
# This avoids issues with spaces, dollar signs, and other shell metacharacters
COMPREPLY=()
while IFS= read -r option; do
# Only add options that match the current word being completed
if [[ -z "$cur" ]] || [[ "$option" == "$cur"* ]]; then
COMPREPLY+=("$option")
fi
done <<< "$dynamicOpts"
return 0
else
# Fall back to file completion
COMPREPLY=($(compgen -f -- "\${cur}"))
return 0
fi
else
# The full CLI command separated by colons (e.g. "mycli command subcommand --fl" -> "command:subcommand")
# This needs to be defined with $COMP_CWORD-1 as opposed to above because the current "word" on the command line is a flag and the command is everything before the flag
normalizedCommand="$( printf "%s" "$(join_by ":" "\${COMP_WORDS[@]:1:($COMP_CWORD - 1)}")" )"

# The line below finds the command in $commands using grep
# Then, using sed, it removes everything from the found command before the --flags (e.g. "command:subcommand:subsubcom --flag1 --flag2" -> "--flag1 --flag2")
opts=$(printf "%s " "\${commands[@]}" | grep "\${normalizedCommand}" | sed -n "s/^\${normalizedCommand} //p")
# The line below finds the command in $commands using grep
# Then, using sed, it removes everything from the found command before the --flags (e.g. "command:subcommand:subsubcom --flag1 --flag2" -> "--flag1 --flag2")
opts=$(printf "%s " "\${commands[@]}" | grep "\${normalizedCommand}" | sed -n "s/^\${normalizedCommand} //p")
fi
fi

COMPREPLY=($(compgen -W "$opts" -- "\${cur}"))
Expand Down
49 changes: 42 additions & 7 deletions src/autocomplete/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,50 @@ _<CLI_BIN>_autocomplete()
if [[ "$cur" != "-"* ]]; then
opts=$(printf "$commands" | grep -Eo '^[a-zA-Z0-9:_-]+')
else
local __COMP_WORDS
if [[ \${COMP_WORDS[2]} == ":" ]]; then
#subcommand
__COMP_WORDS=$(printf "%s" "\${COMP_WORDS[@]:1:3}")
# Check if we're completing a flag value (previous word is a flag)
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
if [[ "$prev" == --* ]] && [[ "$cur" != "-"* ]]; then
# We're completing a flag value, try dynamic completion
local __COMP_WORDS
if [[ \${COMP_WORDS[2]} == ":" ]]; then
#subcommand
__COMP_WORDS=$(printf "%s" "\${COMP_WORDS[@]:1:3}")
else
#simple command
__COMP_WORDS="\${COMP_WORDS[@]:1:1}"
fi

local flagName="\${prev#--}"
# Try to get dynamic completions
local dynamicOpts=$(<CLI_BIN> autocomplete:options --command="\${__COMP_WORDS}" --flag="\${flagName}" 2>/dev/null)

if [[ -n "$dynamicOpts" ]]; then
# Handle dynamic options line-by-line to properly support special characters
# This avoids issues with spaces, dollar signs, and other shell metacharacters
COMPREPLY=()
while IFS= read -r option; do
# Only add options that match the current word being completed
if [[ -z "$cur" ]] || [[ "$option" == "$cur"* ]]; then
COMPREPLY+=("$option")
fi
done <<< "$dynamicOpts"
return 0
else
# Fall back to file completion
COMPREPLY=($(compgen -f -- "\${cur}"))
return 0
fi
else
#simple command
__COMP_WORDS="\${COMP_WORDS[@]:1:1}"
local __COMP_WORDS
if [[ \${COMP_WORDS[2]} == ":" ]]; then
#subcommand
__COMP_WORDS=$(printf "%s" "\${COMP_WORDS[@]:1:3}")
else
#simple command
__COMP_WORDS="\${COMP_WORDS[@]:1:1}"
fi
opts=$(printf "$commands" | grep "\${__COMP_WORDS}" | sed -n "s/^\${__COMP_WORDS} //p")
fi
opts=$(printf "$commands" | grep "\${__COMP_WORDS}" | sed -n "s/^\${__COMP_WORDS} //p")
fi
_get_comp_words_by_ref -n : cur
COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
Expand Down
70 changes: 61 additions & 9 deletions src/autocomplete/powershell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default class PowerShellComp {
constructor(config: Config) {
this.config = config
this.topics = this.getTopics()
this.commands = this.getCommands()
this.commands = []
}

private get coTopics(): string[] {
Expand All @@ -48,7 +48,12 @@ export default class PowerShellComp {
return this._coTopics
}

public generate(): string {
public async generate(): Promise<string> {
// Ensure commands are loaded with completion properties
if (this.commands.length === 0) {
await this.init()
}

const genNode = (partialId: string): Record<string, any> => {
const node: Record<string, any> = {}

Expand Down Expand Up @@ -206,9 +211,34 @@ $scriptblock = {

# Start completing command.
if ($NextArg._command -ne $null) {
# Complete flags
# \`cli config list -<TAB>\`
if ($WordToComplete -like '-*') {
# Check if we're completing a flag value
$PrevWord = if ($CurrentLine.Count -gt 0) { $CurrentLine[-1] } else { "" }
$IsCompletingFlagValue = $PrevWord -like '--*' -and $WordToComplete -notlike '-*'

if ($IsCompletingFlagValue) {
# Try dynamic flag value completion
$FlagName = $PrevWord.TrimStart('-')
$CommandId = $NextArg._command.id

try {
$DynamicOptions = & ${this.config.bin} autocomplete:options --command=$CommandId --flag=$FlagName 2>$null
if ($DynamicOptions) {
$DynamicOptions | Where-Object {
$_.StartsWith("$WordToComplete")
} | ForEach-Object {
New-Object -Type CompletionResult -ArgumentList \`
$($Mode -eq "MenuComplete" ? "$_ " : "$_"),
$_,
"ParameterValue",
" "
}
}
} catch {
# Fall back to no completions if dynamic completion fails
}
} elseif ($WordToComplete -like '-*') {
# Complete flags
# \`cli config list -<TAB>\`
$NextArg._command.flags.GetEnumerator() | Sort-Object -Property key
| Where-Object {
# Filter out already used flags (unless \`flag.multiple = true\`).
Expand Down Expand Up @@ -269,6 +299,10 @@ Register-ArgumentCompleter -Native -CommandName ${
return compRegister
}

async init(): Promise<void> {
this.commands = await this.getCommands()
}

private genCmdHashtable(cmd: CommandCompletion): string {
const flaghHashtables: string[] = []

Expand All @@ -289,18 +323,19 @@ Register-ArgumentCompleter -Native -CommandName ${

if (f.type === 'option' && f.multiple) {
flaghHashtables.push(
` "${f.name}" = @{
` "${flagName}" = @{
"summary" = "${flagSummary}"
"multiple" = $true
}`,
)
} else {
flaghHashtables.push(` "${f.name}" = @{ "summary" = "${flagSummary}" }`)
flaghHashtables.push(` "${flagName}" = @{ "summary" = "${flagSummary}" }`)
}
}
}

const cmdHashtable = `@{
"id" = "${cmd.id}"
"summary" = "${cmd.summary}"
"flags" = @{
${flaghHashtables.join('\n')}
Expand Down Expand Up @@ -365,14 +400,31 @@ ${flaghHashtables.join('\n')}
return leafTpl
}

private getCommands(): CommandCompletion[] {
private async getCommands(): Promise<CommandCompletion[]> {
const cmds: CommandCompletion[] = []

for (const p of this.config.getPluginsList()) {
for (const c of p.commands) {
if (c.hidden) continue
const summary = this.sanitizeSummary(c.summary ?? c.description)
const {flags} = c

// Try to load actual command class to get flags with completion properties
// This allows us to see dynamic completions, but gracefully falls back to
// manifest flags if loading fails - preserving existing behavior
let {flags} = c
try {
// eslint-disable-next-line no-await-in-loop
const CommandClass = await c.load()
// Use flags from command class if available and not empty (includes completion properties)
if (CommandClass.flags && Object.keys(CommandClass.flags).length > 0) {
flags = CommandClass.flags as CommandFlags
}
} catch {
// Fall back to manifest flags if loading fails
// This ensures existing commands without completions continue to work exactly as before
flags = c.flags
}

cmds.push({
flags,
id: c.id,
Expand Down
Loading
Loading