How to Bash Complete Three-Part Pattern
Asked Answered
O

1

15

I have a command line tool which takes arguments in an three-part form:

$ t first_second_third

I have a set of valid values for first, a set of valid values for second, and a set of valid values for third. I want to use Bash complete functionality to complete each part of the option value, as in this example:

$ t [tab][tab]           # shows options for first part
walk run skip bike
$ t w[tab]               # completes first part and appends delimiter
$ t walk_[tab][tab]      # shows options for second part
home work park
$ t walk_h[tab]          # completes second part and appends delimiter
$ t walk_home_[tab][tab] # shows options for second part
morning afternoon evening
$ t walk_home_a[tab]     # completes second part and appends space
$ t walk_home_afternoon

I have this code:

_tool () {
  local cur="${COMP_WORDS[COMP_CWORD]}"

  local first="walk run skip bike"
  local second="home work park"
  local third="morning afternoon evening"

  case "${cur}" in
    *_*_*)
      COMPREPLY=( $(compgen -W "${third}" -- "") ); return 0;;
    *_*)
      COMPREPLY=( $(compgen -W "${second}" -S "_" -- ${cur}) ); compopt -o nospace; return 0;;
    *)
      COMPREPLY=( $(compgen -W "${first}" -S "_" -- ${cur}) ); compopt -o nospace; return 0;;
  esac
}

complete -F _tool t

The first clause works great; nothing special there. But the second clause gives me no options or completions.

$ t [tab][tab]
bike_  run_   skip_  walk_
$ t b[tab]
$ t bike_[tab][tab] # nothing but bells

I replaced the second clause with the following (i.e. I replaced ${cur} with an empty string in the COMPREPLY call):

COMPREPLY=( $(compgen -W "${second}" -S "_" -- "") ); compopt -o nospace; return 0;;

I get a list of all options on the command line, but no completions.

$ t bike_[tab][tab]
home_  park_  work_
$ t bike_h[tab][tab]
home_  park_  work_
$ t bike_ho[tab][tab]
home_  park_  work_

I thought there might be some bad interaction with ${cur} and the COMPREPLY word list, so I changed the second clause again, adding a prefix which matches the first part of the current word:

local prefix=( $(expr "${cur}" : '\(.*_\)') )
COMPREPLY=( $(compgen -W "${second}" -P "${prefix}" -S "_" -- "") ); compopt -o nospace; return 0;;

This did not help much. With ${cur} still in the COMPREPLY command, I got no options or completions again. With an empty string instead, I got the full option (not just the curent part). But, pressing tab would erase what is typed.

$ t bike_[tab][tab]
bike_home_  bike_park_  bike_work_
$ t bike_ho[tab]
$ t bike_ # "ho" was erased

Of course, I have the same problem with the third part, too.

  1. How can I get Bash to complete the current word, part by part?
  2. How can I get Bash to not append the delimiter until an option has been selected?

This is similar to another Bash complete question, but I do not want to list every possible permutation of the values and they are not formatted as filenames (not sure if the filename pattern makes a difference).

Thanks for the help!
--ap

Optime answered 28/4, 2013 at 0:50 Comment(4)
I might be able to solve this problem by setting the special $IFS variable to include the _ character? I will need to give that a try.Optime
Hi Alexander, see my answer below, it should work fine. I also added an explanation to help you understand it better, as I don't understand it well myself :DSypher
Thanks, @msb. I will give it a shot and let you know.Optime
bash completion is a finicky beast, there are a lot of hidden pitfalls, which is why I use a bash completion framework that lets you define your completion hierarchy in yaml and let the framework figure out the complexities for you github.com/arikast/askbashBrozak
S
5

I was able to create what you want. See my code below:

_tool () {
  local cur="${COMP_WORDS[COMP_CWORD]}"

  local first="walk run skip bike"

  case "${cur}" in
    *_*_*)
      local firstsecond=`echo ${cur} | awk -F '_' '{print $1"_"$2}'`
      local third="${firstsecond}_morning ${firstsecond}_afternoon ${firstsecond}_evening"
      COMPREPLY=( $(compgen -W "${third}" -- ${cur}) ); return 0;;
    *_*)
      local firstcomp=`echo ${cur} | awk -F '_' '{print $1}'`
      local second="${firstcomp}_home ${firstcomp}_work ${firstcomp}_park"
      COMPREPLY=( $(compgen -W "${second}" -S "_" -- ${cur}) ); compopt -o nospace; return 0;;
    *)
      COMPREPLY=( $(compgen -W "${first}" -S "_" -- ${cur}) ); compopt -o nospace; return 0;;
  esac
}

complete -F _tool t

I am not much familiar with autocomplete, and it was the first time I used compgen or complete, so I'm not sure I can answer your questions about the theory of the thing. I can tell you what I did, in case it helps you understand how this badly-documented functions work. First I tried to understand what you had. I reproduced your problems successfully. Then I had this idea that, even though you didn't want to list every permutation yourself in the code, I could use the local variables to mimic as if it had. The first thing I did was to get the "first" element after we had the first underscore, and add the second element to it, and feed that to compgen. To my surprise, it worked fine. Then I tried to do the same to the 3rd level... and then problems started. I found out that the second part was not working as fine as I had imagined. after completing the first word, I would get the suggestions for the second; after chosing one word to be the second and hit [TAB], the first underscore would disappear.

$ t [tab][tab]           # shows options for first part
bike_ run_ skip_ walk_
$ t w[tab]               # completes first part and appends delimiter
$ t walk_[tab][tab]      # shows options for second part
walk_home_ walk_park_ walk_work_
$ t walk_h[tab]          # completes second part and appends delimiter
$ t walk_home_[tab][tab] # screws up with it
$ t walkhome_            

I tried moving the underscore into awk, to no success:

      local first=`echo ${cur} | awk -F '_' '{print $1"_"}'`
      local second="${first}home ${first}work ${first}park"

For some reason I had to change the variable name to something different from "first", and it started working. Then, on the 3rd element I had the same problem that you described last, where whatever I typed in the 3rd element would disappear, but [tab][tab] would give me the options:

$ t walk_home_[tab][tab] # shows options for second part
morning afternoon evening
$ t walk_home_aft[tab]     # erases what I typed
$ t walk_home_

To which I added ${cur} to compgen in the 3rd word and it worked. This is just a summary of what I did... but I got a working version, so I'm happy with it. :-)

Sypher answered 4/5, 2013 at 2:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.