Adding New Features to bash Using Loadable Built-ins

The material in this recipe also appears in Learning the bash Shell by Cameron Newham (O’Reilly).

Problem

You have something that you’d like bash to do, but there’s no built-in command for it. For efficiency reasons, you want it to be built-in to the shell rather than an external program. Or, you already have the code in C and don’t want to or can’t rewrite it.

Solution

Use the dynamically loadable built-ins introduced in bash version 2.0. The bash archive contains a number of pre-written built-ins in the directory ./examples/ loadables/, especially the canonical hello.c. You can build them by uncommenting the lines in the file Makefile that are relevant to your system, and typing make. We’ll take one of these built-ins, tty, and use it as a case study for built-ins in general.

The following is a list of the built-ins provided in bash version 3.2’s ./examples/

basename.c

id.c

push.c

truefalse.c

cat.c

ln.c

realpath.c

tty.c

cut.c

logname.c

rmdir.c

uname.c

dirname.c

mkdir.c

sleep.c

unlink.c

finfo.c

necho.c

strftime.c

whoami.c

getconf.c

pathchk.c

sync.c

perl/bperl.c

head.c

print.c

tee.c

perl/iperl.c

hello.c

printenv.c

template.c

 

Discussion

On systems that support dynamic loading, you can write your own built-ins in C, compile them into shared objects, and load them at any time from within the shell with the enable built-in.

We will discuss briefly how to go about writing a built-in and loading it in bash. The discussion assumes that you have experience with writing, compiling, and linking C programs.

tty will mimic the standard Unix command tty. It will print the name of the terminal that is connected to standard input. The built-in will, like the command, return true if the device is a TTY and false if it isn’t. In addition, it will take an option, -s, which specifies that it should work silently (i.e., print nothing and just return a result).

The C code for a built-in can be divided into three distinct sections: the code that implements the functionality of the built-in, a help text message definition, and a structure describing the built-in so that bash can access it.

The description structure is quite straightforward and takes the form:

struct builtinbuiltin_name_struct = {
    "builtin_name",
    function_name,
    BUILTIN_ENABLED,
    help_array,
    "usage",
    0
};

The trailing _struct is required on the first line to give the enable built-in a way to find the symbol name. builtin_name is the name of the built-in as it appears in bash. The next field, function-name, is the name of the C function that implements the built-in. We’ll look at this in a moment. BUILTIN_ENABLED is the initial state of the built-in, whether it is enabled or not. This field should always be set to BUILTIN_ ENABLED. help_array is an array of strings that are printed when help is used on the built-in. usage is the shorter form of help: the command and its options. The last field in the structure should be set to 0.

In our example we’ll call the built-in tty, the C function tty_builtin, and the help array tty_doc. The usage string will be tty [-s]. The resulting structure looks like this:

struct builtin tty_struct =
    "tty",
    tty_builtin,
    BUILTIN_ENABLED,
    tty_doc,
    "tty [-s]",
    0
};

The next section is the code that does the work. It looks like this:

tty_builtin (list) WORD_LIST *list;
{
    int opt, sflag;
    char *t;

    reset_internal_getopt ( );
    sflag = 0;
    while ((opt = internal_getopt (list, "s")) != -1)
    {
      switch (opt)
      {
          case 's':
              sflag = 1;
              break;
          default:
              builtin_usage ( );
              return (EX_USAGE);
      }
    }
    list = loptend;

    t = ttyname (0);
    if (sflag == 0)
        puts (t ? t : "not a tty");
    return (t ? EXECUTION_SUCCESS : EXECUTION_FAILURE);
}

Built-in functions are always given a pointer to a list of type WORD_LIST. If the built-in doesn’t actually take any options, you must call no_options(list) and check its return value before any further processing. If the return value is nonzero, your function should immediately return with the value EX_USAGE.

You must always use internal_getopt rather than the standard C library getopt to process the built-in options. Also, you must reset the option processing first by calling reset_internal_getopt.

Option processing is performed in the standard way, except if the options are incorrect, in which case you should return EX_USAGE. Any arguments left after option processing are pointed to by loptend. Once the function is finished, it should return the value EXECUTION_SUCCESS or EXECUTION_FAILURE.

In the case of our tty built-in, we then just call the standard C library routine ttyname, and if the -s option wasn’t given, print out the name of the TTY (or “not a tty” if the device wasn’t). The function then returns success or failure, depending upon the result from the call to ttyname.

The last major section is the help definition. This is simply an array of strings, the last element of the array being NULL. Each string is printed to standard output when help is run on the built-in. You should, therefore, keep the strings to 76 characters or less (an 80-character standard display minus a 4-character margin). In the case of tty, our help text looks like this:

char *tty_doc[] = {
  "tty writes the name of the terminal that is opened for standard",
  "input to standard output. If the `-s' option is supplied, nothing",
  "is written; the exit status determines whether or not the standard",
  "input is connected to a tty.",
(char *)NULL
};

The last things to add to our code are the necessary C header files. These are stdio.h and the bash header files config.h, builtins.h, shell.h, and bashgetopt.h.

Here is the C program in its entirety:

# cookbook filename: builtin_tty.c

#include "config.h"
#include <stdio.h>
#include "builtins.h"
#include "shell.h"
#include "bashgetopt.h"


extern char *ttyname ( );

tty_builtin (list)
    WORD_LIST *list;
{
    int opt, sflag;
    char *t;

    reset_internal_getopt ( );
    sflag = 0;
    while ((opt = internal_getopt (list, "s")) != -1)
    {
        switch (opt)
        {
            case 's':
                sflag = 1;
                break;
            default:
                builtin_usage ( );
                return (EX_USAGE);
        }
    }
    list = loptend;

    t = ttyname (0);
    if (sflag == 0)
        puts (t ? t : "not a tty");
    return (t ? EXECUTION_SUCCESS : EXECUTION_FAILURE);
}

char *tty_doc[] = {
    "tty writes the name of the terminal that is opened for standard",
    "input to standard output. If the `-s' option is supplied, nothing",
    "is written; the exit status determines whether or not the standard",
    "input is connected to a tty.",
    (char *)NULL
};

struct builtin tty_struct = {
    "tty",
    tty_builtin,
    BUILTIN_ENABLED,
    tty_doc,
    "tty [-s]",
    0
};

We now need to compile and link this as a dynamic shared object. Unfortunately, different systems have different ways to specify how to compile dynamic shared objects.

The configure script should put the correct commands into the Makefile automatically. If for some reason it doesn’t, Table 16-1 lists some common systems and the commands needed to compile and link tty.c. Replace archive with the path of the top level of the bash archive.

Table 16-1. Common systems and commands to compile and link tty.c

System

Commands

SunOS 4

cc -pic -Iarchive -Iarchive/builtins -Iarchive/lib -c tty.c

ld -assert pure-text -o tty tty.o

SunOS 5

cc -K pic -Iarchive -Iarchive/builtins -Iarchive/lib -c tty.c

cc -dy -z text -G -i -h tty -o tty tty.o

SVR4, SVR4.2, Irix

cc -K PIC -Iarchive -Iarchive/builtins -Iarchive/lib -c tty.c

ld -dy -z text -G -h tty -o tty tty.o

AIX

cc -K -Iarchive -Iarchive/builtins -Iarchive/lib -c tty.c

ld -bdynamic -bnoentry -bexpall -G -o tty tty.o

Linux

cc -fPIC -Iarchive -Iarchive/builtins -Iarchive/lib -c tty.

ld -shared -o tty tty.o

NetBSD, FreeBSD

cc -fpic -Iarchive -Iarchive/builtins -Iarchive/lib -c tty.c

ld -x -Bshareable -o tty tty.o

After you have compiled and linked the program, you should have a shared object called tty. To load this into bash, just type enable -f tty tty. You can remove a loaded built-in at any time with the -d option, e.g., enable-d tty.

You can put as many built-ins as you like into one shared object as long as the three main sections for each built-in are in the same C file. It is best, however, to keep the number of built-ins per shared object small. You will also probably find it best to keep similar built-ins, or built-ins that work together (e.g., pushd, popd, dirs), in the same shared object.

bash loads a shared object as a whole, so if you ask it to load one built-in from a shared object that has 20 built-ins, it will load all 20 (but only one will be enabled). For this reason, keep the number of built-ins small to save loading memory with unnecessary things, and group similar built-ins so that if the user enables one of them, all of them will be loaded and ready in memory for enabling.

See Also

  • ./examples/loadables in any bash tarball newer than 2.0

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.57.126