8

How to tap into the completion of another command programmatically?

Supposing my current directory has files a1, a2, and a3, then how can I make my command invoke the autocompletion of ls a to get back a1 a2 a3?

Is this possible?


Clarification and justification:

I chose ls because people can relate to it. It is a contrived example that is intentionally simple so that people can understand the question without distractions, but unfortunately such examples sometimes take us on tangents. :)

Let me try to exemplify the value of this feature. I have a command called build which, given a directory, can autocomplete to the targets that can be built in that directory. Those targets may not correspond to the files from that directory, and so glob completion (* and other wildcard characters) will not work. The targets might be mined by the build command from a build file that I don't want to be parsing. In other words:

build path/to/dir/TABTAB

Might give:

path/to/dir/a_target
path/to/dir/b_target

Again, a_target and b_target are not files or directories.

build is a pre-existing command, not something I can go ahead and modify to suit my purposes. And the manner in which it comes up with the valid completions is something I certainly don't want to know or reinvent.

Now suppose I have an entire repository of buildable projects, and most of my work and therefore most of my build work happens in only one project. In other words, I always build targets under my/project/directory.

So far so good.

I want to write a wrapper around the build command that doesn't require me to feed it the directory path each time I run it. I want it to know my preferred project directory (or directories, why not) and let me reference the targets without qualifying them:

So under the assumption that I have:

my/project/directory/a_target
my/project/directory/b_target

I want this:

mybuild TABTAB

to give me:

a_target
b_target

Basically I want to decorate and simplify the behavior of build to suit my particular needs.

I will need to write my own completion code for mybuild, but I want it to rely on the completion for build, because I can't ask the developers of build to code a build listtargets command just to make me happy. (Although that would be much more robust.) The build command already has code somewhere that given a prefix can return all the matching targets. It's in the completion for build, and I need to tap into it.

(Of course, when I run mybuild a_target, I will make sure that it knows to run build my/project/directory/a_target. I know how to implement this and it is not in scope for this question.)

I hope this illustrates why I need to tap into the completion of the build command and invoke it as a black box.

4
  • 2
    Why do you think that you need to automate this interactive feature of the shell? What are you trying to do? (ls a* will give you a list of all files starting with 'a'.) Commented Jul 1, 2014 at 15:39
  • See my newly added justification. Commented Jul 2, 2014 at 14:04
  • You can define your own completions for mybuild in bash, so that tab-completion will only suggest files that exist in a directory specified by some environment variable. I would suggest reading up on programmable completion in the bash man page (it's not the clearest documentation, but give it a try), and ask again after you've made a first try at coding it. Commented Jul 2, 2014 at 14:07
  • The completion for the build command doesn't work with files. It extracts build targets from some make file. I will define my own completion, but it needs to use build's completion to know what targets to present me with. Commented Jul 2, 2014 at 14:08

4 Answers 4

7

This is a bit of an odd thing to do, and the command you need to execute depends on the number of files in the directory - none, one, or more than one. But this command works for the example case:

echo echo a$'\t'$'\t' | bash -i 2>&1 | head -3 | tail -1

The command being autocompleted is

echo a

so send that as a character stream, followed by two tab characters, into an interactive bash shell. bash produces the autocompletion output on stderr, so redirect that to stdout and pipe that through head and tail to select one line of output from the whole. That produces, in this case, the one-line output

a1  a2  a3 

But, as others say, just using

echo a*

might be easier!

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

11 Comments

That's cool! You can say echo l$'\t\ty' | bash -i to get a list of commands starting with l also. I'm not sure how that is useful... yet :D
It hasn't worked in my initial testing, but I will sit on it to see if I can make it work. The question isn't about completing files. It's about completing anything. That's why I need to tap into the completion of a given command. My specific command doesn't complete files -- it completes build targets extracted from a manifest file with a complex structure that (a) I can't reasonably parse in a shell script, and (b) I don't want to parse in a shell script because I don't want to reinvent the wheel.
Hmm, it does work, with a couple of caveats: 1. I should suppress the PS1 (perhaps starting bash with a special bashrc will do the trick), and 2. the shell should not also attempt to execute the command (perhaps if I can send a Ctrl+C -- didn't work in initial testing).
I can suppress PS1 by using | PS1= bash -i, but I haven't yet had luck in preventing the command from executing.
So here it is in all its glory. echo if false\; then Header\; echo a$'\t'$'\t'y\; Footer fi | bash -i 2>&1 | sed -n '/Header/{:a;n;/Footer/q;p;ba}' | grep -v ^'Display all '. The sed bit captures the lines between header and footer. Of course we'll want to use more sensible, unique names in place of Header and Footer. It is important not to filter the unnecessary line using line counting jiu-jistu assuming that PS1 is on a single line. I've seen a colleague with a two-line PS1. Also note the y which responds to the possible Bash question "display all possibilities?" :D
|
3

Here is how you can run a completion function or executable for a command and collect its results.

While these are possible ways to achieve your goal, I have found that looking at the code of the completion function seeing how it does what it does has been more successful for me so at the end I give a few examples demonstrating what I mean.

When the completions are generated by a shell function

When completions are shell functions, complete -p <your-command> outputs complete -F <shell-function> <your-command>, that means that when the shell you press TAB while the first word on the command line is your command, bash will

  • Set some variables (look for COMP_WORDS in man bash)
  • Call this function
  • Look in the variable COMPREPLY expecting it to be a shell array of possible completions.

The following is an example for make. It assumes you have a Makefile in the current directory with some targets beginning with x and this will list all the targets that begin with 'x'.

#!/bin/bash

source /usr/share/bash-completion/bash_completion
source /usr/share/bash-completion/completions/make

COMP_WORDS=(make x)
COMP_CWORD=1 # The index of the word containing the cursor (the C is for cursor, not count)
COMP_LINE="make x"
COMP_POINT=${#COMP_LINE}
COMP_TYPE=9
COMP_KEY=$'\t'
COMPREPLY=()
_make
for comp in "${COMPREPLY[@]}" ; do
    echo "$comp is one of the completion candidates"
done

The thing is many completion functions might call compopt to change completion options during their execution which is an obstacle to calling them outside of bash completion.

When the completions are generated by a command

If complete -p <your-command> outputs complete -C <a-command> <your-command>, then that means that bash will run an external command.

  • First arg is the command
  • Second arg is the word being completed
  • Third arg is the word before the one being completed
  • The environment variables COMP_LINE, COMP_KEY, COMP_POINT, COMP_TYPE are also set (but not COMP_WORDS and COMP_CWORD).

Note that when completions come from a shell function, the function also gets the same three arguments but I've never seen a completion function that looks at them since completion functions can look at the COMP_WORDS array.

For this example, suppose our command is salad and its completions are generated by _salad, an executable (not a shell function).

#!/bin/bash

# assume command to be completed is salad
# and completions are generated by the executable
# ./_salad

COMP_WORDS=(salad -f ap)
COMP_CWORD=2 # The index of the word containing the cursor (the C is for cursor, not count)
COMP_LINE="salad -f ap"
COMP_POINT=${#COMP_LINE}
COMP_TYPE=9
COMP_KEY=$'\t'
oifs="$IFS"
IFS=$'\n'
candidates=($(./_salad salad ap -f))
IFS="$oifs"
for comp in "${candidates[@]}" ; do
    echo "$comp is one of the completion candidates"
done

Since this is an external command, it can't tell the difference between being called by us or by bash during a real completion. However, I don't know of any command that is completed this way.

What I usually do

The first thing I would recommend is to try to understand the completion function itself and possibly steal parts of it to suit your needs.

Example getting targets from a makefile

For example, for Makefiles, looking at the file _make that comes with bash_completion, I came up with

get_makefile_targets ()
{
    dir=$1
    prefix=$2
    #
    # Completion for `make` is normally lazy loaded the first time you invoke
    # completion for `make in a given shell.  This provides the function _make
    # and _make_target_extract_script used below
    #
    if [[ "$(type -t _make)" != function ]] ; then
        source /usr/share/bash-completion/bash_completion
        source /usr/share/bash-completion/completions/make
    fi

    ( 
        cd $dir
        make -npq __BASH_MAKE_COMPLETION__=1 | sed -nf <(_make_target_extract_script -- "$2") 
    )
}

Example scp: getting remote files

Another example, scp it completes filenames on the remote host when you do scp host:<TAB> so if that interests you, rather than triggering the completion and looking at the candidates, it may be easier to just look at how it does it.

We find that the function is _scp and looking at that function we see that it does a bunch of stuff but at some point, it has

case $cur in
    ...
    *:*) _scp_remote_files ; return
esac

so looking at _scp_remote_files(), we see that it does some escaping stuff and then does

path=${cur#*:}
host=${cur%%?(\\):*}
# escaping stuff
files=$(a big command)
COMPREPLY=($files)

so we could adapt this to our needs by doing

get_remote_files(){
    host=$1
    prefix=$2
    prefix=$(escaping stuff)
    a big command
}

Easier examples

A lot of times, the programs have functions that help with completion like tmux which has tmux list-commands if we want to get a list of subcommands.

Git also has this. Looking at git-completion.bash from the contrib directory, we see that it does git --list-cmds=list-main,porcelain,others,alias,... to make git itself give a list of possible commands.

We can also see that git commit --git-completion-helper produces the list of options for commit and the same thing works for other git subcommands.

Other tips

  1. Completions are normally lazy loaded so you may need to trigger it once before complete -p will tell you anything. For example: complete -p make in a brand new shell will usually say that there is nothing, but if you do make <TAB> and then run complete -p make, you will get complete -F _make make. This is because for commands that have no completion, bash invokes _load_completion <cmd> which will look in various directories for a file named _<cmd> (and some variations) and source the first one it finds, then reattempt completion.
  2. To find the where the completion function set shopt -s extdebug which causes declare -F <shell-function> to tell you the file where the shell function is defined. As described in point 1, you may need to trigger the completion once for the file containing that function to be sourced.
  3. A lot of these completion functions are loaded only in interactive shells so if you want to use them, you may need to explicitly source some files like bash_completion and _make for the first example.

Comments

1

Bash has something similar to this called globbing.

So for example in your case you could run the command

echo a*

Which would produce:

a1 a2 a3

This is very useful where you have spaces in the names of your files as you can say

for i in a*
do
   echo $i
done

And it would work for a1 as well as a 1

1 Comment

The thing is that command-specific completions are coded by someone and there is code somewhere that does the completion. I want to tap into that code. See my clarifications in the very question.
0

Auto-completion is a feature provided by your shell (e.g. bash). The shell will try to offer auto-complete suggestions based on the context of the command you're trying to run and the environment. E.g. it knows that some commands will work on files and can offer some auto-completion based on file paths. But the command itself is not aware on how the arguments it has been run with have been specified, either by the user or with help of auto-completion.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.