Context
I have an arduino-cli wrapper script named ino that reads target/build configuration from:
JSON files located in the sketch directory
Command-line flags/arguments
It then constructs and exec's the corresponding arduino-cli command-line.
Problem
As a convenience wrapper script, ino isn't intended to support every feature of arduino-cli. So for those tasks that ino doesn't automate, the user can instead invoke arduino-cli indirectly using the cli subcommand of ino.
For example, if the user types the following commands:
% ino cli update
% ino cli core list --all
The ino script will take everything following cli and simply append them to the arduino-cli executable. So they would be equivalent to the following commands:
% arduino-cli update
% arduino-cli core list --all
Since arduino-cli has nice bash completion for all of its subcommands and flags, I would like to hijack the same completion functionality for my ino cli subcommand.
What I've tried
The accepted answers here:
How do I autocomplete nested, multi-level subcommands?
Multi Level Bash Completion
These helped me understand how to identify the current subcommand and discriminate the completion results based upon it.
However, I couldn't figure out how to then invoke the arduino-cli completion handler using the remaining args.
Completion handler derived from accepted answer here:
How do I get bash completion for command aliases?
See my ino completion handler based on that answer below (Reference 1).
This question/answer isn't quite the same, because they can basically just install a completion handler on their alias. I'm needing to "install" one on an argument to a command/alias.
This almost seems to work. Try it with xtrace option enabled (set -x), and you can see the arduino-cli command-line is appearing in the args ... but following ino at position $0.
E.g., given ino cli core list --all to the wrapper handler, the arduino-cli handler receives ino arduino-cli core list --all. Not sure how to get rid of $0!
Reference
ino completion wrapper derived from alias-based wrappers
joinstr() {
local d=${1-} f=${2-}
shift 2 && printf %s "$f" "${#/#/$d}"
}
complete-subcmd() {
[[ ${#} -gt 2 ]] || {
printf "usage:\n\tcomplete-subcmd src-command... -- comp-func dst-command...\n"
return 1
}
# parse the command-line by splitting it into two command-lines
# of variable length, src-command and dst-command:
# 1. src-command is the trigger that invokes the real completion
# handler, comp-func.
# 2. dst-command is the leading args of the command-line passed
# to the real completion handler, comp-func, to produce the
# resulting completion choices.
unset -v dstparse
local -a srccmd dstcmd
local func
while [[ ${#} -gt 0 ]]; do
case "${1}" in
--)
# when we reach the delimiter, also shift in comp-func as
# the next argument (the real completion handler).
dstparse=1
shift
func=${1:-}
;;
*)
# if we aren't processing the delimiter, then all other
# args are appended to either src-command or dst-command.
if [[ -z ${dstparse} ]]; then
srccmd+=( "${1}" )
else
dstcmd+=( "${1}" )
fi
;;
esac
shift
done
# if the completer is dynamic and not yet loaded, try to load it
# automatically using the given command
if [[ $( type -t "${func}" ) != function ]]; then
type -p _completion_loader &> /dev/null &&
_completion_loader "${dstcmd[#]}"
fi
local wrap=$( joinstr _ "${srccmd[#]}" | tr -d -c '[A-Za-z_]' )
# replace our args with dst-command followed by whatever remains
# from the invoking command-line.
eval "
function _${wrap} {
(( COMP_CWORD+=$(( ${#dstcmd[#]} )) ))
COMP_WORDS=( "${dstcmd[#]}" \${COMP_WORDS[#]:1} )
"${func}"
return 0
}
"
# install this wrapper handler on the first word in src-command
complete -F "_${wrap}" "${srccmd[0]}"
}
complete-subcmd ino cli -- __start_arduino-cli arduino-cli
completion.bash from arduino-cli
# bash completion V2 for arduino-cli -*- shell-script -*-
__arduino-cli_debug()
{
if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
fi
}
# Macs have bash3 for which the bash-completion package doesn't include
# _init_completion. This is a minimal version of that function.
__arduino-cli_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$#" cur prev words cword
}
# This function calls the arduino-cli program to obtain the completion
# results and the directive. It fills the 'out' and 'directive' vars.
__arduino-cli_get_completion_results() {
local requestComp lastParam lastChar args
# Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly arduino-cli allows to handle aliases
args=("${words[#]:1}")
requestComp="${words[0]} __completeNoDesc ${args[*]}"
lastParam=${words[$((${#words[#]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
__arduino-cli_debug "lastParam ${lastParam}, lastChar ${lastChar}"
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go method.
__arduino-cli_debug "Adding extra empty parameter"
requestComp="${requestComp} ''"
fi
# When completing a flag with an = (e.g., arduino-cli -n=<TAB>)
# bash focuses on the part after the =, so we need to remove
# the flag part from $cur
if [[ "${cur}" == -*=* ]]; then
cur="${cur#*=}"
fi
__arduino-cli_debug "Calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval "${requestComp}" 2>/dev/null)
# Extract the directive integer at the very end of the output following a colon (:)
directive=${out##*:}
# Remove the directive
out=${out%:*}
if [ "${directive}" = "${out}" ]; then
# There is not directive specified
directive=0
fi
__arduino-cli_debug "The completion directive is: ${directive}"
__arduino-cli_debug "The completions are: ${out[*]}"
}
__arduino-cli_process_completion_results() {
local shellCompDirectiveError=1
local shellCompDirectiveNoSpace=2
local shellCompDirectiveNoFileComp=4
local shellCompDirectiveFilterFileExt=8
local shellCompDirectiveFilterDirs=16
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
# Error code. No completion.
__arduino-cli_debug "Received error from custom completion go code"
return
else
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__arduino-cli_debug "Activating no space"
compopt -o nospace
else
__arduino-cli_debug "No space directive not supported in this version of bash"
fi
fi
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__arduino-cli_debug "Activating no file completion"
compopt +o default
else
__arduino-cli_debug "No file completion directive not supported in this version of bash"
fi
fi
fi
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
# File extension filtering
local fullFilter filter filteringCmd
# Do not use quotes around the $out variable or else newline
# characters will be kept.
for filter in ${out[*]}; do
fullFilter+="$filter|"
done
filteringCmd="_filedir $fullFilter"
__arduino-cli_debug "File filtering command: $filteringCmd"
$filteringCmd
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
# File completion for directories only
# Use printf to strip any trailing newline
local subdir
subdir=$(printf "%s" "${out[0]}")
if [ -n "$subdir" ]; then
__arduino-cli_debug "Listing directories in $subdir"
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
else
__arduino-cli_debug "Listing directories in ."
_filedir -d
fi
else
__arduino-cli_handle_standard_completion_case
fi
__arduino-cli_handle_special_char "$cur" :
__arduino-cli_handle_special_char "$cur" =
}
__arduino-cli_handle_standard_completion_case() {
local tab comp
tab=$(printf '\t')
local longest=0
# Look for the longest completion so that we can format things nicely
while IFS='' read -r comp; do
# Strip any description before checking the length
comp=${comp%%$tab*}
# Only consider the completions that match
comp=$(compgen -W "$comp" -- "$cur")
if ((${#comp}>longest)); then
longest=${#comp}
fi
done < <(printf "%s\n" "${out[#]}")
local completions=()
while IFS='' read -r comp; do
if [ -z "$comp" ]; then
continue
fi
__arduino-cli_debug "Original comp: $comp"
comp="$(__arduino-cli_format_comp_descriptions "$comp" "$longest")"
__arduino-cli_debug "Final comp: $comp"
completions+=("$comp")
done < <(printf "%s\n" "${out[#]}")
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${completions[*]}" -- "$cur")
# If there is a single completion left, remove the description text
if [ ${#COMPREPLY[*]} -eq 1 ]; then
__arduino-cli_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
comp="${COMPREPLY[0]%% *}"
__arduino-cli_debug "Removed description from single completion, which is now: ${comp}"
COMPREPLY=()
COMPREPLY+=("$comp")
fi
}
__arduino-cli_handle_special_char()
{
local comp="$1"
local char=$2
if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
local word=${comp%"${comp##*${char}}"}
local idx=${#COMPREPLY[*]}
while [[ $((--idx)) -ge 0 ]]; do
COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"}
done
fi
}
__arduino-cli_format_comp_descriptions()
{
local tab
tab=$(printf '\t')
local comp="$1"
local longest=$2
# Properly format the description string which follows a tab character if there is one
if [[ "$comp" == *$tab* ]]; then
desc=${comp#*$tab}
comp=${comp%%$tab*}
# $COLUMNS stores the current shell width.
# Remove an extra 4 because we add 2 spaces and 2 parentheses.
maxdesclength=$(( COLUMNS - longest - 4 ))
# Make sure we can fit a description of at least 8 characters
# if we are to align the descriptions.
if [[ $maxdesclength -gt 8 ]]; then
# Add the proper number of spaces to align the descriptions
for ((i = ${#comp} ; i < longest ; i++)); do
comp+=" "
done
else
# Don't pad the descriptions so we can fit more text after the completion
maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
fi
# If there is enough space for any description text,
# truncate the descriptions that are too long for the shell width
if [ $maxdesclength -gt 0 ]; then
if [ ${#desc} -gt $maxdesclength ]; then
desc=${desc:0:$(( maxdesclength - 1 ))}
desc+="…"
fi
comp+=" ($desc)"
fi
fi
# Must use printf to escape all special characters
printf "%q" "${comp}"
}
__start_arduino-cli()
{
local cur prev words cword split
COMPREPLY=()
# Call _init_completion from the bash-completion package
# to prepare the arguments properly
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -n "=:" || return
else
__arduino-cli_init_completion -n "=:" || return
fi
__arduino-cli_debug
__arduino-cli_debug "========= starting completion logic =========="
__arduino-cli_debug "cur is ${cur}, words[*] is ${words[*]}, #words[#] is ${#words[#]}, cword is $cword"
# The user could have moved the cursor backwards on the command-line.
# We need to trigger completion from the $cword location, so we need
# to truncate the command-line ($words) up to the $cword location.
words=("${words[#]:0:$cword+1}")
__arduino-cli_debug "Truncated words[*]: ${words[*]},"
local out directive
__arduino-cli_get_completion_results
__arduino-cli_process_completion_results
}
if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_arduino-cli arduino-cli
else
complete -o default -o nospace -F __start_arduino-cli arduino-cli
fi
# ex: ts=4 sw=4 et filetype=sh
UPDATE2:
After I posted this I checked your links and after seeing the accepted answer here, I was thinking I am just old and forget that I just copied this code from that link, and it wasn't me who wrote it. Even the example used there is the same, but investigating the code further, it looks I did write this and used a different approach, and maybe this will help you understand what's going on. As I mentioned at the bottom UPDATE1 section: you need to tune the COMP variables then call the original function
Original:
I wrote an 'alias wrapper' script a couple of years ago.
The idea is to use the original bash completion with aliases even with parameters.
For example:
alias apti='apt-get install'
source alias-completion-wrapper _apt_get apti apt-get install
#here _apt_get is the original completion function
Now you can use tab to complete the package name after apti just like after apt-get install
#alias-completion-wrapper
#Example: . alias-completion-wrapper _apt_get apti apt-get install
comp_function_name="$1"
ali="$2"
shift 2
x="$#"
function_name=`echo _$# |tr ' ' _`
function="
function $function_name {
_completion_loader $1
(( COMP_CWORD += $# - 1 ))
COMP_WORDS=( $# \"\${COMP_WORDS[#]:1}\")
COMP_LINE=\"\${COMP_WORDS[#]}\"
let COMP_POINT=\${COMP_POINT}-${#ali}+${#x}
$comp_function_name
return 0
}"
eval "$function"
complete -F $function_name $ali
unset function function_name ali x
To be honest, I can't remember how it works and I didn't commented the script :)
But I think you will be able to tune this for your needs.
UPDATE1:
As I investigated the code a bit, it looks like the idea is to tune the COMP variables, then call the original function :)
UPDATE3:
I had some time, so the modification you need are:
This ${COMP_WORDS[#]} contains the current command line. ${COMP_WORDS[#]:1} cuts off the first word, which is originally the alias/command. As you want to use it after a parameter you have to cut off the parameter too.
COMP_WORDS=( $# \"\${COMP_WORDS[#]:2}\")
I don't see COMP_LINE and COMP_POINT in the other solution, but as I can recall without those, it doesn't worked well in certain circumstances. So I suppose you need:
COMP_LINE=\"\${COMP_WORDS[#]:1}\"
And here ${#ali} is the length of the command. You need to replace this with the length of your command with the parameter. eg,:"xcmd prm" -> 8 (count the space too)
let COMP_POINT=\${COMP_POINT}-${#ali}+${#x}
Not sure about (( COMP_CWORD += $# - 1 )) either remove the -1 or use -2 or leave it as it is :)
After the modifications, just change the eval to echo and remove the complete -F line. And source the script as described. This way it will echo the function what you can insert into your completion script.
i am trying for suggest custom options in bash completion in commands linux ( centos 7 )
i am know the this code add custom command to bash and suggest options
path : /etc/bash_completion.d/foo
_foo()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="-i(--incoming) -o(-outgoing) -m(--missed) -a(-all) "
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -F _foo foo
foo -[tab]
-i(--incoming) -o(-outgoing) -m(--missed) -a(-all)
goal me is extend this source code to built in commands in linux such as ls
trying me is :
path : /etc/bash_completion.d/ls
_ls()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="-a(--all) -h(--human-readable) -r(--reverse) "
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -F _ls ls
when this code used not suggest folders and files complete in ls command
ls -[tab]
-a(--all) -h(--human-readable) -r(--reverse)
ls[tab]
not suggest files and directory
goal me is add custom option for suggest in bash and not behaviour command
I'm trying to add auto-complete function to mytool command with following _mytool complete function:
_mytool()
{
local cur
_get_comp_words_by_ref -n : cur
# my implementation here
COMPREPLY=() # Array variable storing the possible completions.
cur=${COMP_WORDS[COMP_CWORD]}
if [[ $cur = -* ]]; then
COMPREPLY=( $(compgen -W '-a:first -a:second' -- $cur) )
fi
__ltrim_colon_completions "$cur"
}
complete -F _mytool mytool
Because there is ":" in my COMPREPLY, I try to make it work by using _get_comp_words_by_ref and __ltrim_colon_completions function, which is learnt from: here
But it still not work when type:
$mytool -a:[TAB]
there is no auto-complete at all. I was expecting bash will print the following completion for me:
-a:first -a:second
my bash version:
4.3.46(1)-release
What am I missing? Thanks!
I'm currently trying to write a bash_completion script for one of our tools.
Was looking at the apt-get and chkconfig scripts for some help.
Basically what I want to do is get a different option selection based on the first value.
There can be more than one option to a value.
command <value1> <--option1> <--option2> ...
command <value2> <--option3> <--option4> ...
Looking at the apt-get script, it will return the same list of options for any first value.
So this is not what I need.
Here is what I got so far:
_supdeploy()
{
local cur prev opts cword
_init_completion || return
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="deploy destroy supported list fulllist status fullstatus getip shutdown powerOff powerOn"
case "${prev}" in
deploy)
if [[ "$cur" == -* ]]; then
if [[ $cword -eq 2 || $cword -eq 3 || $cword -eq 4 ]]; then
COMPREPLY=( $( compgen -W '--hostname --os --version' -- "$cur" ) )
fi
fi
return 0
;;
destroy)
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W '--name --silent' -- "$cur" ) )
fi
return 0
;;
*)
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
;;
esac
} &&
complete -F _supdeploy supdeploy
I get a different selection of options for both deploy and destroy.
But I can only use one -- option.
When I try to use -- again, nothing happens, and without the -- I get the list from opts.
It is probably something easy, but I can't find the error here at the moment.
I also have it tried without the $cword before, same result
Instead of prev, you just want to look at the value of the first argument each time:
case ${COMP_WORDS[1]} of
It gets a little tricker if you allow "general" options to precede the subcommand, but in general you want to look at the first non-option argument, not necessarily the previous argument.
I tried this other questions's accepted answer but it doesn't work for me. So please don't vote this as duplicate.
My script is named "tracker" and it accepts the following switches: --dummy --audit_sessiones --user_count --detailed_user_count --parfile
The --parfile switcj should be followed by a filename.
I have this autocompletion script:
_tracker()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="--dummy --audit_sessiones --user_count --detailed_user_count --parfile"
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
opts="ls *"
if [[ ${prev} == --parfile ]]; then
COMPREPLY=( "${files[#]##*/}" )
return 0
fi
}
complete -F _tracker tracker
Autocompletion of switches works fine.
But I also want the user to be able to use filename autocompletion right after the parameter --parfile but I haven't been able to make it work.
complete has a -o default option so you can remove the opts="ls *"; if ... fi part and just do complete -F _tracker -o default tracker.
According to bash manual:
If the -o default option was supplied to complete when
the compspec was defined, readline's default completion will
be performed if the compspec (and, if attempted, the default
bash completions) generate no matches.
Try replacing COMPREPLY=( "${files[#]##*/}" ) with COMPREPLY=( $(compgen -f ${cur}) )
More information about auto completion can be found in the following links
An introduction to bash completion: part 1
An introduction to bash completion: part 2