可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Is it possible to make bash auto-completion look like in Cisco IOS shell?
I mean to add short descriptions for each completion, like this:
telnet 10.10.10. (TAB Pressed)
10.10.10.10 - routerA
10.10.10.11 - routerB
where 10.10.10.10 and 10.10.10.11 are possible completions
and routerA & routerB just descriptions (not to be executed).
I know that bash can complete commands with "complete -W", but is it able to print descriptions for them?
回答1:
I have a solution to this that does not require pressing TAB more than twice or echoing any extra information. The key is to check whether there is only one completion, then strip that completion down to the valid portion, usually by removing the largest matching suffix after your "comment" delimiter. To accomplish the OP's example:
_telnet() {
COMPREPLY=()
local cur
cur=$(_get_cword)
local completions="10.10.10.10 - routerA
10.10.10.11 - routerB
10.20.1.3 - routerC"
local OLDIFS="$IFS"
local IFS=$'\n'
COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
IFS="$OLDIFS"
if [[ ${#COMPREPLY[*]} -eq 1 ]]; then #Only one completion
COMPREPLY=( ${COMPREPLY[0]%% - *} ) #Remove ' - ' and everything after
fi
return 0
}
complete -F _telnet -A hostnames telnet
This gives the exact output you're looking for, and when there is only one possible completion, the comment is stripped from it before completing.
回答2:
I'd use conversion based on whether the number of candidates become one (as shown by @bonsaiviking) for simple cases and the following if I needed more flexibility in what I want to show the user.
__foo () {
local WORDS
WORDS=("1|10.10.10.10|routerA" "2|10.10.10.11|routerB")
local FOR_DISPLAY=1
if [ "${__FOO_PREV_LINE:-}" != "$COMP_LINE" ] ||
[ "${__FOO_PREV_POINT:-}" != "$COMP_POINT" ]; then
__FOO_PREV_LINE=$COMP_LINE
__FOO_PREV_POINT=$COMP_POINT
FOR_DISPLAY=
fi
local IFS=$'\n'
COMPREPLY=($(
for WORD in "${WORDS[@]}"; do
IFS=\| read -ra SP <<<"$WORD"
if [ "${SP[1]:0:${#2}}" == "$2" ]; then
if [ -n "$FOR_DISPLAY" ]; then
printf "%-*s\n" "$COLUMNS" "${SP[0]}: ${SP[1]} - ${SP[2]}"
else
echo "${SP[1]}"
fi
fi
done
))
}
complete -F __foo x
Note: You could probably use COMP_TYPE
to set FOR_DISPLAY
in Bash 4.x but I needed to support Bash 3.x as well.
This behaves as follows:
$ x 1
Tab
$ x 10.10.10.1
TabTab
1: 10.10.10.10 - routerA
2: 10.10.10.11 - routerB
$ x 10.10.10.1
回答3:
Yes, but you need a bit of bash kung foo in order to build such system. The way completion usually works is by binding normal functions to the commands you want to complete. You can find some basic examples around to better understand how completion works, and start developing your completion functions. Also, if you happen to have the bash-completion
package installed, you could search your system for a number of other examples that currently drive completion in your shell.
You could also have a look at the completion section of the official bash manual.
EDIT
I tried some experiments, and my conclusion is now that you can't do exactly what you're after: bash doesn't support help text next to complete
results. What you can do is to add the legend for the provided completing words. This can be done either in a bash function _myfoo
to be used as complete -F _myfoo
, or a command via complete -C myfoo
, which prints out the legend before completing.
The main difference is that using a function you're bound to Bash, while commands can be written in any language you choose, as long as it's able to set the required environment variables.
Here's a little example:
skuro$ touch ~/bin/myfoo
skuro$ chmod +x ~/bin/myfoo
skuro$ _myfoo(){
> echo "result1 -- number one"
> echo "result2 -- number two"
> local cur prev
> _get_comp_words_by_ref cur prev
> COMPREPLY=( $(compgen -W "result1 result2" "$cur") )
> return 0
> }
skuro$ complete -F _myfoo myfoo
skuro$ myfoo result<TAB>
result1 -- number one
result2 -- number two
result1 result2
回答4:
After some research I've found a solution. I don't know how it looks in Cisco, but I know how it works in Vyatta. The only flaw is that in this variant you have to press TAB 3 times to get a detailed help for the first time (first two times normal completion is printed). Once detailed help was shown, next TABs will toggle normal and detailed completion.
comment_show_last_detailed=1
comment_show_last_position=0
_comment_show()
{
local cur opts i opt comment opts comments
opts="result1
result2"
comments="comment1
comment2"
[ $comment_show_last_position -gt $COMP_POINT ] &&
comment_show_last_position=0
if [ $comment_show_last_detailed = 0 ] &&
[ $comment_show_last_position = $COMP_POINT ]; then
for ((i=1; ;++i)); do
opt=`echo "$opts" | cut -f$i -d$'\n'`
[ -z "$opt" ] && break
comment=`echo "$comments" | cut -f$i -d$'\n'`
echo
echo -n "$opt - $comment"
done
comment_show_last_detailed=1
COMPREPLY=
else
cur="${COMP_WORDS[COMP_CWORD]}"
SAVEIFS="$IFS"
IFS=$'\n'
COMPREPLY=( $(compgen -W "${opts}" ${cur}) )
IFS="$SAVEIFS"
comment_show_last_detailed=0
fi
comment_show_last_position=$COMP_POINT
}
complete -F _comment_show comment
I even managed to reduce TAB pressings to only 2 using COMP_TYPE
variable, but there is a problem that bash doesn't reprint current command line at the bottom line if some symbols were inserted after first TAB pressing, so there is a space for further research.
回答5:
Inspired from https://github.com/CumulusNetworks/NetworkDocopt
The basic trick is to print help text, PS1 (expanded) and the original command, to stderr
, and then print the completions options to stdout
.
Here is the snippet to source in bash to like a completion function to telnet
. It will call a ruby script (called p.rb
) to generate the actual completion output.
_telnet_complete()
{
COMPREPLY=()
COMP_WORDBREAKS=" "
local cur=${COMP_WORDS[COMP_CWORD]}
local cmd=(${COMP_WORDS[*]})
local choices=$(./p.rb ${cmd[*]} --completions ${COMP_CWORD} ${PS1@P})
COMPREPLY=($(compgen -W '${choices}' -- ${cur} ))
return 0
}
complete -F _telnet_complete telnet
Here is an implementation of p.rb
:
#!/usr/bin/env ruby
ip = ""
out_ps1 = []
out_args = []
state = :init
completion_req = false
ARGV.each do |e|
case state
when :init
if e == "--completions"
completion_req = true
state = :complte
else
out_args << e
if /^\d+\.\d+\.\d+\.\d+$/ =~ e
ip = e
end
end
when :complte
state = :ps1
when :ps1
out_ps1 << e
end
end
routes = {
"10.10.10.10" => "routerA",
"10.10.10.11" => "routerB",
}
if completion_req
$stderr.puts ""
routes.each do |k, v|
if k[0..ip.size] == ip or ip.size == 0
$stderr.puts "#{k} - #{v}"
$stdout.puts k
end
end
$stderr.write "#{out_ps1.join(" ")}#{out_args.join(" ")} "
exit 0
end
Example:
$ telnet <tab>
10.10.10.10 - routerA
10.10.10.11 - routerB
$ telnet 10.10.10.1