Build Your Own Commands

UNIX is remarkably capable and includes hundreds of different commands, but sometimes that's not sufficient. When it becomes time to create your own unique commands, a simple shell script often suffices.

Task 15.1: Shell Variables

Programming languages usually include variables, and the shell naturally does too. Variables are just tags to identify values that may change as a program is used. In the shell, these variables can take a single value and are always interpreted as strings. Even numeric values are strings to the shell.


The C shell and Korn shell both support arrays and have means of representing numeric values.


You can use any string-manipulation command, such as sed or cut, to change a shell variable.

  1. Here is an example of setting the value of a shell variable:

    $ color=blue
    							

    This sets the variable color to the string blue. One can output the value of any variable with the echo command:

    $ echo $color
    blue
    

    This also indicates how to reference a shell variable: It must be preceded by the dollar sign ($). This can cause some problems.

  2. If you are using a shell variable as a prefix, and you want to append text immediately, you might think that this would work:

    $ leaning='anti-'
    $ echo Joe is basically $leaningtaxes
    							

    The output here is just Joe is basically. The shell does not know to differentiate between the variables $leaning and what appears to be a new variable, $leaningtaxes. Because no value is assigned to $leaningtaxes, the output is a NULL string. To solve this problem, enclose the variable in curly braces.

    $ echo Joe is basically ${leaning}taxes
    Joe is basically anti-taxes
    

    If leaning is undefined, the output might not make sense. It would be Joe is basically taxes. Fortunately, the shell provides a means to have a default value if a variable is undefined:

    $ echo Joe is basically ${leaning:-pro }taxes
    Joe is basically pro taxes
    
  3. If leaning is undefined, the :- syntax tells the shell to use the following string, including the space character, instead of leaving the output blank. This does not assign a new value to the variable. If you need to use the variable repeatedly, you might want to assign a new value to it, if it is undefined. The = character does this:

    $ echo Joe is basically ${leaning=pro }taxes and ${leaning}spending.
    Joe is basically pro taxes and pro spending.
    

    The first interpretation of the variable finds it undefined, so the shell assigns “pro ” (pro with a trailing space) to the variable and outputs that. The second time the variable is interpreted, it has the value “pro ”.

  4. Variables often are assigned by the read command. This assigns an individual word to a specified variable, with the last variable in the list being assigned the remaining words.

    $ read city state message
    CapeMay, New Jersey Hi Mom!
    $ echo $city is city
    CapeMay, is city
    $ echo $state is state
    New is state
    $ echo $message is message
    Jersey Hi Mom! is message
    

    As you can see, only New is assigned to state. The best way around this is to escape the space (make sure that the space isn't interpreted as separating variable values) with a backslash:

    $ read city state message
    CapeMay, NewJersey Hi Mom!
    $ echo $city is city
    CapeMay, is city
    $ echo $state is state
    New Jersey is state
    $ echo $message is message
    Hi Mom! is message
    

    This can be a bit tricky at first.

  5. The third common way to assign variables is from command-line arguments. The shell has built-in variables to access the command line. If you've written a script to copy files and named it copy-files, you might want to list all the files on the command line:

    $ copy-files file1 file2 file3
    							

    The program would access these arguments as $1, $2, and $3:

    cp $1 destination
    cp $2 destination
    cp $3 destination
    

    The $0 variable is a special case for looking at the command name, and $* lists all the command-line variables.

The standard data in any shell program is the variable. These variables can be assigned in several ways: directly assigned, read in from a user's typing, or assigned from the command line. The shell also provides means to provide some default manipulation of variables.


Task 15.2: Shell Arithmetic

Although the shell treats variables as strings, there are methods for performing some basic mathematics on shell variables. Again, the C shell and the Korn shell provide more extensive mathematical capabilities.


  1. If a shell is assigned a numeric value, you can perform some basic arithmetic on the value by using the command expr. This command takes several arguments to perform arithmetic:

    $ expr 1 + 1
    2
    

    Arguments must be separated by spaces, and present, for the expr command to work. If a variable is undefined or does not have a value assigned to it (sometimes called zero length), the result is a syntax error. Here is where the :- syntax is particularly helpful:

    $ expr $undef + 1
    expr: syntax error
    $ expr ${undef:-0}  + 1
    1
    

    expr also supports subtraction, multiplication, integer division, and remainders. These are illustrated here:

    $ expr 11 - 5
    6
    $ expr 11 '*' 5
    55
    $ expr 11 / 5
    2
    $ expr 11 % 5
    1
    

    Note that I had to include the asterisk in single quotation marks. If I didn't do that, the shell would expand it to be the list of files in the current directory, and the expr program wouldn't understand that.

  2. You can assign the results of the arithmetic to other variables by enclosing the command in backquotes:

    $ newvalue=`expr ${oldvalue:-0) + 1`
    							

    If oldvalue is assigned, it is incremented by 1. If not, newvalue is set to 1. This is useful when looping through data for a number of iterations.

  3. The expr command also can work with complex arithmetic. You can write an expression to add two numbers and then multiply by a third number. Normally, you would need to worry about operator precedence, but expr is not that sophisticated. Instead, you just group the operations in parentheses:

    $ expr ( 11 + 5 ) * 6
    2
    

    This command first adds 11 and 5, then multiplies the result by 6. Because the parentheses are important shell characters, I need to escape them with backslashes.

The expr is a very useful command for performing arithmetic in the Bourne shell. Strings must be numbers, or there will be errors; and the results of the expr command can be assigned to other variables.


The expr command is much more powerful than described here; it includes the capability to perform logical operations and perform operations on strings. For more information, check the man page.


Task 15.3: Comparison Functions

Often, when writing a program, you may want the actions taken to be dependent on certain values. A simple example is the rm -i command, where the -i flag tells rm to prompt you before deleting a file. Type y, and a file is deleted. Type n, and it remains. The shell also has similar options. These next two tasks cover how to use those options.


Just as expr is a powerful program for solving arithmetic expressions and performing operations on strings, the test command can be used to perform comparisons. test will perform comparisons on strings, as well as numeric values. test will always return 1 if the condition is true, and 0 if it is false. It is standard for UNIX shells to use these values as true and false.

There are three types of operations for which test is used: numeric comparisons, string comparisons, and status tests for the file system. First up are the numeric comparisons.

  1. Because the shell treats the less-than and greater-than symbols as redirection characters, they can't be used within the test command. Instead, I have a series of two-letter flags, as described in Table 15.1. These flags are always placed between the two arguments:

    test 3 -eq 4
    

    This example would return false because 3 and 4 are not equal.

Table 15.1. Test Operators
Comparison Flag Meaning
-eq True if the numbers are equal
-ne True if the numbers are not equal
-lt True if the first number is less than the second number
-le True if the first number is less than or equal to the second number
-gt True if the first number is greater than the second number
-ge True if the first number is greater than or equal to the second number

  1. You can use the result of expr, or any other command that returns a numeric value, in test. There is also a special expression in test, -l string, that returns the length of a string. So you can write the following tests:

    test $value -eq -l $string
    test `wc -l filename` -ge 10000
    

    The first test determines whether $value is the same as the length of $string. The second takes a count of the number of lines in a file, and it is true if 10,000 or more lines are present.

  2. The second type of comparison is on strings. The first two are unary, which means they apply to only one string:

    test -z $string
    test -n $string
    

    The first test is true if the string is of zero length or undefined. The second is true if the string has some content.

  3. The next two tests compare strings with each other. The simple equals sign and the exclamation point (commonly used to mean “not!” in UNIX) are used for these comparisons:

    test alphabet = Alphabet
    test alphabet != Alphabet
    

    The first is false; the second is true.

When comparing string variables, you may see something like this:

test X$string1 = X$string2

The presence of the X prevents a null string from confusing test. If string1 is null, and string2 is string, you'd expand to this:

test X = Xstring

Without the X, the test would be expanded to this:

test = string

This is a syntax error. The other option is to enclose the string in quotation marks:

test "$string1" = "$string2"

That expands to this:

test "" = "string"


  1. The final test operators work on the file system. They are single flags, as listed in Table 15.2, followed by a path.

    Here's a sample test:

    test -d $HOME/bin
    

    This checks to see whether you have a directory named bin in your home directory. The most common flags you see in shell programs are the -f flag and the -d flag. The others are used only in unusual situations.

Table 15.2. The Most Useful File System Flags
Option Meaning
-L True if the file exists and points to another file (symbolic link)
-d True if the file is a directory
-e True if the file exists
-f True if the file exists and is a regular file
-g True if the file exists and runs in a specific group
-r True if the file exists and is readable
-s True if the file exists and has data
-w True if the file exists and is writable
-x True if the file exists and is executable

  1. The file system also has three binary comparisons. The -ef test determines whether the two files are the same. (When you create a link between files, this is true.) The -nt flag is true if the first file is newer than the second, and the -ot flag is true if the first file is older than the second. You might see a test in a looping statement like this:

    test file1 -ot file2
    							

    This test compares the two files, and it is true if file1 is older than file2. If you are waiting for data to appear in file1, you might use this test to cause a shell program to wait for the first file to appear.

  2. Test commands can be negated with the exclamation point, or combined with -a for and, and -o for or. You can make arbitrarily long conditions, at the cost of readability:

    test $var -eq 0 -a ! -e file
    							

    This checks to see whether the value of $var is zero, and whether file exists.

  3. test also has a second form. Instead of explicitly calling test, you can surround the condition by square brackets:

    [ -f file ]
    

    Doing this makes shell programs more readable.

One of the most used commands in shell programming is the test command. It is essential to understanding the next two tasks, conditional expressions and loops.


Task 15.4: Conditional Expressions

Sometimes, when writing a program, you want to perform an action only when another action returns the value true. Shell programming provides you with this capability by way of the if command, the case command, and two special command separators.


  1. The if command is the most commonly seen conditional command. It takes the following form:

    if
        command-block
    then
        command-block
    fi
    

    A command-block is a sequence of one or more shell commands. The first command-block is always executed. The return value of the last statement executed is used to determine whether the second block is executed. The most commonly used command at the end of the first command-block is the test command in its [] notation:

    if
        [ -f $file ]
    then
        echo $file is a regular file
    fi
    

    This if statement notifies the user that a file is a regular file. If the file is not a regular file (such as a directory), you don't see output.

  2. Sometimes, you may want output regardless of the situation. In the preceding case, you may be interested in the status of the file even if it is not a regular file. The if command can expand with the else keyword to provide that second option:

    if
        [ -f $file ]
    then
        echo $file is a regular file
    else
        echo $file is not a regular file
    fi
    

    This statement provides output regardless of the status of the file.

  3. For these simple tests and output, the shell provides a second, quicker means of executing the if statement. If the two commands are joined by &&, the second command is executed if the first command is true. If the commands are joined by ||, the second command is executed if the first is false. The preceding command would, therefore, look like:

    [ -f $file ] && echo $file is a regular file
    [ -f $file ] || echo $file is not a regular file
    

    This shorthand is very useful but can be confusing for a novice. If you accidentally place a space between the characters, you have a wildly different command; the & will run the first command at the same time as the echo, and the | will pipe the output of the test (none) to the echo.

  4. If you want even more information, your if statement can have more than two options. You need multiple tests and the elif keyword:

    if
        [ -f $file ]
    then
        echo $file is a regular file
    elif
        [ -d $file ]
    then
        echo $file is a directory
    else
        echo $file is not a regular file or a directory.
    fi
    

    This command first tests to see whether the file is a regular file; if not, it checks to see whether it is a directory; if it is neither, it gives output. You can expand any if statement with an unlimited number of elif branches.

  5. At some point, though, the code will become confusing. When you have many possible branches, you should use the case command. The syntax is more complicated than that for if:

    case string in
    pattern) command-block ;;
    pattern) command-block ;;
    ...
    esac
    

    If you were looking for possible values for a variable, you could use case:

    echo What do you want:
    read var remainder
    case $var in
    house) echo The price must be very high;;
    car) echo The price must be high;;
    popsicle) echo The price must be low;;
    *) echo I do not know the price;;
    esac
    

    This case statement follows an input request and gives the user a rough idea of the price. A case list can contain any number of items.

    The pattern-matching algorithms used are for wildcards.

There are two basic conditional expressions, and a third shortcut. You can test a condition and perform alternative actions by using if statements and their shortcuts. Or you can compare strings and perform any number of actions by using the case statement.


Task 15.5: Looping Expressions

If you want to run the same set of commands many times, instead of writing them once for each time, you are better off using looping commands. There are two types of loops: the determinate loop and the indeterminate loop.


A determinate loop is one where you know exactly how many times you want to execute the commands before you enter the loop. Stepping through a list of files is a good example; you may not know the exact number of files, but once you do, you can start the loop for those files.

An indeterminate loop is one where you need to keep executing a command-block until a condition is no longer true. You might be either waiting for something or performing a series of modifications to reach a goal.

  1. The usual command for a determinate loop is the for command. It has the following syntax:

    for var in list
    do
        command-block
    done
    

    You can build any list you like. It could be a sequence of numbers or the output of a command. Earlier, I mentioned looping through a list of files. This is performed with the following loop:

    for var in 'ls'
    do
        if
            [ -f $var ]
        then
            echo $var is a regular file
        fi
    done
    

    This steps through all the files listed in the 'ls' output, showing only regular files.

  2. A nice trick that can be performed in a shell program is to step through the list of command-line arguments. The for loop provides a neat mechanism: If the in list part is omitted from the command, the for loop steps through the list of command-line arguments.

    j=0
    for i
    do
       j=`expr $j + 1`
        echo $i is argument $j
    done
    

    This snippet steps through the command-line arguments and identifies where they are in the order of arguments.

    In both cases, when you enter the for loop, you know how many times you need to run the loop. If you look at the case where you are waiting for something to happen, though, you need to use a different loop. The while loop is the solution for this problem.

  3. In Task 15.3, I mentioned the case where you might want to wait on the arrival of a file. This echoes a real-world situation I recently faced. We were processing a program log file, but we did not know exactly when it would be placed in our directory. We tried to set up the job to run after the file arrived, but this approach still ran into problems.

    Using the while loop, we solved the problem. At the end of the execution of our script, we created a checkpoint file. At the beginning, if the checkpoint file was newer than the log file, we would wait. Programmatically, that is:

    while
        [ checkpoint -nt logfile ]
    do
        sleep 60
    done
    

    This program would wait one minute between checks. If the new logfile had not been written, the program would go back to sleep for a minute.

  4. While loops also can be used in a determinate manner. In the case where you are not concerned with a variable's value, but know a count of times to run a command-block, you can use a counter to increment through the number:

    i=0
    while
        [ $i -lt 100 ]
    do
        i=`expr "$i" + 1`
        commands
    done
    

    This is certainly easier than listing 100 items in a list!

The shell provides two convenient mechanisms for running a group of commands repeatedly. These loop commands are useful from both the command line and a program.


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

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