github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/etc/bash_completion.d/juju-2.0 (about) 1 # juju-core.bash_completion.sh: dynamic bash completion for juju 2.0 cmdline, 2 # from parsed (and cached) juju status output. 3 # 4 # Author: JuanJo Ciarlante <jjo@canonical.com> 5 # Copyright 2016+, Canonical Ltd. 6 # License: GPLv3 7 # 8 # Includes --model and --controller handling: 9 # juju list-models --controller <TAB> 10 # juju switch <TAB> 11 # juju status --model <TAB> 12 # juju ssh --model <TAB> [... will complete with proper model's units/etc ...] 13 # 14 15 # Print (return) cache filename for "juju status" 16 _juju_2_0_juju_status_cache_fname() { 17 local model=$(_get_current_model) 18 local juju_status_file=${cache_dir}/juju-status-"${model}" 19 _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} \ 20 echo ${_juju_cmd_JUJU_2_0?} status --model "${model}" --format json 21 return $? 22 } 23 # Print (return) all machines 24 _juju_2_0_machines_from_status() { 25 local cache_fname=$(_juju_2_0_juju_status_cache_fname) 26 [ -n "${cache_fname}" ] || return 0 27 ${_juju_cmd_PYTHON?} -c ' 28 import json, sys 29 sys.stderr.close() 30 j = json.load(sys.stdin) 31 print ("\n".join(j.get("machines", {}).keys())); 32 ' < ${cache_fname} 33 } 34 35 # Print (return) all units, each optionally postfixed by $2 (eg. 'myservice/0:') 36 _juju_2_0_units_from_status() { 37 local cache_fname=$(_juju_2_0_juju_status_cache_fname) 38 [ -n "${cache_fname}" ] || return 0 39 ${_juju_cmd_PYTHON?} -c ' 40 trail = "'${2}'" 41 import json, sys 42 sys.stderr.close() 43 j = json.load(sys.stdin) 44 all_units = [] 45 for k, v in j.get("applications", {}).items(): 46 all_units.extend(v.get("units", {}).keys()) 47 print ("\n".join([unit + trail for unit in all_units])) 48 ' < ${cache_fname} 49 } 50 51 # Print (return) all applications 52 _juju_2_0_applications_from_status() { 53 local cache_fname=$(_juju_2_0_juju_status_cache_fname) 54 [ -n "${cache_fname}" ] || return 0 55 ${_juju_cmd_PYTHON?} -c ' 56 import json, sys 57 sys.stderr.close() 58 j = json.load(sys.stdin) 59 print ("\n".join(j.get("applications", {}).keys())) 60 ' < ${cache_fname} 61 } 62 63 # Print (return) all actions IDS from (cached) "juju show-action-status" output 64 _juju_2_0_action_ids_from_action_status() { 65 local model=$(_get_current_model) 66 local juju_status_file=${cache_dir}/juju-status-"${model}" 67 local cache_fname=$( 68 _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} \ 69 echo ${_juju_cmd_JUJU_2_0?} show-action-status --model "${model}" --format json 70 ) || return $? 71 [ -n "${cache_fname}" ] || return 0 72 ${_juju_cmd_PYTHON?} -c ' 73 import json, sys 74 sys.stderr.close() 75 print ("\n".join([x["id"] for x in json.load(sys.stdin).get("actions", {})])) 76 ' < ${cache_fname} 77 } 78 79 # Print (return) all storage IDs from (cached) "juju list-storage" output 80 # Caches "juju list-storage" output, print(return) cache filename 81 _juju_2_0_storage_ids_from_list_storage() { 82 local model=$(_get_current_model) 83 local juju_status_file=${cache_dir}/juju-status-"${model}" 84 local cache_fname=$( 85 _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} \ 86 echo ${_juju_cmd_JUJU_2_0?} list-storage --model "${model}" --format json 87 ) || return $? 88 [ -n "${cache_fname}" ] || return 0 89 ${_juju_cmd_PYTHON?} -c ' 90 import json, sys 91 sys.stderr.close() 92 print ("\n".join(json.load(sys.stdin).get("storage", {}).keys())) 93 ' < ${cache_fname} 94 } 95 96 # Print (return) both applications and units, currently used for juju status completion 97 _juju_2_0_applications_and_units_from_status() { 98 _juju_2_0_applications_from_status 99 _juju_2_0_units_from_status 100 } 101 102 # Print (return) both units and machines 103 _juju_2_0_units_and_machines_from_status() { 104 _juju_2_0_units_from_status 105 _juju_2_0_machines_from_status 106 } 107 108 # Print (return) all juju commands 109 _juju_2_0_list_commands() { 110 ${_juju_cmd_JUJU_2_0?} help commands 2>/dev/null | awk '{print $1}' 111 } 112 113 # Print (return) flags for juju action 114 _juju_2_0_flags_for() { 115 [ -n "${1}" ] || return 0 116 ${_juju_cmd_JUJU_2_0?} help ${1} 2>/dev/null |egrep -o -- '(^|-)-[a-z-]+'|sort -u 117 } 118 119 _juju_2_0_list_controllers_from_stdin() { 120 sed '1s/^$/{}/' |\ 121 ${_juju_cmd_PYTHON?} -c ' 122 import json, sys 123 sys.stderr.close() 124 print ("\n".join( 125 json.load(sys.stdin).get("controllers", {}).keys()) 126 )' 127 } 128 _juju_2_0_list_models_from_stdin() { 129 sed '1s/^$/{}/' |\ 130 ${_juju_cmd_PYTHON?} -c ' 131 import json, sys 132 sys.stderr.close() 133 print ("\n".join( 134 ["'$1'" + m["name"] for m in json.load(sys.stdin).get("models", {})] 135 ))' 136 } 137 # List all controllers 138 _juju_2_0_list_controllers_noflags() { 139 _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} cat \ 140 ${_juju_cmd_JUJU_2_0?} list-controllers --format json | \ 141 _juju_2_0_list_controllers_from_stdin 142 } 143 # Print: 144 # - list of controllers as: <controller>:<current_model> 145 # - list of models under current controller 146 _juju_2_0_list_controllers_models_noflags() { 147 # derive cur_controller from fully specified current model: CONTROLLER:MODEL 148 local cur_controller=$(_get_current_model) 149 cur_controller=${cur_controller%%:*} 150 151 # List all controller:models 152 local controllers=$(_juju_2_0_list_controllers_noflags 2>/dev/null) 153 [ -n "${controllers}" ] || { echo "ERROR: no valid controller found (current: ${cur_controller})" >&2; return 0 ;} 154 local controller= 155 for controller in ${controllers};do 156 _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} cat \ 157 ${_juju_cmd_JUJU_2_0?} list-models --controller ${controller} --format json |\ 158 _juju_2_0_list_models_from_stdin "${controller}:" 159 # early break, specially if user hit Ctrl-C 160 [ $? -eq 0 ] || return 1 161 done 162 163 # List all models under current controller 164 _juju_2_0_cache_cmd ${_juju_2_0_cache_TTL} cat \ 165 ${_juju_cmd_JUJU_2_0?} list-models --controller ${cur_controller} --format json |\ 166 _juju_2_0_list_models_from_stdin 167 } 168 169 # Print (return) guessed completion function for cmd. 170 # Guessing is done by parsing 1st line of juju help <cmd>, 171 # see case switch below. 172 _juju_2_0_completion_func_for_cmd() { 173 local action=${1} cword=${2} 174 # if cword==1 or action==help, use _juju_2_0_list_commands 175 if [ "${cword}" -eq 1 -o "${action}" = help ]; then 176 echo _juju_2_0_list_commands 177 return 0 178 fi 179 # normally prev_word is just that ... 180 local prev_word=${COMP_WORDS[cword-1]} 181 # special case for eg: 182 # juju ssh -m myctrl:<TAB> => COMP_WORDS[cword] is ':' 183 # juju ssh -m myctrl:f<TAB> => COMP_WORDS[cword-1] is ':' 184 [[ ${COMP_WORDS[cword]} == : ]] && prev_word=${COMP_WORDS[cword-2]} 185 [[ ${COMP_WORDS[cword-1]} == : ]] && prev_word=${COMP_WORDS[cword-3]} 186 case "${prev_word}" in 187 --controller|-c) 188 echo _juju_2_0_list_controllers_noflags; return 0;; 189 --model|-m) 190 echo _juju_2_0_list_controllers_models_noflags; return 0;; 191 --application) 192 echo _juju_2_0_applications_from_status; return 0;; 193 --unit) 194 echo _juju_2_0_units_from_status; return 0;; 195 --machine) 196 echo _juju_2_0_machines_from_status; return 0;; 197 esac 198 # parse 1st line of juju help <cmd>, to guess the completion function 199 # order below is important (more specific matches 1st) 200 case $(${_juju_cmd_JUJU_2_0?} help ${action} 2>/dev/null| head -1) in 201 # special case for ssh, scp: 202 *bootstrap*) 203 echo true ;; # help ok, existing command, no more expansion 204 *juju?ssh*|*juju?scp*) 205 echo _juju_2_0_units_and_machines_from_status;; 206 *\<unit*) 207 echo _juju_2_0_units_from_status;; 208 *\<service*) 209 echo _juju_2_0_applications_from_status;; 210 *\<application*) 211 echo _juju_2_0_applications_from_status;; 212 *\<machine*) 213 echo _juju_2_0_machines_from_status;; 214 *\<action*) 215 echo _juju_2_0_action_ids_from_action_status;; 216 *show-storage*) 217 echo _juju_2_0_storage_ids_from_list_storage;; 218 *pattern*|*application-or-unit*) 219 echo _juju_2_0_applications_and_units_from_status;; # e.g. status 220 *\<controller*:*\<model*|*--model*) 221 echo _juju_2_0_list_controllers_models_noflags;; 222 *\<controller?name*) 223 echo _juju_2_0_list_controllers_noflags;; 224 ?*) 225 echo true ;; # help ok, existing command, no more expansion 226 *) 227 echo false;; # failed, not a command 228 esac 229 } 230 231 # Print (return) current model as found in the cmdline --model <...> 232 # else default from $JUJU_MODEL or $(juju switch) 233 _get_current_model() { 234 set +e 235 local model="" 236 if [[ ${COMP_LINE} =~ .*(--model|-m)\ ([^ ]+)\ (: [^ ]+\ )?.* ]];then 237 model="${BASH_REMATCH[2]}${BASH_REMATCH[3]}" 238 model="${model// /}" 239 fi 240 if [ -z "${model}" ];then 241 model=${JUJU_MODEL:-$(${_juju_cmd_JUJU_2_0?} switch)} 242 fi 243 echo "$model" 244 } 245 # Generic command cache function: caches cmdline output, called as: 246 # _juju_2_0_cache_cmd TTL ACTION cmd args ... 247 # TTL: cache expiration in mins 248 # ACTION: what to do with cached filename: 249 # - cat (return content) 250 # - echo (return cache filename, think "pointer") 251 _juju_2_0_cache_cmd() { 252 local cache_ttl="${1:?missing TTL}" # TTL in mins 253 local ret_action=${2:?missing what to return: "echo" or "cat"} 254 shift 2 255 local cmd="${*:?}" 256 local cache_dir=$HOME/.cache/juju 257 local cache_file=${cmd} 258 # replace / by _ 259 cache_file=${cache_file//\//_} 260 # replace space by __ 261 cache_file=${cache_file// /__} 262 # under cache_dir 263 cache_file=${cache_dir}/${cache_file} 264 local cmd_pid= 265 test -d ${cache_dir} || install -d ${cache_dir} -m 700 266 # older than TTL => remove 267 find "${cache_file}" -mmin +${cache_ttl} -a -size +64c -delete 2> /dev/null 268 # older than TTL/2 or missing => refresh in background 269 local cache_refresh=$((${cache_ttl}/2)) 270 if [[ -z $(find "${cache_file}" -mmin -${cache_refresh} -a -size +64c 2> /dev/null) ]]; then 271 # ... create it in background (locking the .tmp to avoid many runs against same cache file 272 coproc flock -xn "${cache_file}".tmp \ 273 sh -c "$cmd > ${cache_file}.tmp && mv -f ${cache_file}.tmp ${cache_file}; rm -f ${cache_file}.tmp" 274 fi 275 # if missing => wait 276 [ ! -s "${cache_file}" -a -n "${COPROC[0]}" ] && read -u ${COPROC[0]} 277 # if still missing => fail 278 [ ! -s "${cache_file}" ] && echo "" && return 1 279 # use it: 280 "${ret_action}" "${cache_file}" 281 } 282 283 # Main completion function wrap: 284 # calls passed completion function, also adding flags for cmd 285 _juju_2_0_complete_with_func() { 286 local action="${1}" func=${2?} 287 # scp is special, as we want ':' appended to unit names, 288 # and filename completion also. 289 local postfix_str= compgen_xtra= 290 if [[ ${action} == scp ]]; then 291 postfix_str=':' 292 compgen_xtra='-A file' 293 compopt -o nospace 294 fi 295 296 # build COMPREPLY from passed function stdout, and _juju_2_0_flags_for $action 297 # don't clutter with cmd flags for functions named *_noflags 298 local flags 299 case "${func}" in 300 *_noflags) flags="";; 301 *) flags=$(_juju_2_0_flags_for "${action}");; 302 esac 303 304 #echo "** comp=$(set|egrep ^COMP) ** func=$func **" >&2 305 306 # properly handle ':' 307 # see http://stackoverflow.com/questions/10528695/how-to-reset-comp-wordbreaks-without-effecting-other-completion-script 308 local cur="${COMP_WORDS[COMP_CWORD]}" 309 _get_comp_words_by_ref -n : cur 310 COMPREPLY=( $( compgen ${compgen_xtra} -W "$(${func} ${postfix_str}) $flags" -- ${cur} )) 311 __ltrim_colon_completions "$cur" 312 313 if [[ ${action} == scp ]]; then 314 compopt +o nospace 315 fi 316 317 318 return 0 319 } 320 321 # Not used here, available to the user for quick cache removal 322 _juju_2_0_rm_completion_cache() { 323 rm -fv $HOME/.cache/juju/juju-status-* 324 } 325 326 # main completion function entry point 327 _juju_complete_2_0() { 328 local action parsing_func 329 action="${COMP_WORDS[1]}" 330 COMPREPLY=() 331 parsing_func=$(_juju_2_0_completion_func_for_cmd "${action}" ${COMP_CWORD}) 332 test -n "${parsing_func}" && \ 333 _juju_2_0_complete_with_func "${action}" "${parsing_func}" 334 return $? 335 } 336 # _juju_2_0_cache_TTL [mins] 337 export _juju_2_0_cache_TTL=2 338 # All above completion is juju-2.0 specific, uses $_juju_cmd_JUJU_2_0 339 export _juju_cmd_JUJU_2_0=/usr/bin/juju-2.0 340 # Select python3, else python2 341 export _juju_cmd_PYTHON 342 for _juju_cmd_PYTHON in /usr/bin/python{3,2};do 343 [ -x ${_juju_cmd_PYTHON?} ] && break 344 done 345 # Add juju-2.0 completion 346 complete -F _juju_complete_2_0 juju-2.0 347 348 # Also hook "juju" (without version) to make this file "self" sufficient. 349 # 350 # Note that in a normal install will be overridden later by 351 # /etc/bash_completion.d/juju-version which does runtime detection 352 # of 1.x or 2.0 autocompletion. 353 complete -F _juju_complete_2_0 juju 354 355 # vim: ai et sw=2 ts=2