From ac9edb4e32b15c7d4f8787b1ad32472a8251f13e Mon Sep 17 00:00:00 2001 From: pvincent Date: Mon, 6 Apr 2026 11:03:29 +0400 Subject: [PATCH] half-done, apt-get in does not complete yet --- lib/RECURSIVE_COMPLETION.md | 127 ++++++++++++++++++++++++++++++++++++ lib/miaou.completion | 122 ++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 lib/RECURSIVE_COMPLETION.md diff --git a/lib/RECURSIVE_COMPLETION.md b/lib/RECURSIVE_COMPLETION.md new file mode 100644 index 0000000..ff6a1b4 --- /dev/null +++ b/lib/RECURSIVE_COMPLETION.md @@ -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 + ↑ + 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. \ No newline at end of file diff --git a/lib/miaou.completion b/lib/miaou.completion index 0fec249..bd8b9f8 100644 --- a/lib/miaou.completion +++ b/lib/miaou.completion @@ -30,4 +30,126 @@ function _miaou_login { esac } +function _miaou_exec2() { + local cur="${COMP_WORDS[COMP_CWORD]}" + local prev="${COMP_WORDS[COMP_CWORD - 1]}" + local container="${COMP_WORDS[1]}" + + # 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 == 1)); then + # first argument — containers + COMPREPLY=($(compgen -W "$(_incus_container)" -- "$cur")) + return + fi + + if ((COMP_CWORD == 2)); then + # second argument - options + COMPREPLY=($(compgen -W "--" -- "$cur")) + return + fi + + if ((COMP_CWORD == 3)); then + # command or file from inside container + COMPREPLY=($( + incus exec "$container" -- bash << EOF + source /etc/bash_completion + compgen -c -- "$cur" + compgen -f -- "$cur" +EOF + )) + fi + + if ((COMP_CWORD > 3)); then + + command=${COMP_WORDS[dashdash_pos + 1]} + completion_command=$( + incus exec "$container" -- bash << EOF + source /etc/bash_completion + for i in /etc/bash_completion.d/*; do source \$i; done + [[ -f /usr/share/bash-completion/completions/$command ]] && source /usr/share/bash-completion/completions/$command + complete -p "$command" | grep -oP '(?<=-F )\S+' +EOF + ) + if [[ -n $completion_command ]]; then + # Remove first $dashdash_pos items + COMP_WORDS=("${COMP_WORDS[@]:dashdash_pos+1}") + COMP_CWORD=$((COMP_CWORD - dashdash_pos - 1)) + COMP_LINE="${COMP_WORDS[@]}" + COMP_POINT=${#COMP_LINE} + + echo -e "\ncompletion_command $completion_command \nprev=$prev \ncur=$cur \nCOMP_WORDS=(${COMP_WORDS[@]}) \nCOMP_CWORD=$COMP_CWORD \nCOMP_LINE=$COMP_LINE \nCOMP_POINT=$COMP_POINT" >&2 + COMPREPLY=($( + incus exec "$container" -- bash << EOF + source /etc/bash_completion + for i in /etc/bash_completion.d/*; do source \$i; done + source /usr/share/bash-completion/bash_completion + [[ -f /usr/share/bash-completion/completions/$command ]] && source /usr/share/bash-completion/completions/$command + COMP_WORDS=(${COMP_WORDS[@]}) + COMP_CWORD=$COMP_CWORD + COMP_LINE="$COMP_LINE" + COMP_POINT=$COMP_CWORD + COMP_TYPE=9 + COMP_KEY=9 + export COMP_WORDS COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY + $completion_command + echo \${COMPREPLY[@]} +EOF + )) + else + COMPREPLY=($( + incus exec "$container" -- bash << EOF + source /etc/bash_completion + compgen -f -- "$cur" +EOF + )) + fi + fi + +} + +function _miaou_exec { + local cur="${COMP_WORDS[COMP_CWORD]}" + local prev="${COMP_WORDS[COMP_CWORD - 1]}" + local container="${COMP_WORDS[1]}" + + case $COMP_CWORD in + 1) + # first argument — containers + COMPREPLY=($(compgen -W "$(_incus_container)" -- "$cur")) + ;; + 2) + # second argument - options + COMPREPLY=($(compgen -W "--" -- "$cur")) + ;; + *) + # echo "coucou" >&2 + # COMPREPLY=($(compgen -W \ + # "$(incus exec "$container" -- bash -c 'compgen -c' 2> /dev/null)" \ + # -- "$cur")) + + COMPREPLY=($(compgen -W \ + "$(incus exec "$container" -- \ + bash -c "compgen -c -- $(printf '%q' "$cur")" 2> /dev/null)" \ + -- "$cur")) + ;; + + esac +} + complete -F _miaou_login "miaou-login" +complete -F _miaou_exec2 "miaou-exec" + +function _toto { + for i in ${!COMP_WORDS[@]}; do echo "COMP_WORDS[$i]=${COMP_WORDS[$i]}"; done + echo COMP_CWORD=$COMP_CWORD + COMPREPLY=(one two three) +} + +complete -F _toto toto