Autocompletion for positional parameters on `~`
We're trying to have our application properly handle paths starting with ~ to represent the user home directory. It seems like bash is already taking care of expanding ~ references with the proper path when invoking our application. The work-around mentioned in https://github.com/remkop/picocli/issues/437#issuecomment-412038752 only seems to be necessary when running our application from the IDE and probably when running from shells that don't natively support ~.
However, in all of the situations listed above, it seems like the auto-completion script doesn't support paths starting with ~ for positional parameters; for options auto-completion seems to work fine. The auto-complete debug output looks completely similar when trying to autocomplete either ~/test-autocomplete/ or /home/rsenden/test-autocomplete/, apart from the function returning 1 for the path with ~ instead of 0 when using the absolute path; see below.
I don't have sufficient experience with auto-completion scripts to understand exactly what's causing this difference, and what would need to be done to properly support paths starting with ~. Any idea?
Sample autocomplete function:
function _picocli_fcli_config_truststore_set() {
# Get completion data
local curr_word=${COMP_WORDS[COMP_CWORD]}
local prev_word=${COMP_WORDS[COMP_CWORD-1]}
local commands=""
local flag_opts="-h --help"
local arg_opts="--env-prefix --log-file --log-level -o --output --store --output-to-file -p --truststore-password -t --truststore-type"
local logLevel_option_args=("TRACE" "DEBUG" "INFO" "WARN" "ERROR") # --log-level values
local formatoptions_option_args=("csv" "csv-plain" "json" "json-flat" "table" "table-plain" "tree" "tree-flat" "xml" "xml-flat" "yaml" "yaml-flat" "expr" "json-properties") # --output values
type compopt &>/dev/null && compopt +o default
case ${prev_word} in
--env-prefix)
return
;;
--log-file)
return
;;
--log-level)
local IFS=$'\n'
COMPREPLY=( $( compReplyArray "${logLevel_option_args[@]}" ) )
return $?
;;
-o|--output)
local IFS=$'\n'
COMPREPLY=( $( compReplyArray "${formatoptions_option_args[@]}" ) )
return $?
;;
--store)
return
;;
--output-to-file)
return
;;
-p|--truststore-password)
return
;;
-t|--truststore-type)
return
;;
esac
if [[ "${curr_word}" == -* ]]; then
COMPREPLY=( $(compgen -W "${flag_opts} ${arg_opts}" -- "${curr_word}") )
else
local positionals=""
local currIndex
currIndex=$(currentPositionalIndex "set" "${arg_opts}" "${flag_opts}")
if (( currIndex >= 0 && currIndex <= 0 )); then
local IFS=$'\n'
type compopt &>/dev/null && compopt -o filenames
positionals=$( compgen -f -- "${curr_word}" ) # files
fi
local IFS=$'\n'
COMPREPLY=( $(compgen -W "${commands// /$'\n'}${IFS}${positionals}" -- "${curr_word}") )
fi
}
Output with /home/rsenden/test-autocomplete/ as input:
+ _picocli_fcli_config_truststore_set
+ local curr_word=/home/rsenden/test-autocomplete/
+ local prev_word=set
+ local commands=
+ local 'flag_opts=-h --help'
+ local 'arg_opts=--env-prefix --log-file --log-level -o --output --store --output-to-file -p --truststore-password -t --truststore-type'
+ logLevel_option_args=("TRACE" "DEBUG" "INFO" "WARN" "ERROR")
+ local logLevel_option_args
+ formatoptions_option_args=("csv" "csv-plain" "json" "json-flat" "table" "table-plain" "tree" "tree-flat" "xml" "xml-flat" "yaml" "yaml-flat" "expr" "json-properties")
+ local formatoptions_option_args
+ type compopt
+ compopt +o default
+ case ${prev_word} in
+ [[ /home/rsenden/test-autocomplete/ == -* ]]
+ local positionals=
+ local currIndex
++ currentPositionalIndex set '--env-prefix --log-file --log-level -o --output --store --output-to-file -p --truststore-password -t --truststore-type' '-h --help'
++ local commandName=set
++ local 'optionsWithArgs=--env-prefix --log-file --log-level -o --output --store --output-to-file -p --truststore-password -t --truststore-type'
++ local 'booleanOptions=-h --help'
++ local previousWord
++ local result=0
+++ seq 3 -1 0
++ for i in $(seq $((COMP_CWORD - 1)) -1 0)
++ previousWord=set
++ '[' set = set ']'
++ break
++ echo 0
+ currIndex=0
+ (( currIndex >= 0 && currIndex <= 0 ))
+ local 'IFS=
'
+ type compopt
+ compopt -o filenames
++ compgen -f -- /home/rsenden/test-autocomplete/
+ positionals='/home/rsenden/test-autocomplete/file1.txt
/home/rsenden/test-autocomplete/file2.txt'
+ local 'IFS=
'
+ COMPREPLY=($(compgen -W "${commands// /'
'}${IFS}${positionals}" -- "${curr_word}"))
++ compgen -W '
/home/rsenden/test-autocomplete/file1.txt
/home/rsenden/test-autocomplete/file2.txt' -- /home/rsenden/test-autocomplete/
+ return 0
Output with ~/test-autocomplete/ as input:
+ _picocli_fcli_config_truststore_set
+ local 'curr_word=~/test-autocomplete/'
+ local prev_word=set
+ local commands=
+ local 'flag_opts=-h --help'
+ local 'arg_opts=--env-prefix --log-file --log-level -o --output --store --output-to-file -p --truststore-password -t --truststore-type'
+ logLevel_option_args=("TRACE" "DEBUG" "INFO" "WARN" "ERROR")
+ local logLevel_option_args
+ formatoptions_option_args=("csv" "csv-plain" "json" "json-flat" "table" "table-plain" "tree" "tree-flat" "xml" "xml-flat" "yaml" "yaml-flat" "expr" "json-properties")
+ local formatoptions_option_args
+ type compopt
+ compopt +o default
+ case ${prev_word} in
+ [[ ~/test-autocomplete/ == -* ]]
+ local positionals=
+ local currIndex
++ currentPositionalIndex set '--env-prefix --log-file --log-level -o --output --store --output-to-file -p --truststore-password -t --truststore-type' '-h --help'
++ local commandName=set
++ local 'optionsWithArgs=--env-prefix --log-file --log-level -o --output --store --output-to-file -p --truststore-password -t --truststore-type'
++ local 'booleanOptions=-h --help'
++ local previousWord
++ local result=0
+++ seq 3 -1 0
++ for i in $(seq $((COMP_CWORD - 1)) -1 0)
++ previousWord=set
++ '[' set = set ']'
++ break
++ echo 0
+ currIndex=0
+ (( currIndex >= 0 && currIndex <= 0 ))
+ local 'IFS=
'
+ type compopt
+ compopt -o filenames
++ compgen -f -- '~/test-autocomplete/'
+ positionals='~/test-autocomplete/file1.txt
~/test-autocomplete/file2.txt'
+ local 'IFS=
'
+ COMPREPLY=($(compgen -W "${commands// /'
'}${IFS}${positionals}" -- "${curr_word}"))
++ compgen -W '
~/test-autocomplete/file1.txt
~/test-autocomplete/file2.txt' -- '~/test-autocomplete/'
+ return 1
So, as mentioned above, auto-completion for ~/ does work fine for options but not for positional parameters. The main difference in the completion script is that for options taking a File, completion candidates are generated using a simple:
COMPREPLY=( $( compgen -f -- "${curr_word}" ) ) # files
For positional parameters, the same compgen command is used to generate the contents of the positionals variable, which is then used in another compgen command which apparently doesn't support ~/:
if (( currIndex >= 0 && currIndex <= 0 )); then
local IFS=$'\n'
type compopt &>/dev/null && compopt -o filenames
positionals=$( compgen -f -- "${curr_word}" ) # files
fi
local IFS=$'\n'
COMPREPLY=( $(compgen -W "${commands// /$'\n'}${IFS}${positionals}" -- "${curr_word}") )
Adding the -f option to the latter compgen command allows for proper handling of ~/:
COMPREPLY=( $(compgen -f -W "${commands// /$'\n'}${IFS}${positionals}" -- "${curr_word}") )
However, given that this statement is part of the generic function footer, adding the -f option would likely result in file-based auto-completion being offered on all positional parameters, which we don't want. Any ideas how to properly fix this issue?
One idea is to replace the generic footer function with one that has the -f option if the positional parameters are of type File or Path.
@remkop Thanks for the suggestion. Just to confirm, the reason that the if-block handling file completions falls through to the footer, instead of returning immediately if there are any file matches, is to allow multiple completion options to be combined, correct? For example, for a positional file parameter with arity 0..*, we may need to combine file completion options with --option and sub-command completion options. So, just adding a return statement to the if-block is not an option.
Instead of replacing the generic footer function, I think the following might be easier as it better fits into current architecture:
- In the generic header, define an empty string or array variable named something like
extraCompGenOpts - In the if-block handling file completion options, append/add array entry
-f - In the generic footer, include the contents of the
extraCompGenOptsvariable in thecompgencommand
I'll look at having a PR created for this.