Advanced Bash-Scripting HOWTO: A guide to shell scripting, using Bash | ||
---|---|---|
Prev | Chapter 3. Tutorial / Reference | Next |
Used properly, variables can add power and flexibility to scripts. This requires learning their subtleties and nuances.
environmental variables affecting bash script behavior
input field separator
This defaults to white space, but may be changed, for example, to parse a comma-separated data file.
home directory of the user (usually /home/username)
path to binaries (usually /usr/bin/, /usr/X11R6/bin/, /usr/local/bin, etc.)
Note that the "working directory", ./, is usually omitted from the $PATH as a security measure.
This is the main prompt, seen at the command line.
The secondary prompt, seen when additional input is expected. It is represented as an ">".
working directory (directory you are in at the time)
the default editor invoked by a script, usually vi or emacs.
If the TMOUT environmental variable is set to a non-zero value time, then the shell prompt will time out after time seconds. This will cause a logout.
Note: Unfortunately, this works only while waiting for input at the shell prompt console or in an xterm. While it would be nice to speculate on the uses of this internal variable for timed input, for example in combination with read, TMOUT does not work in that context and is virtually useless for shell scripting.
Implementing timed input in a script is certainly possible, but hardly seems worth the effort. It requires setting up a timing loop to signal the script when it times out. Additionally, a signal handling routine is necessary to trap (see Example 3-82) the interrupt generated by the timing loop (whew!).
#!/bin/bash # TMOUT=3 useless in a script TIMELIMIT=3 # Three seconds in this instance, may be set to different value. PrintAnswer() { if [ $answer = TIMEOUT ] then echo $answer else # Don't want to mix up the two instances. echo "Your favorite veggie is $answer" kill $! # Kills no longer needed TimerOn function running in background. # $! is PID of last job running in background. fi } TimerOn() { sleep $TIMELIMIT && kill -s 14 $$ & # Waits XXX seconds, then sends sigalarm to script. } Int14Vector() { answer="TIMEOUT" PrintAnswer exit 14 } trap Int14Vector 14 # Timer interrupt - 14 - subverted for our purposes. echo "What is your favorite vegetable " TimerOn read answer PrintAnswer # Admittedly, this is a kludgy implementation of timed input, # but pretty much as good as can be done with Bash. # (Challenge to reader: come up with something better.) # If you need something a bit more elegant... # consider writing the application in C or C++, # using appropriate library functions, such as 'alarm' and 'setitimer'. exit 0 |
The number of seconds the script has been running.
#!/bin/bash ENDLESS_LOOP=1 echo echo "Hit Control-C to exit this script." echo while [ $ENDLESS_LOOP ] do if [ $SECONDS -eq 1 ] then units=second else units=seconds fi echo "This script has been running $SECONDS $units." sleep 1 done exit 0 |
The default value when a variable is not supplied to read. Also applicable to select menus, but only supplies the item number of the variable chosen, not the value of the variable itself.
#!/bin/bash echo echo -n "What is your favorite vegetable? " read echo "Your favorite vegetable is $REPLY." # REPLY holds the value of last "read" if and only if no variable supplied. echo echo -n "What is your favorite fruit? " read fruit echo "Your favorite fruit is $fruit." echo "but..." echo "Value of \$REPLY is still $REPLY." # $REPLY is still set to its previous value because # the variable $fruit absorbed the new "read" value. echo exit 0 |
the path to the bash binary itself, usually /bin/bash
an environmental variable pointing to a bash startup file to be read when a script is invoked
positional parameters (passed from command line to script, passed to a function, or set to a variable)
number of command line arguments or positional parameters
process id of script, often used in scripts to construct temp file names
exit status of command, function, or the script itself
All of the positional parameters
Same as $*, but each parameter is a quoted string, that is, the parameters are passed on intact, without interpretation or expansion
Flags passed to script
PID of last job run in background
Initializing or changing the value of a variable
the assignment operator (no space before & after)
Do not confuse this with = and -eq, which test, rather than assign!
Note: = can be either an assignment or a test operator, depending on context.
Example 3-16. Variable Assignment
#!/bin/bash echo # When is a variable "naked", i.e., lacking the '$' in front? # Assignment a=879 echo "The value of \"a\" is $a" # Assignment using 'let' let a=16+5 echo "The value of \"a\" is now $a" echo # In a 'for' loop (really, a type of disguised assignment) echo -n "The values of \"a\" in the loop are " for a in 7 8 9 11 do echo -n "$a " done echo echo # In a 'read' statement echo -n "Enter \"a\" " read a echo "The value of \"a\" is now $a" echo exit 0 |
Example 3-17. Variable Assignment, plain and fancy
#!/bin/bash a=23 # Simple case echo $a b=$a echo $b # Now, getting a little bit fancier... a=`echo Hello!` # Assigns result of 'echo' command to 'a' echo $a a=`ls -l` # Assigns result of 'ls -l' command to 'a' echo $a exit 0 |
Variable assignment using the $() mechanism (a newer method than back quotes)
# From /etc/rc.d/rc.local R=$(cat /etc/redhat-release) arch=$(uname -m) |
variables visible only within a code block or function (see Section 3.18)
variables that affect the behavior of the shell and user interface, such as the path and the prompt
If a script sets environmental variables, they need to be "exported", that is, reported to the environment local to the script. This is the function of the export command.
Note: A script can export variables only to child processes, that is, only to commands or processes which that particular script initiates. A script invoked from the command line cannot export variables back to the command line environment. Child processes cannot export variables back to the parent processes that spawned them.
---
arguments passed to the script from the command line - $0, $1, $2, $3... ($0 is the name of the script itself, $1 is the first argument, etc.)
Example 3-18. Positional Parameters
#!/bin/bash echo echo The name of this script is $0 # Adds ./ for current directory echo The name of this script is `basename $0` # Strip out path name info (see 'basename') echo if [ $1 ] then echo "Parameter #1 is $1" # Need quotes to escape # fi if [ $2 ] then echo "Parameter #2 is $2" fi if [ $3 ] then echo "Parameter #3 is $3" fi echo exit 0 |
Some scripts can perform different operations, depending on which name they are invoked with. For this to work, the script needs to check $0, the name it was invoked by. There also have to be symbolic links present to all the alternate names of the same script.
Note: If a script expects a command line parameter but is invoked without one, this may cause a null variable assignment, certainly an undesirable result. One way to prevent this is to append an extra character to both sides of the assignment statement using the expected positional parameter.
variable1x=$1x # This will prevent an error, even if positional parameter is absent. # The extra character can be stripped off later, if desired, like so. variable1=${variable1x/x/} # This uses one of the parameter substitution templates previously discussed. # Leaving out the replacement pattern results in a deletion. |
---
Example 3-19. wh, whois domain name lookup
#!/bin/bash # Does a 'whois domain-name' lookup # on any of 3 alternate servers: # ripe.net, cw.net, radb.net # Place this script, named 'wh' in /usr/local/bin # Requires symbolic links: # ln -s /usr/local/bin/wh /usr/local/bin/wh-ripe # ln -s /usr/local/bin/wh /usr/local/bin/wh-cw # ln -s /usr/local/bin/wh /usr/local/bin/wh-radb if [ -z $1 ] then echo "Usage: `basename $0` [domain-name]" exit 1 fi case `basename $0` in # Checks script name and calls proper server "wh" ) whois $1@whois.ripe.net;; "wh-ripe") whois $1@whois.ripe.net;; "wh-radb") whois $1@whois.radb.net;; "wh-cw" ) whois $1@whois.cw.net;; * ) echo "Usage: `basename $0` [domain-name]";; esac exit 0 |
---
The shift command reassigns the positional parameters, in effect shifting them to the left one notch.
$1 <--- $2, $2 <--- $3, $3 <--- $4, etc.
The old $1 disappears, but $0 does not change. If you use a large number of positional parameters to a script, shift lets you access those past 10.
Example 3-20. Using shift
#!/bin/bash # Name this script something like shift000, # and invoke it with some parameters, for example # ./shift000 a b c def 23 skidoo # Demo of using 'shift' # to step through all the positional parameters. until [ -z "$1" ] do echo -n "$1 " shift done echo # Extra line feed. exit 0 |
The declare or typeset keywords (they are exact synonyms) permit restricting the properties of variables. This is a very weak form of the typing available in certain programming languages. The declare command is not available in version 1 of bash.
declare -r var1 |
(declare -r var1 works the same as readonly var1)
This is the rough equivalent of the C const type qualifier. An attempt to change the value of a readonly variable fails with an error message.
declare -i var2 |
The script treats subsequent occurrences of var2 as an integer. Note that certain arithmetic operations are permitted for declared integer variables without the need for expr or let.
declare -a indices |
The variable indices will be treated as an array.
declare -f # (no arguments) |
A declare -f line within a script causes a listing of all the functions contained in that script.
declare -x var3 |
This declares a variable as available for exporting outside the environment of the script itself.
Example 3-21. Using declare to type variables
#!/bin/bash declare -f # Lists the function below. func1 () { echo This is a function. } declare -r var1=13.36 echo "var1 declared as $var1" # Attempt to change readonly variable. var1=13.37 # Generates error message. echo "var1 is still $var1" echo declare -i var2 var2=2367 echo "var2 declared as $var2" var2=var2+1 # Integer declaration eliminates the need for 'let'. echo "var2 incremented by 1 is $var2." # Attempt to change variable declared as integer echo "Attempting to change var2 to floating point value, 2367.1." var2=2367.1 # results in error message, with no change to variable. echo "var2 is still $var2" exit 0 |
Assume that the value of a variable is the name of a second variable. Is it somehow possible to retrieve the value of this second variable from the first one? For example, if a=letter_of_alphabet and letter_of_alphabet=z, can a reference to a return z? This can indeed be done, and it is called an indirect reference. It uses the unusual eval var1=\$$var2 notation.
Example 3-22. Indirect References
#!/bin/bash # Indirect variable referencing. a=letter_of_alphabet letter_of_alphabet=z # Direct reference. echo "a = $a" # Indirect reference. eval a=\$$a echo "Now a = $a" echo # Now, let's try changing the second order reference. t=table_cell_3 table_cell_3=24 eval t=\$$t echo "t = $t" # So far, so good. table_cell_3=387 eval t=\$$t echo "Value of t changed to $t" # ERROR! # Cannot indirectly reference changed value of variable this way. # For this to work, must use ${!t} notation. exit 0 |
Note: This method of indirect referencing has its limitations. If the second order variable changes its value, an indirect reference to the first order variable produces an error. Fortunately, this flaw has been fixed in the newer ${!variable} notation introduced with version 2 of Bash (see Example 3-85).
Note: $RANDOM is an internal Bash function (not a constant) that returns a pseudorandom integer in the range 0 - 32767. $RANDOM should not be used to generate an encryption key.
Example 3-23. Generating random numbers
#!/bin/bash # $RANDOM returns a different random integer at each invocation. # Nominal range: 0 - 32767 (signed integer). MAXCOUNT=10 count=1 echo echo "$MAXCOUNT random numbers:" echo "-----------------" while [ $count -le $MAXCOUNT ] # Generate 10 ($MAXCOUNT) random integers. do number=$RANDOM echo $number let "count += 1" # Increment count. done echo "-----------------" # If you need a random int within a certain range, then use the 'modulo' operator. RANGE=500 echo number=$RANDOM let "number %= $RANGE" echo "Random number less than $RANGE --> $number" echo # If you need a random int greater than a lower bound, # then set up a test to discard all numbers below that. FLOOR=200 number=0 #initialize while [ $number -le $FLOOR ] do number=$RANDOM done echo "Random number greater than $FLOOR --> $number" echo # May combine above two techniques to retrieve random number between two limits. number=0 #initialize while [ $number -le $FLOOR ] do number=$RANDOM let "number %= $RANGE" done echo "Random number between $FLOOR and $RANGE --> $number" echo # May generate binary choice, that is, "true" or "false" value. BINARY=2 number=$RANDOM let "number %= $BINARY" if [ $number -eq 1 ] then echo "TRUE" else echo "FALSE" fi echo # May generate toss of the dice. SPOTS=7 DICE=2 die1=0 die2=0 # Tosses each die separately, and so gives correct odds. while [ $die1 -eq 0 ] #Can't have a zero come up. do let "die1 = $RANDOM % $SPOTS" done while [ $die2 -eq 0 ] do let "die2 = $RANDOM % $SPOTS" done let "throw = $die1 + $die2" echo "Throw of the dice = $throw" echo exit 0 |