Integer Variables and Arithmetic

6.3 Integer Variables and Arithmetic

The 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.

[8] You can also use the older form $[...] , but we don't recommend this because it will be phased out in future versions of bash .

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 System V-derived 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 C language. [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.

[9] The assignment forms of these operators are also permitted. For example, $((x += 2)) adds 2 to x and stores the result back in x .

Table 6.2. Arithmetic Operators

Operator

Meaning

+

Plus

-

Minus

*

Multiplication

/

Division (with truncation )

%

Remainder

<<

Bit-shift left

>>

Bit-shift right

&

Bitwise and

Bitwise or

~

Bitwise not

!

Bitwise not

^

Bitwise exclusive or

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.

Table 6.3. Relational Operators

Operator

Meaning

<

Less than

>

Greater than

<=

Less than or equal to

>=

Greater than or equal to

==

Equal to

!=

Not equal to

&&

Logical and

Logical or

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 Conditionals

In 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 .

Table 6.4. Test Relational Operators

Operator

Meaning

-lt

Less than

-gt

Greater than

-le

Less than or equal to

-ge

Greater than or equal to

-eq

Equal to

-ne

Not equal to

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). [10]

[10] Note that the truth values returned by $((...)) are 1 for true, 0 for false ”the reverse of the test and exit statuses.

There is an even neater and more efficient way of performing an arithmetic test: by using the ((...)) construct. [11] This returns an exit status of 0 if the expression is true, and 1 otherwise.

[11] ((...)) is not available in versions of bash prior to 2.0.

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 Assignment

As 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.

Table 6.5. Sample Integer Expression Assignments

Assignment

Value

let x=

$x

1+4

5

'1 + 4'

5

'(2+3) * 5'

25

'2 + 3 * 5'

17

'17 / 3'

5

'17 % 3'

2

'1<<4'

16

'48>>3'

6

'17 & 3'

1

'17 3'

19

'17 ^ 3'

18

 

Task 6-1

Here is a small task that makes use of integer arithmetic. Write a script called ndu that prints a summary of the disk space usage for each directory argument (and any subdirectories), both in terms of bytes, and kilobytes or megabytes (whichever is appropriate).

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:

·                 pushd +n takes the n th directory in the stack (starting with 0), rotates it to the top, and cd s to it.

·                 pushd without arguments, instead of complaining, swaps the two top directories on the stack and cd s to the new top.

·                 popd +n takes the n th directory in the stack and just deletes it.

The most useful of these features is the ability to get at the n th directory in the stack. Here are the latest versions of both functions:

 .ps 8 
 pushd () 
 { 
 dirname= 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 n th 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  ]; 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 n th 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 n th 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   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 "" ]; 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= 
 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   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 cd s 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 cd s 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 n th 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 n th 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:

1.             Implement bash 's dirs command and the options +n and -l . dirs by itself displays the list of currently remembered directories (those in the stack). The +n option prints out the n th directory (starting at 0) and the -l option produces a long listing; any tildes ( ~ ) are replaced by the full pathname.

2.             Modify the getNdirs function so that it checks for N exceeding the number of directories in the stack and exits with an appropriate error message if true.

3.             Modify pushd , popd , and getNdirs so that they use variables of type integer in the arithmetic expressions.

4.             Change getNdirs so that it uses cut (with command substitution), instead of the while loop, to extract the first N directories. This uses less code but runs more slowly because of the extra processes generated.

5.             bash 's versions of pushd and popd also have a - N option. In both cases - N causes the n th directory from the right-hand side of the list to have the operation performed on it. As with + N , it starts at 0. Add this functionality.

6.             Use getNdirs to reimplement the selectd function from the last chapter.

 



Learning the Bash Shell
Learning the bash Shell: Unix Shell Programming (In a Nutshell (OReilly))
ISBN: 0596009658
EAN: 2147483647
Year: 1998
Pages: 104

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