4

You can find a related question here: How to autocomplete a bash commandline with file paths?

Context

I am creating a shell program which is a command line tool. I want to create my own auto-completion for this tool.

For the options --unit-test and -t, I want to auto-complete on file paths from a particular directory which I can get running my_app --directory.

e.G.

Run:

user@computer:~$ my_app --install [TAB][TAB]

would do:

Public/          bin/                 Desktop/              
Documents/       Music/               Downloads/
user@computer:~$ my_app --install 

(display the current directory)

Run:

user@computer:~$ my_app --unit-tests [TAB][TAB]

would do:

folder/              folder2/             folder3/
.hidden_file         file.extension       file2.extension
user@computer:~$ my_app --unit-tests 

(display suggestions for specific directory without complete with it)

my_app_autocomplete file

__my_app_autocomplete()
{
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts="--help -h --install -i --run -r --rebuild -rb --show-running-containers -ps --stop -s --remove -rm --logs -l --bash -b --sass -css --unit-tests -t"
    containers="nginx php mysql mongo node"
    sass="watch"

    # By default, autocomplete with options
    if [[ ${prev} == my_app ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
    # By default, autocomplete with options
    if [[ ${cur} == -* ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
    # For --install and -i options, autocomplete with folder
    if [ ${prev} == --install ] || [ ${prev} == -i ] ; then
        COMPREPLY=( $(compgen -d -- ${cur}) )
        return 0
    fi
    # For --stop --remove --logs and --bash, autocomplete with containers
    if [ ${prev} == --stop ] || [ ${prev} == -s ] || [ ${prev} == --remove ] || [ ${prev} == -rm ] || [ ${prev} == --logs ] || [ ${prev} == -l ] || [ ${prev} == --bash ] || [ ${prev} == -b ] ; then
        COMPREPLY=( $(compgen -W "${containers}" -- ${cur}) )
        return 0
    fi
    # For --sass and -css, complete with sass options
    if [ ${prev} == --sass ] || [ ${prev} == -css ] ; then
        COMPREPLY=( $(compgen -W "${sass}" -- ${cur}) )
        return 0
    fi
    # For --unit-tests and -t, complete from a specific folder
    if [ ${prev} == --unit-tests ] || [ ${prev} == -t ] ; then
        COMPREPLY=( $(compgen -d -- ${cur}) )
        return 0
    fi
}
complete -o filenames -F __my_app_autocomplete my_app

Problem

I can't find a way to do it. Do you have any ideas?

Investigations

Using a variable containing the specific directory

Suggested by @D'Arcy Nader

Adding at the beginning of my_app_autocomplete

_directory=/absolute/path/to/the/directory/  

and then substitute the variable in the compgen command

# For --unit-tests and -t, complete with relative to my_app folder paths
if [ ${prev} == --unit-tests ] || [ ${prev} == -t ] ; then
    COMPREPLY=( $(compgen -d -- "${_directory}") )
    return 0
fi

Behavior:

Run

user@computer:~$ my_app --unit-tests [TAB][TAB]

do

user@computer:~$ my_app --unit-tests /absolute/path/to/the/directory/

It adds the path to the directory.

Run

user@computer:~$ my_app --unit-tests /absolute/path/to/the/directory/file.ext[TAB][TAB]

do

user@computer:~$ my_app --unit-tests /absolute/path/to/the/directory/

It removes the file.ext part.

Problems:

  • I don't want to add the specific path in the command line
  • It removes what I add after the specific directory instead of auto-complete it.
darckcrystale
  • 1,582
  • 2
  • 18
  • 40

2 Answers2

3

After a lot of try and error, I think I got a solution to your problem (which was my problem as well):

_complete_specific_path() {
  # declare variables
  local _item _COMPREPLY _old_pwd

  # if we already are in the completed directory, skip this part
  if [ "${PWD}" != "$1" ]; then
    _old_pwd="${PWD}"
    # magic here: go the specific directory!
    pushd "$1" &>/dev/null || return

    # init completion and run _filedir inside specific directory
    _init_completion -s || return
    _filedir

    # iterate on original replies
    for _item in "${COMPREPLY[@]}"; do
      # this check seems complicated, but it handles the case
      # where you have files/dirs of the same name
      # in the current directory and in the completed one:
      # we want only one "/" appended
      if [ -d "${_item}" ] && [[ "${_item}" != */ ]] && [ ! -d "${_old_pwd}/${_item}" ]; then
        # append a slash if directory
        _COMPREPLY+=("${_item}/")
      else
        _COMPREPLY+=("${_item}")
      fi
    done

    # popd as early as possible
    popd &>/dev/null

    # if only one reply and it is a directory, don't append a space
    # (don't know why we must check for length == 2 though)
    if [ ${#_COMPREPLY[@]} -eq 2 ]; then
      if [[ "${_COMPREPLY}" == */ ]]; then
        compopt -o nospace
      fi
    fi

    # set the values in the right COMPREPLY variable
    COMPREPLY=( "${_COMPREPLY[@]}" )

    # clean up
    unset _COMPREPLY
    unset _item
  else
    # we already are in the completed directory, easy
    _init_completion -s || return
    _filedir
  fi
}

I found this solution by looking at how cat is autocompleted. It uses the _longopt function, which in turn uses _filedir for arguments that are not options (not beginning with -).

Now you can declare a completion function for each directory you need, like:

_complete_git_home_path() {
  _complete_specific_path "${GIT_HOME}"
}

And attach it to the right commands:

complete -F _complete_git_home_path cdrepo lsrepo rmrepo cdwiki pyinst

Or use it inside your own completion function, to trigger it for a specific option like --unit-test!

pawamoy
  • 3,382
  • 1
  • 26
  • 44
0

Improvements upon @pawamoy answer

when calling:

_init_completion -s || return

if _init_completion return a non null value the script will exit without having executed the popd command, this could leave you in the directory specified when calling pushd (but it even crashes my terminal!). I suggest doing this instead (see grouping commands for { } explanation)

_init_completion -s || { popd > /dev/null 2>&1; return; }

Also if you aim for portability, &> redirection is non portable since it is not part of the official POSIX shell spec (see this answer), you should use

> /dev/null 2>&1

instead of

&> /dev/null
mDeram
  • 69
  • 1
  • 9