Customizing bash completion output: each suggestion on a new line
Asked Answered
P

2

24

When you type something, you often use bash autocompletion: you start writing a command, for example, and you type TAB to get the rest of the word.

As you have probably noticed, when multiple choices match your command, bash displays them like this :

foobar@myserv:~$ admin-
admin-addrsync         admin-adduser          admin-delrsync         admin-deluser          admin-listsvn
admin-addsvn           admin-chmod            admin-delsvn           admin-listrsync

I'm looking for a solution to display each possible solution on a new line, similar to the last column on a ls -l. Ever better, it would be perfect if I could apply a rule like this: "if you find less than 10 suggestions, display them one by line, if more => actual display".

Platinum answered 16/7, 2014 at 13:12 Comment(3)
Are you looking to do this for a specific custom completion you're working on, or are you looking to change this behavior globally?Milissa
I'm interested in both, actually !Platinum
I will accept @Milissa 's answer in a few days, but if ANYONE find a way to change this behaviour globally (ie, control the output format of bash completions), let me now, I'm really interested in !Platinum
M
10

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://mcmap.net/q/499244/-how-to-expand-ps1) 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://mcmap.net/q/499244/-how-to-expand-ps1.
        # !! 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
Milissa answered 18/7, 2014 at 5:48 Comment(2)
Great job @mklement0, really. That's a pitty we can change this behaviour globally, and that's suprised me (I thought we can do ANYTHING). About your answer, it nearly works : After the suggestions on each line, I get a "command not found" because of my colored PS1. You can try yourself by writing in your shell PS1='${debian_chroot:+($debian_chroot)}\[\033[01;31m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' and then test again your implementation - it crashed :(Platinum
@Ash_: Thanks for pointing out the prompt issue: it's nontrivial to fix, but I've put a workaround in place that should work with typical prompt strings (including coloring), including yours - but it's not foolproof.Milissa
S
8

bash 4.2+ (and, more generally, applications using readline 6.2+) support this with the use of the completion-display-width variable.

The number of screen columns used to display possible matches when performing completion. The value is ignored if it is less than 0 or greater than the terminal screen width. A value of 0 will cause matches to be displayed one per line. The default value is -1.

Run the following to set the behavior for all completions1 for your current session:

bind 'set completion-display-width 0'

Or modify your ~/.inputrc2 file to have:

set completion-display-width 0

to change the behavior for all new shells.

1 See here for a method for controlling this behavior for individual custom completion functions.

2 The search path for the readline init file is $INPUTRC, ~/.inputrc, /etc/inputrc so modify the file appropriate for you.

Sideling answered 13/2, 2017 at 23:39 Comment(3)
wow, this is insanely simple! Well done. I'm gonna deeply test it, and I will come fwd.Platinum
Better late than never, right? ;) This is a native solution, and a great one. However, is there a way to achieve the if more than X matches, go back to completion-display-width -1 with this? (@mkelement0 solution is working, but I'm curious about mixing both answers)Platinum
@Platinum a little hacky but you can accomplish that with the technique from my other answer I linked above, by changing completion-display-width during your function execution based on the number of matches generated and then have PROMPT_COMMAND set it back to the original value.Sideling

© 2022 - 2024 — McMap. All rights reserved.