< Day Day Up > |
6.3. Integer Variables and ArithmeticThe expression $(($OPTIND - 1)) in the last graphics utility example shows another way that the shell can do integer arithmetic. As you might guess, the shell interprets words surrounded by $(( and )) as arithmetic expressions.[8] Variables in arithmetic expressions do not need to be preceded by dollar signs, though it is not wrong to do so.
Arithmetic expressions are evaluated inside double quotes, like tildes, variables, and command substitutions. We're finally in a position to state the definitive rule about quoting strings: when in doubt, enclose a string in single quotes, unless it contains tildes or any expression involving a dollar sign, in which case you should use double quotes. For example, the date command on modern versions of UNIX accepts arguments that tell it how to format its output. The argument +%j tells it to print the day of the year, i.e., the number of days since December 31st of the previous year. We can use +%j to print a little holiday anticipation message: echo "Only $(( (365-$(date +%j)) / 7 )) weeks until the New Year" We'll show where this fits in the overall scheme of command-line processing in Chapter 7. The arithmetic expression feature is built into bash's syntax, and was available in the Bourne shell (most versions) only through the external command expr. Thus it is yet another example of a desirable feature provided by an external command being better integrated into the shell. getopts, as we have already seen, is another example of this design trend. bash arithmetic expressions are equivalent to their counterparts in the Java and C languages.[9] Precedence and associativity are the same as in C. Table 6-2 shows the arithmetic operators that are supported. Although some of these are (or contain) special characters, there is no need to backslash-escape them, because they are within the $((...)) syntax.
The ++ and - operators are useful when you want to increment or decrement a value by one.[11] They work the same as in Java and C, e.g., value++ increments value by 1. This is called post-increment; there is also a pre-increment: ++value. The difference becomes evident with an example:
$ i=0 $ echo $i 0 $ echo $((i++)) 0 $ echo $i 1 $ echo $((++i)) 2 $ echo $i 2 In both cases the value has been incremented by one. However, in the first case (post-increment) the value of the variable was passed to echo and then the variable was incremented. In the second case (pre-increment) the increment was performed and then the variable passed to echo. Parentheses can be used to group subexpressions. The arithmetic expression syntax also (as in C) supports relational operators as "truth values" of 1 for true and 0 for false. Table 6-3 shows the relational operators and the logical operators that can be used to combine relational expressions.
For example, $((3 > 2)) has the value 1; $(( (3 > 2) || (4 <= 1) )) also has the value 1, since at least one of the two subexpressions is true. The shell also supports base N numbers, where N can be from 2 to 36. The notation B#N means "N base B". Of course, if you omit the B#, the base defaults to 10. 6.3.1. Arithmetic ConditionalsIn Chapter 5, we saw how to compare strings by the use of [...] notation (or with the test built-in). Arithmetic conditions can also be tested in this way. However, the tests have to be carried out with their own operators. These are shown in Table 6-4.
And as with string comparisons, the arithmetic test returns a result of true or false; 0 if true, 1 otherwise. So, for example, [ 3 -gt 2 ] produces exit status 0, as does [ \( 3 -gt 2 \) || \( 4 -le 1 \) ], but [ \( 3 -gt 2 \) && \( 4 -le 1 \) ] has exit status 1 since the second subexpression isn't true. In these examples we have had to escape the parentheses and pass them to test as separate arguments. As you can see, the result can look rather unreadable if there are many parentheses. Another way to make arithmetic tests is to use the $((...)) form to encapsulate the condition. For example: [ $(((3 > 2) && (4 <= 1))) = 1 ]. This evaluates the conditionals and then compares the resulting value to 1 (true).[12]
There is an even neater and more efficient way of performing an arithmetic test: by using the ((...)) construct.[13] This returns an exit status of 0 if the expression is true, and 1 otherwise.
The above expression using this construct becomes (( (3 > 2) && (4 <= 1) )). This example returns with an exit status of 1 because, as we said, the second subexpression is false. 6.3.2. Arithmetic Variables and AssignmentAs we saw earlier, you can define integer variables by using declare. You can also evaluate arithmetic expressions and assign them to variables with the use of let. The syntax is: let intvar=expression It is not necessary (because it's actually redundant) to surround the expression with $(( and )) in a let statement. let doesn't create a variable of type integer; it only causes the expression following the assignment to be interpreted as an arithmetic one. As with any variable assignment, there must not be any space on either side of the equal sign (=). It is good practice to surround expressions with quotes, since many characters are treated as special by the shell (e.g., *, #, and parentheses); furthermore, you must quote expressions that include whitespace (spaces or TABs). See Table 6-5 for examples.
Here is the code: for dir in ${*:-.}; do if [ -e $dir ]; then result=$(du -s $dir | cut -f 1) let total=$result*1024 echo -n "Total for $dir = $total bytes" if [ $total -ge 1048576 ]; then echo " ($((total/1048576)) Mb)" elif [ $total -ge 1024 ]; then echo " ($((total/1024)) Kb)" fi fi done To obtain the disk usage of files and directories, we can use the UNIX utility du. The default output of du is a list of directories with the amount of space each one uses, and looks something like this: 6 ./toc 3 ./figlist 6 ./tablist 1 ./exlist 1 ./index/idx 22 ./index 39 . If you don't specify a directory to du, it will use the current directory (.). Each directory and subdirectory is listed along with the amount of space it uses. The grand total is given in the last line. The amount of space used by each directory and all the files in it is listed in terms of blocks. Depending on the UNIX system you are running on, one block can represent 512 or 1024 bytes. Each file and directory uses at least one block. Even if a file or directory is empty, it is still allocated a block of space in the filesystem. In our case, we are only interested in the total usage, given on the last line of du's output. To obtain only this line, we can use the -s option of du. Once we have the line, we want only the number of blocks and can throw away the directory name. For this we use our old friend cut to extract the first field. Once we have the total, we can multiply it by the number of bytes in a block (1024 in this case) and print the result in terms of bytes. We then test to see if the total is greater than the number of bytes in one megabyte (1048576 bytes, which is 1024 x 1024) and if it is, we can print how many megabytes it is by dividing the total by this large number. If not, we see if it can be expressed in kilobytes, otherwise nothing is printed. We need to make sure that any specified directories exist, otherwise du will print an error message and the script will fail. We do this by using the test for file or directory existence (-e) that we saw in Chapter 5 before calling du. To round out this script, it would be nice to imitate du as closely as possible by providing for multiple arguments. To do this, we wrap the code in a for loop. Notice how parameter substitution has been used to specify the current directory if no arguments are given. As a bigger example of integer arithmetic, we will complete our emulation of the pushd and popd functions (Task 4-8). Remember that these functions operate on DIR_STACK, a stack of directories represented as a string with the directory names separated by spaces. bash's pushd and popd take additional types of arguments, which are:
The most useful of these features is the ability to get at the nth directory in the stack. Here are the latest versions of both functions: .ps 8 pushd ( ) { dirname=$1 if [ -n $dirname ] && [ \( -d $dirname \) -a \( -x $dirname \) ]; then DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}" cd $dirname echo "$DIR_STACK" else echo "still in $PWD." fi } popd ( ) { if [ -n "$DIR_STACK" ]; then DIR_STACK=${DIR_STACK#* } cd ${DIR_STACK%% *} echo "$PWD" else echo "stack empty, still in $PWD." fi } To get at the nth directory, we use a while loop that transfers the top directory to a temporary copy of the stack n times. We'll put the loop into a function called getNdirs that looks like this: getNdirs ( ) { stackfront='' let count=0 while [ $count -le $1 ]; do target=${DIR_STACK%${DIR_STACK#* }} stackfront="$stackfront$target" DIR_STACK=${DIR_STACK#$target} let count=count+1 done stackfront=${stackfront%$target} } The argument passed to getNdirs is the n in question. The variable target contains the directory currently being moved from DIR_STACK to a temporary stack, stackfront. target will contain the nth directory and stackfront will have all of the directories above (and including) target when the loop finishes. stackfront starts as null; count, which counts the number of loop iterations, starts as 0. The first line of the loop body copies the first directory on the stack to target. The next line appends target to stackfront and the following line removes target from the stack ${DIR_STACK#$target}. The last line increments the counter for the next iteration. The entire loop executes n+1 times, for values of count from 0 to N. When the loop finishes, the directory in $target is the nth directory. The expression ${stackfront%$target} removes this directory from stackfront so that stackfront will contain the first n-1 directories. Furthermore, DIR_STACK now contains the "back" of the stack, i.e., the stack without the first n directories. With this in mind, we can now write the code for the improved versions of pushd and popd: pushd ( ) { if [ $(echo $1 | grep '^+[0-9][0-9]*$') ]; then # case of pushd +n: rotate n-th directory to top let num=${1#+} getNdirs $num DIR_STACK="$target$stackfront$DIR_STACK" cd $target echo "$DIR_STACK" elif [ -z "$1" ]; then # case of pushd without args; swap top two directories firstdir=${DIR_STACK%% *} DIR_STACK=${DIR_STACK#* } seconddir=${DIR_STACK%% *} DIR_STACK=${DIR_STACK#* } DIR_STACK="$seconddir $firstdir $DIR_STACK" cd $seconddir else # normal case of pushd dirname dirname=$1 if [ \( -d $dirname \) -a \( -x $dirname \) ]; then DIR_STACK="$dirname ${DIR_STACK:-$PWD" "}" cd $dirname echo "$DIR_STACK" else echo still in "$PWD." fi fi } popd ( ) { if [ $(echo $1 | grep '^+[0-9][0-9]*$') ]; then # case of popd +n: delete n-th directory from stack let num=${1#+} getNdirs $num DIR_STACK="$stackfront$DIR_STACK" cd ${DIR_STACK%% *} echo "$PWD" else # normal case of popd without argument if [ -n "$DIR_STACK" ]; then DIR_STACK=${DIR_STACK#* } cd ${DIR_STACK%% *} echo "$PWD" else echo "stack empty, still in $PWD." fi fi } These functions have grown rather large; let's look at them in turn. The if at the beginning of pushd checks if the first argument is an option of the form +N. If so, the first body of code is run. The first let simply strips the plus sign (+) from the argument and assigns the result—as an integer—to the variable num. This, in turn, is passed to the getNdirs function. The next assignment statement sets DIR_STACK to the new ordering of the list. Then the function cds to the new directory and prints the current directory stack. The elif clause tests for no argument, in which case pushd should swap the top two directories on the stack. The first four lines of this clause assign the top two directories to firstdir and seconddir, and delete these from the stack. Then, as above, the code puts the stack back together in the new order and cds to the new top directory. The else clause corresponds to the usual case, where the user supplies a directory name as argument. popd works similarly. The if clause checks for the +N option, which in this case means "delete the nth directory." A let extracts the N as an integer; the getNdirs function puts the first n directories into stackfront. Finally, the stack is put back together with the nth directory missing, and a cd is performed in case the deleted directory was the first in the list. The else clause covers the usual case, where the user doesn't supply an argument. Before we leave this subject, here are a few exercises that should test your understanding of this code:
6.3.3. Arithmetic for LoopsChapter 5 introduced the for loop and briefly mentioned another type of for loop, more akin to the construct found in many programming languages like Java and C. This type of for loop is called an arithmetic for loop.[14]
The form of an arithmetic for loop is very similar to those found in Java and C: for (( initialisation ; ending condition ; update )) do statements... done There are four sections to the loop, the first three being arithmetic expressions and the last being a set of statements just as in the standard loop that we saw in the last chapter. The first expression, initialisation, is something that is done once at the start of the loop and if it evaluates to true the loop continues its process; otherwise, it skips the loop and continues with the next statement. When initialisation is true the loop then evaluates ending condition. If this is true then it executes statements, evaluates update and repeats the cycle again by evaluation ending condition. The loop continues until ending condition becomes false or the loop is exited via one of the statements. Usually initialisation is used to set an arithmetic variable to some initial value, update updates that variable, and ending condition tests the variable. Any of the values may be left out in which case they automatically evaluate to true. The following simple example: for ((;;)) do read var if [ "$var" = "." ]; then break fi done loops forever reading lines until a line consisting of a . is found. We'll look at using the expressions in an arithmetic for loop in our next task.
This task is best accomplished using nested for loops: for (( i=1; i <= 12 ; i++ )) do for (( j=1 ; j <= 12 ; j++ )) do echo -ne "$(( j * i ))\t" done echo done The script begins with a for loop using a variable i; the initialisation clause sets i to 1, the ending condition clause tests i against the limit (12 in our case), and the update clause adds 1 to i each time around the loop. The body of the loop is another for loop, this time with a variable called j. This is identical to the i for loop except that j is being updated. The body of the j loop has an echo statement where the two variables are multiplied together and printed along with a trailing tab. We deliberately don't print a newline (with the -n option to echo) so that the numbers appear on one line. Once the inner loop has finished a newline is printed so that the set of numbers starts on the next line. Arithmetic for loops are useful when dealing with arrays, which we'll now look at. |
< Day Day Up > |