Customize Tab Completion in Shell

Custom bash tab completion

Write a bash completion script for cooltool. The bash-completion package comes with scripts for many popular programs, which you can use as examples.

Customize tab completion in shell

You will likely find a file on your system called /etc/bash_completion which is full of functions and complete commands that set up this behavior. The file will be sourced by one of your shell startup files such as ~/.bashrc.

There may also be a directory called /etc/bash_completion.d which contains individual files with more completion functions. These files are sourced by /etc/bash_completion.

This is what the wine completion command looks like from the /etc/bash_completion on my system:

complete -f -X '!*.@(exe|EXE|com|COM|scr|SCR|exe.so)' wine

This set of files is in large part maintained by the Bash Completion Project.

How can I make bash tab completion behave like vim tab completion and cycle through matching matches?

By default TAB is bound to the complete readline command. Your desired behavior would be menu-complete instead. You can change your readlines settings by editing ~/.inputrc. To rebind TAB, add this line:

TAB: menu-complete

For more details see the READLINE section in man bash.

Hints for custom bash completion

After reviewing a similar post about host completion bash autocompletion: add description for possible completions I got the right behaviour more or less. I am using - as a delimiter in my job id query command

function _mycurrentjobs()
{
local cur=${COMP_WORDS[COMP_CWORD]}
local OLDIFS="$IFS"
local IFS=$'\n'
COMPREPLY=( $(compgen -W "$(bjobs -noheader -u $USER \
-o "JOBID JOB_NAME delimiter='-'")" -- $cur))
IFS="$OLDIFS"
if [[ ${#COMPREPLY[*]} -eq 1 ]]; then #Only one completion
COMPREPLY=( ${COMPREPLY[0]%%-*} ) #Remove the separator and everything after
fi
return 0
}
complete -F _mycurrentjobs bkill bjobs bstatus bpeek bstop bresume

Customizing bash completion output: each suggestion on a new line

bash prior to version 4.2 doesn't allow any control over the output format of completions, unfortunately.

Bash 4.2+ allows switching to 1-suggestion-per-line output globally, as explained in Grisha Levit's helpful answer, which also links to a clever workaround to achieve a per-completion-function solution.

The following is a tricky workaround for a custom completion.
Solving this problem generically, for all defined completions, would be much harder (if there were a way to invoke readline functions directly, it might be easier, but I haven't found a way to do that).

To test the proof of concept below:

  • Save to a file and source it (. file) in your interactive shell - this will:

    • define a command named foo (a shell function)
    • whose arguments complete based on matching filenames in the current directory.
    • (When foo is actually invoked, it simply prints its argument in diagnostic form.)
  • Invoke as:
    foo [fileNamePrefix], then press tab:

    • If between 2 and 9 files in the current directory match, you'll see the desired line-by-line display.
    • Otherwise (1 match or 10 or more matches), normal completion will occur.

Limitations:

  • Completion only works properly when applied to the LAST argument on the command line being edited.
  • When a completion is actually inserted in the command line (once the match is unambiguous), NO space is appended to it (this behavior is required for the workaround).
  • Redrawing the prompt the first time after printing custom-formatted output may not work properly: Redrawing the command line including the prompt must be simulated and since there is no direct way to obtain an expanded version of the prompt-definition string stored in $PS1, a workaround (inspired by https://stackoverflow.com/a/24006864/45375) is used, which should work in typical cases, but is not foolproof.

Approach:

  • Defines and assigns a custom completion shell function to the command of interest.
  • The custom function determines the matches and, if their count is in the desired range, bypasses the normal completion mechanism and creates custom-formatted output.
  • The custom-formatted output (each match on its own line) is sent directly to the terminal >/dev/tty, and then the prompt and command line are manually "redrawn" to mimic standard completion behavior.
  • See the comments in the source code for implementation details.
# Define the command (function) for which to establish custom command completion.
# The command simply prints out all its arguments in diagnostic form.
foo() { local a i=0; for a; do echo "\$$((i+=1))=[$a]"; done; }

# Define the completion function that will generate the set of completions
# when <tab> is pressed.
# CAVEAT:
# Only works properly if <tab> is pressed at the END of the command line,
# i.e., if completion is applied to the LAST argument.
_complete_foo() {

local currToken="${COMP_WORDS[COMP_CWORD]}" matches matchCount

# Collect matches, providing the current command-line token as input.
IFS=$'\n' read -d '' -ra matches <<<"$(compgen -A file "$currToken")"

# Count matches.
matchCount=${#matches[@]}

# Output in custom format, depending on the number of matches.
if (( matchCount > 1 && matchCount < 10 )); then

# Output matches in CUSTOM format:
# print the matches line by line, directly to the terminal.
printf '\n%s' "${matches[@]}" >/dev/tty
# !! We actually *must* pass out the current token as the result,
# !! as it will otherwise be *removed* from the redrawn line,
# !! even though $COMP_LINE *includes* that token.
# !! Also, by passing out a nonempty result, we avoid the bell
# !! signal that normally indicates a failed completion.
# !! However, by passing out a single result, a *space* will
# !! be appended to the last token - unless the compspec
# !! (mapping established via `complete`) was defined with
# !! `-o nospace`.
COMPREPLY=( "$currToken" )
# Finally, simulate redrawing the command line.
# Obtain an *expanded version* of `$PS1` using a trick
# inspired by https://stackoverflow.com/a/24006864/45375.
# !! This is NOT foolproof, but hopefully works in most cases.
expandedPrompt=$(PS1="$PS1" debian_chroot="$debian_chroot" "$BASH" --norc -i </dev/null 2>&1 | sed -n '${s/^\(.*\)exit$/\1/p;}')
printf '\n%s%s' "$expandedPrompt" "$COMP_LINE" >/dev/tty

else # Just 1 match or 10 or more matches?

# Perform NORMAL completion: let bash handle it by
# reporting matches via array variable `$COMPREPLY`.
COMPREPLY=( "${matches[@]}" )

fi

}

# Map the completion function (`_complete_foo`) to the command (`foo`).
# `-o nospace` ensures that no space is appended after a completion,
# which is needed for our workaround.
complete -o nospace -F _complete_foo -- foo

How can I customize tab completion in Perl's Term::Shell?

With the comp_* style handlers one can only match one's completions against the last incomplete word. Fortunately, however, you can get the desired result by overriding the catch_comp function like below; it lets one match against whole command line:

my %completion_tree = (
stack => { under => [],
over => [qw(flow sample junk)] }
);

sub catch_comp {
my $o = shift;
my ($cmd, $word, $line, $start) = @_;

my $completed = substr $line, 0, $start;
$completed =~ s/^\s*//;

my $tree = \%completion_tree;
foreach (split m'\s+', $completed) {
last if ref($tree) ne 'HASH';
$tree = $tree->{$_};
}

my @completions;
$_ = ref($tree);
@completions = @$tree if /ARRAY/;
@completions = keys %$tree if /HASH/;
@completions = ($tree)if /SCALAR/;

return $o->completions($word, [@completions]);
}

Possible to change tab completion behavior in fish shell?

I commend you for writing up this detailed and thoughtful post, and it deserves an equally detailed and thoughtful response!

The tab completion behavior has been rewritten in fish top-of-tree (not yet released), and is referred to as the "new pager." You can see the design goals and discussion here. I put a note at the bottom of this reply for how to get it.

Shells are personal, and like anything personal, rationalizations and justifications aren't worth anything: you either like it, or you don't, and we may not even be conscious of the factors influencing our feelings. So all I can really say is try it, see how you feel, and (please) report back.

I put up a short little screencast of the new pager on YouTube. Things to notice: 1. the menu is dismissed just by typing more, 2. it "unfurls" progressively (requires a few tabs to become fully visible), never modally takes over your screen even when there's a huge number of completions, and is easily searchable and navigable, and 3. escape will always dismiss it and put your command line back to just what you typed.

Let me go through your concerns individually:

"I have to hit tab an unknown number of times to get the value I wanted". With the new pager, the selected item is highlighted in the menu. This sounds minor, but personally I believe this makes a huge difference: the number of additional times to hit tab becomes known, and since your finger is over tab, it's often easier to just hit it a few more times than to type additional letters. You can also use the arrow keys to navigate.

"I have no way of getting the entry context back to only the letters that I've actually typed up to this point". With the new pager, the escape key does exactly that. It's easy to press since escape is right above tab, where your finger is.

"What if there happen to be a whole bunch of things that start with ba in this directory -- I'm totally screwed is what happens". Neither bash nor old-pager-fish handles large numbers of completions well. fish would drop you into this modal paging environment, while bash breaks your flow with the modal "Display all 1002 possibilities? (y or n)" dialog that forces you to stop what you're doing and hit 'n'.

I think you'll love how the new pager handles this. Initially you get a short menu, that fills a maximum of five lines below your prompt (not above, and not replacing). This menu is non-modal, and is dismissed by typing more or hitting escape. If you hit tab again, the menu grows to show more completions, but is still non-modal. There's never a jarring transition.

"it does not allow using tab to complete nested paths" Sorry, I'm not sure what you mean by this. Both bash and fish append a / when tab completing a directory.

"much more difficult to discover disambiguation sequences when in large directories" With the new pager, you can hit escape, type some more, and then tab again. Or you can search the menu: put the focus in the menu and type something, and it's filtered. See the screencast above.

"in general requires you to 'be careful' before you hit tab which makes you hit it less often and decreases its utility" A very valid point, which the new pager addresses in a few ways. First of all, it uses a notion of progressive disclosure, which means that it takes "work" to output a lot of data. Second, it never "takes over your screen" like the old modal pager. And lastly, you can hit escape to get back to just what you typed, and since the pager appears below the prompt, it won't leave little turds in your scrollback like bash does.

If you're using homebrew, you can install from master via brew install fish --HEAD. There's also nightly builds for Linux. And lastly, feel free to open an issue at https://github.com/fish-shell/fish-shell/issues with any ideas for improvements you have.

Creating a tab completion script. In one case I want to use the completion for another command (e.g. git), is it possible?

We might use _completion_loader, (which is used to define completions on the fly) in order to load git completions.

Then we might observe in official git-completion.sh file, that function used to complete git "submodules" is called _git, so we can simply pass it to complete via -F <function> option, see complete documentation.

So by writing

source /usr/share/bash-completion/bash_completion
_completion_loader git

complete -F _git all-git.sh

we can achieve desired behaviour (if I got your issue correctly).

Note: That bash_completion script, might be located on your machine in different folder. I've tested it on Ubuntu 16.04 with bash 4.3.48.

EDIT: In case this doesn't fully solve your issue, I believe this is the way you want to go :). You can pretty much bend the solution for your needs.

EDIT-2: I've also assumed, you have already installed and working git completions.

EDIT-3: I've found a few interesting links, which are not solving your particular issue, but can give some light and deeper knowledge:

  • Accessing tab-completion programmatically in Bash
  • Perform tab-completion for aliases in Bash
  • Accessing bash completions for specific commands programmatically


Related Topics



Leave a reply



Submit