Chapter 6. Shell Logic and Arithmetic

One of the big improvements in modern versions of bash compared with the original Bourne shell is in the area of arithmetic. Early versions of the shell had no built-in arithmetic; it had to be done by invoking a separate executable, even just to add 1 to a variable. In a way it’s a tribute to how useful and powerful the shell was (and is) that it could be used for so many tasks despite that awful mechanism for arithmetic. After a while, though, it became clear that simple, straightforward syntax was needed for the simple counting useful for automating repetitive tasks. The lack of such capability in the original Bourne shell contributed to the success of the C shell (csh) when it introduced C-like syntax for shell programming, including numeric variables. Well, that was then and this is now. If you haven’t looked at shell arithmetic in bash for a while, you’re in for a big surprise.

Beyond arithmetic, there are the control structures familiar to any programmer. There is an if/then/else construct for decision making, as well as while loops and for loops, though you will see some bash peculiarities to all of these. There is a case statement made quite powerful by its string pattern matching, and an odd construct called select. After discussing these features, we will end the chapter by using them to build two simple command-line calculators.

6.1 Doing Arithmetic in Your Shell Script

Problem

You need to do some simple arithmetic in your shell script.

Solution

Use $(( )) or let for integer arithmetic expressions. For example:

COUNT=$((COUNT + 5 + MAX * 2))
let COUNT+='5+MAX*2'

Discussion

As long as you keep to integer arithmetic, you can use all the standard (i.e., C-like) operators inside of $(( )) for arithmetic expressions. There is one additional operator, too: you can use ** for raising to a power, as in MAX=$((2**8)), which yields 256.

Spaces are not needed, nor are they prohibited around operators and arguments (though ** must be together) within a $(( )) expression. But you must not have spaces around the equals sign, as with any bash variable assignment. Also, be sure to quote let expressions since the let statement is a bash builtin and its arguments will undergo word expansion.

Warning

Do not put spaces around the equals sign of an assignment! If you write:

COUNT  =  $((COUNT+5)) # not what you think!

then bash will try to run a program named COUNT with its first argument an equals sign and its second argument the number you get by adding 5 to the value of $COUNT. Remember: no spaces around the assignment equals sign!

Another oddity to these expressions is that the $ that we normally put in front of a shell variable to say we want its value (as in $COUNT or $MAX) is not needed inside the double parentheses. For example, we can write:

$((COUNT + 5 + MAX * 2))

without including the dollar sign on the shell variables—in effect, the outer $ applies to the entire expression. We do need the dollar sign, though, if we are using a positional parameter (e.g., $2), to distinguish it from a numeric constant (e.g., 2). Here’s an example:

COUNT=$((COUNT + $2 + OFFSET))

There is a similar mechanism for integer arithmetic with shell variables using the bash builtin let statement. It uses the same arithmetic operators as the $(( )) construct:

let COUNT=COUNT+5

When using let there are some fancy assignment operators we can use, such as these (which will accomplish the same thing as the previous line):

let COUNT+=5

(These should look familiar to programmers of C/C++ and Java.) This example adds five to the previous value of COUNT without our having to repeat the variable name.

Table 6-1 shows a list of those special assignment operators.

Table 6-1. Explanation of assignment operators in bash
Operator Operation with assignment Use Meaning

=

Simple assignment

a=b

a=b

*=

Multiplication

a*=b

a=(a*b)

/=

Division

a/=b

a=(a/b)

%=

Remainder

a%=b

a=(a%b)

+=

Addition

a+=b

a=(a+b)

-=

Subtraction

a-=b

a=(a-b)

<<=

Bit-shift left

a<<=b

a=(a<<b)

>>=

Bit-shift right

a>>=b

a=(a>>b)

&=

Bitwise “and”

a&=b

a=(a&b)

^=

Bitwise “exclusive or”

a^=b

a=(a^b)

=

Bitwise “or”

a|=b

These assignment operators are also available with $(( )) provided they occur inside the double parentheses. The outermost assignment is still just plain old shell variable assignment.

The assignments can also be cascaded, through the use of the comma operator:

echo $(( X+=5 , Y*=3 ))

which will do both assignments and then echo the result of the second expression (since the comma operator returns the value of its second operand). If you don’t want to echo the result, the more common usage would be with the let statement:

let   X+=5 Y*=3

The comma operator is not needed here, as each word of a let statement is its own arithmetic expression.

One other important difference between the let statement and the $(( )) syntax is how they handle whitespace (i.e., the space character). The let statement either requires quotes or that there be no spaces around not only the assignment operator (the equals sign), but any of the other operators as well; it must all be packed together into a single word. These both work:

let i=2+2
let "i = 2 + 2"

The $(( )) syntax, however, can be much more generous, allowing all sorts of whitespace within the parentheses. For that reason, it is less prone to errors and makes the code much more readable, and is, therefore our preferred way of doing bash integer arithmetic. However, an exception can be made for the occasional += assignment or ++ operator, or when we get nostalgic for the early days of BASIC programming (which had a LET statement).

Warning

Remember that this is integer arithmetic, not floating point. Don’t expect much out of an expression like 2/3, which in integer arithmetic evaluates to 0 (zero). The division is integer division, which will truncate any fractional result.

See Also

  • help let

  • The bash manpage

6.2 Branching on Conditions

Problem

You want to check if you have the right number of arguments and take actions accordingly. You need a branching construct.

Solution

The if statement in bash is similar in appearance to that in other programming languages:

if [ $# -lt 3 ]
then
    printf "%b" "Error. Not enough arguments.
"
    printf "%b" "usage: myscript file1 op file2
"
    exit 1
fi

or alternatively:

if (( $# < 3 ))
then
    printf "%b" "Error. Not enough arguments.
"
    printf "%b" "usage: myscript file1 op file2
"
    exit 1
fi

Here’s a full-blown if with an elif (bash-talk for else-if) and an else clause:

if (( $# < 3 ))
then
    printf "%b" "Error. Not enough arguments.
"
    printf "%b" "usage: myscript file1 op file2
"
    exit 1
elif (( $# > 3 ))
then
    printf "%b" "Error. Too many arguments.
"
    printf "%b" "usage: myscript file1 op file2
"
    exit 2
else
    printf "%b" "Argument count correct. Proceeding...
"
fi

You can even do things like this:

[ $result = 1 ] 
  && { echo "Result is 1; excellent." ; exit 0;   } 
  || { echo "Uh-oh, ummm, RUN AWAY! " ; exit 120; }

(For a discussion of this last example, see Recipe 2.14.)

Discussion

We have two things we need to discuss: the basic structure of the if statement and how it is that we have different syntax (parentheses or brackets, operators or options) for the if expression. The first may help explain the second. The general form for an if statement, from the manpage for bash, is:

if list; then list; [ elif list; then list; ] ... [ else list; ] fi

The [ and ] are used to delineate optional parts of the statement (e.g., some if statements have no else clause). So let’s look for a moment at the if without any optional elements.

The simplest form for an if statement would be:

if list; then list; fi
Tip

In bash, the semicolon serves the same purpose as a newline—it ends a statement. We could have crammed the examples in the Solution section onto fewer lines by using semicolons, but it is more readable to use newlines.

The then list seems to make sense—it’s the statement or statements that will execute provided that the if condition is true (or so we would surmise from other programming languages). But what’s with the if list? Wouldn’t you expect it to be if expression?

You might, except that this is a shell—a command processor. Its primary operation is to execute commands. So, the list after the if is a place where you can put a list of commands. What, you ask, will be used to determine the branching—the alternate paths of the then or the else? It will be determined by the return value of the last command in the list. (The return value, you might remember, is also available as the value of the $? variable.)

Let’s take a somewhat strange example to make this point:

$ cat trythis.sh
if ls; pwd; cd $1;
then
    echo success
else
    echo failed
fi
pwd

$ bash ./trythis.sh /tmp
...
$ bash ./trythis.sh /nonexistent
...
$

In this strange script, the shell will execute three commands (an ls, a pwd, and a cd) before doing any branching. The argument to the cd in this example is the first argument supplied on the shell script invocation. If there is no argument supplied, it will just execute cd, which returns you to your home directory.

So what happens? Try it yourself and find out. The result showing “success” or “failed” will depend on whether or not the cd command succeeds. In our example, the cd is the last command in the if list of commands. If the cd fails, the else clause is taken, but if it succeeds, the then clause is taken.

Properly written commands and builtins return a value of 0 (zero) when they encounter no errors in their execution. If they detect a problem (e.g., bad parameters, I/O errors, file not found), they will return some nonzero value (often a different value for each different kind of error they detect).

This is why it is important for both shell script writers and C (and other language) programmers to be sure to return sensible values upon exiting from their scripts and programs. Someone’s if statement may be depending on it!

OK, so how do we get from this strange if construct to something that looks like a real if statement—the kind that you are used to seeing in programs? What’s going on with the examples that began this recipe? After all, they don’t look like lists of statements.

Let’s try this on for size:

if test $# -lt 3
then
    echo try again.
fi

Do you see something that looks like, if not an entire list, then at least a single shell command—the builtin command test, which will take its arguments and compare their values? The test command will return a 0 if true or a 1 otherwise. To see this yourself, try the test command on a line by itself, and then echo $? to see its return value.

The first example we gave that began if [ $# -lt 3 ] looks a lot like the test statement. That’s because the [ is actually just a different name for the same command. (When invoked with the name [ it also requires a trailing ] as the last parameter, for readability and aesthetic reasons.) So that explains the first syntax—the expression in the if statement is actually a list of only one command, a test command.

Tip

In the early days of Unix, test was its own separate executable and [ was just a link to the same executable. They still exist as executables, but bash implements them as a builtin command.

Now what about the if (( $# < 3 )) expression in our list of examples in the Solution section? The double parentheses are one of several types of compound commands. This kind is useful for if statements because it performs an arithmetic evaluation of the expression between the double parentheses. This is a more recent bash improvement, added for just such an occasion as its use in if statements.

The important distinctions to make with the two kinds of syntax that can be used with the if statement are the ways to express the tests, and the kinds of things for which they test. The double parentheses are strictly for arithmetic expressions. The square brackets can also test for file characteristics, but the syntax is much less streamlined for arithmetic expressions. This is particularly true if you need to group larger expressions with parentheses (which need to be quoted or escaped when using square brackets).

6.3 Testing for File Characteristics

Problem

You want to make your script robust by checking to see if your input file is there before reading from it; you would also like to see if your output file has write permissions before writing to it and you would like to see if there is a directory there before you attempt to cd into it. How do you do all that in bash scripts?

Solution

Use the various file characteristic tests in the test command as part of your if statements. Your specific problems might be solved with scripting that looks something like Example 6-1.

Example 6-1. ch06/checkfile
#!/usr/bin/env bash
# cookbook filename: checkfile
#
DIRPLACE=/tmp
INFILE=/home/yucca/amazing.data
OUTFILE=/home/yucca/more.results

if [ -d "$DIRPLACE" ]
then
    cd $DIRPLACE
    if [ -e "$INFILE" ]
    then
        if [ -w "$OUTFILE" ]
        then
            doscience < "$INFILE" >> "$OUTFILE"
        else
            echo "cannot write to $OUTFILE"
        fi
    else
        echo "cannot read from $INFILE"
    fi
else
    echo "cannot cd into $DIRPLACE"
fi

Discussion

We put all the references to the various filenames in quotes in case they have any embedded spaces in the pathnames. There are none in this example, but if you change the script you might use other pathnames.

We tested and executed the cd before we tested the other two conditions. In this example it wouldn’t matter, but if $INFILE or $OUTFILE were relative pathnames (not beginning from the root of the filesystem, i.e., with a leading /), then the test might evaluate to true before the cd and not after, or vice versa. This way, we test right before we use the files.

We use the double-greater-than operator (>>) to concatenate output onto our results file, rather than replacing the old content with the new content.

The several tests could be combined into one large if statement using the -a (read “and”) operator, but then if a test failed you couldn’t give a very helpful error message since you wouldn’t know which test didn’t pass.

There are several other characteristics for which you can test. Three of them are tested using binary operators, each taking two filenames:

FILE1 -nt FILE2

Is newer than (it checks the modification date). An existing file is considered “newer” than one that doesn’t exist.

FILE1 -ot FILE2

Is older than; also, a file that doesn’t exist is considered older than one that does.

FILE1 -ef FILE2

Have the same device and inode numbers (identical files, even if pointed to by different links)

Table 6-2 shows the other tests related to files (see “Test Operators” in Appendix A for a more complete list). They all are unary operators, taking the form option filename as in if [ -e myfile ].

Table 6-2. Unary operators that check file characteristics
Option Description

-b

File is a block special device (for files like /dev/hda1)

-c

File is character special (for files like /dev/tty)

-d

File is a directory

-e

File exists

-f

File is a regular file

-g

File has its set-group-ID (setgid) bit set

-h

File is a symbolic link (same as -L)

-G

File is owned by the effective group ID

-k

File has its sticky bit set

-L

File is a symbolic link (same as -h)

-N

File has been modified since it was last read

-O

File is owned by the effective user ID

-p

File is a named pipe

-r

File is readable

-s

File has a size greater than zero

-S

File is a socket

-u

File has its set-user-ID (setuid) bit set

-w

File is writable

-x

File is executable

6.4 Testing for More than One Thing

Problem

What if you want to test for more than one characteristic? Do you have to nest your if statements?

Solution

Use the operators for logical AND (-a) and OR (-o) to combine more than one test in an expression. For example:

if [ -r $FILE -a -w $FILE ]

will test to see that the file is both readable and writable.

Discussion

All the file test conditions include an implicit test for existence, so you don’t need to test if a file exists and is readable. It won’t be readable if it doesn’t exist.

These conjunctions (-a for AND and -o for OR) can be used for all the various test conditions. They aren’t limited to just the file conditions.

You can make several AND/OR conjunctions in one statement. You might need to use parentheses to get the proper precedence, as in a and (b or c), but if you use parentheses, be sure to escape their special meaning from the shell by putting a backslash before each or by quoting each parenthesis. Don’t try to quote the entire expression in one set of quotes, however, as that will make your entire expression a single term that will be treated as a test for an empty string (see Recipe 6.5).

Here’s an example of a more complex test with the parentheses properly escaped:

if [ -r "$FN" -a ( -f "$FN" -o -p "$FN" ) ]

Don’t make the assumption that these expressions are evaluated in quite the same order as in Java or C. In C and Java, if the first part of the AND expression is false (or the first part true in an OR expression), the second part of the expression won’t be evaluated (we say the expression short-circuits). However, because the shell makes multiple passes over the statement while preparing it for evaluation (e.g., doing parameter substitution, etc.), both parts of the joined condition may have been partially evaluated. While it doesn’t matter in this simple example, in more complicated situations it might. For example:

if [ -z "$V1" -o -z "${V2:=YIKES}" ]

Even if $V1 is empty, satisfying enough of the if statement that the second part of the condition (checking if $V2 is empty) need not occur, the value of $V2 may have already been modified (as a side effect of the parameter substitution for $V2). The parameter substitution step occurs before the -z tests are made. Confused? Don’t be…just don’t count on short circuits in your conditionals. If you need that kind of behavior, just break the if statement into two nested if statements or use && and ||.

See Also

6.5 Testing for String Characteristics

Problem

You want your script to check the values of some strings before using them. The strings could be user input, read from a file, or environment variables passed to your script. How do you do that with bash scripts?

Solution

There are some simple tests that you can do with the builtin test command, using the single-bracket if statements. You can check to see whether a variable has any text, and you can check to see whether two variables are equal as strings.

Discussion

Take a look at Example 6-2.

Example 6-2. ch06/checkstr
#!/usr/bin/env bash
# cookbook filename: checkstr
#
# if statement
# test a string to see if it has any length
#
# use the command-line argument
VAR="$1"
#
# if [ "$VAR" ] will usually work but is bad form, using -n is more clear
if [ -n "$VAR" ]
then
    echo has text
else
    echo zero length
fi
#
if [ -z "$VAR" ]
then
    echo zero length
else
    echo has text
fi

We use the phrase “has any length” deliberately. There are two types of variables that will have no length—those that have been set to an empty string and those that have not been set at all. This test does not distinguish between those two cases. All it asks is whether there are some characters in the variable.

It is important to put quotes around the "$VAR" expression because without them your syntax could be disturbed by odd user input. If the value of $VAR were x -a 7 -lt 5 and if there were no quotes around the $VAR, then the expression:

if [ -z $VAR ]

would become (after variable substitution):

if [ -z x -a 7 -lt 5 ]

which is legitimate syntax for a more elaborate test, but one that will yield a result that is not what you wanted (i.e., one not based on whether the string has characters).

6.6 Testing for Equality

Problem

You want to check to see if two shell variables are equal, but there are two different test operators: -eq and = (or ==). Which one should you use?

Solution

The type of comparison you need determines which operator you should use. Use the -eq operator for numeric comparisons and the equality primary = (or ==) for string comparisons.

Discussion

Example 6-3 is a simple script to illustrate the situation.

Example 6-3. ch06/strvsnum
#!/usr/bin/env bash
# cookbook filename: strvsnum
#
# the old string vs. numeric comparison dilemma
#
VAR1=" 05 "
VAR2="5"

printf "%s" "do they -eq as equal? "
if [ "$VAR1" -eq "$VAR2" ]
then
    echo YES
else
    echo NO
fi

printf "%s" "do they = as equal? "
if [ "$VAR1" = "$VAR2" ]
then
    echo YES
else
    echo NO
fi

When we run the script, here is what we get:

$ bash strvsnum
do they -eq as equal? YES
do they = as equal? NO
$

While the numeric value is the same (5) for both variables, characters such as leading zeros and whitespace can mean that the strings are not equal as strings.

Both = and == are accepted, but the single equals sign follows the POSIX standard and is more portable.

It may help you to remember which comparison to use if you recognize that the -eq operator is similar to the FORTRAN .eq. operator. (FORTRAN is a very numbers-oriented language, used for scientific computation.) In fact, there are several numerical comparison operators in bash, each similar to an old FORTRAN operator. The abbreviations, all listed in Table 6-3, are rather mnemonic and easy to figure out.

Another way to remember which to use is that it feels “backward” or “opposite”: the string-like comparators (the syntax using characters; e.g., -eq) are for numbers and the numeric-looking comparators (e.g., the math-like +<=+) are for strings.

Table 6-3. bash’s comparison operators
Numeric String Meaning

-lt

<

Less than

-le

<=

Less than or equal to

-gt

>

Greater than

-ge

>=

Greater than or equal to

-eq

=, = =

Equal to

-ne

!=

Not equal to

This is the opposite of Perl, in which eq, ne, etc. are the string operators, while ==, !=, etc. are numeric.

Maybe the best solution is to always do your numerical tests with the double-parentheses syntax and your string comparisons with the double-square-brackets syntax. Then you can always use the math-style symbols for comparison.

6.7 Testing with Pattern Matches

Problem

You want to test a string not for a literal match, but to see if it fits a pattern. For example, you want to know if a file is named like a JPEG file might be named.

Solution

Use the double-bracket compound statement in an if statement to enable shell-style pattern matches on the righthand side of the equality operator:

if [[ "${MYFILENAME}" == *.jpg ]]

Discussion

The double-bracket syntax is not the old-fashioned [ of the test command, but a newer bash mechanism (available since v2.01 or so). It uses the same operators that work with the single-bracket form, but in the double-bracket syntax the equals sign is a more powerful string comparator. You can use a single or a double equals sign, as we have used here; they are the same semantically. We prefer to use the double equals sign (especially when doing pattern matching) to emphasize the difference, but it is not the reason that we get pattern matching—that comes from the double-bracket compound statement.

The standard pattern matching includes the * to match any number of characters, the question mark (?) to match a single character, and brackets ([]) for including a list of possible characters. Note that these resemble shell file wildcards, and are not regular expressions.

Don’t put quotes around the pattern if you want it to behave as a pattern. If our string had been quoted, it would have only matched strings with a literal asterisk as the first character.

There are more powerful pattern-matching capabilities available by turning on some additional options in bash. Let’s expand our example to look for filenames that end in either .jpg or .jpeg. We can do that with this bit of code:

shopt -s extglob
if [[ "$FN" == *.@(jpg|jpeg) ]]
then
   # and so on

The shopt -s command is the way to turn on shell options. The extglob option deals with extended pattern matching (or globbing). With this extended pattern matching we can have several patterns, separated by the | character and grouped by parentheses. The first character preceding the parentheses says whether the list should match just one occurrence of a pattern in the list (using a leading @) or some other criteria. Table 6-4 lists the possibilities.

Table 6-4. Grouping symbols for extended pattern matching
Grouping Meaning

@( … )

Only one occurrence

*( … )

Zero or more occurrences

+( … )

One or more occurrences

?( … )

Zero or one occurrence

!( … )

Not this, but anything else

Matches are case-sensitive, but you may use shopt -s nocasematch (in bash versions 3.1+) to change that. This option affects case and [[ commands.

6.8 Testing with Regular Expressions

Problem

Sometimes even the extended pattern matching of the extglob option isn’t enough. What you really need are regular expressions. Let’s say that you rip a CD of classical music into a directory, ls that directory, and see these names:

$ ls
Ludwig Van Beethoven - 01 - Allegro.ogg
Ludwig Van Beethoven - 02 - Adagio un poco mosso.ogg
Ludwig Van Beethoven - 03 - Rondo - Allegro.ogg
Ludwig Van Beethoven - 04 - "Coriolan" Overture, Op. 62.ogg
Ludwig Van Beethoven - 05 - "Leonore" Overture, No. 2 Op. 72.ogg
$

You’d like to write a script to rename these files to something simple, such as just the track number. How can you do that?

Solution

Use the regular expression matching of the =~ operator. Once it has matched the string, the various parts of the pattern are available in the shell variable $BASH_REMATCH. Example 6-4 is the part of the script that deals with the pattern match.

Example 6-4. ch06/trackmatch
#!/usr/bin/env bash
# cookbook filename: trackmatch
#
for CDTRACK in *
do
    if [[ "$CDTRACK" =~ "([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$" ]]
    then
        echo Track ${BASH_REMATCH[2]} is ${BASH_REMATCH[3]}
        mv "$CDTRACK" "Track${BASH_REMATCH[2]}"
    fi
done
Warning

This requires bash version 3.0 or newer—older versions don’t have the =~ operator. In addition, bash version 3.2 unified the handling of the pattern in the == and =~ conditional command operators but introduced a subtle quoting bug that was corrected in 3.2 patch #3. If the solution shown here fails, you may be using bash version 3.2 without that patch. You might want to upgrade to a newer version. You might also avoid the bug with a less readable version of the regular expression by removing the quotes around the regex and escaping each parenthesis and space character individually, which gets ugly quickly:

if [[ "$CDTRACK" =~ ([[:alpha:][:blank:]]*)- 
> ([[:digit:]]*) - (.*)$ ]]

Discussion

If you are familiar with regular expressions from sed, awk, and older shells, you may notice a few slight differences with this newer form. Most noticeable are the character classes such as [:alpha:] and that the grouping parentheses don’t need to be escaped—we don’t write ( here. as we would in sed. Here, ( would mean a literal parenthesis.

The subexpressions, each enclosed in parentheses, are used to populate the bash builtin array variable $BASH_REMATCH. The zeroth element (${BASH_REMATCH[0]}) is the entire string matched by the regular expression. Any subexpressions are available as ${BASH_REMATCH[1]}, ${BASH_REMATCH[2]}, and so on. Any time a regular expression is used this way, it will populate the variable $BASH_REMATCH. Since other bash functions may want to use regular expression matching, you may want to assign this variable to one of your own naming as soon as possible, so as to preserve the values for your later use. In our example we use the values right away, inside our if/then clause, so we don’t bother to save them for use elsewhere.

Regular expressions have often been described as write-only expressions because they can be very difficult to decipher. We’ll build this one up in several steps to show how we arrived at the final expression. The general layout of the filenames given to our datafiles, as in this example, seems to be like this:

Ludwig Van Beethoven - 04 - "Coriolan" Overture, Op. 62.ogg

That is, a composer’s name, a track number, and then the title of the piece, ending in .ogg (these were saved in Ogg Vorbis format, for smaller space and higher fidelity).

At the lefthand side of the expression is an opening (or left) parenthesis. That begins our first subexpression. Inside it, we will write an expression to match the first part of the filename, the composer’s name—marked in bold here:

([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$

The composer’s name consists of any number of alphabetic characters and blanks. We use the square brackets to group the set of characters that will make up the name. Rather than write [a-zA-Z], we use the character class names [:alpha:] and [:blank:] and put them inside the square brackets. This is followed by an asterisk to indicate zero or more repetitions. The right parenthesis closes off the first sub-expression, followed by a literal hyphen and a blank.

The second subexpression (marked in bold here) will attempt to match the track number:

([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$

The second subexpression begins with another left parenthesis. The track numbers are integers, composed of digits (the character class [:digit:]), which we write inside another pair of brackets followed by an asterisk as [[:++digit++:]]* to indicate zero or more of what is in the brackets (i.e., digits). Then our pattern has the literals blank, hyphen, and blank.

The final subexpression will catch everything else, including the track name and the file extension:

([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$

This is the common and familiar .* regular expression, which means any number (*) of any character (.), again enclosed in parentheses. We end the expression with a dollar sign, which matches the end of the string. Matches are case-sensitive, but you may use shopt -s nocasematch (available in bash versions 3.1+) to change that. This option affects case and [[ commands.

See Also

6.9 Changing Behavior with Redirections

Problem

Normally you want a script to behave the same regardless of whether input comes from a keyboard or a file, or whether output is going to the screen or a file. Occasionally, though, you want to make that distinction. How do you do that in a script?

Solution

Use test -t 0 in an if statement to branch between the two desired behaviors. The 0 is the file descriptor for standard input; use a 1 to test for redirection of standard output. The test is true if the file descriptor is connected to a terminal, and false otherwise (e.g., false when redirected to a file or piped into another program).

Discussion

Think long and hard before you do this. So much of the power and flexibility of bash scripting comes from the fact that scripts can be pipelined together. Be sure you have a really good reason to make your script behave oddly when input or output is redirected.

6.10 Looping for a While

Problem

You want your shell script to perform some actions repeatedly as long as some condition is met.

Solution

Use the while looping construct for arithmetic conditions:

while (( COUNT < MAX ))
do
    some stuff
    let COUNT++
done

for filesystem-related conditions:

while [ -z "$LOCKFILE" ]
do
    some things
done

or for reading input:

while read lineoftext
do
    process $lineoftext
done

Discussion

The double parentheses in our first while statement delimit an arithmetic expression, very much like the $(( )) expression for shell variable assignment (see Recipe 6.1). The variable names mentioned inside the parentheses are meant to be dereferenced. That is, you don’t write $VAR, and instead use VAR inside the parentheses.

The use of the square brackets in while [ -z"$LOCKFILE" ] is the same as with the if statement—the single square bracket is the same as using the test statement.

The last example, while read lineoftext, doesn’t have any parentheses, brackets, or braces. The syntax of the while statement in bash is defined such that the condition of the while statement is a list of statements to be executed (just like the if statement), and the exit status of the last one determines whether the condition is true or false. An exit status of zero indicates the condition is true; otherwise it’s false.

The read statement returns a 0 on a successful read and a 1 on end-of-file, which means that the while will find it true for any successful read, but when the end of file is reached (and a 1 is returned) the while condition will be false and the looping will end. At that point, the next statement to be executed will be the statement after the done statement.

This logic of “keep looping while the statement returns zero” might seem a bit flipped—most C-like languages use the opposite, namely, “loop while nonzero.” But in the shell, a zero return value means everything went well; nonzero return values indicate an error exit.

This explains what happens with the (( )) construct, too. Any expression inside the parentheses is evaluated, and if the result is nonzero, then the result of the (( )) is to return a 0; similarly, a zero result returns a 1. This means we can write expressions like Java or C programmers would, but the while statement still works as always in bash, expecting a zero result to be true.

In practical terms, it means we can write an infinite loop like this:

while (( 1 )); do
    ...
done

which “feels right” to a C programmer. But remember that the while statement is looking for a zero return value—which it gets because (( 1 )) returns 0 for a true (i.e., nonzero) result.

Before we leave the while loop, let’s take one more look at that while read example, which is reading from standard input (i.e., the keyboard), and see how it might get modified in order to read input from a file instead of the keyboard.

This is typically done in one of three ways. The first requires no real modifications to the statements at all. Rather, when the script is invoked, standard input is redirected from a file like this:

myscript < file.name

But suppose you don’t want to leave it up to the caller. If you know what file you want to process, or if it was supplied as a command-line argument to your script, then you can use this same while loop as is, but redirect the input from the file as follows:

while read lineoftext
do
    process that line
done < file.input

As a third option, you could begin by cat-ing the file to dump it to standard output, and then connect the standard output of that program to the standard input for the while statement:

cat file.input |
while read lineoftext
do
    process that line
done
Warning

Because of the pipe, the cat command and the while loop (including the process that line part) are each executing in their own separate processes. This means that if you use this method, the script commands inside the while loop cannot affect the other parts of the script outside the loop. For example, any variables that you set within the while loop will no longer have those values after the loop ends. Such is not the case, however, if you use while read ... done < file.input, because that isn’t a pipeline.

6.11 Looping with a read

Problem

You’re using the Subversion revision control system, which is executable as svn. (This example is very similar to what you would do for CVS as well.) When you check the status of a directory subtree to see what files have been changed, you see something like this:

$ svn status bcb
M      bcb/amin.c
?      bcb/dmin.c
?      bcb/mdiv.tmp
A      bcb/optrn.c
M      bcb/optson.c
?      bcb/prtbout.4161
?      bcb/rideaslist.odt
?      bcb/x.maxc
$

The lines that begin with question marks are files about which Subversion has not been told; in this case they’re scratch files and temporary copies of files. The lines that begin with A are newly added files, and those that begin with M have been modified since the last changes were committed.

To clean up this directory, it would be nice to get rid of all the scratch files.

Solution

A common use of a while loop is to read files and the output of previous commands. Try:

svn status mysrc | grep '^?' | cut -c8- |
  while read FN; do echo "$FN"; rm -rf "$FN"; done

or:

svn status mysrc |
while read TAG FN
do
    if [[ $TAG == ? ]]
    then
        echo $FN
        rm -rf "$FN"
    fi
done

Discussion

Both scripts will do the same thing—remove files that svn reports with a question mark. The same solutions may be adapted to work with other revision control systems.

The first approach uses several subprograms to do its work (not a big deal in these days of gigahertz processors), and would fit on a single line in a typical terminal window. It uses grep to select only the lines that begin (signified by the ^) with a question mark. The expression '^?' is put in single quotes to avoid any special meanings that those characters have for bash. It then uses cut to take only the characters beginning in column eight (through the end of the line). That leaves just the filenames for the while loop to read.

The read statement will return a nonzero value when there is no more input, so at that point the loop will end. Until then, it will assign the line of text that it reads each time into the variable $FN, and that is the filename that we remove. We use the -rf options in case the unknown file is actually a directory of files, and to remove even read-only files. If you don’t want/need to be so drastic in what you remove, leave those options off.

The second script can be described as more shell-like, since it doesn’t need grep to do its searching (it uses the if statement) and it doesn’t need cut to do its parsing (it uses the read statement). We’ve also formatted it more like you would format a script in a file. If you were typing this at a command prompt, you could collapse the indentation, but for our use here the readability is much more important than saving a few keystrokes.

The read in this second script reads into two variables, not just one. That is how we get bash to parse the line into two pieces—the leading character and the filename. The read statement parses its input into words, like words on a shell command line. The first word on the input line is assigned to the first word in the list of variables in the read statement, the second word to the second variable, and so on. The last variable in the list gets the entire remainder of the line, even if it’s more than a single word. In our example, $TAG gets the first word, which is the character (M, A, or ?); the whitespace defines the end of that word and the beginning of the next. The variable $FN gets the remainder of the line as the filename, which is significant here in case the filenames have embedded spaces. (We wouldn’t want just the first word of the filename.) The script then removes the filename and the loop continues.

See Also

6.12 Looping with a Count

Problem

You need to loop a fixed number of times. You could use a while loop and do the counting and testing, but programming languages have for loops for such a common idiom. How does one do this in bash ?

Solution

Use a special case of the for syntax, one that looks a lot like C, but with double parentheses:

for (( i=0 ; i < 10 ; i++ )) ; do echo $i ; done

Discussion

In early versions of the shell, the original syntax for the for loop only included iterating over a fixed list of items. It was a neat innovation for word-oriented shell scripts dealing with filenames and such. But when users needed to count, they sometimes found themselves writing:

for i in 1 2 3 4 5 6 7 8 9 10
do
    echo $i
done

Now that’s not too bad, especially for small loops, but let’s face it—it’s not going to work for 500 iterations. (Yes, you could nest loops 5 × 10, but come on!) What you really need is a for loop that can count.

The variation of the for loop with C-like syntax has been in bash since version 2.04. Its more general form can be described as:

for (( expr1 ; expr2 ; expr3 )) ; do list ; done

The use of double parentheses is meant to indicate that these are arithmetic expressions. You don’t need to use the $ construct (as in $i, except for arguments like $1) when referring to variables inside the double parentheses (just like in the other places where double parentheses are used in bash). The expressions are integer arithmetic expressions and offer a rich variety of operators, including the use of the comma to put multiple operations within one expression:

for (( i=0, j=0 ; i+j < 10 ; i++, j++ ))
do
    echo $((i*j))
done

That for loop initializes two variables ($i and $j), then has a more complex second expression adding the two together before doing the less-than comparison. The comma operator is used again in the third expression to increment both variables.

6.13 Looping with Floating-Point Values

Problem

The for loop with arithmetic expressions only does integer arithmetic. What do you do for floating-point values?

Solution

Use the seq command to generate your floating-point values, if your system provides it:

for fp in $(seq 1.0 .01 1.1)
do
     echo $fp; other stuff too
done

or:

seq 1.0 .01 1.1 |
while read fp
do
    echo $fp; other stuff too
done

Discussion

The seq command will generate a sequence of floating-point numbers, one per line. The arguments to seq are the starting value, the increment, and the ending value. This is not the intuitive order if you are used to the C language for loop, or if you learned your looping from BASIC (e.g., FOR I=4 TO 10 STEP 2). With seq the increment is the middle argument.

In the first example, the $() runs the command in a subshell and returns the result with the newlines replaced by just whitespace, so each value is a string value for the for loop.

In the second example, seq is run as a command with its output piped into a while loop that reads each line and does something with it. This would be the preferred approach for a really long sequence, as it can run the seq command in parallel with the while. The for loop version has to run seq to completion and put all of its output on the command line for the for statement. For very large sequences, this could be time- and memory-consuming.

6.14 Branching Many Ways

Problem

You have a series of comparisons to make, and the if/then/else is getting pretty long and repetitive. Isn’t there an easier way?

Solution

Use the case statement for a multiway branch:

case $FN in
    *.gif) gif2png $FN
        ;;
    *.png) pngOK $FN
        ;;
    *.jpg) jpg2gif $FN
        ;;
    *.tif | *.TIFF) tif2jpg $FN
        ;;
    *) printf "File not supported: %s" $FN
        ;;
esac

The equivalent to this using if/then/else statements is:

if [[ $FN == *.gif ]]
then
    gif2png $FN
elif [[ $FN == *.png ]]
then
    pngOK $FN
elif [[ $FN == *.jpg ]]
then
    jpg2gif $FN
elif [[ $FN == *.tif || $FN == *.TIFF ]]
then
    tif2jpg $FN
else
    printf "File not supported: %s" $FN
fi

Discussion

The case statement will expand the word (including parameter substitution) between the case and in keywords. It will then try to match the word with the patterns listed in order. This is a very powerful feature of the shell. It is not just doing simple value comparisons, but string pattern matches (though not regular expressions). We have simple patterns in our example: *.gif matches any character sequence (signified by the *) that ends with the literal characters .gif.

Use |, a vertical bar meaning logical OR, to separate different patterns for which you want to take the same action. In our example, if $FN ends either with .tif or .TIFF then the pattern will match and the (fictional) tif2jpg command will be executed.

There is no else or default keyword to indicate the statements to execute if no pattern matches. Instead, use * as the last pattern, since that pattern will match anything. Placing it last makes it act as the default and match anything that hasn’t already been matched.

The double semicolon (;;) ends the set of statements associated with a pattern. As of bash version 4, there are two other ways to end a set of statements. The ;;& construct means that even if a match is found, the next pattern will be tested for a match and its statements will be executed as well if another match is found. The ;& construct means that execution will “fall through,” and the next set of statements will be executed regardless of whether its pattern matches. Here is a somewhat pointless example to show the use of these features:

# use other endings for case

case $FN in
    *.gif) gif2png $FN
            ;;&        # keep looking
    *.png) pngOK $FN
            ;;&        # keep looking
    *.jpg) jpg2gif $FN
            ;;&        # keep looking
    *.tif) tif2jpg $FN
            ;&         # fall through
    *.* ) echo "two.words"
            ;;
    * ) echo "oneword"
esac

If $FN matches any of the first four patterns bash will execute its (fictional) conversion command, but also keep looking; it will find that it matches the fifth pattern as well and therefore also echo the phrase two.words.

Note

An aside to C/C++ and Java programmers: the bash case is similar to the switch statement, and each pattern corresponds to a case. Notice, though, that the variable on which you can switch/case is a shell variable (typically a string value) and the cases are patterns (not just constant values). The patterns end with a right parenthesis (not a colon). The equivalent to the break in C/C++ and Java switch statements is, in bash, a double semicolon. The equivalent to their default keyword is, in bash, the * pattern.

Matches are case-sensitive, but you may use shopt -s nocasematch (available in bash versions 3.1+) to change that This option affects case and [[ commands.

We end the case statement with an esac (that’s “c-a-s-e” spelled backward; this came from Algol 68).

See Also

6.15 Parsing Command-Line Arguments

Problem

You want to write a simple shell script to print a line of dashes, but you want to parameterize it so that you can specify different line lengths and specify a character to use other than just a dash. The syntax would look like this:

dashes           # would print out 72 dashes
dashes 50        # would print out 50 dashes
dashes -c = 50   # would print out 50 equals signs
dashes -c x      # would print out 72 x characters

What’s an easy way to parse those simple arguments?

Solution

For serious scripting, you should use the getopts builtin. But we would like to show you the case statement in action, so for this simple situation we’ll use case for argument parsing.

Example 6-5 shows the beginning of the script (see Recipe 12.1 for a complete version).

Example 6-5. ch06/dashes
#!/usr/bin/env bash
# cookbook filename: dashes
#
# dashes - print a line of dashes
#
# options: # how many (default 72)
# -c X use char X instead of dashes
#

LEN=72
CHAR='-'
while (( $# > 0 ))
do
    case $1 in
        [0-9]*) LEN=$1
        ;;
        -c) shift;
               CHAR=${1:--}
        ;;
        *) printf 'usage: %s [-c X] [#]
' ${0##*/} >&2
            exit 2
        ;;
    esac
    shift
done
#
# more...

Discussion

The default length (72) and the default character (-) are set at the beginning of the script (after some useful comments). The while loop allows us to parse more than one parameter. It will keep looping while the number of arguments ($#) is above zero.

The case statement matches three different patterns. First, the [0-9]* will match any digit followed by any other characters. We could have used a more elaborate expression to allow only pure numbers, but we’ll assume that any argument that begins with a digit is a number. If that isn’t true (e.g., if the user types 1T4), then the script will error when it tries to use $LEN. We can live with that for now.

The second pattern is a literal -c. There is no pattern to this, just an exact match. In that case, we use the shift builtin command to throw away that argument (now that we know what it is) and we take the next argument (which has now become the first argument, so it is referenced as $1) and save that as the new character choice. We use :- when referencing $1 (as in ${1:-x}) to specify a default value if the parameter isn’t set. That way, if the user types -c but fails to specify an argument, it will use the default, specified as the character immediately following the :-. In the expression ${1:-x} it would be x. For our script, we wrote ${1:--} (note the two minus signs), so the character taken as the default is the (second) minus sign.

The third pattern is the wildcard pattern (*), which matches everything, so that any argument unmatched by the previous patterns will be matched here. Placed last in the case statement, it is the catch-all that notifies the user of an error (since it wasn’t one of the prescribed parameters); it prints a message instructing the user about correct usage.

That printf error message probably needs explaining if you’re new to bash. There are four sections of that statement to look at. The first is simply the command name, printf. The second is the format string that printf will use (see Recipe 2.3 and “printf” in Appendix A). We use single quotes around the string so that the shell doesn’t try to interpret any of the string. The last part of the line (>&2) tells the shell to redirect the output to standard error. Since this is an error message, that seems appropriate. Many script writers are casual about this and often neglect this redirection on error messages. We think it is a good habit to always redirect error messages to standard error.

The third part of the line uses string manipulation on $0. This is a common idiom used to strip off any leading path part of how the command was invoked. For example, consider what would happen if we used only $0. Here are two different but erroneous invocations of the same script. Notice the error messages:

$ dashes -g
usage: dashes [-c X] [#]

$ /usr/local/bin/dashes -g
usage: /usr/local/bin/dashes [-c X] [#]

In the second invocation, we used the full pathname. The error message then also contained the full pathname. Some people find this annoying. So, we strip $0 down to just the script’s base name (similar to using the basename command). Then the error messages look the same regardless of how the script is invoked:

$ dashes -g
usage: dashes [-c X] [#]

$ /usr/local/bin/dashes -g
usage: dashes [-c X] [#]

While this certainly takes a bit more time than just hardcoding the script name or using $0 without trimming it, the script is more portable this way—if you change the script’s name you don’t have to modify the code. If you prefer to use the basename command in a subshell, that is also worthwhile, as the extra time isn’t that vital. This is an error message and the script is about to exit anyway.

We end the case statement with an esac and then do a shift so as to consume the argument that we just matched in our case statement. If we didn’t do that, we’d be stuck in the while loop, parsing the same argument over and over. The shift will cause the second argument ($2) to become the first ($1) and the third to become the second, and so on, but also $# to be one smaller. On some iteration of the loop $# finally reaches zero (when there are no more arguments), and the loop terminates.

The actual printing of the dashes (or other character) is not shown here, as we wanted to focus on the case statement and related actions. You can see the complete script, with a function for the usage message, in its entirety in Recipe 12.1.

6.16 Creating Simple Menus

Problem

You have a simple SQL script that you would like to run against different databases to reset them for tests that you want to run. You could supply the name of the database on the command line, but you want something more interactive. How can you write a shell script to choose from a list of names?

Solution

Use the select statement to create simple character-based screen menus, as in Example 6-6.

Example 6-6. ch06/dbinit.1
#!/usr/bin/env bash
# cookbook filename: dbinit.1
#
DBLIST=$(sh ./listdb | tail -n +2)
select DB in $DBLIST
do
    echo Initializing database: $DB
    mysql -u user -p $DB <myinit.sql
done

Ignore for a moment how $DBLIST gets its values; just know that it is a list of words (like the output from ls would give). The select statement will display those words, each preceded by a number, and the user will be prompted for input. The user makes a choice by typing the number and the corresponding word is assigned to the variable specified after the keyword select (in this case, DB).

Here’s what the running of this script might look like:

$ ./dbinit
1) testDB
2) simpleInventory
3) masterInventory
4) otherDB
#? 2
Initializing database: simpleInventory
#?
$

Discussion

When the user types “2” the variable DB is assigned the word simpleInventory. If you really want to get at the user’s literal choice, the variable $REPLY will hold it; in this case it would be 2.

The select statement is really a loop. When the user has entered a choice it will execute the body of the loop (between the do and the done) and then reprompt for the next value.

It doesn’t redisplay the list every time, only if the user makes no choice and just presses the Enter key. So, to see the list again, the user can press Enter.

It also does not reevaluate the code after the in—that is, you can’t alter the list once you’ve begun. If you modified $DBLIST inside the loop, it wouldn’t change the list of choices.

The looping will stop when it reaches the end of the file, which for interactive use means when the user types Ctrl-D. (If you piped a series of choices into a select loop, it would end when the input ends.)

There isn’t any formatting control over the list, though it will take the value of $COLUMNS into account. If you’re going to use select, you have to be satisfied with the way it displays your choices. You can, however, alter the prompt on the select using the $PS3 variable, as we discuss next and in Recipe 16.12.

6.17 Changing the Prompt on Simple Menus

Problem

You just don’t like that prompt in the select menus. How can it be changed?

Solution

The bash environment variable $PS3 is the prompt used by select. Set it to a new value and you’ll get a new prompt.

Discussion

This is the third of the bash prompts. The first ($PS1) is the prompt you get before most commands. (We’ve used $ in our examples, but it can be much more elaborate than that, including the user ID or directory name.) If a line of command input needs to be continued, the second prompt is used ($PS2).

For select loops, the third prompt, $PS3, is used. Set it before the select statement to make the prompt be whatever you want. You can even modify it within the loop to have it change as the loop progresses.

The script in Example 6-7 is similar to the one in the previous recipe, but it counts how many times it has handled a valid input.

Example 6-7. ch06/dbinit.2
#!/usr/bin/env bash
# cookbook filename: dbinit.2
#
DBLIST=$(sh ./listdb | tail -n +2)

PS3="0 inits >"

select DB in $DBLIST
do
    if [ $DB ]
    then
        echo Initializing database: $DB

        PS3="$((++i)) inits> "

        mysql -u user -p $DB <myinit.sql
    fi
done

We’ve added some extra whitespace to make the setting of $PS3 stand out more. The if statement assures us that we’re only counting the times when the user entered a valid choice. Such a check would have been useful in the previous version, but we were keeping it simple.

6.18 Creating a Simple RPN Calculator

Problem

You may be able to convert binary to decimal, octal, or hex in your head, but it seems that you can’t do simple arithmetic anymore and you can never find a calculator when you need one. What to do?

Solution

Create a calculator using shell arithmetic and RPN notation, as in Example 6-8.

Example 6-8. ch06/rpncalc
#!/usr/bin/env bash
# cookbook filename: rpncalc
#
# simple RPN command-line (integer) calculator
#
# takes the arguments and computes with them
# of the form a b op
# allow the use of x instead of *
#
# error check our argument counts:
if [ ( $# -lt 3 ) -o ( $(($# % 2)) -eq 0 ) ]
then
    echo "usage: calc number number op [ number op ] ..."
    echo "use x or '*' for multiplication"
    exit 1
fi

ANS=$(($1 ${3//x/*} $2))
shift 3
while [ $# -gt 0 ]
do
    ANS=$((ANS ${2//x/*} $1))
    shift 2
done
echo $ANS

Discussion

The RPN (or postfix) style of notation puts the operands (the numbers) first, followed by the operator. If we are using RPN, we don’t write 5 + 4 but rather 5 4 + as our expression. If you want to multiply the result by 2, then you just put 2 * on the end, so the whole expression would be 5 4 + 2 *, which is great for computers to parse because you can go left to right and never need parentheses. The result of any operation becomes the first operand for the next expression.

In our simple bash calculator we will allow the use of a lowercase x as a substitute for the multiplication symbol since * has special meaning to the shell. But if you escape that special meaning by writing '*' or * we want that to work, too.

How do we error check the arguments? We will consider it an error if there are less than three arguments (we need two operands and one operator, e.g., 6 3 /). There can be more than three arguments, but in that case there will always be an odd number (since we start with three and add two more, a second operand and the next operator, and so on, always adding two more; the valid number of arguments would be 3 or 5 or 7 or 9 or…). We check that with the expression:

$(($# % 2)) -eq 0

to see if the result is zero. The $(( )) says we’re doing some shell arithmetic inside. We are using the % operator (called the remainder operator) to see if $# (which is the number of arguments) is divisible by 2 with no remainder (i.e., -eq 0).

Warning

Any arithmetic done within $(( )) is integer arithmetic only.

Now that we know there are the right number of arguments, we can use them to compute the result. We write:

ANS=$(($1 ${3//x/*} $2))

which will compute the result and substitute the asterisk for the letter x at the same time. When you invoke the script you give it an RPN expression on the command line, but the shell syntax for arithmetic is our normal (infix) notation. So, we can evaluate the expression inside of $(( )) but we have to switch the arguments around. Ignoring the x-to-* substitution for the moment, you can see it is just:

ANS=$(($1 $3 $2))

which just moves the operator between the two operands. bash will substitute the parameters before doing the arithmetic evaluation, so if $1 is 5 and $2 is 4 and $3 is a +, then after parameter substitution bash will have:

ANS=$((5 + 4))

and it will evaluate that and assign the result, 9, to $ANS. Done with those three arguments, we shift 3 to toss them and get the new arguments into play. Since we’ve already checked that there are an odd number of arguments, if we have any more arguments to process we will have at least two more (only one more and it would be an even number, since 3+1=4).

From that point on we loop, taking two arguments at a time. The previous answer is the first operand, the next argument (now $1 as a result of the shift) is our second operand, and we put the operator inside $2 in between and evaluate it all much like before. Once we are out of arguments, the answer is what we have in $ANS.

One last word about the substitution. ${2} would be how we refer to the second argument. Though we often don’t bother with the {} and just write $2, we need them here for the additional operations we will ask bash to perform on the argument. We write ${2//x/*} to say that we want to replace or substitute (//) an x with (indicated by the next /) an * before returning the value of $2. We could have written this in two steps by creating an extra variable:

OP=${2//x/*}
ANS=$((ANS OP $1))

That extra variable can be helpful as you first begin to use these features of bash, but once you are familiar with these common expressions, you’ll find yourself putting them all together on one line (even though it’ll be harder to read).

Are you wondering why we didn’t write $ANS and $OP in the expression that does the evaluation? We don’t have to use the $ on variable names inside of $(( )) expressions, except for the positional parameters (e.g., $1, $2). The positional parameters need it to distinguish them from regular numbers (e.g., 1, 2).

6.19 Creating a Command-Line Calculator

Problem

You need more than just integer arithmetic, and you’ve never been very fond of RPN notation. How about a different approach to a command-line calculator?

Solution

Create a trivial command-line calculator using awk’s built-in floating-point arithmetic expressions, as in Example 6-9.

Example 6-9. ch06/func_calc
# cookbook filename: func_calc

# Trivial command-line calculator
function calc {
    # INTEGER ONLY! --> echo The answer is: $(( $* ))
    # Floating point
    awk "BEGIN {print "The answer is: " $* }";
} # end of calc

Discussion

You may be tempted to skip the awk command and try echo The answer is: $$$*$$. This will work fine for integers, but will truncate the results of floating-point operations.

We use a function because aliases (see Recipe 10.7) do not allow the use of arguments.

You will probably want to add this function to your global /etc/bashrc or local ~/.bashrc.

The operators are what you’d expect and are the same as in C:

$ calc 2 + 3 + 4
The answer is: 9

$ calc 2 + 3 + 4.5
The answer is: 9.5

Watch out for shell metacharacters. For example:

$ calc (2+2-3)*4
-bash: syntax error near unexpected token `2+2-3'

You need to escape the special meaning of the parentheses. You can put the expression inside single quotes, or just use a backslash in front of any special (to the shell) character to escape its meaning. For example:

$ calc '(2+2-3)*4'
The answer is: 4

$ calc (2+2-3)*4
The answer is: 4

$ calc '(2+2-3)*4.5'
The answer is: 4.5

We need to escape the multiplication symbol too, since that has special meaning to bash as the wildcard for filenames. This is especially true if you like to put whitespace around your operators, as in 17 + 3 * 21, because then * will match all the files in the current directory, putting their names on the command line in place of the asterisk—definitely not what you want.

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

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