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.

2 Answers 2

4

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!

Sign up to request clarification or add additional context in comments.

Comments

1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.