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