3

There is a command line feature I've been wanting for a long time, and I've thought about how to best realize it, but I got nothing...

So what I'd like to have is when I start typing a filename and hit tab,for example:

# git add Foo<tab>

I'd like it to run a find . -name "*$1*" and basically autocomplete the complete path to the matched File to my command line.

What I have so far:

I know I'll have to write a function that will call the app with the parameters I want, for example git add. After that it needs to catch the tab-keystroke event and do the find mentioned above, and display the results if many, or fill in the result if one.

What I haven't been able to figure out:

How to catch the tab key event within a function within function.

So basically in pseudocode:

gadd() {git add autocomplete_file_search($1)}

autocomplete_file_search(keyword) {
  if( tab-key-pressed ){
    files = find . -name "*$1*";
    if( filecount > 1 ) {
      show list;
    }
    if( files == 1 ) {
      return files
    }
  }
}

Any ideas?

thanks.

4 Answers 4

3
+200

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>.

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

4 Comments

Problem is, I would like all sorts of commands, editor, ls, git etc to work while inside the repo.
@chx, Then either use that function for all those commands, or set it as a default completion. Default completion will apply to all commands that doesn't have a completion defined already, so it won't work if you're using the bash-completion package for instance. Setting a default completion: complete -F _git_add_complete -o default -o bashdefault -D You might want to rename the function to something more fitting though.
So there's no clean way to do this when bash-completion is used?
@chx, well you have to override the current completion for each command you care about that you want "git-completable". You can check individual commands with complete -p. E.g. complete -p ls. The ones that don't have completion set already will use the default one; complete -p -D. I don't use bash-completion myself, so I'm not sure how many of its completions you may need to override.
1

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.

Comments

0

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]

Comments

0

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.

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.