From 806f1a0ed3d8f37de92c58d2c920197830712140 Mon Sep 17 00:00:00 2001 From: Iyigun Cevik Date: Fri, 3 Apr 2026 18:01:20 +0200 Subject: [PATCH 1/2] feat(juju): add native zsh completion Add _juju: native zsh completion script replacing bash-completion sourcing; supports subcommand, flag, model, and controller tab completion including "controller:model" format --- plugins/juju/_juju | 236 +++++++++++++++++++++++++++++++++++ plugins/juju/juju.plugin.zsh | 14 ++- 2 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 plugins/juju/_juju diff --git a/plugins/juju/_juju b/plugins/juju/_juju new file mode 100644 index 000000000..83dc505fa --- /dev/null +++ b/plugins/juju/_juju @@ -0,0 +1,236 @@ +#compdef juju +(( $+functions[compdef] )) && compdef _juju juju + +# zsh completion for juju -*- shell-script -*- + +__juju_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +__juju_help_options() +{ + local out line token cleaned desc f + local -a opts pending + typeset -U opts + + __juju_debug "[options] called with args: $*" + out=$(command juju help "$@" 2>/dev/null) + local rc=$? + __juju_debug "[options] juju help exit code: $rc, output length: ${#out}" + (( rc )) && return 1 + + while IFS= read -r line; do + if [[ "$line" =~ '^[[:space:]]{0,3}-' ]]; then + for f in "${pending[@]}"; do opts+=("$f"); done + pending=() + for token in ${(z)line}; do + cleaned="${token%%,*}" + cleaned="${cleaned%%;*}" + cleaned="${cleaned%%]*}" + cleaned="${cleaned%%)*}" + cleaned="${cleaned%%=<*}" + cleaned="${cleaned%%=*}" + cleaned="${cleaned%%<*}" + cleaned="${cleaned%%\[*}" + cleaned="${cleaned%%\(*}" + [[ "$cleaned" == --* || "$cleaned" == -[[:alnum:]] ]] || continue + [[ "$cleaned" == "-" || "$cleaned" == "--" ]] && continue + __juju_debug "[options] found flag: $cleaned" + pending+=("$cleaned") + done + elif (( ${#pending} )) && [[ -n "$line" ]]; then + desc="${line#"${line%%[![:space:]]*}"}" + desc="${desc//:/\\:}" + __juju_debug "[options] desc for ${pending[*]}: $desc" + for f in "${pending[@]}"; do opts+=("${f}:${desc}"); done + pending=() + elif [[ -z "$line" ]]; then + for f in "${pending[@]}"; do opts+=("$f"); done + pending=() + fi + done < <(printf "%s\n" "$out") + + for f in "${pending[@]}"; do opts+=("$f"); done + __juju_debug "[options] total opts: ${#opts}, first few: ${opts[1]} ${opts[2]} ${opts[3]}" + + printf "%s\n" "${opts[@]}" +} + + +__juju_help_commands() +{ + local line cmd desc out + out=$(command juju help commands 2>/dev/null) || return 1 + + while IFS= read -r line; do + # Strip leading whitespace + line="${line#"${line%%[![:space:]]*}"}" + # Only process lines starting with an alphanumeric (command names) + [[ "$line" =~ '^[[:alnum:]]' ]] || continue + # Split on the first run of 2+ spaces: left = cmd, right = description + cmd="${line%% *}" + # Validate it's a clean command token (no spaces, only alnum and dash) + [[ "$cmd" =~ '^[[:alnum:]][[:alnum:]-]*$' ]] || continue + desc="${line#"$cmd"}" + desc="${desc#"${desc%%[![:space:]]*}"}" + if [[ -n "$desc" ]]; then + printf "%s:%s\n" "$cmd" "$desc" + else + printf "%s\n" "$cmd" + fi + done <<< "$out" +} + +__juju_models() +{ + # Optional argument: controller name. If given, fetch models for that controller. + if [[ -n "$1" ]]; then + command juju models -c "$1" --format=json 2>/dev/null \ + | command jq -r '.models[]."short-name"' 2>/dev/null + else + command juju models --format=json 2>/dev/null \ + | command jq -r '.models[]."short-name"' 2>/dev/null + fi +} + +# Complete a model token that may be prefixed with "controller:" — if a colon is +# present, fetch models for that controller and offer "ctrl:model" completions. +__juju_complete_model() +{ + local current="$1" + local -a completions + + __juju_debug "[complete_model] current='${current}'" + + if [[ "$current" == *:* ]]; then + local ctrl="${current%%:*}" + local models + models=("${(@f)$(__juju_models "$ctrl")}") + completions=("${models[@]/#/${ctrl}:}") + __juju_debug "[complete_model] ctrl=${ctrl} completions=${#completions}: ${completions[*]}" + compadd -S '' -q -- "${completions[@]}" + else + local -a models ctrls + models=("${(@f)$(__juju_models)}") + ctrls=("${(@f)$(__juju_controllers)}") + __juju_debug "[complete_model] models=${#models}: ${models[*]}" + __juju_debug "[complete_model] ctrls=${#ctrls}: ${ctrls[*]}" + __juju_debug "[complete_model] calling _alternative" + _alternative \ + 'models:models:{__juju_debug "[complete_model] compadd models"; compadd "$expl[@]" -a models}' \ + 'controllers:controllers:{__juju_debug "[complete_model] compadd ctrls"; compadd "$expl[@]" -S : -q -a ctrls}' + __juju_debug "[complete_model] _alternative returned $?" + fi +} + +# Commands whose first positional argument is a model name. +_juju_model_commands=( + destroy-model + grant-model + revoke-model + switch +) + +# Flags that take a model name as their value. +_juju_model_flags=( + -m + --model +) + +__juju_controllers() +{ + command juju controllers --format=json 2>/dev/null \ + | command jq -r '.controllers | keys | .[]' 2>/dev/null +} + +# Commands whose first positional argument is a controller name. +_juju_controller_commands=( + destroy-controller + kill-controller + login + logout + unregister +) + +# Flags that take a controller name as their value. +_juju_controller_flags=( + -c + --controller +) + +_juju() +{ + __juju_debug "[_juju] curcontext: ${curcontext}" + local -a completions + + # Must be set at completion time (not just at sourcing time) so the + # completion system picks it up when rendering groups. + zstyle ':completion:*' group-name '' + zstyle ':completion::complete:juju:*' format '%B%d%b' + + __juju_debug "[_juju] words: ${words[*]}, CURRENT: $CURRENT" + + # Find the subcommand: first non-flag word typed after "juju", excluding the + # word currently being completed (words[CURRENT]). + local subcmd="" + local i + for (( i = 2; i < CURRENT; i++ )); do + if [[ "${words[i]}" != -* ]]; then + subcmd="${words[i]}" + break + fi + done + + local current="${words[CURRENT]}" + local prev="${words[CURRENT-1]}" + + __juju_debug "[_juju] subcmd: '${subcmd}', current: '${current}', prev: '${prev}'" + + # Controller name completion: flag value (e.g. juju status -c ) + if (( ${_juju_controller_flags[(I)$prev]} )); then + completions=("${(@f)$(__juju_controllers)}") + __juju_debug "[_juju] controller flag completions: ${#completions}" + (( ${#completions} )) && _describe "controller" completions && return 0 + return 1 + fi + + # Model name completion: flag value (e.g. juju status -m or -m ctrl:) + if (( ${_juju_model_flags[(I)$prev]} )); then + __juju_debug "[_juju] model flag completion, current: '${current}'" + __juju_complete_model "$current" && return 0 + return 1 + fi + + if [[ -z "$subcmd" ]]; then + # No subcommand yet — complete subcommand names. + completions=("${(@f)$(__juju_help_commands)}") + __juju_debug "[_juju] command completions count: ${#completions}" + (( ${#completions} )) && _describe "command" completions && return 0 + return 1 + fi + + # Controller name completion: positional arg (e.g. juju destroy-controller ) + if (( ${_juju_controller_commands[(I)$subcmd]} )) && [[ "$current" != -* ]]; then + completions=("${(@f)$(__juju_controllers)}") + __juju_debug "[_juju] controller command completions: ${#completions}" + (( ${#completions} )) && _describe "controller" completions && return 0 + return 1 + fi + + # Model name completion: positional arg (e.g. juju destroy-model or ctrl:) + if (( ${_juju_model_commands[(I)$subcmd]} )) && [[ "$current" != -* ]]; then + __juju_debug "[_juju] model command completion, current: '${current}'" + __juju_complete_model "$current" && return 0 + return 1 + fi + + # Flag completion for all other subcommands (also shown without leading dash) + completions=("${(@f)$(__juju_help_options "$subcmd")}") + __juju_debug "[_juju] option completions count: ${#completions}" + (( ${#completions} )) && _describe "option" completions && return 0 + return 1 +} diff --git a/plugins/juju/juju.plugin.zsh b/plugins/juju/juju.plugin.zsh index 3c159da22..dae85f3d5 100644 --- a/plugins/juju/juju.plugin.zsh +++ b/plugins/juju/juju.plugin.zsh @@ -3,12 +3,14 @@ # ---------------------------------------------------------- # # Load TAB completions -# You need juju's bash completion script installed. By default bash-completion's -# location will be used (i.e. pkg-config --variable=completionsdir bash-completion). -completion_file="$(pkg-config --variable=completionsdir bash-completion 2>/dev/null)/juju" || \ - completion_file="/usr/share/bash-completion/completions/juju" -[[ -f "$completion_file" ]] && source "$completion_file" -unset completion_file +source "${0:A:h}/_juju" + +# group-name '' enables visual separation between completion groups (e.g. models +# vs controllers in juju switch ). This is a safe global setting that +# improves completion display for all commands. +zstyle ':completion:*' group-name '' +# Show group headers only for juju completions. +zstyle ':completion::complete:juju:*' format '%B%d%b' # ---------------------------------------------------------- # # Aliases (in alphabetic order) # From c4c34b1446c1a9f6ff693d611c751e39e5660d52 Mon Sep 17 00:00:00 2001 From: Iyigun Cevik Date: Fri, 3 Apr 2026 18:02:11 +0200 Subject: [PATCH 2/2] fix(juju): plugin utilities - Respect $JUJU_DATA env var in jcontroller() and jmodel() - Fix missing `local controller` declaration in jclean() - Fix jmodel() to handle controller names with special chars and "null" yq output - Change wjst() default poll interval from 5s to 1s --- plugins/juju/juju.plugin.zsh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/plugins/juju/juju.plugin.zsh b/plugins/juju/juju.plugin.zsh index dae85f3d5..43ed76bac 100644 --- a/plugins/juju/juju.plugin.zsh +++ b/plugins/juju/juju.plugin.zsh @@ -134,6 +134,7 @@ jclean() { fi echo + local controller for controller in ${=controllers}; do timeout 2m juju destroy-controller --destroy-all-models --destroy-storage --force --no-wait -y $controller timeout 2m juju kill-controller -y -t 0 $controller 2>/dev/null @@ -167,10 +168,11 @@ jreld() { # Return Juju current controller jcontroller() { - local controller="$(awk '/current-controller/ {print $2}' ~/.local/share/juju/controllers.yaml)" - if [[ -z "$controller" ]]; then - return 1 - fi + local file=${JUJU_DATA:=~/.local/share/juju}/controllers.yaml + [[ -f "$file" ]] || return 1 + + local controller="$(awk '/current-controller/ {print $2}' "$file")" + [[ -z "$controller" ]] && return 1 echo $controller return 0 @@ -178,6 +180,9 @@ jcontroller() { # Return Juju current model jmodel() { + local file=${JUJU_DATA:=~/.local/share/juju}/models.yaml + [[ -f "$file" ]] || return 1 + local yqbin="$(whereis yq | awk '{print $2}')" if [[ -z "$yqbin" ]]; then @@ -185,9 +190,10 @@ jmodel() { return 1 fi - local model="$(yq e ".controllers.$(jcontroller).current-model" < ~/.local/share/juju/models.yaml | cut -d/ -f2)" + local controller="$(jcontroller)" + local model="$(yq e ".controllers.[\"${controller}\"].current-model" < "${file}" | cut -d/ -f2)" - if [[ -z "$model" ]]; then + if [[ -z "$model" || $model == "null" ]]; then echo "--" return 1 fi @@ -196,9 +202,10 @@ jmodel() { return 0 } -# Watch juju status, with optional interval (default: 5 sec) +# Watch juju status, with optional interval (default: 1 sec) wjst() { - local interval="${1:-5}" + command -v juju >/dev/null 2>&1 || return 1 + local interval="${1:-1}" shift $(( $# > 0 )) watch -n "$interval" --color juju status --relations --color "$@" }