Appendix D. Programmable Completion

 < Day Day Up > 

Programmable completion is a feature that was introduced in bash 2.0.[1] It extends the built-in textual completion that is discussed in Chapter 2 by providing hooks into the completion mechanism. This means that it is possible to write virtually any form of completion desired. For instance, if you were typing the man command, wouldn't it be nice to be able to hit TAB and have the manual sections listed for you. Programmable completion allows you to do this and much more.

[1] Technically it was added in bash Version 2.04.

This Appendix will only look at the basics of programmable completion. While completion is a feature you are very likely to use in everyday shell operation, you are unlikely to need to delve into the inner depths and actually write your own completion code. Fortunately the feature has been around for some time and there are already several libraries of completion commands developed by other people. We'll just outline the basic commands and procedures needed to use the completion mechanism should you ever need to work on it yourself.

In order to be able to do textual completion in a particular way you first have to tell the shell how to do it when you press the TAB key. This is done via the complete command.

The main argument of complete is a name that can be the name of a command or anything else that you want textual completion to work with. As an example we will look at the gunzip command that allows compressed archives of various types to be uncompressed. Normally, if you were to type:[2]

[2] For the rest of this Appendix we will denote typing a TAB character as [TAB].

$ gunzip [TAB][TAB]

you would get a list of filenames from which to complete. This list will include all kinds of things that are unsuitable for the gunzip command. What we really would like is the subset of those files that are suitable for the command to work on. We can set this up by using complete:[3]

[3] In order for @(...) to work you will need extended pattern matching switched on (shopt -s extglob).

complete -A file -X '!*.@(Z|gz|tgz)' gunzip

Here we are telling completion mechanism that when the gunzip command is typed in we want it to do something special. The -A flag is an action and takes a variety of arguments. In this case we provide file as the argument, which asks the mechanism to provide a list of files as possible completions. The next step is to cut this down by selecting only the files that we know will work with gunzip. We've done this with the -X option, which takes as its argument a filter pattern. When applied to the completion list the filter removes anything matching the pattern, i.e., the result is everything that doesn't match the pattern. gunzip can uncompress a number of file types including those with the extensions .Z, .gz, and .tgz. We want to match all filenames with extensions that have one of these three patterns. We then have to negate this with a ! (remember, the filter removes the patterns that match).

We can actually try this out first and see what completions would be returned without having to install the completion with complete. We can do this via the compgen command:

compgen -A file -X '!*.@(Z|gz|tgz)'

This produces a list of completion strings (assuming you have some files in the current directory with these extensions). compgen is useful for trying out filters to see what completion strings are produced. It is also needed when more complex completion is required. We'll see an example of this later in the Appendix.

Once we install the complete command above, either by sourcing a script with it in or executing it on the command line, we can use the augmented completion mechanism with the gunzip command:

$gunzip [TAB][TAB] archive.tgz  archive1.tgz  file.Z $gunzip 

You can probably see that there are other things we could do. What about providing a list of possible arguments for specific options to a command? For instance, the kill command can takes a process ID but can optionally take a signal name preceded by a dash (-) or a signal name following the option -n. We should be able to complete with PIDs but, if there is a dash or a -n, with signal names.

This is slightly more complex than the one-line example above. Here we will need some code to distinguish what has already been typed in. We'll also need to get the PIDs and the signal names. We'll put the code in a function and call the function via the completion mechanism. Here's the code to call our function, which we'll name _kill:

complete -F _kill kill

The -F option to complete tells it to call the function named _kill when it is performing textual completion for the kill command. The next step is to code the function:

_kill( ) {     local cur         local sign     COMPREPLY=( )     cur=${COMP_WORDS[COMP_CWORD]}     if (($COMP_CWORD == 2)) && [[ ${COMP_WORDS[1]} == -n ]]; then        # return list of available signals            _signals     elif (($COMP_CWORD == 1 )) && [[ "$cur" == -* ]]; then        # return list of available signals        sign="-"        _signals         else        # return list of available PIDs        COMPREPLY=( $( compgen -W '$( command ps axo pid | sed 1d )' $cur ) )     fi }

The code is fairly standard apart from the use of some special environment variables and a call to a function called _signals, which we'll come to shortly.

The variable COMPREPLY is used to hold the result that is returned back to the completion mechanism. It is an array that holds a set of completion strings. Initially this is set to an empty array.

The local variable cur is a convenience variable to make the code more readable because the value is used in several places. Its value is derived from an element in the array COMP_WORDS. This array holds the individual words on the current command line. COMP_CWORD is an index into the array; it gives the word containing the current cursor position. The value of cur is the word currently containing the cursor.

The first if statement tests for the condition where the kill command is followed by the -n option. If the first word was -n and we are on the second word, then we need to provide a list of signal names for the completion mechanism.

The second if statement is similar, except this time we are looking to complete on the current word, which starts with a dash and is followed by anything else. The body of this if again calls _signals but this time it sets the sign variable to a dash. The reason for this will become obvious when we look at the _signals function.

The remaining part in the else block returns a list of process IDs. This uses the compgen command to help in creating the array of completion strings. First it runs the ps command to obtain a list of PIDs and then pipes the result through sed to remove the first line (which is the heading "PID").[4] This is then given as an argument to the -W option of compgen, which takes a word list. compgen then returns all completion strings that match the value of the variable cur and the resulting array is assigned to COMPREPLY.

[4] On AIX and Solaris you will have to use the command ps -efo pid.

compgen is important here because we can't just return the complete list of PIDs provided by ps. The user may have already typed part of a PID and then attempted completion. As the partial PID will be in the variable cur, compgen restricts the results to those that match or partially match that value. For example if cur had the value 5 then compgen would return only values beginning with a "5", such as 5, 59 or 562.

The last piece of the puzzle is the _signals function:

_signals( ) {         local i         COMPREPLY=( $( compgen -A signal SIG${cur#-} ))         for (( i=0; i < ${#COMPREPLY[@]}; i++ )); do                 COMPREPLY[i]=$sign${COMPREPLY[i]#SIG}         done }

While we can get a list of signal names by using complete -A signal, the names are unfortunately not in a form that is very usable and so we can't use this to directly generate the array of names. The names generated begin with the letters "SIG" while the names needed by the kill command don't. The _signal function should assign to COMPREPLY an array of signal names, optionally preceded by a dash.

First we generate the list of signal names with compgen. Each name starts with the letters "SIG". In order to get complete to provide the correct subset if the user has begun to type a name, we add "SIG" to the beginning of the value in cur. We also take the opportunity to remove any preceding dash that the value has so it will match.

We then loop on the array removing the letters "SIG" and adding a dash if needed (the value of the variable sign) to each entry.

Both complete and compgen have many other options and actions; far more than we can cover in a few simple exercises. If you are interested in taking programmable completion further, we recommend looking in the bash manual and downloading some of the many examples that are available on the Internet or in the bash archive under bash-3.0\examples\complete.

As you can see, textual completion can get quite involved and creating the necessary code can be time-consuming. Fortunately there are already completion libraries available for bash. One of these is the bash Completion Project, which can be found at http://freshmeat.net/projects/bashcompletion/ .

     < Day Day Up > 


    Learning the bash Shell
    Learning the bash Shell: Unix Shell Programming (In a Nutshell (OReilly))
    ISBN: 0596009658
    EAN: 2147483647
    Year: 2005
    Pages: 139

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net