how to make it convenient in the CLI
Next I show “how” using my own example.
“Fish”:
_lun() {
local cur="$2"
local prev="$3"
local obj cmd base keys key val
local LIST=""
local WWID=""
local LUN=""
local cmd="${COMP_WORDS[1]}"
local DC="${COMP_WORDS[2]}"
…
} && complete -F _lun lun
complete -F
calls a function_lun()
when complements the commandlun
.For simple manipulations, three arguments to the called function are enough (more details). Which command are we supplementing (
$1
), which we complement ($2
) and what happened before ($3
).Keyword
local
is designed to protect variables inside the function from “leakage” outside. In general, all autocompletion in bash is lumped together, and with illiterate actions it is easy to break the work of someone else’s code.For more complex manipulations, an array is available
COMP_WORDS[]
and a pointer to its last elementCOMP_CWORD
. Above you can see how I extract a couple of “positional” arguments from it.For “aerobatics” access to
COMP_LINE
AndCOMP_POINT
(the entire line to be completed and the current cursor position).
First argument:
if [ "${COMP_CWORD}" = "1" ]
then
# first level -> base objects
base="/usr/local/sbin/lun.d"
obj=$(cd ${base} && ls -1 *.py | cut -f 1 -d "." | sort -u)
COMPREPLY=( $(compgen -W "help ${obj}" -- "${cur}") )
The Bashists in this place should hit my hands for
cd
. Because there is a couplepushd
/popd
.Take a list of all modules
*.py
add another keywordhelp
(output hint to wrapperlun
) and from this we form a set of “words” forcompgen -W
. INCOMPREPLY
a bash array is returned (what is inside the parentheses “( … )”).At the end of the call
compgen
it is necessary to put the complement (${cur}
). “--
“tell GNU utilities what's next-ключей
will not be.
Second argument:
elif [ "${COMP_CWORD}" = "2" ]
then
# second level -> commands
DC=$(sudo lun get list-dc)
if [ "${cmd}" = "get" ]; then
COMPREPLY=( $(compgen -W "help ${DC} list-dc" -- "${cur}") )
else
COMPREPLY=( $(compgen -W "help ${DC}" -- "${cur}") )
fi
Application
sudo
this is really important. The user has access to some privileged commands, but notsudo -i
for everyone!Call
sudo lun
will break auto-completion. Because the addition will be forsudo
and the opinion of those who wrotesudo
doesn't always match mine. For everything to work for the user, an alias is needed (somewhere in~/.bashrc
):alias lun="sudo lun"
Third argument:
elif [ "${COMP_CWORD}" = "3" ]
then
# third level -> keys
keys=`sudo lun ${cmd} args`
if [[ "${cmd}" =~ get|add|edit ]]; then
if [ "${DC}" = "list-dc" ]; then
return
fi
WWID=$(sudo lun get ${DC} --fields=wwid --compact | cut -d ']' -f 1 | cut -d ' ' -f 2 | tail -n 5)
COMPREPLY=( $(compgen -W "${keys} ${WWID}" -- "${cur}" | awk '{ if ($0 !~ "^-") {print "'\''"$0"'\''"} else print $0 }') )
elif [[ "${cmd}" == vm || "${cmd}" == attach || "${cmd}" == resize ]]; then
# compopt -o nospace
LIST=`sudo lun vm ${DC} list "${cur}" --cached`
COMPREPLY=( $(compgen -W "${LIST}" -- "${cur}" ) )
else
COMPREPLY=( $(compgen -W "${keys}" -- "${cur}" | awk '{ if ($0 !~ "^-") {print "'\''"$0"'\''"} else print $0 }') )
fi
About
nospace
will be lower.Each module is trained to output a list of accepted arguments by key
args
. We show it in the general case.list-dc
Instead of the data center abbreviation, it displays a list of known DCs. There is nothing further to add,return
.For teams
get|add|edit
Additionally, we show a list of the five most recently added LUNs. Not a super good solution, because… in the process it pulls out a listing of all LUNs in the DC. It would be more correct to push the restriction in a non-Unix waylun get
.This is the trick with quotes for WWN (who is not clear what is written here, googles “bash escape quotes”):
{print "'''"$0"'''"}
. It is for auto-completion of the divided “:
“. Because “:
” is included in the list of word separators by default. I was too lazy to dig deeper in this place.For teams
vm
,attach
,resize
We supplement the VM name from the list cached in the local database. The above comparison was through “=~
“, and here it is like this. Simply because at first the team was alone.Autocomplete well, very desirable from somewhere in the “fast” cache. Don't be like writers
yum
/dnf
and others like him. Long requests via ssh fail due to timeout. I didn't find this place in bash-completion, but I didn't try too hard.For all other commands, we display only a list of keys.
Other arguments:
else
# other level -> options
if [ "${COMP_WORDS[COMP_CWORD]}" = "=" ]; then
key=$((COMP_CWORD - 1))
elif [ "${COMP_WORDS[COMP_CWORD-1]}" = "=" ]; then
key=$((COMP_CWORD - 2))
fi
For non-Boolean arguments, keys of the form --key=value
without spaces. I didn't do much with the spaces. We will consider this “homework” for better mastery of the material.
Let's go add:
if [ ! -z "${key}" ]; then
if [ "${COMP_WORDS[$key]}" = "--fields" ]; then
val="alias host vm scsi blkdeviotune ,"
local list=$(echo "${cur}" | egrep -o '([a-z]+,)+')
cur="${cur/[[:alpha:]]*,/}"
COMPREPLY=( "${list}"$(compgen -W "${val}" -- "${cur}") )
compopt -o nospace
elif [ "${COMP_WORDS[$key]}" = "--file" ]; then # [ "${cmd}" = "edit" ]
compopt -o filenames
COMPREPLY=( $(compgen -f -- "${cur}") )
elif [ "${COMP_WORDS[$key]}" = "--vm" ]; then # [ "${cmd}" = "get" ]
LUN=`sudo lun vm ${DC} list --cached`
COMPREPLY=( $(compgen -W "${LUN}" -- "${cur}" ) )
elif [ "${COMP_WORDS[$key]}" = "--lun" ]; then # [ "${cmd}" = "attach"|"resize" ]
local vm="${COMP_WORDS[3]}"
LUN=`sudo lun vm ${DC} ${vm} --cached --luns --json | jq -r ".luns[]"`
COMPREPLY=( $(compgen -W "${LUN}" -- "${cur}" ) )
else
val="<`echo ${COMP_WORDS[$key]} | tr -d '=-'`>"
COMPREPLY=( $(compgen -W "${val}" -- "${cur}") )
compopt -o nospace
fi
else
keys=`sudo lun "${COMP_WORDS[1]}" args`
COMPREPLY=( $(compgen -W "${keys}" -- "${cur}") )
fi
If you haven’t started entering the key, we show a list of keys for the module (lower block
else
). It would be correct to exclude already used keys from the list. Inside is Python’s “argparse”, it will stupidly take the last one it comes across.Key
--fields
takes a list of fields as input via “,
“. This solution again goes against the default delimiter settings for libreadline, so the next step is the trick:cur="${cur/[[:alpha:]]*,/}"
. We cut everything down to the last “comma”. In general, in bash foot wraps I try to limit myself to bash means. Because he himself then threatens (if something happens) to unwind the audit logs.At this point one should also exclude everything previously listed through “
,
“fields from autocompletion.For
--file
you need to autocomplete file names from the current directory. For this we ordercompopt -o filenames
otherwise it will not allow it to “fall through” into subdirectories. Somewhere it is written that this can be inserted directly into the callcompgen -f
. But this doesn’t work (for me, bash 4.4.20 from Oracle Linux 8).For
--lun
take a list of LUNs related to the specified VM. Requestjq -r ".luns[]"
retrieves values (LUN names) from the dictionary provided in json. JSON and “jq” are generally quite convenient when parsing what is sent to the CLI. For those utilities that know how to write JSON.Everything else (after
else
) – we don’t know how to supplement. Using the “tabulator” we display the name of the key in angle brackets (--key=<key>
).
To autocomplete after “=
“, please do not add a space:
if [[ "${COMPREPLY[@]}" =~ =$ ]]; then
# Add space, if there is not a '=' in suggestions
compopt -o nospace
fi
All. I told him as best I could. Good luck improving the UI/UX for command line tools!
This is my first experience of writing (not presented as a lecture) educational material for adult aunts and uncles. I would be extremely grateful for constructive criticism. And, as it becomes available, I will try to finalize this article to improve readability.