Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocompletion for positional parameters on ~ #2068

Open
rsenden opened this issue Jul 18, 2023 · 3 comments
Open

Autocompletion for positional parameters on ~ #2068

rsenden opened this issue Jul 18, 2023 · 3 comments

Comments

@rsenden
Copy link
Contributor

rsenden commented Jul 18, 2023

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 #437 (comment) 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
@rsenden rsenden changed the title Autocompletion for ~ Autocompletion for positional parameters on ~ Jul 19, 2023
@rsenden
Copy link
Contributor Author

rsenden commented Jul 21, 2023

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?

@remkop
Copy link
Owner

remkop commented Jul 22, 2023

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.

@rsenden
Copy link
Contributor Author

rsenden commented Jul 27, 2023

@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 extraCompGenOpts variable in the compgen command

I'll look at having a PR created for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants