Merge pull request #1708 from seefood/ira/fix-knife-load

Fix knife completion load time (and pre-commit compliance)
pull/1732/head
Noah Gorny 2020-12-09 08:44:33 +02:00 committed by GitHub
commit 9b837b4f59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 125 additions and 130 deletions

View File

@ -31,5 +31,6 @@ themes/powerline
# completions # completions
# #
completion/available/cargo.completion.bash completion/available/cargo.completion.bash
completion/available/knife.completion.bash
completion/available/pipx.completion.bash completion/available/pipx.completion.bash
completion/available/rustup.completion.bash completion/available/rustup.completion.bash

View File

@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Published originally as public domain code at https://github.com/wk8/knife-bash-autocomplete
############## ##############
### CONFIG ### ### CONFIG ###
############## ##############
@ -19,7 +21,7 @@ _KNIFE_AUTOCOMPLETE_MAX_CACHE_AGE=86400
### init ### init
_KAC_CACHE_TMP_DIR="$_KNIFE_AUTOCOMPLETE_CACHE_DIR/tmp" _KAC_CACHE_TMP_DIR="$_KNIFE_AUTOCOMPLETE_CACHE_DIR/tmp"
# make sure the cache dir exists # make sure the cache dir exists
mkdir -p $_KAC_CACHE_TMP_DIR mkdir -p "$_KAC_CACHE_TMP_DIR"
############################## ##############################
### Cache helper functions ### ### Cache helper functions ###
@ -31,177 +33,169 @@ stat -c %Y /dev/null > /dev/null 2>&1 && _KAC_STAT_COMMAND="stat -c %Y" || _KAC_
# returns 0 iff the file whose path is given as 1st argument # returns 0 iff the file whose path is given as 1st argument
# exists and has last been modified in the last $2 seconds # exists and has last been modified in the last $2 seconds
# returns 1 otherwise # returns 1 otherwise
_KAC_is_file_newer_than() _KAC_is_file_newer_than() {
{ [ -f "$1" ] || return 1
[ -f "$1" ] || return 1 [ $(($(date +%s) - $($_KAC_STAT_COMMAND "$1"))) -gt "$2" ] && return 1 || return 0
[ $(( $(date +%s) - $($_KAC_STAT_COMMAND "$1") )) -gt $2 ] && return 1 || return 0
} }
# helper function for _KAC_get_and_regen_cache, see doc below # helper function for _KAC_get_and_regen_cache, see doc below
_KAC_regen_cache() _KAC_regen_cache() {
{ local CACHE_NAME=$1
local CACHE_NAME=$1 local CACHE_PATH="$_KNIFE_AUTOCOMPLETE_CACHE_DIR/$CACHE_NAME"
local CACHE_PATH="$_KNIFE_AUTOCOMPLETE_CACHE_DIR/$CACHE_NAME" local TMP_FILE=$(mktemp "$_KAC_CACHE_TMP_DIR/$CACHE_NAME.XXXX")
local TMP_FILE=$(mktemp "$_KAC_CACHE_TMP_DIR/$CACHE_NAME.XXXX") shift 1
shift 1 # discard the temp file if it's empty AND the previous command didn't exit successfully, but still mark the cache as updated
"$@" > $TMP_FILE 2> /dev/null if ! "$@" > "$TMP_FILE" 2> /dev/null; then
# discard the temp file if it's empty AND the previous command didn't exit successfully, but still mark the cache as updated [[ $(wc -l "$TMP_FILE") == 0 ]] && rm -f "$TMP_FILE" && touch "$CACHE_PATH" && return 1
[[ $? != 0 ]] && [[ $(cat $TMP_FILE | wc -l) == 0 ]] && rm -f $TMP_FILE && touch $CACHE_PATH && return 1 \ else
|| mv -f $TMP_FILE $CACHE_PATH mv -f "$TMP_FILE" "$CACHE_PATH"
fi
} }
# cached files can't have spaces in their names # cached files can't have spaces in their names
_KAC_get_cache_name_from_command() _KAC_get_cache_name_from_command() {
{ echo "${@/ /_SPACE_}"
echo "$@" | sed 's/ /_SPACE_/g'
} }
# the reverse operation from the function above # the reverse operation from the function above
_KAC_get_command_from_cache_name() _KAC_get_command_from_cache_name() {
{ echo "${@/_SPACE_/ }"
echo "$@" | sed 's/_SPACE_/ /g'
} }
# given a command as argument, it fetches the cache for that command if it can find it # given a command as argument, it fetches the cache for that command if it can find it
# otherwise it waits for the cache to be generated # otherwise it waits for the cache to be generated
# in either case, it regenerates the cache, and sets the _KAC_CACHE_PATH env variable # in either case, it regenerates the cache, and sets the _KAC_CACHE_PATH env variable
# for obvious reason, do NOT call that in a sub-shell (in particular, no piping) # for obvious reason, do NOT call that in a sub-shell (in particular, no piping)
_KAC_get_and_regen_cache() _KAC_get_and_regen_cache() {
{ # the cache name can't have space in it
# the cache name can't have space in it local CACHE_NAME=$(_KAC_get_cache_name_from_command "$@")
local CACHE_NAME=$(_KAC_get_cache_name_from_command "$@") local REGEN_CMD="_KAC_regen_cache $CACHE_NAME $*"
local REGEN_CMD="_KAC_regen_cache $CACHE_NAME $@" _KAC_CACHE_PATH="$_KNIFE_AUTOCOMPLETE_CACHE_DIR/$CACHE_NAME"
_KAC_CACHE_PATH="$_KNIFE_AUTOCOMPLETE_CACHE_DIR/$CACHE_NAME" # no need to wait for the regen if the file already exists
# no need to wait for the regen if the file already exists if [[ -f "$_KAC_CACHE_PATH" ]]; then
[ -f $_KAC_CACHE_PATH ] && ($REGEN_CMD &) || $REGEN_CMD ($REGEN_CMD &)
else
$REGEN_CMD
fi
} }
# performs two things: first, deletes all obsolete temp files # performs two things: first, deletes all obsolete temp files
# then refreshes stale caches that haven't been called in a long time # then refreshes stale caches that haven't been called in a long time
_KAC_clean_cache() _KAC_clean_cache() {
{ local FILE CMD
local FILE CMD # delete all obsolete temp files, could be lingering there for any kind of crash in the caching process
# delete all obsolete temp files, could be lingering there for any kind of crash in the caching process for FILE in "$_KAC_CACHE_TMP_DIR"/*; do
for FILE in $(ls $_KAC_CACHE_TMP_DIR) _KAC_is_file_newer_than "$FILE" "$_KNIFE_AUTOCOMPLETE_MAX_CACHE_AGE" || rm -f "$FILE"
do done
_KAC_is_file_newer_than $FILE $_KNIFE_AUTOCOMPLETE_MAX_CACHE_AGE || rm -f $FILE # refresh really stale caches
done find "$_KNIFE_AUTOCOMPLETE_CACHE_DIR" -maxdepth 1 -type f -not -name '.*' \
# refresh really stale caches | while read -r FILE; do
for FILE in $(find $_KNIFE_AUTOCOMPLETE_CACHE_DIR -maxdepth 1 -type f -not -name '.*') _KAC_is_file_newer_than "$FILE" "$_KNIFE_AUTOCOMPLETE_MAX_CACHE_AGE" && continue
do # first let's get the original command
_KAC_is_file_newer_than $FILE $_KNIFE_AUTOCOMPLETE_MAX_CACHE_AGE && continue CMD=$(_KAC_get_command_from_cache_name "$(basename "$FILE")")
# first let's get the original command # then regen the cache
CMD=$(_KAC_get_command_from_cache_name $(basename "$FILE")) _KAC_get_and_regen_cache "$CMD" > /dev/null
# then regen the cache done
_KAC_get_and_regen_cache "$CMD" > /dev/null
done
} }
# perform a cache cleaning when loading this file # perform a cache cleaning when loading this file
_KAC_clean_cache # On big systems this could baloon up to a 30 second run or more, so not enabling by default.
[[ "${KNIFE_CACHE_CLEAN}" ]] && _KAC_clean_cache
##################################### #####################################
### End of cache helper functions ### ### End of cache helper functions ###
##################################### #####################################
# returns all the possible knife sub-commands # returns all the possible knife sub-commands
_KAC_knife_commands() _KAC_knife_commands() {
{ knife --help | grep -E "^knife" | sed -E 's/ \(options\)//g'
knife --help | grep -E "^knife" | sed -E 's/ \(options\)//g'
} }
# rebuilds the knife base command currently being completed, and assigns it to $_KAC_CURRENT_COMMAND # rebuilds the knife base command currently being completed, and assigns it to $_KAC_CURRENT_COMMAND
# additionnally, returns 1 iff the current base command is not complete, 0 otherwise # additionnally, returns 1 iff the current base command is not complete, 0 otherwise
# also sets $_KAC_CURRENT_COMMAND_NB_WORDS if the base command is complete # also sets $_KAC_CURRENT_COMMAND_NB_WORDS if the base command is complete
_KAC_get_current_base_command() _KAC_get_current_base_command() {
{ local PREVIOUS="knife"
local PREVIOUS="knife" local I=1
local I=1 local CURRENT
local CURRENT while [ $I -le "$COMP_CWORD" ]; do
while [ $I -le $COMP_CWORD ] # command words are all lower-case
do echo "${COMP_WORDS[$I]}" | grep -E "^[a-z]+$" > /dev/null || break
# command words are all lower-case CURRENT="$PREVIOUS ${COMP_WORDS[$I]}"
echo ${COMP_WORDS[$I]} | grep -E "^[a-z]+$" > /dev/null || break grep -E "^$CURRENT" "$_KAC_CACHE_PATH" > /dev/null || break
CURRENT="$PREVIOUS ${COMP_WORDS[$I]}" PREVIOUS=$CURRENT
cat $_KAC_CACHE_PATH | grep -E "^$CURRENT" > /dev/null || break I=$((I + 1))
PREVIOUS=$CURRENT done
I=$(( $I + 1)) _KAC_CURRENT_COMMAND=$PREVIOUS
done [ $I -le "$COMP_CWORD" ] && _KAC_CURRENT_COMMAND_NB_WORDS=$I
_KAC_CURRENT_COMMAND=$PREVIOUS
[ $I -le $COMP_CWORD ] && _KAC_CURRENT_COMMAND_NB_WORDS=$I
} }
# searches the position of the currently completed argument in the current base command # searches the position of the currently completed argument in the current base command
# (i.e. handles "plural" arguments such as knife cookbook upload cookbook1 cookbook2 and so on...) # (i.e. handles "plural" arguments such as knife cookbook upload cookbook1 cookbook2 and so on...)
# assumes the current base command is complete # assumes the current base command is complete
_KAC_get_current_arg_position() _KAC_get_current_arg_position() {
{ local CURRENT_ARG_POS=$((_KAC_CURRENT_COMMAND_NB_WORDS + 1))
local CURRENT_ARG_POS=$(( $_KAC_CURRENT_COMMAND_NB_WORDS + 1 )) local COMPLETE_COMMAND=$(grep -E "^$_KAC_CURRENT_COMMAND" "$_KAC_CACHE_PATH")
local COMPLETE_COMMAND=$(cat $_KAC_CACHE_PATH | grep -E "^$_KAC_CURRENT_COMMAND") local CURRENT_ARG
local CURRENT_ARG while [ "$CURRENT_ARG_POS" -le "$COMP_CWORD" ]; do
while [ $CURRENT_ARG_POS -le $COMP_CWORD ] CURRENT_ARG=$(echo "$COMPLETE_COMMAND" | cut -d ' ' -f "$CURRENT_ARG_POS")
do # we break if the current arg is a "plural" arg
CURRENT_ARG=$(echo $COMPLETE_COMMAND | cut -d ' ' -f $CURRENT_ARG_POS) echo "$CURRENT_ARG" | grep -E "^\\[[^]]+(\\.\\.\\.\\]|$)" > /dev/null && break
# we break if the current arg is a "plural" arg CURRENT_ARG_POS=$((CURRENT_ARG_POS + 1))
echo $CURRENT_ARG | grep -E "^\\[[^]]+(\\.\\.\\.\\]|$)" > /dev/null && break done
CURRENT_ARG_POS=$(( $CURRENT_ARG_POS + 1 )) echo "$CURRENT_ARG_POS"
done
echo $CURRENT_ARG_POS
} }
# the actual auto-complete function # the actual auto-complete function
_knife() _knife() {
{ _KAC_get_and_regen_cache _KAC_knife_commands
_KAC_get_and_regen_cache _KAC_knife_commands local RAW_LIST ITEM REGEN_CMD ARG_POSITION
local RAW_LIST ITEM REGEN_CMD ARG_POSITION COMREPLY=()
COMREPLY=() # get correct command & arg pos
# get correct command & arg pos _KAC_get_current_base_command && ARG_POSITION=$(_KAC_get_current_arg_position) || ARG_POSITION=$((COMP_CWORD + 1))
_KAC_get_current_base_command && ARG_POSITION=$(_KAC_get_current_arg_position) || ARG_POSITION=$(( $COMP_CWORD + 1 )) RAW_LIST=$(grep -E "^$_KAC_CURRENT_COMMAND" "$_KAC_CACHE_PATH" | cut -d ' ' -f $ARG_POSITION | uniq)
RAW_LIST=$(cat $_KAC_CACHE_PATH | grep -E "^$_KAC_CURRENT_COMMAND" | cut -d ' ' -f $ARG_POSITION | uniq)
# we need to process that raw list a bit, most notably for placeholders # we need to process that raw list a bit, most notably for placeholders
# NOTE: I chose to explicitely fetch & cache _certain_ informations for the server (cookbooks & node names, etc) # NOTE: I chose to explicitely fetch & cache _certain_ informations for the server (cookbooks & node names, etc)
# as opposed to a generic approach by trying to find a 'list' knife command corresponding to the # as opposed to a generic approach by trying to find a 'list' knife command corresponding to the
# current base command - that might limit my script in some situation, but that way I'm sure it caches only # current base command - that might limit my script in some situation, but that way I'm sure it caches only
# not-sensitive stuff (a generic approach could be pretty bad e.g. with the knife-rackspace plugin) # not-sensitive stuff (a generic approach could be pretty bad e.g. with the knife-rackspace plugin)
LIST="" LIST=""
for ITEM in $RAW_LIST for ITEM in $RAW_LIST; do
do # always relevant if only lower-case chars : continuation of the base command
# always relevant if only lower-case chars : continuation of the base command echo "$ITEM" | grep -E "^[a-z]+$" > /dev/null && LIST="$LIST $ITEM" && continue
echo $ITEM | grep -E "^[a-z]+$" > /dev/null && LIST="$LIST $ITEM" && continue case "$ITEM" in
case $ITEM in *COOKBOOK*)
*COOKBOOK*) # special case for cookbooks : from site or local
# special case for cookbooks : from site or local [[ ${COMP_WORDS[2]} == 'site' ]] && REGEN_CMD="knife cookbook site list" || REGEN_CMD="knife cookbook list"
[[ ${COMP_WORDS[2]} == 'site' ]] && REGEN_CMD="knife cookbook site list" || REGEN_CMD="knife cookbook list" _KAC_get_and_regen_cache "$REGEN_CMD"
_KAC_get_and_regen_cache $REGEN_CMD LIST="$LIST $(cut -d ' ' -f 1 < "$_KAC_CACHE_PATH")"
LIST="$LIST $(cat $_KAC_CACHE_PATH | cut -d ' ' -f 1)" continue
continue ;;
;; *ITEM*)
*ITEM*) # data bag item : another special case
# data bag item : another special case local DATA_BAG_NAME=${COMP_WORDS[$((COMP_CWORD - 1))]}
local DATA_BAG_NAME=${COMP_WORDS[$(( COMP_CWORD-1 ))]} REGEN_CMD="knife data bag show $DATA_BAG_NAME"
REGEN_CMD="knife data bag show $DATA_BAG_NAME" ;;
;; *INDEX*)
*INDEX*) # see doc @ http://docs.opscode.com/knife_search.html
# see doc @ http://docs.opscode.com/knife_search.html LIST="$LIST client environment node role"
LIST="$LIST client environment node role" REGEN_CMD="knife data bag list"
REGEN_CMD="knife data bag list" ;;
;; *BAG*) REGEN_CMD="knife data bag list" ;;
*BAG*) REGEN_CMD="knife data bag list";; *CLIENT*) REGEN_CMD="knife client list" ;;
*CLIENT*) REGEN_CMD="knife client list";; *NODE*) REGEN_CMD="knife node list" ;;
*NODE*) REGEN_CMD="knife node list";; *ENVIRONMENT*) REGEN_CMD="knife environment list" ;;
*ENVIRONMENT*) REGEN_CMD="knife environment list";; *ROLE*) REGEN_CMD="knife role list" ;;
*ROLE*) REGEN_CMD="knife role list";; *USER*) REGEN_CMD="knife user list" ;;
*USER*) REGEN_CMD="knife user list";; # not a generic argument we support...
# not a generic argument we support... *) continue ;;
*) continue;; esac
esac _KAC_get_and_regen_cache "$REGEN_CMD"
_KAC_get_and_regen_cache $REGEN_CMD LIST="$LIST $(cat "$_KAC_CACHE_PATH")"
LIST="$LIST $(cat $_KAC_CACHE_PATH)" done
done # shellcheck disable=SC2207,SC2086
COMPREPLY=( $(compgen -W "${LIST}" -- ${COMP_WORDS[COMP_CWORD]})) COMPREPLY=($(compgen -W "${LIST}" -- ${COMP_WORDS[COMP_CWORD]}))
} }
#complete -f -F _knife knife
complete -F _knife knife complete -F _knife knife