# juju-core.bash_completion.sh: dynamic bash completion for juju 2.0 cmdline, # from parsed (and cached) juju status output. # # Author: JuanJo Ciarlante # Copyright 2016+, Canonical Ltd. # License: GPLv3 # # Includes --model and --controller handling: # juju list-models --controller # juju switch # juju status --model # juju ssh --model [... will complete with proper model's units/etc ...] # # Print (return) cache filename for "juju status" _juju_2_0_juju_status_cache_fname() { local model=$(_get_current_model) local juju_status_file=${cache_dir}/juju-status-"${model}" _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} \ echo ${_juju_cmd_JUJU_2_0?} status --model "${model}" --format json return $? } # Print (return) all machines _juju_2_0_machines_from_status() { local cache_fname=$(_juju_2_0_juju_status_cache_fname) [ -n "${cache_fname}" ] || return 0 ${_juju_cmd_PYTHON?} -c ' import json, sys sys.stderr.close() j = json.load(sys.stdin) print ("\n".join(j.get("machines", {}).keys())); ' < ${cache_fname} } # Print (return) all units, each optionally postfixed by $2 (eg. 'myservice/0:') _juju_2_0_units_from_status() { local cache_fname=$(_juju_2_0_juju_status_cache_fname) [ -n "${cache_fname}" ] || return 0 ${_juju_cmd_PYTHON?} -c ' trail = "'${2}'" import json, sys sys.stderr.close() j = json.load(sys.stdin) all_units = [] for k, v in j.get("applications", {}).items(): all_units.extend(v.get("units", {}).keys()) print ("\n".join([unit + trail for unit in all_units])) ' < ${cache_fname} } # Print (return) all applications _juju_2_0_applications_from_status() { local cache_fname=$(_juju_2_0_juju_status_cache_fname) [ -n "${cache_fname}" ] || return 0 ${_juju_cmd_PYTHON?} -c ' import json, sys sys.stderr.close() j = json.load(sys.stdin) print ("\n".join(j.get("applications", {}).keys())) ' < ${cache_fname} } # Print (return) all actions IDS from (cached) "juju show-action-status" output _juju_2_0_action_ids_from_action_status() { local model=$(_get_current_model) local juju_status_file=${cache_dir}/juju-status-"${model}" local cache_fname=$( _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} \ echo ${_juju_cmd_JUJU_2_0?} show-action-status --model "${model}" --format json ) || return $? [ -n "${cache_fname}" ] || return 0 ${_juju_cmd_PYTHON?} -c ' import json, sys sys.stderr.close() print ("\n".join([x["id"] for x in json.load(sys.stdin).get("actions", {})])) ' < ${cache_fname} } # Print (return) all storage IDs from (cached) "juju list-storage" output # Caches "juju list-storage" output, print(return) cache filename _juju_2_0_storage_ids_from_list_storage() { local model=$(_get_current_model) local juju_status_file=${cache_dir}/juju-status-"${model}" local cache_fname=$( _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} \ echo ${_juju_cmd_JUJU_2_0?} list-storage --model "${model}" --format json ) || return $? [ -n "${cache_fname}" ] || return 0 ${_juju_cmd_PYTHON?} -c ' import json, sys sys.stderr.close() print ("\n".join(json.load(sys.stdin).get("storage", {}).keys())) ' < ${cache_fname} } # Print (return) both applications and units, currently used for juju status completion _juju_2_0_applications_and_units_from_status() { _juju_2_0_applications_from_status _juju_2_0_units_from_status } # Print (return) both units and machines _juju_2_0_units_and_machines_from_status() { _juju_2_0_units_from_status _juju_2_0_machines_from_status } # Print (return) all juju commands _juju_2_0_list_commands() { ${_juju_cmd_JUJU_2_0?} help commands 2>/dev/null | awk '{print $1}' } # Print (return) flags for juju action _juju_2_0_flags_for() { [ -n "${1}" ] || return 0 ${_juju_cmd_JUJU_2_0?} help ${1} 2>/dev/null |egrep -o -- '(^|-)-[a-z-]+'|sort -u } _juju_2_0_list_controllers_from_stdin() { sed '1s/^$/{}/' |\ ${_juju_cmd_PYTHON?} -c ' import json, sys sys.stderr.close() print ("\n".join( json.load(sys.stdin).get("controllers", {}).keys()) )' } _juju_2_0_list_models_from_stdin() { sed '1s/^$/{}/' |\ ${_juju_cmd_PYTHON?} -c ' import json, sys sys.stderr.close() print ("\n".join( ["'$1'" + m["name"] for m in json.load(sys.stdin).get("models", {})] ))' } # List all controllers _juju_2_0_list_controllers_noflags() { _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} cat \ ${_juju_cmd_JUJU_2_0?} list-controllers --format json | \ _juju_2_0_list_controllers_from_stdin } # Print: # - list of controllers as: : # - list of models under current controller _juju_2_0_list_controllers_models_noflags() { # derive cur_controller from fully specified current model: CONTROLLER:MODEL local cur_controller=$(_get_current_model) cur_controller=${cur_controller%%:*} # List all controller:models local controllers=$(_juju_2_0_list_controllers_noflags 2>/dev/null) [ -n "${controllers}" ] || { echo "ERROR: no valid controller found (current: ${cur_controller})" >&2; return 0 ;} local controller= for controller in ${controllers};do _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} cat \ ${_juju_cmd_JUJU_2_0?} list-models --controller ${controller} --format json |\ _juju_2_0_list_models_from_stdin "${controller}:" # early break, specially if user hit Ctrl-C [ $? -eq 0 ] || return 1 done # List all models under current controller _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} cat \ ${_juju_cmd_JUJU_2_0?} list-models --controller ${cur_controller} --format json |\ _juju_2_0_list_models_from_stdin } # Print (return) guessed completion function for cmd. # Guessing is done by parsing 1st line of juju help , # see case switch below. _juju_2_0_completion_func_for_cmd() { local action=${1} cword=${2} # if cword==1 or action==help, use _juju_2_0_list_commands if [ "${cword}" -eq 1 -o "${action}" = help ]; then echo _juju_2_0_list_commands return 0 fi # normally prev_word is just that ... local prev_word=${COMP_WORDS[cword-1]} # special case for eg: # juju ssh -m myctrl: => COMP_WORDS[cword] is ':' # juju ssh -m myctrl:f => COMP_WORDS[cword-1] is ':' [[ ${COMP_WORDS[cword]} == : ]] && prev_word=${COMP_WORDS[cword-2]} [[ ${COMP_WORDS[cword-1]} == : ]] && prev_word=${COMP_WORDS[cword-3]} case "${prev_word}" in --controller|-c) echo _juju_2_0_list_controllers_noflags; return 0;; --model|-m) echo _juju_2_0_list_controllers_models_noflags; return 0;; --application) echo _juju_2_0_applications_from_status; return 0;; --unit) echo _juju_2_0_units_from_status; return 0;; --machine) echo _juju_2_0_machines_from_status; return 0;; esac # parse 1st line of juju help , to guess the completion function # order below is important (more specific matches 1st) case $(${_juju_cmd_JUJU_2_0?} help ${action} 2>/dev/null| head -1) in # special case for ssh, scp: *bootstrap*) echo true ;; # help ok, existing command, no more expansion *juju?ssh*|*juju?scp*) echo _juju_2_0_units_and_machines_from_status;; *\ # else default from $JUJU_MODEL or $(juju switch) _get_current_model() { set +e local model="" if [[ ${COMP_LINE} =~ .*(--model|-m)\ ([^ ]+)\ (: [^ ]+\ )?.* ]];then model="${BASH_REMATCH[2]}${BASH_REMATCH[3]}" model="${model// /}" fi if [ -z "${model}" ];then model=${JUJU_MODEL:-$(${_juju_cmd_JUJU_2_0?} switch)} fi echo "$model" } # Generic command cache function: caches cmdline output, called as: # _juju_2_0_cache_cmd TTL ACTION cmd args ... # TTL: cache expiration in mins # ACTION: what to do with cached filename: # - cat (return content) # - echo (return cache filename, think "pointer") _juju_2_0_cache_cmd() { local cache_ttl="${1:?missing TTL}" # TTL in mins local ret_action=${2:?missing what to return: "echo" or "cat"} shift 2 local cmd="${*:?}" local cache_dir=$HOME/.cache/juju local cache_file=${cmd} # replace / by _ cache_file=${cache_file//\//_} # replace space by __ cache_file=${cache_file// /__} # under cache_dir cache_file=${cache_dir}/${cache_file} local cmd_pid= test -d ${cache_dir} || install -d ${cache_dir} -m 700 # older than TTL => remove find "${cache_file}" -mmin +${cache_ttl} -a -size +64c -delete 2> /dev/null # older than TTL/2 or missing => refresh in background local cache_refresh=$((${cache_ttl}/2)) if [[ -z $(find "${cache_file}" -mmin -${cache_refresh} -a -size +64c 2> /dev/null) ]]; then # ... create it in background (locking the .tmp to avoid many runs against same cache file coproc flock -xn "${cache_file}".tmp \ sh -c "$cmd > ${cache_file}.tmp && mv -f ${cache_file}.tmp ${cache_file}; rm -f ${cache_file}.tmp" fi # if missing => wait [ ! -s "${cache_file}" -a -n "${COPROC[0]}" ] && read -u ${COPROC[0]} # if still missing => fail [ ! -s "${cache_file}" ] && echo "" && return 1 # use it: "${ret_action}" "${cache_file}" } # Main completion function wrap: # calls passed completion function, also adding flags for cmd _juju_2_0_complete_with_func() { local action="${1}" func=${2?} # scp is special, as we want ':' appended to unit names, # and filename completion also. local postfix_str= compgen_xtra= if [[ ${action} == scp ]]; then postfix_str=':' compgen_xtra='-A file' compopt -o nospace fi # build COMPREPLY from passed function stdout, and _juju_2_0_flags_for $action # don't clutter with cmd flags for functions named *_noflags local flags case "${func}" in *_noflags) flags="";; *) flags=$(_juju_2_0_flags_for "${action}");; esac #echo "** comp=$(set|egrep ^COMP) ** func=$func **" >&2 # properly handle ':' # see http://stackoverflow.com/questions/10528695/how-to-reset-comp-wordbreaks-without-effecting-other-completion-script local cur="${COMP_WORDS[COMP_CWORD]}" _get_comp_words_by_ref -n : cur COMPREPLY=( $( compgen ${compgen_xtra} -W "$(${func} ${postfix_str}) $flags" -- ${cur} )) __ltrim_colon_completions "$cur" if [[ ${action} == scp ]]; then compopt +o nospace fi return 0 } # Not used here, available to the user for quick cache removal _juju_2_0_rm_completion_cache() { rm -fv $HOME/.cache/juju/juju-status-* } # main completion function entry point _juju_complete_2_0() { local action parsing_func action="${COMP_WORDS[1]}" COMPREPLY=() parsing_func=$(_juju_2_0_completion_func_for_cmd "${action}" ${COMP_CWORD}) test -n "${parsing_func}" && \ _juju_2_0_complete_with_func "${action}" "${parsing_func}" return $? } # _juju_2_0_cache_TTL [mins] export _juju_2_0_cache_TTL=2 # All above completion is juju-2.0 specific, uses $_juju_cmd_JUJU_2_0 export _juju_cmd_JUJU_2_0=/usr/bin/juju-2.0 # Select python3, else python2 export _juju_cmd_PYTHON for _juju_cmd_PYTHON in /usr/bin/python{3,2};do [ -x ${_juju_cmd_PYTHON?} ] && break done # Add juju-2.0 completion complete -F _juju_complete_2_0 juju-2.0 # Also hook "juju" (without version) to make this file "self" sufficient. # # Note that in a normal install will be overridden later by # /etc/bash_completion.d/juju-version which does runtime detection # of 1.x or 2.0 autocompletion. complete -F _juju_complete_2_0 juju # vim: ai et sw=2 ts=2