预计阅读本页时间:-
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
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
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 nth directory in the stack (starting with 0), rotates it to the top, and cds to it.
- pushd without arguments, instead of complaining, swaps the two top directories on the stack and cds to the new top.
- popd +n takes the nth directory in the stack and just deletes it.
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:
- 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 nth directory (starting at 0) and the -l option produces a long listing; any tildes (~) are replaced by the full pathname.
- 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.
- Modify pushd, popd, and getNdirs so that they use variables of type integer in the arithmetic expressions.
- 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.
- bash's versions of pushd and popd also have a -N option. In both cases -N causes the nth 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.
- Use getNdirs to reimplement the selectd function from the last chapter.