find based filename autocomplete in Bash script

孤人 提交于 2019-12-04 17:29:51

You should take a look at this introduction to bash completion. Briefly, bash has a system for configuring and extending tab completion. Other shells do this, too, and each one has a different way to set it up. Using this system it is not necessary to do everything yourself and adding custom argument completion to a command is relatively easy.

Matching anywhere in the filename is rather complicated, and I'm not sure it's really all that useful. Matching at the start of filenames makes more sense and is much easier to implement, even recursively.

Now, you mentioned find as a requirement, but bash (since version 4.0) can also find files recursively, and it should be more efficient to let bash do that part. To match recursively in bash, you enable the globstar shell option by running shopt -s globstar, then two consecutive asterisks, **, will match recursively.

Next up, given that you want to match files recursively inside a git repository, we best have a way to detect that we're actually in a git repository, otherwise, if you accidentally trigger it in / for instance, your prompt will hang while waiting for bash to search through your entire filesystem. The following function should be fairly efficient at determining if we're inside a git repository. Given the current working directory, e.g. /foo/bar/baz, it'll look for /foo/bar/baz/.git, /foo/bar/.git, /foo/.git, /.git and return true if it finds one, false otherwise.

isgit() {
    local p=$PWD
    while [[ $p ]]; do
        [[ -d $p/.git ]] && return
        p=${p%/*}
    done
    return 1
}

For simplicity, we'll create a gadd command to add the completions for. A completion function can only be applied to the first word of the command. E.g. we can add completion for git, but not git add, thus we'll make a new command that turns git add into one word.

gadd() {
    git add "$@"
}

Now for the actual completion function. When triggered by hitting TAB, the function will be invoked with three arguments. $1 is the command being completed, $2 is the current word of the command line being completed, and $3 is the previous word on the line. So the files we want to search will be matched by the glob **/"$2"*; all files starting with "$2". We iterate these filenames, and append them to the COMPREPLY array. If the COMPREPLY array only contains one value when the function is done, the word will be replaced by that value. If it contains more than one value, hit tab another time to get a list of all the matches.

shopt -s globstar
_git_add_complete() {
    local file
    isgit || return
    for file in **/"$2"*; do
        # If the glob doesn't match, we'll get the glob itself, so make sure
        # we have an existing file
        [[ -e $file ]] || continue

        # If it's a directory, add a trailing /
        [[ -d $file ]] && file+=/
        COMPREPLY+=( "$file" )
    done
}
complete -F _git_add_complete gadd

Add the above three code blocks to your ~/.bashrc, then open a new terminal, enter a git repository and try gadd something<tab>.

Does this work?

$ cat .bash_completion
_foo()
{
    local files
    cur=${COMP_WORDS[COMP_CWORD]}
    local files=$(for x in `find -type f`; do echo ${x}; done)
    COMPREPLY=( $( compgen -W "${files}" -- ${cur} ) )
    return 0
}
complete -F _foo foo

$ . /etc/bash_completion
$ foo ./[tab]
holygeek

I wrote git-number so that I never have to hit tab when specifying files to git.

With git-number I can use numbers to represent the filenames that I want git to handle.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!