2 changed files with 249 additions and 0 deletions
@ -0,0 +1,127 @@ |
|||
## Recursive completion — call container's own bash completion |
|||
|
|||
The idea: instead of reimplementing completion, **delegate to bash inside the container** and get back its own completion results. |
|||
|
|||
--- |
|||
|
|||
## The approach |
|||
|
|||
```bash |
|||
_incus_exec_delegate() { |
|||
local cur="${COMP_WORDS[COMP_CWORD]}" |
|||
local container="${COMP_WORDS[2]}" |
|||
|
|||
# find '--' position |
|||
local dashdash_pos=0 |
|||
local i |
|||
for (( i=0; i < ${#COMP_WORDS[@]}; i++ )); do |
|||
[[ "${COMP_WORDS[$i]}" == "--" ]] && dashdash_pos=$i |
|||
done |
|||
|
|||
# complete container names |
|||
if (( COMP_CWORD == 2 )); then |
|||
COMPREPLY=($(compgen -W \ |
|||
"$(incus list --format csv --columns n 2>/dev/null)" \ |
|||
-- "$cur")) |
|||
return |
|||
fi |
|||
|
|||
# before '--' |
|||
if (( dashdash_pos == 0 )); then |
|||
COMPREPLY=($(compgen -W "-- --env --user --cwd" -- "$cur")) |
|||
return |
|||
fi |
|||
|
|||
# after '--' — delegate full completion to container's bash |
|||
local inner_words=("${COMP_WORDS[@]:$((dashdash_pos + 1))}") |
|||
local inner_cword=$(( COMP_CWORD - dashdash_pos - 1 )) |
|||
local inner_cmd="${inner_words[0]}" |
|||
local inner_line="${inner_words[*]}" |
|||
|
|||
# ask container's bash to run completion |
|||
COMPREPLY=($(incus exec "$container" -- bash -c " |
|||
# load bash completion |
|||
source /usr/share/bash-completion/bash_completion 2>/dev/null |
|||
source /etc/bash_completion 2>/dev/null |
|||
|
|||
# rebuild COMP_ variables inside container |
|||
COMP_WORDS=(${inner_words[*]@Q}) |
|||
COMP_CWORD=$inner_cword |
|||
COMP_LINE=${inner_line@Q} |
|||
COMP_POINT=\${#COMP_LINE} |
|||
|
|||
# find and call the completion function for the command |
|||
local compfunc |
|||
compfunc=\$(complete -p ${inner_cmd@Q} 2>/dev/null \ |
|||
| grep -oP '(?<=-F )\S+') |
|||
|
|||
if [[ -n \"\$compfunc\" ]]; then |
|||
\$compfunc ${inner_cmd@Q} ${cur@Q} ${inner_words[$((inner_cword - 1))]@Q} |
|||
printf '%s\n' \"\${COMPREPLY[@]}\" |
|||
else |
|||
# fallback to file completion |
|||
compgen -f -- ${cur@Q} |
|||
fi |
|||
" 2>/dev/null)) |
|||
} |
|||
|
|||
complete -F _incus_exec_delegate incus |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## How it works |
|||
|
|||
``` |
|||
host$ incus exec mycontainer -- git che<TAB> |
|||
↑ |
|||
inner_cmd="git" |
|||
↓ |
|||
incus exec mycontainer -- bash -c ' |
|||
source /usr/share/bash-completion/bash_completion |
|||
COMP_WORDS=(git che) |
|||
COMP_CWORD=1 |
|||
compfunc=$(complete -p git | grep -oP "(?<=-F )\S+") |
|||
$compfunc ... # calls __git_wrap__git_main |
|||
printf "%s\n" "${COMPREPLY[@]}" |
|||
' |
|||
↓ |
|||
COMPREPLY=(checkout cherry cherry-pick) |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Caveats |
|||
|
|||
```bash |
|||
# completion inside container requires bash-completion installed |
|||
incus exec mycontainer -- apt install -y bash-completion |
|||
|
|||
# verify completion available for a command |
|||
incus exec mycontainer -- bash -c "complete -p git" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Fallback chain |
|||
|
|||
``` |
|||
container has completion function for cmd? |
|||
↓ yes → delegate to it |
|||
↓ no → compgen -c (commands) |
|||
↓ |
|||
compgen -f (files) |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Quick reference |
|||
|
|||
| Step | What happens | |
|||
|------|-------------| |
|||
| rebuild `COMP_WORDS` | strip `incus exec CTR --` prefix | |
|||
| `complete -p CMD` | find completion function in container | |
|||
| `$compfunc` | call it with correct args | |
|||
| fallback | `compgen -f` for file completion | |
|||
|
|||
The key insight is **rebuilding `COMP_WORDS` and `COMP_CWORD`** with the `incus exec CTR --` prefix stripped — the container's bash then sees the command line exactly as if it were running locally. |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue