| Advanced Bash-Scripting Guide: A complete guide to shell scripting, using Bash | ||
|---|---|---|
| Prev | Chapter 3. Tutorial / Reference | Next |
3.7. Variables Revisited
Used properly, variables can add power and flexibility to scripts. This requires learning their subtleties and nuances.
- Internal (builtin) variables
variables affecting bash script behavior
- $BASH
the path to the Bash binary itself, usually /bin/bash
- $BASH_ENV
an environmental variable pointing to a Bash startup file to be read when a script is invoked
- $BASH_VERSION
the version of Bash installed on the system
bash$ echo $BASH_VERSION 2.04.12(1)-release
tcsh% echo $BASH_VERSION BASH_VERSION: Undefined variable.
Checking $BASH_VERSION is a good method of determining which shell is running. $SHELL does not necessarily give the correct answer.
- $DIRSTACK
contents of the directory stack (affected by pushd and popd)
This builtin variable is the counterpart to the dirs command.
- $EDITOR
the default editor invoked by a script, usually vi or emacs.
- $EUID
"effective" user id number
Identification number of whatever identity the current user has assumed, perhaps by means of su.

The $EUID is not necessarily the same as the $UID.
- $GROUPS
groups current user belongs to
This is a listing (array) of the group id numbers for current user, as recorded in /etc/passwd.
- $HOME
home directory of the user, usually /home/username (see Example 3-7)
- $HOSTNAME
The hostname command assigns the system name at bootup in an init script. However, the gethostname() function sets the Bash internal variable $HOSTNAME. See also Example 3-7.
- $HOSTTYPE
host type
Like $MACHTYPE, identifies the system hardware.
bash$ echo $HOSTTYPE i686
- $IFS
input field separator
This defaults to whitespace (space, tab, and newline), but may be changed, for example, to parse a comma-separated data file. Note that $* uses the first character held in $IFS. See Example 3-12.
bash$ echo $IFS | cat -vte $ bash$ bash -c 'set w x y z; IFS=":-;"; echo "$*"' w:x:y:z
(Thanks, S. C., for clarification and examples.)
- $IGNOREEOF
ignore EOF: how many end-of-files (control-D) the shell will ignore before logging out.
- $LINENO
This variable is the line number of the shell script in which this variable appears. It has significance only within the script in which it appears, and is chiefly useful for debugging purposes.
1 last_cmd_arg=$_ # Save it. 2 3 echo "At line number $LINENO, variable \"v1\" = $v1" 4 echo "Last command argument processed = $last_cmd_arg"
- $MACHTYPE
machine type
Identifies the system hardware.
bash$ echo $MACHTYPE i686-debian-linux-gnu
- $OLDPWD
old working directory ("OLD-print-working-directory", previous directory you were in)
- $OSTYPE
operating system type
bash$ echo $OSTYPE linux-gnu
- $PATH
path to binaries, usually /usr/bin/, /usr/X11R6/bin/, /usr/local/bin, etc.
When given a command, the shell automatically does a hash table search on the directories listed in the path for the executable. The path is stored in the environmental variable, $PATH, a list of directories, separated by colons. Normally, the system stores the $PATH definition in /etc/profile and/or ~/.bashrc (see Section 3.25).
bash$ echo $PATH /bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin:/sbin:/usr/sbin
PATH=${PATH}:/opt/bin appends the /opt/bin directory to the current path. In a script, it may be expedient to temporarily add a directory to the path in this way. When the script exits, this restores the original $PATH (a child process, such as a script, may not change the environment of the parent process, the shell).

The current "working directory", ./, is usually omitted from the $PATH as a security measure.
- $PPID
The $PPID of a process is the process id (pid) of its parent process. [1]
Compare this with the pidof command.
- $PS1
This is the main prompt, seen at the command line.
- $PS2
The secondary prompt, seen when additional input is expected. It displays as ">".
- $PS3
The tertiary prompt, displayed in a select loop (see Example 3-67).
- $PS4
The quartenary prompt, shown at the beginning of each line of output when invoking a script with the -x option. It displays as "+".
- $PWD
working directory (directory you are in at the time)
This is the analog to the pwd builtin command.
1 #!/bin/bash 2 3 WRONG_DIRECTORY=33 4 5 clear # Clear screen. 6 7 TargetDirectory=/home/bozo/projects/GreatAmericanNovel 8 9 cd $TargetDirectory 10 echo "Deleting stale files in $TargetDirectory." 11 12 if [ "$PWD" != "$TargetDirectory" ] # Keep from wiping out wrong directory by accident. 13 then 14 echo "Wrong directory!" 15 echo "In $PWD, rather than $TargetDirectory!" 16 echo "Bailing out!" 17 exit $WRONG_DIRECTORY 18 fi 19 20 rm -rf * 21 rm .[A-Za-z0-9]* # Delete dotfiles. 22 # rm -f .[^.]* ..?* to remove filenames beginning with multiple dots. 23 # (shopt -s dotglob; rm -f *) will also work. 24 # Thanks, S.C. for pointing this out. 25 26 # Filenames may contain all characters in the 0 - 255 range, except "/". 27 # Deleting files beginning with weird characters is left as an exercise. 28 29 30 # Various other operations here, as necessary. 31 32 echo 33 echo "Done." 34 echo "Old files deleted in $TargetDirectory." 35 echo 36 37 38 exit 0
- $REPLY
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.
1 #!/bin/bash 2 3 4 echo 5 echo -n "What is your favorite vegetable? " 6 read 7 8 echo "Your favorite vegetable is $REPLY." 9 # REPLY holds the value of last "read" if and only if no variable supplied. 10 11 12 echo 13 echo -n "What is your favorite fruit? " 14 read fruit 15 echo "Your favorite fruit is $fruit." 16 echo "but..." 17 echo "Value of \$REPLY is still $REPLY." 18 # $REPLY is still set to its previous value because 19 # the variable $fruit absorbed the new "read" value. 20 21 echo 22 23 exit 0
- $SECONDS
The number of seconds the script has been running.
1 #!/bin/bash 2 3 ENDLESS_LOOP=1 4 INTERVAL=1 5 6 echo 7 echo "Hit Control-C to exit this script." 8 echo 9 10 while [ $ENDLESS_LOOP ] 11 do 12 if [ "$SECONDS" -eq 1 ] 13 then 14 units=second 15 else 16 units=seconds 17 fi 18 19 echo "This script has been running $SECONDS $units." 20 sleep $INTERVAL 21 done 22 23 24 exit 0
- $SHELLOPTS
the list of enabled shell options, a readonly variable
- $TMOUT
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.

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. (Reportedly the ksh version of a timed read does work).
Implementing timed input in a script is certainly possible, but hardly seems worth the effort. One method is to set up a timing loop to signal the script when it times out. This also requires a signal handling routine to trap (see Example 3-162) the interrupt generated by the timing loop (whew!).
Example 3-24. Timed Input
1 #!/bin/bash 2 3 # TMOUT=3 useless in a script 4 5 TIMELIMIT=3 # Three seconds in this instance, may be set to different value. 6 7 PrintAnswer() 8 { 9 if [ "$answer" = TIMEOUT ] 10 then 11 echo $answer 12 else # Don't want to mix up the two instances. 13 echo "Your favorite veggie is $answer" 14 kill $! # Kills no longer needed TimerOn function running in background. 15 # $! is PID of last job running in background. 16 fi 17 18 } 19 20 21 22 TimerOn() 23 { 24 sleep $TIMELIMIT && kill -s 14 $$ & 25 # Waits 3 seconds, then sends sigalarm to script. 26 } 27 28 Int14Vector() 29 { 30 answer="TIMEOUT" 31 PrintAnswer 32 exit 14 33 } 34 35 trap Int14Vector 14 36 # Timer interrupt - 14 - subverted for our purposes. 37 38 echo "What is your favorite vegetable " 39 TimerOn 40 read answer 41 PrintAnswer 42 43 44 # Admittedly, this is a kludgy implementation of timed input, 45 # but pretty much as good as can be done with Bash. 46 # (Challenge to reader: come up with something better.) 47 48 # If you need something a bit more elegant... 49 # consider writing the application in C or C++, 50 # using appropriate library functions, such as 'alarm' and 'setitimer'. 51 52 exit 0An alternative is using stty.
Example 3-25. Once more, timed input
1 #!/bin/bash 2 # timeout.sh 3 4 # Written by Stephane Chazelas, 5 # and modified by the document author. 6 7 INTERVAL=5 # timeout interval 8 9 timedout_read() { 10 timeout=$1 11 varname=$2 12 old_tty_settings=`stty -g` 13 stty -icanon min 0 time ${timeout}0 14 eval read $varname # or just read $varname 15 stty "$old_tty_settings" 16 # See man page for "stty". 17 } 18 19 echo; echo -n "What's your name? Quick! " 20 timedout_read $INTERVAL your_name 21 22 # This may not work on every terminal type. 23 # The maximum timeout depends on the terminal. 24 # (it is often 25.5 seconds). 25 26 echo 27 28 if [ ! -z "$your_name" ] # If name input before timeout... 29 then 30 echo "Your name is $your_name." 31 else 32 echo "Timed out." 33 fi 34 35 echo 36 37 # This script behaves slightly different than the previous "timed input" example. 38 # At each keystroke, the counter resets. 39 40 exit 0- $UID
user id number
current user's user identification number, as recorded in /etc/passwd
This is the current user's real id, even if she has temporarily assumed another identity through su. $UID is a readonly variable, not subject to change from the command line or within a script, and is the counterpart to the id builtin.
Example 3-26. Am I root?
1 #!/bin/bash 2 # am-i-root.sh: Am I root or not? 3 4 ROOT_UID=0 # Root has $UID 0. 5 6 if [ "$UID" -eq "$ROOT_UID" ] 7 then 8 echo "You are root." 9 else 10 echo "You are just an ordinary user (but mom loves you just the same)." 11 fi 12 13 exit 0
See also Example 2-2.

The variables $USER, $USERNAME, $LOGNAME, $MAIL, and $ENV are not Bash builtins. These are, however, often set as environmental variables in one of the Bash startup files. $SHELL, the name of the user's login shell, may be set from /etc/passwd or in an "init" script, and it is likewise not a Bash builtin.
- $0, $1, $2, etc.
positional parameters, passed from command line to script, passed to a function, or set to a variable (see Example 3-33 and Example 3-75)
- $#
number of command line arguments [2] or positional parameters (see Example 2-4)
- $$
process id of script, often used in scripts to construct temp file names (see Example A-7 and Example 3-163)
- $?
exit status of a command, function, or the script itself (see Example 3-139)
- $*
All of the positional parameters, seen as a single word
- $@
Same as $*, but each parameter is a quoted string, that is, the parameters are passed on intact, without interpretation or expansion. This means, among other things, that each parameter in the argument list is seen as a separate word.
Example 3-27. arglist: Listing arguments with $* and $@
1 #!/bin/bash 2 # Invoke this script with several arguments, such as "one two three". 3 4 if [ ! -n "$1" ] 5 then 6 echo "Usage: `basename $0` argument1 argument2 etc." 7 exit 65 8 fi 9 10 11 echo 12 13 index=1 14 15 echo "Listing args with \"\$*\":" 16 for arg in "$*" # Doesn't work properly if "$*" isn't quoted. 17 do 18 echo "Arg #$index = $arg" 19 let "index+=1" 20 done # $* sees all arguments as single word. 21 echo "Entire arg list seen as single word." 22 23 echo 24 25 index=1 26 27 echo "Listing args with \"\$@\":" 28 for arg in "$@" 29 do 30 echo "Arg #$index = $arg" 31 let "index+=1" 32 done # $@ sees arguments as separate words. 33 echo "Arg list seen as separate words." 34 35 echo 36 37 exit 0
The $@ special parameter finds use as a tool for filtering input into shell scripts. The cat "$@" construction accepts input to a script either from stdin or from files given as parameters to the script. See Example 3-98.

The $* and $@ parameters sometimes display inconsistent and puzzling behavior, depending on the setting of $IFS.
Example 3-28. Inconsistent $* and $@ behavior
1 #!/bin/bash 2 3 # Erratic behavior of the "$*" and "$@" internal Bash variables, 4 # depending on whether these are quoted or not. 5 # Word splitting and linefeeds handled inconsistently. 6 7 # This example script by Stephane Chazelas, 8 # and slightly modified by the document author. 9 10 11 set -- "First one" "second" "third:one" "" "Fifth: :one" 12 # Setting the script arguments, $1, $2, etc. 13 14 echo 15 16 echo 'IFS unchanged, using "$*"' 17 c=0 18 for i in "$*" # quoted 19 do echo "$((c+=1)): [$i]" # This line remains the same in every instance. 20 # Echo args. 21 done 22 echo --- 23 24 echo 'IFS unchanged, using $*' 25 c=0 26 for i in $* # unquoted 27 do echo "$((c+=1)): [$i]" 28 done 29 echo --- 30 31 echo 'IFS unchanged, using "$@"' 32 c=0 33 for i in "$@" 34 do echo "$((c+=1)): [$i]" 35 done 36 echo --- 37 38 echo 'IFS unchanged, using $@' 39 c=0 40 for i in $@ 41 do echo "$((c+=1)): [$i]" 42 done 43 echo --- 44 45 IFS=: 46 echo 'IFS=":", using "$*"' 47 c=0 48 for i in "$*" 49 do echo "$((c+=1)): [$i]" 50 done 51 echo --- 52 53 echo 'IFS=":", using $*' 54 c=0 55 for i in $* 56 do echo "$((c+=1)): [$i]" 57 done 58 echo --- 59 60 var=$* 61 echo 'IFS=":", using "$var" (var=$*)' 62 c=0 63 for i in "$var" 64 do echo "$((c+=1)): [$i]" 65 done 66 echo --- 67 68 echo 'IFS=":", using $var (var=$*)' 69 c=0 70 for i in $var 71 do echo "$((c+=1)): [$i]" 72 done 73 echo --- 74 75 var="$*" 76 echo 'IFS=":", using $var (var="$*")' 77 c=0 78 for i in $var 79 do echo "$((c+=1)): [$i]" 80 done 81 echo --- 82 83 echo 'IFS=":", using "$var" (var="$*")' 84 c=0 85 for i in "$var" 86 do echo "$((c+=1)): [$i]" 87 done 88 echo --- 89 90 echo 'IFS=":", using "$@"' 91 c=0 92 for i in "$@" 93 do echo "$((c+=1)): [$i]" 94 done 95 echo --- 96 97 echo 'IFS=":", using $@' 98 c=0 99 for i in $@ 100 do echo "$((c+=1)): [$i]" 101 done 102 echo --- 103 104 var=$@ 105 echo 'IFS=":", using $var (var=$@)' 106 c=0 107 for i in $var 108 do echo "$((c+=1)): [$i]" 109 done 110 echo --- 111 112 echo 'IFS=":", using "$var" (var=$@)' 113 c=0 114 for i in "$var" 115 do echo "$((c+=1)): [$i]" 116 done 117 echo --- 118 119 var="$@" 120 echo 'IFS=":", using "$var" (var="$@")' 121 c=0 122 for i in "$var" 123 do echo "$((c+=1)): [$i]" 124 done 125 echo --- 126 127 echo 'IFS=":", using $var (var="$@")' 128 c=0 129 for i in $var 130 do echo "$((c+=1)): [$i]" 131 done 132 133 echo 134 135 # Try this script with ksh or zsh -y. 136 137 exit 0

The $@ and $* parameters differ only when between double quotes.
Example 3-29. $* and $@ when $IFS is empty
1 #!/bin/bash 2 3 # If $IFS set, but empty, 4 # then "$*" and "$@" do not echo positional params as expected. 5 6 mecho () # Echo pos params. 7 { 8 echo "$1,$2,$3"; 9 } 10 11 12 IFS="" # Set, but empty. 13 set a b c # Positional parameters. 14 15 mecho "$*" # abc,, 16 mecho $* # a,b,c 17 18 mecho $@ # a,b,c 19 mecho "$@" # a,b,c 20 21 # The behavior of $* and $@ when $IFS is empty depends 22 # on whatever Bash or sh version being run. 23 # It is therefore inadvisable to depend on this "feature" in a script. 24 25 26 # Thanks, S.C. 27 28 exit 0- $-
Flags passed to script

This was originally a ksh construct adopted into Bash, and unfortunately it does not seem to work reliably in Bash scripts. One possible use for it is to have a script self-test whether it is interactive.
- $!
PID (process id) of last job run in background
- $_
Special variable set to last argument of previous command executed.
- variable assignment
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 that = can be either an assignment or a test operator, depending on context.
Example 3-31. Variable Assignment
1 #!/bin/bash 2 3 echo 4 5 # When is a variable "naked", i.e., lacking the '$' in front? 6 7 # Assignment 8 a=879 9 echo "The value of \"a\" is $a" 10 11 # Assignment using 'let' 12 let a=16+5 13 echo "The value of \"a\" is now $a" 14 15 echo 16 17 # In a 'for' loop (really, a type of disguised assignment) 18 echo -n "The values of \"a\" in the loop are " 19 for a in 7 8 9 11 20 do 21 echo -n "$a " 22 done 23 24 echo 25 echo 26 27 # In a 'read' statement 28 echo -n "Enter \"a\" " 29 read a 30 echo "The value of \"a\" is now $a" 31 32 echo 33 34 exit 0
Example 3-32. Variable Assignment, plain and fancy
1 #!/bin/bash 2 3 a=23 4 # Simple case 5 echo $a 6 b=$a 7 echo $b 8 9 # Now, getting a little bit fancier... 10 11 a=`echo Hello!` 12 # Assigns result of 'echo' command to 'a' 13 echo $a 14 15 a=`ls -l` 16 # Assigns result of 'ls -l' command to 'a' 17 echo $a 18 19 exit 0
Variable assignment using the $() mechanism (a newer method than backquotes)
1 # From /etc/rc.d/rc.local 2 R=$(cat /etc/redhat-release) 3 arch=$(uname -m)
- local variables
variables visible only within a code block or function (see also local variables in functions)
- environmental variables
variables that affect the behavior of the shell and user interface

In a more general context, each process has an "environment", that is, a group of variables that hold information that the process may reference. In this sense, the shell behaves like any other process.
Every time a shell starts, it creates shell variables that correspond to its own environmental variables. Updating or adding new shell variables causes the shell to update its environment, and all the shell's child processes (the commands it executes) inherit this environment.

The space allotted to the environment is limited. Creating too many environmental variables or ones that use up excessive space may cause problems.
bash$ eval "`seq 10000 | sed -e 's/.*/export var&=ZZZZZZZZZZZZZZ/'`" bash$ du bash: /usr/bin/du: Argument list too long
(Thank you, S. C. for the clarification, and for providing the above example.)
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.

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.
---
- positional parameters
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, $2 the second, $3 the third, and so forth. [3] After $9, the arguments must be enclosed in brackets, for example, ${10}, ${11}, ${12}.
Example 3-33. Positional Parameters
1 #!/bin/bash 2 3 # Call this script with at least 10 parameters, for example 4 # ./scriptname 1 2 3 4 5 6 7 8 9 10 5 6 echo 7 8 echo "The name of this script is \"$0\"." 9 # Adds ./ for current directory 10 echo "The name of this script is \"`basename $0`\"." 11 # Strip out path name info (see 'basename') 12 13 echo 14 15 if [ -n "$1" ] # Tested variable is quoted. 16 then 17 echo "Parameter #1 is $1" 18 # Need quotes to escape # 19 fi 20 21 if [ -n "$2" ] 22 then 23 echo "Parameter #2 is $2" 24 fi 25 26 if [ -n "$3" ] 27 then 28 echo "Parameter #3 is $3" 29 fi 30 31 # ... 32 33 if [ -n "${10}" ] # Parameters > $9 must be enclosed in {brackets}. 34 then 35 echo "Parameter #10 is ${10}" 36 fi 37 38 echo 39 40 exit 0Some 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 must also exist symbolic links to all the alternate names of the script.

If a script expects a command line parameter but is invoked without one, this may cause a null variable assignment, generally 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.
1 variable1x=$1x 2 # This will prevent an error, even if positional parameter is absent. 3 4 critical_argument01=$variable1x 5 6 # The extra character can be stripped off later, if desired, like so. 7 variable1=${variable1x/x/} # It works even if variable1x contains an 'x'. 8 # This uses one of the parameter substitution templates previously discussed. 9 # Leaving out the replacement pattern results in a deletion. 10 11 # A more straightforward way of dealing with this is to simply test 12 # to simply test whether expected positional parameters have been passed. 13 if [ -z $1 ] 14 then 15 exit $POS_PARAMS_MISSING 16 fi---
Example 3-34. wh, whois domain name lookup
1 #!/bin/bash 2 3 # Does a 'whois domain-name' lookup 4 # on any of 3 alternate servers: 5 # ripe.net, cw.net, radb.net 6 7 # Place this script, named 'wh' in /usr/local/bin 8 9 # Requires symbolic links: 10 # ln -s /usr/local/bin/wh /usr/local/bin/wh-ripe 11 # ln -s /usr/local/bin/wh /usr/local/bin/wh-cw 12 # ln -s /usr/local/bin/wh /usr/local/bin/wh-radb 13 14 15 if [ -z "$1" ] 16 then 17 echo "Usage: `basename $0` [domain-name]" 18 exit 65 19 fi 20 21 case `basename $0` in 22 # Checks script name and calls proper server 23 "wh" ) whois $1@whois.ripe.net;; 24 "wh-ripe") whois $1@whois.ripe.net;; 25 "wh-radb") whois $1@whois.radb.net;; 26 "wh-cw" ) whois $1@whois.cw.net;; 27 * ) echo "Usage: `basename $0` [domain-name]";; 28 esac 29 30 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, although {bracket} notation also permits this (see Example 3-33).
Example 3-35. Using shift
1 #!/bin/bash 2 3 # Name this script something like shift000, 4 # and invoke it with some parameters, for example 5 # ./shift000 a b c def 23 skidoo 6 7 # Demo of using 'shift' 8 # to step through all the positional parameters. 9 10 until [ -z "$1" ] 11 do 12 echo -n "$1 " 13 shift 14 done 15 16 echo 17 # Extra line feed. 18 19 exit 0
3.7.1. Typing variables: declare or typeset
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 specific to version 2 or later of Bash. The typeset command also works in ksh scripts.
- -r readonly
1 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.
- -i integer
1 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.
- -a array
1 declare -a indices
The variable indices will be treated as an array.
- -f functions
1 declare -f
A declare -f line with no arguments in a script causes a listing of all the functions previously defined in that script.
1 declare -f function_name
A declare -f function_name in a script lists just the function named.
- -x export
1 declare -x var3
This declares a variable as available for exporting outside the environment of the script itself.
Example 3-36. Using declare to type variables
1 #!/bin/bash
2
3 func1 ()
4 {
5 echo This is a function.
6 }
7
8 declare -f
9 # Lists the function above.
10
11 echo
12
13 declare -i var1 # var1 is an integer.
14 var1=2367
15 echo "var1 declared as $var1"
16 var1=var1+1
17 # Integer declaration eliminates the need for 'let'.
18 echo "var1 incremented by 1 is $var1."
19 # Attempt to change variable declared as integer
20 echo "Attempting to change var1 to floating point value, 2367.1."
21 var1=2367.1
22 # Results in error message, with no change to variable.
23 echo "var1 is still $var1"
24
25 echo
26
27 declare -r var2=13.36
28 echo "var2 declared as $var2"
29 # Attempt to change readonly variable.
30 var2=13.37
31 # Generates error message, and exit from script.
32 echo "var2 is still $var2" # This line will not execute.
33
34 exit 0 # Script will not exit here. |
3.7.2. Indirect References to Variables
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-37. Indirect References
1 #!/bin/bash
2
3 # Indirect variable referencing.
4
5
6 a=letter_of_alphabet
7 letter_of_alphabet=z
8
9 echo
10
11 # Direct reference.
12 echo "a = $a"
13
14 # Indirect reference.
15 eval a=\$$a
16 echo "Now a = $a"
17
18 echo
19
20
21 # Now, let's try changing the second order reference.
22
23 t=table_cell_3
24 table_cell_3=24
25 echo "\"table_cell_3\" = $table_cell_3"
26 echo -n "dereferenced \"t\" = "; eval echo \$$t
27 # In this simple case,
28 # eval t=\$$t; echo "\"t\" = $t"
29 # also works (why?).
30
31 echo
32
33 t=table_cell_3
34 NEW_VAL=387
35 table_cell_3=$NEW_VAL
36 echo "Changing value of \"table_cell_3\" to $NEW_VAL."
37 echo "\"table_cell_3\" now $table_cell_3"
38 echo -n "dereferenced \"t\" now "; eval echo \$$t
39 # "eval" takes the two arguments "echo" and "\$$t" (set equal to $table_cell_3)
40 echo
41
42 # (Thanks, S.C., for clearing up the above behavior.)
43
44
45 # Another method is the ${!t} notation, discussed in "Bash, version 2" section.
46 # See also example "ex78.sh".
47
48 exit 0 |
Example 3-38. Passing an indirect reference to awk
1 #!/bin/bash
2
3 # Another version of the "column totaler" script
4 # that adds up a specified column (of numbers) in the target file.
5 # This uses indirect references.
6
7 ARGS=2
8 WRONGARGS=65
9
10 if [ $# -ne "$ARGS" ]
11 # Check for proper no. of command line args.
12 then
13 echo "Usage: `basename $0` filename column-number"
14 exit $WRONGARGS
15 fi
16
17 filename=$1
18 column_number=$2
19
20 #===== Same as original script, up to this point =====#
21
22
23 # A multi-line awk script is invoked by awk ' ..... '
24
25
26 # Begin awk script.
27 # ------------------------------------------------
28 awk "
29
30 { total += \$${column_number} # indirect reference
31 }
32 END {
33 print total
34 }
35
36 " "$filename"
37 # ------------------------------------------------
38 # End awk script.
39
40 # Indirect variable reference avoids the hassles
41 # of referencing a shell variable within the embedded awk script.
42 # Thanks, Stephane Chazelas.
43
44
45 exit 0 |
![]() | This method of indirect referencing is a bit tricky. If the second order variable changes its value, then the the first order variable must be properly dereferenced (as in the above example). Fortunately, the ${!variable} notation introduced with version 2 of Bash (see Example 3-167) makes indirect referencing more intuitive. |
3.7.3. $RANDOM: generate random integer
![]() | $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-39. Generating random numbers
1 #!/bin/bash 2 3 # $RANDOM returns a different random integer at each invocation. 4 # Nominal range: 0 - 32767 (signed integer). 5 6 MAXCOUNT=10 7 count=1 8 9 echo 10 echo "$MAXCOUNT random numbers:" 11 echo "-----------------" 12 while [ "$count" -le $MAXCOUNT ] # Generate 10 ($MAXCOUNT) random integers. 13 do 14 number=$RANDOM 15 echo $number 16 let "count += 1" # Increment count. 17 done 18 echo "-----------------" 19 20 # If you need a random int within a certain range, then use the 'modulo' operator. 21 # This returns the remainder of a division operation. 22 23 RANGE=500 24 25 echo 26 27 number=$RANDOM 28 let "number %= $RANGE" 29 echo "Random number less than $RANGE --> $number" 30 31 echo 32 33 # If you need a random int greater than a lower bound, 34 # then set up a test to discard all numbers below that. 35 36 FLOOR=200 37 38 number=0 #initialize 39 while [ "$number" -le $FLOOR ] 40 do 41 number=$RANDOM 42 done 43 echo "Random number greater than $FLOOR --> $number" 44 echo 45 46 47 # May combine above two techniques to retrieve random number between two limits. 48 number=0 #initialize 49 while [ "$number" -le $FLOOR ] 50 do 51 number=$RANDOM 52 let "number %= $RANGE" # Scales $number down within $RANGE. 53 done 54 echo "Random number between $FLOOR and $RANGE --> $number" 55 echo 56 57 58 # May generate binary choice, that is, "true" or "false" value. 59 BINARY=2 60 number=$RANDOM 61 T=1 62 63 let "number %= $BINARY" 64 # let "number >>= 14" gives a better random distribution 65 # (right shifts out everything except last binary digit). 66 if [ "$number" -eq $T ] 67 then 68 echo "TRUE" 69 else 70 echo "FALSE" 71 fi 72 73 echo 74 75 76 # May generate toss of the dice. 77 SPOTS=7 78 DICE=2 79 ZERO=0 80 die1=0 81 die2=0 82 83 # Tosses each die separately, and so gives correct odds. 84 85 while [ "$die1" -eq $ZERO ] #Can't have a zero come up. 86 do 87 let "die1 = $RANDOM % $SPOTS" 88 done 89 90 while [ "$die2" -eq $ZERO ] 91 do 92 let "die2 = $RANDOM % $SPOTS" 93 done 94 95 let "throw = $die1 + $die2" 96 echo "Throw of the dice = $throw" 97 echo 98 99 100 exit 0 |
Just how random is RANDOM? The best way to test this is to write a script that tracks the distribution of "random" numbers generated by RANDOM. Let's roll a RANDOM die a few times...
Example 3-40. Rolling the die with RANDOM
1 #!/bin/bash
2 # Test RANDOM for randomness.
3
4 RANDOM=$$ # Reseed the random number generator using process ID.
5
6 PIPS=6 # A die has 6 pips.
7 MAXTHROWS=600 # Increase this, if you have nothing better to do with your time.
8 throw=0 # Throw count.
9
10 zeroes=0 # Must initialize counts to zero.
11 ones=0 # since an uninitialized variable is null, not zero.
12 twos=0
13 threes=0
14 fours=0
15 fives=0
16 sixes=0
17
18 print_result ()
19 {
20 echo
21 echo "ones = $ones"
22 echo "twos = $twos"
23 echo "threes = $threes"
24 echo "fours = $fours"
25 echo "fives = $fives"
26 echo "sixes = $sixes"
27 echo
28 }
29
30 update_count()
31 {
32 case "$1" in
33 0) let "ones += 1";; # Since die has no "zero", this corresponds to 1.
34 1) let "twos += 1";; # And this to 2, etc.
35 2) let "threes += 1";;
36 3) let "fours += 1";;
37 4) let "fives += 1";;
38 5) let "sixes += 1";;
39 esac
40 }
41
42 echo
43
44
45 while [ "$throw" -lt "$MAXTHROWS" ]
46 do
47 let "die1 = RANDOM % $PIPS"
48 update_count $die1
49 let "throw += 1"
50 done
51
52 print_result
53
54 # The scores should distribute fairly evenly, assuming RANDOM is fairly random.
55 # With $MAXTHROWS at 600, all should cluster around 100, plus-or-minus 20 or so.
56 #
57 # Keep in mind that RANDOM is a pseudorandom generator,
58 # and not a spectacularly good one at that.
59
60 # Exercise for the reader (easy):
61 # Rewrite this script to flip a coin 1000 times.
62 # Choices are "HEADS" or "TAILS".
63
64 exit 0 |
As we have seen in the last example, it is best to "reseed" the RANDOM generator each time it is invoked. Using the same seed for RANDOM repeats the same series of numbers. (This mirrors the behavior of the random() function in C.)
Example 3-41. Reseeding RANDOM
1 #!/bin/bash
2 # Seeding the RANDOM variable.
3
4 MAXCOUNT=25 # How many numbers to generate.
5
6 random_numbers ()
7 {
8 count=0
9 while [ "$count" -lt "$MAXCOUNT" ]
10 do
11 number=$RANDOM
12 echo -n "$number "
13 let "count += 1"
14 done
15 }
16
17 echo; echo
18
19 RANDOM=1 # Setting RANDOM seeds the random number generator.
20 random_numbers
21
22 echo; echo
23
24 RANDOM=1 # Same seed for RANDOM...
25 random_numbers # ...reproduces the exact same number series.
26
27 echo; echo
28
29 RANDOM=2 # Trying again, but with a different seen...
30 random_numbers # gives a different number series.
31
32 echo; echo
33
34 # RANDOM=$$ seeds RANDOM from process id of script.
35 # It is also possible to seed RANDOM from 'time' or 'date'.
36
37 # Getting fancy...
38 SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
39 # Pseudo-random output fetched from /dev/urandom (system pseudo-random "device"),
40 # then converted to line of printable (octal) numbers by "od",
41 # finally "awk" retrieves just one number for SEED.
42 RANDOM=$SEED
43 random_numbers
44
45 echo; echo
46
47 exit 0 |
![]() | The /dev/urandom device/file provides a means of generating much more "random" pseudorandom numbers than the $RANDOM variable. dd if=/dev/urandom of=targetfile bs=1 count=XX creates a file of well-scattered pseudorandom numbers. However, assigning these numbers to a variable in a script requires a workaround, such as filtering through od (as in above example). |
3.7.4. The Double Parentheses Construct
Similar to the let command, the ((...)) construct permits arithmetic expansion and evaluation. In its simplest form, a=$(( 5 + 3 )) would set "a" to "5 + 3", or 8. However, this double parentheses construct is also a mechanism for allowing C-type manipulation of variables in Bash.
Example 3-42. C-type manipulation of variables
1 #!/bin/bash 2 # Manipulating a variable, C-style, using the ((...)) construct. 3 4 echo 5 6 (( a = 23 )) # Setting a value, C-style, with spaces on both sides of the "=". 7 echo "a (initial value) = $a" 8 9 (( a++ )) # Post-increment 'a', C-style. 10 echo "a (after a++) = $a" 11 12 (( a-- )) # Post-decrement 'a', C-style. 13 echo "a (after a--) = $a" 14 15 16 (( ++a )) # Pre-increment 'a', C-style. 17 echo "a (after ++a) = $a" 18 19 (( --a )) # Pre-decrement 'a', C-style. 20 echo "a (after --a) = $a" 21 22 echo 23 24 (( t = a<45?7:11 )) # C-style trinary operator. 25 echo "If a < 45, then t = 7, else t = 11." 26 echo "t = $t " # Yes! 27 28 echo 29 30 31 # ----------------- 32 # Easter Egg alert! 33 # ----------------- 34 # Chet Ramey apparently snuck a bunch of undocumented C-style constructs into Bash. 35 # In the Bash docs, Ramey calls ((...)) shell arithmetic, but it goes far beyond that. 36 # Sorry, Chet, the secret is now out. 37 38 # See also "for" and "while" loops using the ((...)) construct. 39 40 # Note: these may not work with early versions of Bash, 41 # or on other platforms besides Linux. 42 43 exit 0 |
See also Example 3-52.
Notes
| [1] | The pid of the currently running script is $$, of course. |
| [2] | The words "argument" and "parameter" are often used interchangeably. In the context of this document, they have the same precise meaning, that of a variable passed to a script or function. |
| [3] | The process calling the script sets the $0 parameter. By convention, this parameter is the name of the script. See the man page for execv. |
