Advanced Bash-Scripting Guide (PART 3)

Part 3. Beyond the Basics

   Table of Contents
   9. Another Look at Variables

        9.1. Internal Variables
        9.2. Typing variables: declare or typeset
        9.3. $RANDOM: generate random integer

   10. Manipulating Variables

        10.1. Manipulating Strings
        10.2. Parameter Substitution

   11. Loops and Branches

        11.1. Loops
        11.2. Nested Loops
        11.3. Loop Control
        11.4. Testing and Branching

   12. Command Substitution
   13. Arithmetic Expansion
   14. Recess Time

Chapter 9. Another Look at Variables

   Used properly, variables can add power and flexibility to scripts.
   This requires learning their subtleties and nuances.

9.1. Internal Variables

   Builtin variables:
          variables affecting bash script behavior

          The path to the Bash binary itself

bash$ echo $BASH

          An environmental variable pointing to a Bash startup file to
          be read when a script is invoked

          A variable indicating the subshell level. This is a new
          addition to Bash, version 3.

          See Example 21-1 for usage.

          Process ID of the current instance of Bash. This is not the
          same as the $$ variable, but it often gives the same result.

bash4$ echo $$

bash4$ echo $BASHPID

bash4$ ps ax | grep bash4
11015 pts/2    R      0:00 bash4

          But ...


echo "\$\$ outside of subshell = $$"                              # 9602
echo "\$BASH_SUBSHELL  outside of subshell = $BASH_SUBSHELL"      # 0
echo "\$BASHPID outside of subshell = $BASHPID"                   # 9602


( echo "\$\$ inside of subshell = $$"                             # 9602
  echo "\$BASH_SUBSHELL inside of subshell = $BASH_SUBSHELL"      # 1
  echo "\$BASHPID inside of subshell = $BASHPID" )                # 9603
  # Note that $$ returns PID of parent process.

          A 6-element array containing version information about the
          installed release of Bash. This is similar to $BASH_VERSION,
          below, but a bit more detailed.

# Bash version info:

for n in 0 1 2 3 4 5
  echo "BASH_VERSINFO[$n] = ${BASH_VERSINFO[$n]}"

# BASH_VERSINFO[0] = 3                      # Major version no.
# BASH_VERSINFO[1] = 00                     # Minor version no.
# BASH_VERSINFO[2] = 14                     # Patch level.
# BASH_VERSINFO[3] = 1                      # Build version.
# BASH_VERSINFO[4] = release                # Release status.
# BASH_VERSINFO[5] = i386-redhat-linux-gnu  # Architecture
                                            # (same as $MACHTYPE).

          The version of Bash installed on the system

bash$ echo $BASH_VERSION

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

          A colon-separated list of search paths available to the cd
          command, similar in function to the $PATH variable for
          binaries. The $CDPATH variable may be set in the local
          ~/.bashrc file.

bash$ cd bash-doc
bash: cd: bash-doc: No such file or directory

bash$ CDPATH=/usr/share/doc
bash$ cd bash-doc

bash$ echo $PWD

          The top value in the directory stack [39] (affected by pushd
          and popd)

          This builtin variable corresponds to the dirs command, however
          dirs shows the entire contents of the directory stack.

          The default editor invoked by a script, usually vi or emacs.

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

          Name of the current function

xyz23 ()
  echo "$FUNCNAME now executing."  # xyz23 now executing.


                                   # Null value outside a function.

          See also Example A-50.

          A list of filename patterns to be excluded from matching in

          Groups current user belongs to

          This is a listing (array) of the group id numbers for current
          user, as recorded in /etc/passwd and /etc/group.

root# echo $GROUPS

root# echo ${GROUPS[1]}

root# echo ${GROUPS[5]}

          Home directory of the user, usually /home/username (see
          Example 10-7)

          The hostname command assigns the system host name at bootup in
          an init script. However, the gethostname() function sets the
          Bash internal variable $HOSTNAME. See also Example 10-7.

          host type

          Like $MACHTYPE, identifies the system hardware.

bash$ echo $HOSTTYPE

          internal field separator

          This variable determines how Bash recognizes fields, or word
          boundaries, when it interprets character strings.

          $IFS 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 5-1.

bash$ echo "$IFS"

(With $IFS set to default, a blank line displays.)

bash$ echo "$IFS" | cat -vte
(Show whitespace: here a single space, ^I [horizontal tab],
  and newline, and display "$" at end-of-line.)

bash$ bash -c 'set w x y z; IFS=":-;"; echo "$*"'
(Read commands from string and assign any arguments to pos params.)


   $IFS does not handle whitespace the same as it does other characters.
   Example 9-1. $IFS and whitespace


# The plus sign will be interpreted as a separator.
echo $var1     # a b c
echo $var2     # d-e-f
echo $var3     # g,h,i


# The plus sign reverts to default interpretation.
# The minus sign will be interpreted as a separator.
echo $var1     # a+b+c
echo $var2     # d e f
echo $var3     # g,h,i


# The comma will be interpreted as a separator.
# The minus sign reverts to default interpretation.
echo $var1     # a+b+c
echo $var2     # d-e-f
echo $var3     # g h i


IFS=" "
# The space character will be interpreted as a separator.
# The comma reverts to default interpretation.
echo $var1     # a+b+c
echo $var2     # d-e-f
echo $var3     # g,h,i

# ======================================================== #

# However ...
# $IFS treats whitespace differently than other characters.

  for arg
    echo "[$arg]"
  done #  ^    ^   Embed within brackets, for your viewing pleasure.

echo; echo "IFS=\" \""
echo "-------"

IFS=" "
var=" a  b c   "
#    ^ ^^   ^^^
output_args_one_per_line $var  # output_args_one_per_line `echo " a  b c   "`
# [a]
# [b]
# [c]

echo; echo "IFS=:"
echo "-----"

var=":a::b:c:::"               # Same pattern as above,
#    ^ ^^   ^^^                #+ but substituting ":" for " "  ...
output_args_one_per_line $var
# []
# [a]
# []
# [b]
# [c]
# []
# []

# Note "empty" brackets.
# The same thing happens with the "FS" field separator in awk.



          (Many thanks, StИphane Chazelas, for clarification and above

          See also Example 16-41, Example 11-7, and Example 19-14 for
          instructive examples of using $IFS.

          Ignore EOF: how many end-of-files (control-D) the shell will
          ignore before logging out.

          Often set in the .bashrc or /etc/profile files, this variable
          controls collation order in filename expansion and pattern
          matching. If mishandled, LC_COLLATE can cause unexpected
          results in filename globbing.


   As of version 2.05 of Bash, filename globbing no longer distinguishes
   between lowercase and uppercase letters in a character range between
   brackets. For example, ls [A-M]* would match both File1.txt and
   file1.txt. To revert to the customary behavior of bracket matching,
   set LC_COLLATE to C by an export LC_COLLATE=C in /etc/profile and/or

          This internal variable controls character interpretation in
          globbing and pattern matching.

          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.

last_cmd_arg=$_  # Save it.

echo "At line number $LINENO, variable \"v1\" = $v1"
echo "Last command argument processed = $last_cmd_arg"

          machine type

          Identifies the system hardware.

bash$ echo $MACHTYPE

          Old working directory ("OLD-Print-Working-Directory", previous
          directory you were in).

          operating system type

bash$ echo $OSTYPE

          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 Appendix G).

bash$ echo $PATH

          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.

          Array variable holding exit status(es) of last executed
          foreground pipe.

bash$ echo $PIPESTATUS

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo ${PIPESTATUS[1]}

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo $?

          The members of the $PIPESTATUS array hold the exit status of
          each respective command executed in a pipe. $PIPESTATUS[0]
          holds the exit status of the first command in the pipe,
          $PIPESTATUS[1] the exit status of the second command, and so


   The $PIPESTATUS variable may contain an erroneous 0 value in a login
   shell (in releases prior to 3.0 of Bash).
tcsh% bash

bash$ who | grep nobody | sort
bash$ echo ${PIPESTATUS[*]}

   The above lines contained in a script would produce the expected 0 1
   0 output.
   Thank you, Wayne Pollock for pointing this out and supplying the
   above example.


   The $PIPESTATUS variable gives unexpected results in some contexts.
bash$ echo $BASH_VERSION

bash$ $ ls | bogus_command | wc
bash: bogus_command: command not found
 0       0       0

bash$ echo ${PIPESTATUS[@]}
141 127 0

   Chet Ramey attributes the above output to the behavior of ls. If ls
   writes to a pipe whose output is not read, then SIGPIPE kills it, and
   its exit status is 141. Otherwise its exit status is 0, as expected.
   This likewise is the case for tr.


   $PIPESTATUS is a "volatile" variable. It needs to be captured
   immediately after the pipe in question, before any other command
bash$ $ ls | bogus_command | wc
bash: bogus_command: command not found
 0       0       0

bash$ echo ${PIPESTATUS[@]}
0 127 0

bash$ echo ${PIPESTATUS[@]}


   The pipefail option may be useful in cases where $PIPESTATUS does not
   give the desired information.

          The $PPID of a process is the process ID (pid) of its parent
          process. [40]

          Compare this with the pidof command.

          A variable holding a command to be executed just before the
          primary prompt, $PS1 is to be displayed.

          This is the main prompt, seen at the command-line.

          The secondary prompt, seen when additional input is expected.
          It displays as ">".

          The tertiary prompt, displayed in a select loop (see Example

          The quartenary prompt, shown at the beginning of each line of
          output when invoking a script with the -x option. It displays
          as "+".

          Working directory (directory you are in at the time)

          This is the analog to the pwd builtin command.



clear # Clear screen.


cd $TargetDirectory
echo "Deleting stale files in $TargetDirectory."

if [ "$PWD" != "$TargetDirectory" ]
then    # Keep from wiping out wrong directory by accident.
  echo "Wrong directory!"
  echo "In $PWD, rather than $TargetDirectory!"
  echo "Bailing out!"

rm -rf *
rm .[A-Za-z0-9]*    # Delete dotfiles.
# rm -f .[^.]* ..?*   to remove filenames beginning with multiple dots.
# (shopt -s dotglob; rm -f *)   will also work.
# Thanks, S.C. for pointing this out.

#  A filename (`basename`) may contain all characters in the 0 - 255 range,
#+ except "/".
#  Deleting files beginning with weird characters, such as -
#+ is left as an exercise.

echo "Done."
echo "Old files deleted in $TargetDirectory."

# Various other operations here, as necessary.

exit $?

          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


# REPLY is the default value for a 'read' command.

echo -n "What is your favorite vegetable? "

echo "Your favorite vegetable is $REPLY."
#  REPLY holds the value of last "read" if and only if
#+ no variable supplied.

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.


exit 0

          The number of seconds the script has been running.



echo "Hit Control-C to exit before $TIME_LIMIT seconds."

while [ "$SECONDS" -le "$TIME_LIMIT" ]
  if [ "$SECONDS" -eq 1 ]

  echo "This script has been running $SECONDS $units."
  #  On a slow or overburdened machine, the script may skip a count
  #+ every once in a while.
  sleep $INTERVAL

echo -e "\a"  # Beep!

exit 0

          The list of enabled shell options, a readonly variable.

bash$ echo $SHELLOPTS

          Shell level, how deeply Bash is nested. [41] If, at the
          command-line, $SHLVL is 1, then in a script it will increment
          to 2.


   This variable is not affected by subshells. Use $BASH_SUBSHELL when
   you need an indication of subshell nesting.

          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.

          As of version 2.05b of Bash, it is now possible to use $TMOUT
          in a script in combination with read.

# Works in scripts for Bash, versions 2.05b and later.

TMOUT=3    # Prompt times out at three seconds.

echo "What is your favorite song?"
echo "Quickly now, you only have $TMOUT seconds to answer!"
read song

if [ -z "$song" ]
  song="(no answer)"
  # Default response.

echo "Your favorite song is $song."

          There are other, more complex, ways of implementing timed
          input in a script. One alternative 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 32-5) the
          interrupt generated by the timing loop (whew!).

          Example 9-2. Timed Input


# TMOUT=3    Also works, as of newer versions of Bash.

TIMELIMIT=3  # Three seconds in this instance.
             # May be set to different value.

  if [ "$answer" = TIMEOUT ]
    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.


  sleep $TIMELIMIT && kill -s 14 $$ &
  # Waits 3 seconds, then sends sigalarm to script.


trap Int14Vector $TIMER_INTERRUPT
# Timer interrupt (14) subverted for our purposes.

echo "What is your favorite vegetable "
read answer

#  Admittedly, this is a kludgy implementation of timed input.
#  However, the "-t" option to "read" simplifies this task.
#  See the "" script.
#  However, what about timing not just single user input,
#+ but an entire script?

#  If you need something really elegant ...
#+ consider writing the application in C or C++,
#+ using appropriate library functions, such as 'alarm' and 'setitimer.'

exit 0

          An alternative is using stty.

          Example 9-3. Once more, timed input


#  Written by Stephane Chazelas,
#+ and modified by the document author.

INTERVAL=5                # timeout interval

timedout_read() {
  old_tty_settings=`stty -g`
  stty -icanon min 0 time ${timeout}0
  eval read $varname      # or just  read $varname
  stty "$old_tty_settings"
  # See man page for "stty."

echo; echo -n "What's your name? Quick! "
timedout_read $INTERVAL your_name

#  This may not work on every terminal type.
#  The maximum timeout depends on the terminal.
#+ (it is often 25.5 seconds).


if [ ! -z "$your_name" ]  # If name input before timeout ...
  echo "Your name is $your_name."
  echo "Timed out."


# The behavior of this script differs somewhat from ""
# At each keystroke, the counter resets.

exit 0

          Perhaps the simplest method is using the -t option to read.

          Example 9-4. Timed read

# Inspired by a suggestion from "syngin seven" (thanks).

TIMELIMIT=4         # 4 seconds

read -t $TIMELIMIT variable <&1
#                           ^^^
#  In this instance, "<&1" is needed for Bash 1.x and 2.x,
#  but unnecessary for Bash 3.x.


if [ -z "$variable" ]  # Is null?
  echo "Timed out, variable still unset."
  echo "variable = $variable"

exit 0

          User ID number

          Current user's user identification number, as recorded in

          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 9-5. Am I root?

#   Am I root or not?

ROOT_UID=0   # Root has $UID 0.

if [ "$UID" -eq "$ROOT_UID" ]  # Will the real "root" please stand up?
  echo "You are root."
  echo "You are just an ordinary user (but mom loves you just the same)."

exit 0

# ============================================================= #
# Code below will not execute, because the script already exited.

# An alternate method of getting to the root of matters:


username=`id -nu`              # Or...   username=`whoami`
if [ "$username" = "$ROOTUSER_NAME" ]
  echo "Rooty, toot, toot. You are root."
  echo "You are just a regular fella."

          See also Example 2-3.


   The variables $ENV, $LOGNAME, $MAIL, $TERM, $USER, and $USERNAME 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.
tcsh% echo $LOGNAME
tcsh% echo $SHELL
tcsh% echo $TERM

bash$ echo $LOGNAME
bash$ echo $SHELL
bash$ echo $TERM

   Positional Parameters

   $0, $1, $2, etc.
          Positional parameters, passed from command line to script,
          passed to a function, or set to a variable (see Example 4-5
          and Example 15-16)

          Number of command-line arguments [42] or positional parameters
          (see Example 36-2)

          All of the positional parameters, seen as a single word


   "$*" must be quoted.

          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.


   Of course, "$@" should be quoted.

          Example 9-6. arglist: Listing arguments with $* and $@

# Invoke this script with several arguments, such as "one two three".


if [ ! -n "$1" ]
  echo "Usage: `basename $0` argument1 argument2 etc."
  exit $E_BADARGS


index=1          # Initialize count.

echo "Listing args with \"\$*\":"
for arg in "$*"  # Doesn't work properly if "$*" isn't quoted.
  echo "Arg #$index = $arg"
  let "index+=1"
done             # $* sees all arguments as single word.
echo "Entire arg list seen as single word."


index=1          # Reset count.
                 # What happens if you forget to do this?

echo "Listing args with \"\$@\":"
for arg in "$@"
  echo "Arg #$index = $arg"
  let "index+=1"
done             # $@ sees arguments as separate words.
echo "Arg list seen as separate words."


index=1          # Reset count.

echo "Listing args with \$* (unquoted):"
for arg in $*
  echo "Arg #$index = $arg"
  let "index+=1"
done             # Unquoted $* sees arguments as separate words.
echo "Arg list seen as separate words."

exit 0

          Following a shift, the $@ holds the remaining command-line
          parameters, lacking the previous $1, which was lost.

# Invoke with ./scriptname 1 2 3 4 5

echo "$@"    # 1 2 3 4 5
echo "$@"    # 2 3 4 5
echo "$@"    # 3 4 5

# Each "shift" loses parameter $1.
# "$@" then contains the remaining parameters.

          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 16-24 and Example 16-25.


   The $* and $@ parameters sometimes display inconsistent and puzzling
   behavior, depending on the setting of $IFS.

          Example 9-7. Inconsistent $* and $@ behavior


#  Erratic behavior of the "$*" and "$@" internal Bash variables,
#+ depending on whether they are quoted or not.
#  Inconsistent handling of word splitting and linefeeds.

set -- "First one" "second" "third:one" "" "Fifth: :one"
# Setting the script arguments, $1, $2, etc.


echo 'IFS unchanged, using "$*"'
for i in "$*"               # quoted
do echo "$((c+=1)): [$i]"   # This line remains the same in every instance.
                            # Echo args.
echo ---

echo 'IFS unchanged, using $*'
for i in $*                 # unquoted
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS unchanged, using "$@"'
for i in "$@"
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS unchanged, using $@'
for i in $@
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using "$*"'
for i in "$*"
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using $*'
for i in $*
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using "$var" (var=$*)'
for i in "$var"
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using $var (var=$*)'
for i in $var
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using $var (var="$*")'
for i in $var
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using "$var" (var="$*")'
for i in "$var"
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using "$@"'
for i in "$@"
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using $@'
for i in $@
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using $var (var=$@)'
for i in $var
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using "$var" (var=$@)'
for i in "$var"
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using "$var" (var="$@")'
for i in "$var"
do echo "$((c+=1)): [$i]"
echo ---

echo 'IFS=":", using $var (var="$@")'
for i in $var
do echo "$((c+=1)): [$i]"


# Try this script with ksh or zsh -y.

exit 0

# This example script by Stephane Chazelas,
# and slightly modified by the document author.


   The $@ and $* parameters differ only when between double quotes.

          Example 9-8. $* and $@ when $IFS is empty


#  If $IFS set, but empty,
#+ then "$*" and "$@" do not echo positional params as expected.

mecho ()       # Echo positional parameters.
echo "$1,$2,$3";

IFS=""         # Set, but empty.
set a b c      # Positional parameters.

mecho "$*"     # abc,,
#                   ^^
mecho $*       # a,b,c

mecho $@       # a,b,c
mecho "$@"     # a,b,c

#  The behavior of $* and $@ when $IFS is empty depends
#+ on which Bash or sh version being run.
#  It is therefore inadvisable to depend on this "feature" in a script.

# Thanks, Stephane Chazelas.


   Other Special Parameters

          Flags passed to script (using set). See Example 15-16.


   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

          PID (process ID) of last job run in background


COMMAND1="sleep 100"

echo "Logging PIDs background commands for script: $0" >> "$LOG"
# So they can be monitored, and killed as necessary.
echo >> "$LOG"

# Logging commands.

echo -n "PID of \"$COMMAND1\":  " >> "$LOG"
echo $! >> "$LOG"
# PID of "sleep 100":  1506

# Thank you, Jacques Lederer, for suggesting this.

          Using $! for job control:

possibly_hanging_job & { sleep ${TIMEOUT}; eval 'kill -9 $!' &> /dev/null; }
# Forces completion of an ill-behaved program.
# Useful, for example, in init scripts.

# Thank you, Sylvain Fourmanoit, for this creative use of the "!" variable.

          Or, alternately:

# This example by Matthew Sage.
# Used with permission.

TIMEOUT=30   # Timeout value in seconds

possibly_hanging_job & {
        while ((count < TIMEOUT )); do
                eval '[ ! -d "/proc/$!" ] && ((count = TIMEOUT))'
                # /proc is where information about running processes is found.
                # "-d" tests whether it exists (whether directory exists).
                # So, we're waiting for the job in question to show up.
                sleep 1
        eval '[ -d "/proc/$!" ] && kill -15 $!'
        # If the hanging job is running, kill it.

          Special variable set to final argument of previous command

          Example 9-9. Underscore variable


echo $_              #  /bin/bash
                     #  Just called /bin/bash to run the script.
                     #  Note that this will vary according to
                     #+ how the script is invoked.

du >/dev/null        #  So no output from command.
echo $_              #  du

ls -al >/dev/null    #  So no output from command.
echo $_              #  -al  (last argument)

echo $_              #  :

          Exit status of a command, function, or the script itself (see
          Example 24-7)

          Process ID (PID) of the script itself. [43] The $$ variable
          often finds use in scripts to construct "unique" temp file
          names (see Example 32-6, Example 16-31, and Example 15-27).
          This is usually simpler than invoking mktemp.

9.2. Typing variables: declare or typeset

   The declare or typeset builtins, which are exact synonyms, permit
   modifying the properties of variables. This is a very weak form of
   the typing [44] 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.

   declare/typeset options

   -r readonly
          (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 -r var1=1
echo "var1 = $var1"   # var1 = 1

(( var1++ ))          # line 4: var1: readonly variable

   -i integer

declare -i number
# The script will treat subsequent occurrences of "number" as an integer.

echo "Number = $number"     # Number = 3

echo "Number = $number"     # Number = 0
# Tries to evaluate the string "three" as an integer.

          Certain arithmetic operations are permitted for declared
          integer variables without the need for expr or let.

echo "n = $n"       # n = 6/3

declare -i n
echo "n = $n"       # n = 2

   -a array

declare -a indices

          The variable indices will be treated as an array.

   -f function(s)

declare -f

          A declare -f line with no arguments in a script causes a
          listing of all the functions previously defined in that

declare -f function_name

          A declare -f function_name in a script lists just the function

   -x export

declare -x var3

          This declares a variable as available for exporting outside
          the environment of the script itself.

   -x var=$value

declare -x var3=373

          The declare command permits assigning a value to a variable in
          the same statement as setting its properties.

   Example 9-10. Using declare to type variables

func1 ()
  echo This is a function.

declare -f        # Lists the function above.


declare -i var1   # var1 is an integer.
echo "var1 declared as $var1"
var1=var1+1       # Integer declaration eliminates the need for 'let'.
echo "var1 incremented by 1 is $var1."
# Attempt to change variable declared as integer.
echo "Attempting to change var1 to floating point value, 2367.1."
var1=2367.1       # Results in error message, with no change to variable.
echo "var1 is still $var1"


declare -r var2=13.36         # 'declare' permits setting a variable property
                              #+ and simultaneously assigning it a value.
echo "var2 declared as $var2" # Attempt to change readonly variable.
var2=13.37                    # Generates error message, and exit from script.

echo "var2 is still $var2"    # This line will not execute.

exit 0                        # Script will not exit here.


   Using the declare builtin restricts the scope of a variable.
foo ()

bar ()
echo $FOO

bar   # Prints bar.

   However . . .
foo (){
declare FOO="bar"

bar ()
echo $FOO

bar  # Prints nothing.

# Thank you, Michael Iatrou, for pointing this out.

9.2.1. Another use for declare

   The declare command can be helpful in identifying variables,
   environmental or otherwise. This can be especially useful with

bash$ declare | grep HOME

bash$ zzy=68
bash$ declare | grep zzy

bash$ Colors=([0]="purple" [1]="reddish-orange" [2]="light green")
bash$ echo ${Colors[@]}
purple reddish-orange light green
bash$ declare | grep Colors
Colors=([0]="purple" [1]="reddish-orange" [2]="light green")

9.3. $RANDOM: generate random integer


   Anyone who attempts to generate random numbers by deterministic means
   is, of course, living in a state of sin.

   --John von Neumann

   $RANDOM is an internal Bash function (not a constant) that returns a
   pseudorandom [45] integer in the range 0 - 32767. It should not be
   used to generate an encryption key.

   Example 9-11. Generating random numbers

# $RANDOM returns a different random integer at each invocation.
# Nominal range: 0 - 32767 (signed 16-bit integer).


echo "$MAXCOUNT random numbers:"
echo "-----------------"
while [ "$count" -le $MAXCOUNT ]      # Generate 10 ($MAXCOUNT) random integer
  echo $number
  let "count += 1"  # Increment count.
echo "-----------------"

# If you need a random int within a certain range, use the 'modulo' operator.
# This returns the remainder of a division operation.



let "number %= $RANGE"
#           ^^
echo "Random number less than $RANGE  ---  $number"


#  If you need a random integer greater than a lower bound,
#+ then set up a test to discard all numbers below that.


number=0   #initialize
while [ "$number" -le $FLOOR ]
echo "Random number greater than $FLOOR ---  $number"

   # Let's examine a simple alternative to the above loop, namely
   #       let "number = $RANDOM + $FLOOR"
   # That would eliminate the while-loop and run faster.
   # But, there might be a problem with that. What is it?

# Combine above two techniques to retrieve random number between two limits.
number=0   #initialize
while [ "$number" -le $FLOOR ]
  let "number %= $RANGE"  # Scales $number down within $RANGE.
echo "Random number between $FLOOR and $RANGE ---  $number"

# Generate binary choice, that is, "true" or "false" value.

let "number %= $BINARY"
#  Note that    let "number >>= 14"    gives a better random distribution
#+ (right shifts out everything except last binary digit).
if [ "$number" -eq $T ]
  echo "TRUE"
  echo "FALSE"


# Generate a toss of the dice.
SPOTS=6   # Modulo 6 gives range 0 - 5.
          # Incrementing by 1 gives desired range of 1 - 6.
          # Thanks, Paulo Marcel Coelho Aragao, for the simplification.
# Would it be better to just set SPOTS=7 and not add 1? Why or why not?

# Tosses each die separately, and so gives correct odds.

    let "die1 = $RANDOM % $SPOTS +1" # Roll first one.
    let "die2 = $RANDOM % $SPOTS +1" # Roll second one.
    #  Which arithmetic operation, above, has greater precedence --
    #+ modulo (%) or addition (+)?

let "throw = $die1 + $die2"
echo "Throw of the dice = $throw"

exit 0

   Example 9-12. Picking a random card from a deck

# This is an example of choosing random elements of an array.

# Pick a card, any card.



# Note variables spread over multiple lines.

suite=($Suites)                # Read into array variable.

num_suites=${#suite[*]}        # Count how many elements.

echo -n "${denomination[$((RANDOM%num_denominations))]} of "
echo ${suite[$((RANDOM%num_suites))]}

# $bozo sh
# Jack of Clubs

# Thank you, "jipe," for pointing out this use of $RANDOM.
exit 0

   Example 9-13. Brownian Motion Simulation
# Author: Mendel Cooper
# Reldate: 10/26/07
# License: GPL3

#  ----------------------------------------------------------------
#  This script models Brownian motion:
#+ the random wanderings of tiny particles in a fluid,
#+ as they are buffeted by random currents and collisions.
#+ This is colloquially known as the "Drunkard's Walk."

#  It can also be considered as a stripped-down simulation of a
#+ Galton Board, a slanted board with a pattern of pegs,
#+ down which rolls a succession of marbles, one at a time.
#+ At the bottom is a row of slots or catch basins in which
#+ the marbles come to rest at the end of their journey.
#  Think of it as a kind of bare-bones Pachinko game.
#  As you see by running the script,
#+ most of the marbles cluster around the center slot.
#+ This is consistent with the expected binomial distribution.
#  As a Galton Board simulation, the script
#+ disregards such parameters as
#+ board tilt-angle, rolling friction of the marbles,
#+ angles of impact, and elasticity of the pegs.
#  To what extent does this affect the accuracy of the simulation?
#  ----------------------------------------------------------------

PASSES=500            #  Number of particle interactions / marbles.
ROWS=10               #  Number of "collisions" (or horiz. peg rows).
RANGE=3               #  0 - 2 output range from $RANDOM.
POS=0                 #  Left/right position.
RANDOM=$$             #  Seeds the random number generator from PID
                      #+ of script.

declare -a Slots      # Array holding cumulative results of passes.
NUMSLOTS=21           # Number of slots at bottom of board.

Initialize_Slots () { # Zero out all elements of the array.
for i in $( seq $NUMSLOTS )

echo                  # Blank line at beginning of run.

Show_Slots () {
echo -n " "
for i in $( seq $NUMSLOTS )   # Pretty-print array elements.
  printf "%3d" ${Slots[$i]}   # Allot three spaces per result.

echo # Row of slots:
echo " |__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|"
echo "                                ^^"
echo #  Note that if the count within any particular slot exceeds 99,
     #+ it messes up the display.
     #  Running only(!) 500 passes usually avoids this.

Move () {              # Move one unit right / left, or stay put.
  Move=$RANDOM         # How random is $RANDOM? Well, let's see ...
  let "Move %= RANGE"  # Normalize into range of 0 - 2.
  case "$Move" in
    0 ) ;;                   # Do nothing, i.e., stay in place.
    1 ) ((POS--));;          # Left.
    2 ) ((POS++));;          # Right.
    * ) echo -n "Error ";;   # Anomaly! (Should never occur.)

Play () {                    # Single pass (inner loop).
while [ "$i" -lt "$ROWS" ]   # One event per row.

SHIFT=11                     # Why 11, and not 10?
let "POS += $SHIFT"          # Shift "zero position" to center.
(( Slots[$POS]++ ))          # DEBUG: echo $POS

Run () {                     # Outer loop.
while [ "$p" -lt "$PASSES" ]
  (( p++ ))
  POS=0                      # Reset to zero. Why?

# --------------
# main ()
# --------------

exit $?

#  Exercises:
#  ---------
#  1) Show the results in a vertical bar graph, or as an alternative,
#+    a scattergram.
#  2) Alter the script to use /dev/urandom instead of $RANDOM.
#     Will this make the results more random?

   Jipe points out a set of techniques for generating random numbers
   within a range.
#  Generate random number between 6 and 30.

#  Generate random number in the same 6 - 30 range,
#+ but the number must be evenly divisible by 3.

#  Note that this will not work all the time.
#  It fails if $RANDOM%30 returns 0.

#  Frank Wang suggests the following alternative:
   rnumber=$(( RANDOM%27/3*3+6 ))

   Bill Gradwohl came up with an improved formula that works for
   positive numbers.

   Here Bill presents a versatile function that returns a random number
   between two specified values.

   Example 9-14. Random between values
# Random number between two specified values.
# Script by Bill Gradwohl, with minor modifications by the document author.
# Corrections in lines 187 and 189 by Anthony Le Clezio.
# Used with permission.

randomBetween() {
   #  Generates a positive or negative random number
   #+ between $min and $max
   #+ and divisible by $divisibleBy.
   #  Gives a "reasonably random" distribution of return values.
   #  Bill Gradwohl - Oct 1, 2003

   syntax() {
   # Function embedded within function.
      echo    "Syntax: randomBetween [min] [max] [multiple]"
      echo -n "Expects up to 3 passed parameters, "
      echo    "but all are completely optional."
      echo    "min is the minimum value"
      echo    "max is the maximum value"
      echo -n "multiple specifies that the answer must be "
      echo     "a multiple of this value."
      echo    "    i.e. answer must be evenly divisible by this number."
      echo    "If any value is missing, defaults area supplied as: 0 32767 1"
      echo -n "Successful completion returns 0, "
      echo     "unsuccessful completion returns"
      echo    "function syntax and 1."
      echo -n "The answer is returned in the global variable "
      echo    "randomBetweenAnswer"
      echo -n "Negative values for any passed parameter are "
      echo    "handled correctly."

   local min=${1:-0}
   local max=${2:-32767}
   local divisibleBy=${3:-1}
   # Default values assigned, in case parameters not passed to function.

   local x
   local spread

   # Let's make sure the divisibleBy value is positive.
   [ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy))

   # Sanity check.
   if [ $# -gt 3 -o ${divisibleBy} -eq 0 -o  ${min} -eq ${max} ]; then
      return 1

   # See if the min and max are reversed.
   if [ ${min} -gt ${max} ]; then
      # Swap them.

   #  If min is itself not evenly divisible by $divisibleBy,
   #+ then fix the min to be within range.
   if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ]; then
      if [ ${min} -lt 0 ]; then

   #  If max is itself not evenly divisible by $divisibleBy,
   #+ then fix the max to be within range.
   if [ $((max/divisibleBy*divisibleBy)) -ne ${max} ]; then
      if [ ${max} -lt 0 ]; then

   #  ---------------------------------------------------------------------
   #  Now, to do the real work.

   #  Note that to get a proper distribution for the end points,
   #+ the range of random values has to be allowed to go between
   #+ 0 and abs(max-min)+divisibleBy, not just abs(max-min)+1.

   #  The slight increase will produce the proper distribution for the
   #+ end points.

   #  Changing the formula to use abs(max-min)+1 will still produce
   #+ correct answers, but the randomness of those answers is faulty in
   #+ that the number of times the end points ($min and $max) are returned
   #+ is considerably lower than when the correct formula is used.
   #  ---------------------------------------------------------------------

   #  Omair Eshkenazi points out that this test is unnecessary,
   #+ since max and min have already been switched around.
   [ ${spread} -lt 0 ] && spread=$((0-spread))
   let spread+=divisibleBy

   return 0

   #  However, Paulo Marcel Coelho Aragao points out that
   #+ when $max and $min are not divisible by $divisibleBy,
   #+ the formula fails.
   #  He suggests instead the following formula:
   #    rnumber = $(((RANDOM%(max-min+1)+min)/divisibleBy*divisibleBy))


# Let's test the function.

#  Generate an array of expected answers and check to make sure we get
#+ at least one of each answer if we loop long enough.

declare -a answer
   if [ $((minimum/divisibleBy*divisibleBy)) -ne ${minimum} ]; then
      if [ ${minimum} -lt 0 ]; then

   #  If max is itself not evenly divisible by $divisibleBy,
   #+ then fix the max to be within range.

   if [ $((maximum/divisibleBy*divisibleBy)) -ne ${maximum} ]; then
      if [ ${maximum} -lt 0 ]; then

#  We need to generate only positive array subscripts,
#+ so we need a displacement that that will guarantee
#+ positive results.

for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do

# Now loop a large number of times to see what we get.
loopIt=1000   #  The script author suggests 100000,
              #+ but that takes a good long while.

for ((i=0; i<${loopIt}; ++i)); do

   #  Note that we are specifying min and max in reversed order here to
   #+ make the function correct for this case.

   randomBetween ${max} ${min} ${divisibleBy}

   # Report an error if an answer is unexpected.
   [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ]
   && echo MIN or MAX error - ${randomBetweenAnswer}!
   [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] \
   && echo DIVISIBLE BY error - ${randomBetweenAnswer}!

   # Store the answer away statistically.

# Let's check the results

for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
   [ ${answer[i+disp]} -eq 0 ] \
   && echo "We never got an answer of $i." \
   || echo "${i} occurred ${answer[i+disp]} times."

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 9-15. Rolling a single die with RANDOM
# How random is RANDOM?

RANDOM=$$       # Reseed the random number generator using script process ID.

PIPS=6          # A die has 6 pips.
MAXTHROWS=600   # Increase this if you have nothing better to do with your tim
throw=0         # Throw count.

ones=0          #  Must initialize counts to zero,
twos=0          #+ since an uninitialized variable is null, not zero.

print_result ()
echo "ones =   $ones"
echo "twos =   $twos"
echo "threes = $threes"
echo "fours =  $fours"
echo "fives =  $fives"
echo "sixes =  $sixes"

case "$1" in
  0) let "ones += 1";;   # Since die has no "zero", this corresponds to 1.
  1) let "twos += 1";;   # And this to 2, etc.
  2) let "threes += 1";;
  3) let "fours += 1";;
  4) let "fives += 1";;
  5) let "sixes += 1";;


while [ "$throw" -lt "$MAXTHROWS" ]
  let "die1 = RANDOM % $PIPS"
  update_count $die1
  let "throw += 1"


exit 0

#  The scores should distribute fairly evenly, assuming RANDOM is fairly rando
#  With $MAXTHROWS at 600, all should cluster around 100, plus-or-minus 20 or
#  Keep in mind that RANDOM is a pseudorandom generator,
#+ and not a spectacularly good one at that.

#  Randomness is a deep and complex subject.
#  Sufficiently long "random" sequences may exhibit
#+ chaotic and other "non-random" behavior.

# Exercise (easy):
# ---------------
# Rewrite this script to flip a coin 1000 times.
# Choices are "HEADS" and "TAILS".

   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. [46] (This mirrors the behavior
   of the random() function in C.)

   Example 9-16. Reseeding RANDOM
# Seeding the RANDOM variable.

MAXCOUNT=25       # How many numbers to generate.

random_numbers ()
while [ "$count" -lt "$MAXCOUNT" ]
  echo -n "$number "
  let "count += 1"

echo; echo

RANDOM=1          # Setting RANDOM seeds the random number generator.

echo; echo

RANDOM=1          # Same seed for RANDOM...
random_numbers    # ...reproduces the exact same number series.
                  # When is it useful to duplicate a "random" number series?

echo; echo

RANDOM=2          # Trying again, but with a different seed...
random_numbers    # gives a different number series.

echo; echo

# RANDOM=$$  seeds RANDOM from process id of script.
# It is also possible to seed RANDOM from 'time' or 'date' commands.

# Getting fancy...
SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
#  Pseudo-random output fetched
#+ from /dev/urandom (system pseudo-random device-file),
#+ then converted to line of printable (octal) numbers by "od",
#+ finally "awk" retrieves just one number for SEED.

echo; echo

exit 0


   The /dev/urandom pseudo-device file provides a method 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, Example 16-14, and Example A-36), or
   even piping to md5sum (see Example 36-14).

   There are also other ways to generate pseudorandom numbers in a
   script. Awk provides a convenient means of doing this.

   Example 9-17. Pseudorandom numbers, using awk
# Returns a pseudorandom number in the range 0 - 1.
# Uses the awk rand() function.

AWKSCRIPT=' { srand(); print rand() } '
#            Command(s) / parameters passed to awk
# Note that srand() reseeds awk's random number generator.

echo -n "Random number between 0 and 1 = "

echo | awk "$AWKSCRIPT"
# What happens if you leave out the 'echo'?

exit 0

# Exercises:
# ---------

# 1) Using a loop construct, print out 10 different random numbers.
#      (Hint: you must reseed the "srand()" function with a different seed
#+     in each pass through the loop. What happens if you fail to do this?)

# 2) Using an integer multiplier as a scaling factor, generate random numbers
#+   in the range between 10 and 100.

# 3) Same as exercise #2, above, but generate random integers this time.

   The date command also lends itself to generating pseudorandom integer

Chapter 10. Manipulating Variables

10.1. Manipulating Strings

   Bash supports a surprising number of string manipulation operations.
   Unfortunately, these tools lack a unified focus. Some are a subset of
   parameter substitution, and others fall under the functionality of
   the UNIX expr command. This results in inconsistent command syntax
   and overlap of functionality, not to mention confusion.

   String Length


   expr length $string
          These are the equivalent of strlen() in C.

   expr "$string" : '.*'


echo ${#stringZ}                 # 15
echo `expr length $stringZ`      # 15
echo `expr "$stringZ" : '.*'`    # 15

   Example 10-1. Inserting a blank line between paragraphs in a text
# Ver. 2.0, Reldate 05Aug08

# Inserts a blank line between paragraphs of a single-spaced text file.
# Usage: $0 <FILENAME

MINLEN=60        # May need to change this value.
#  Assume lines shorter than $MINLEN characters ending in a period
#+ terminate a paragraph. See exercises at end of script.

while read line  # For as many lines as the input file has...
  echo "$line"   # Output the line itself.

  if [[ "$len" -lt "$MINLEN" && "$line" =~ \[*\.\] ]]
    then echo    #  Add a blank line immediately
  fi             #+ after short line terminated by a period.


# Exercises:
# ---------
#  1) The script usually inserts a blank line at the end
#+    of the target file. Fix this.
#  2) Line 17 only considers periods as sentence terminators.
#     Modify this to include other common end-of-sentence characters,
#+    such as ?, !, and ".

   Length of Matching Substring at Beginning of String

   expr match "$string" '$substring'
          $substring is a regular expression.

   expr "$string" : '$substring'
          $substring is a regular expression.

#       |------|
#       12345678

echo `expr match "$stringZ" 'abc[A-Z]*.2'`   # 8
echo `expr "$stringZ" : 'abc[A-Z]*.2'`       # 8


   expr index $string $substring
          Numerical position in $string of first character in $substring
          that matches.

#       123456 ...
echo `expr index "$stringZ" C12`             # 6
                                             # C position.

echo `expr index "$stringZ" 1c`              # 3
# 'c' (in #3 position) matches before '1'.

          This is the near equivalent of strchr() in C.

   Substring Extraction

          Extracts substring from $string at $position.

          If the $string parameter is "*" or "@", then this extracts the
          positional parameters, [47] starting at $position.

          Extracts $length characters of substring from $string at

#       0123456789.....
#       0-based indexing.

echo ${stringZ:0}                            # abcABC123ABCabc
echo ${stringZ:1}                            # bcABC123ABCabc
echo ${stringZ:7}                            # 23ABCabc

echo ${stringZ:7:3}                          # 23A
                                             # Three characters of substring.

# Is it possible to index from the right end of the string?

echo ${stringZ:-4}                           # abcABC123ABCabc
# Defaults to full string, as in ${parameter:-default}.
# However . . .

echo ${stringZ:(-4)}                         # Cabc
echo ${stringZ: -4}                          # Cabc
# Now, it works.
# Parentheses or added space "escape" the position parameter.

# Thank you, Dan Jacobson, for pointing this out.

          The position and length arguments can be "parameterized," that
          is, represented as a variable, rather than as a numerical

          Example 10-2. Generating an 8-character "random" string

# Generating an 8-character "random" string.

if [ -n "$1" ]  #  If command-line argument present,
then            #+ then set start-string to it.
else            #  Else use PID of script as start-string.

POS=2  # Starting from position 2 in the string.
LEN=8  # Extract eight characters.

str1=$( echo "$str0" | md5sum | md5sum )
# Doubly scramble:     ^^^^^^   ^^^^^^

# Can parameterize ^^^^ ^^^^

echo "$randstring"

exit $?

# bozo$ ./ my-password
# 1bdd88c4

#  No, this is is not recommended
#+ as a method of generating hack-proof passwords.

          If the $string parameter is "*" or "@", then this extracts a
          maximum of $length positional parameters, starting at

echo ${*:2}          # Echoes second and following positional parameters.
echo ${@:2}          # Same as above.

echo ${*:2:3}        # Echoes three positional parameters, starting at second.

   expr substr $string $position $length
          Extracts $length characters from $string starting at

#       123456789......
#       1-based indexing.

echo `expr substr $stringZ 1 2`              # ab
echo `expr substr $stringZ 4 3`              # ABC

   expr match "$string" '\($substring\)'
          Extracts $substring at beginning of $string, where $substring
          is a regular expression.

   expr "$string" : '\($substring\)'
          Extracts $substring at beginning of $string, where $substring
          is a regular expression.

#       =======

echo `expr match "$stringZ" '\(.[b-c]*[A-Z]..[0-9]\)'`   # abcABC1
echo `expr "$stringZ" : '\(.[b-c]*[A-Z]..[0-9]\)'`       # abcABC1
echo `expr "$stringZ" : '\(.......\)'`                   # abcABC1
# All of the above forms give an identical result.

   expr match "$string" '.*\($substring\)'
          Extracts $substring at end of $string, where $substring is a
          regular expression.

   expr "$string" : '.*\($substring\)'
          Extracts $substring at end of $string, where $substring is a
          regular expression.

#                ======

echo `expr match "$stringZ" '.*\([A-C][A-C][A-C][a-c]*\)'`    # ABCabc
echo `expr "$stringZ" : '.*\(......\)'`                       # ABCabc

   Substring Removal

          Deletes shortest match of $substring from front of $string.

          Deletes longest match of $substring from front of $string.

#       |----|          shortest
#       |----------|    longest

echo ${stringZ#a*C}      # 123ABCabc
# Strip out shortest match between 'a' and 'C'.

echo ${stringZ##a*C}     # abc
# Strip out longest match between 'a' and 'C'.

          Deletes shortest match of $substring from back of $string.

          For example:

# Rename all filenames in $PWD with "TXT" suffix to a "txt" suffix.
# For example, "file1.TXT" becomes "file1.txt" . . .


for i in $(ls *.$SUFF)
  mv -f $i ${i%.$SUFF}.$suff
  #  Leave unchanged everything *except* the shortest pattern match
  #+ starting from the right-hand-side of the variable $i . . .
done ### This could be condensed into a "one-liner" if desired.

# Thank you, Rory Winston.

          Deletes longest match of $substring from back of $string.

#                    ||     shortest
#        |------------|     longest

echo ${stringZ%b*c}      # abcABC123ABCa
# Strip out shortest match between 'b' and 'c', from back of $stringZ.

echo ${stringZ%%b*c}     # a
# Strip out longest match between 'b' and 'c', from back of $stringZ.

          This operator is useful for generating filenames.

          Example 10-3. Converting graphic file formats, with filename

#  Converts all the MacPaint image files in a directory to "pbm" format.

#  Uses the "macptopbm" binary from the "netpbm" package,
#+ which is maintained by Brian Henderson (
#  Netpbm is a standard part of most Linux distros.

SUFFIX=pbm          # New filename suffix.

if [ -n "$1" ]
  directory=$1      # If directory name given as a script argument...
  directory=$PWD    # Otherwise use current working directory.

#  Assumes all files in the target directory are MacPaint image files,
#+ with a ".mac" filename suffix.

for file in $directory/*    # Filename globbing.
  filename=${file%.*c}      #  Strip ".mac" suffix off filename
                            #+ ('.*c' matches everything
                            #+ between '.' and 'c', inclusive).
  $OPERATION $file > "$filename.$SUFFIX"
                            # Redirect conversion to new filename.
  rm -f $file               # Delete original files after converting.
  echo "$filename.$SUFFIX"  # Log what is happening to stdout.

exit 0

# Exercise:
# --------
#  As it stands, this script converts *all* the files in the current
#+ working directory.
#  Modify it to work *only* on files with a ".mac" suffix.

          Example 10-4. Converting streaming audio files to ogg

# Convert streaming audio files (*.ra) to ogg.

# Uses the "mplayer" media player program:
# Uses the "ogg" library and "oggenc":
# This script may need appropriate codecs installed, such as ...
# Possibly also the compat-libstdc++ package.

OFILEPREF=${1%%ra}      # Strip off the "ra" suffix.
OFILESUFF=wav           # Suffix for wav file.

if [ -z "$1" ]          # Must specify a filename to convert.
  echo "Usage: `basename $0` [filename]"
  exit $E_NOARGS

mplayer "$1" -ao pcm:file=$OUTFILE
oggenc "$OUTFILE"  # Correct file extension automatically added by oggenc.

rm "$OUTFILE"      # Delete intermediate *.wav file.
                   # If you want to keep it, comment out above line.

exit $?

#  Note:
#  ----
#  On a Website, simply clicking on a *.ram streaming audio file
#+ usually only downloads the URL of the actual *.ra audio file.
#  You can then use "wget" or something similar
#+ to download the *.ra file itself.

#  Exercises:
#  ---------
#  As is, this script converts only *.ra filenames.
#  Add flexibility by permitting use of *.ram and other filenames.
#  If you're really ambitious, expand the script
#+ to do automatic downloads and conversions of streaming audio files.
#  Given a URL, batch download streaming audio files (using "wget")
#+ and convert them on the fly.

          A simple emulation of getopt using substring-extraction

          Example 10-5. Emulating getopt

# Author: Chris Morgan
# Used in the ABS Guide with permission.

    echo "getopt_simple()"
    echo "Parameters are '$*'"
    until [ -z "$1" ]
      echo "Processing parameter of: '$1'"
      if [ ${1:0:1} = '/' ]
          tmp=${1:1}               # Strip off leading '/' . . .
          parameter=${tmp%%=*}     # Extract name.
          value=${tmp##*=}         # Extract value.
          echo "Parameter: '$parameter', value: '$value'"
          eval $parameter=$value

# Pass all options to getopt_simple().
getopt_simple $*

echo "test is '$test'"
echo "test2 is '$test2'"

exit 0  # See also,, a modified versio of this script.


sh /test=value1 /test2=value2

Parameters are '/test=value1 /test2=value2'
Processing parameter of: '/test=value1'
Parameter: 'test', value: 'value1'
Processing parameter of: '/test2=value2'
Parameter: 'test2', value: 'value2'
test is 'value1'
test2 is 'value2'

   Substring Replacement

          Replace first match of $substring with $replacement. [48]

          Replace all matches of $substring with $replacement.


echo ${stringZ/abc/xyz}       # xyzABC123ABCabc
                              # Replaces first match of 'abc' with 'xyz'.

echo ${stringZ//abc/xyz}      # xyzABC123ABCxyz
                              # Replaces all matches of 'abc' with # 'xyz'.

echo  ---------------
echo "$stringZ"               # abcABC123ABCabc
echo  ---------------
                              # The string itself is not altered!

# Can the match and replacement strings be parameterized?
echo ${stringZ/$match/$repl}  # 000ABC123ABCabc
#              ^      ^         ^^^
echo ${stringZ//$match/$repl} # 000ABC123ABC000
# Yes!          ^      ^        ^^^         ^^^


# What happens if no $replacement string is supplied?
echo ${stringZ/abc}           # ABC123ABCabc
echo ${stringZ//abc}          # ABC123ABC
# A simple deletion takes place.

          If $substring matches front end of $string, substitute
          $replacement for $substring.

          If $substring matches back end of $string, substitute
          $replacement for $substring.


echo ${stringZ/#abc/XYZ}          # XYZABC123ABCabc
                                  # Replaces front-end match of 'abc' with 'XY

echo ${stringZ/%abc/XYZ}          # abcABC123ABCXYZ
                                  # Replaces back-end match of 'abc' with 'XYZ

10.1.1. Manipulating strings using awk

   A Bash script may invoke the string manipulation facilities of awk as
   an alternative to using its built-in operations.

   Example 10-6. Alternate ways of extracting and locating substrings

#      012345678    Bash
#      123456789    awk
# Note different string indexing system:
# Bash numbers first character of string as 0.
# Awk  numbers first character of string as 1.

echo ${String:2:4} # position 3 (0-1-2), 4 characters long
                                         # skid

# The awk equivalent of ${string:pos:length} is substr(string,pos,length).
echo | awk '
{ print substr("'"${String}"'",3,4)      # skid
#  Piping an empty "echo" to awk gives it dummy input,
#+ and thus makes it unnecessary to supply a filename.

echo "----"

# And likewise:

echo | awk '
{ print index("'"${String}"'", "skid")      # 3
}                                           # (skid starts at position 3)
'   # The awk equivalent of "expr index" ...

exit 0

10.1.2. Further Reference

   For more on string manipulation in scripts, refer to Section 10.2 and
   the relevant section of the expr command listing.

   Script examples:

    1. Example 16-9
    2. Example 10-9
    3. Example 10-10
    4. Example 10-11
    5. Example 10-13
    6. Example A-36
    7. Example A-41

10.2. Parameter Substitution

   Manipulating and/or expanding variables

          Same as $parameter, i.e., value of the variable parameter. In
          certain contexts, only the less ambiguous ${parameter} form

          May be used for concatenating variables with strings.

echo "$your_id"
echo "Old \$PATH = $PATH"
PATH=${PATH}:/opt/bin  # Add /opt/bin to $PATH for duration of script.
echo "New \$PATH = $PATH"

   ${parameter-default}, ${parameter:-default}
          If parameter not set, use default.

# var3 is unset.

echo ${var1-$var2}   # 1
echo ${var3-$var2}   # 2
#           ^          Note the $ prefix.

echo ${username-`whoami`}
# Echoes the result of `whoami`, if variable $username is still unset.


   ${parameter-default} and ${parameter:-default} are almost equivalent.
   The extra : makes a difference only when parameter has been declared,
   but is null.


#  Whether a variable has been declared
#+ affects triggering of the default option
#+ even if the variable is null.

echo "username0 has been declared, but is set to null."
echo "username0 = ${username0-`whoami`}"
# Will not echo.


echo username1 has not been declared.
echo "username1 = ${username1-`whoami`}"
# Will echo.

echo "username2 has been declared, but is set to null."
echo "username2 = ${username2:-`whoami`}"
#                            ^
# Will echo because of :- rather than just - in condition test.
# Compare to first instance, above.


# Once again:

# variable has been declared, but is set to null.

echo "${variable-0}"    # (no output)
echo "${variable:-1}"   # 1
#               ^

unset variable

echo "${variable-2}"    # 2
echo "${variable:-3}"   # 3

exit 0

          The default parameter construct finds use in providing
          "missing" command-line arguments in scripts.
#  If not otherwise specified, the following command block operates
#+ on the file "".
#  Begin-Command-Block
#  ...
#  ...
#  ...
#  End-Command-Block

#  From "hanoi2.bash" example:
DISKS=${1:-E_NOPARAM}   # Must specify how many disks.
#  Set $DISKS to $1 command-line-parameter,
#+ or to $E_NOPARAM if that is unset.

          See also Example 3-4, Example 31-2, and Example A-6.

          Compare this method with using an and list to supply a default
          command-line argument.

   ${parameter=default}, ${parameter:=default}
          If parameter not set, set it to default.

          Both forms nearly equivalent. The : makes a difference only
          when $parameter has been declared and is null, [49] as above.

echo ${var=abc}   # abc
echo ${var=xyz}   # abc
# $var had already been set to abc, so it did not change.

   ${parameter+alt_value}, ${parameter:+alt_value}
          If parameter set, use alt_value, else use null string.

          Both forms nearly equivalent. The : makes a difference only
          when parameter has been declared and is null, see below.

echo "###### \${parameter+alt_value} ########"

echo "a = $a"      # a =

echo "a = $a"      # a = xyz

echo "a = $a"      # a = xyz

echo "###### \${parameter:+alt_value} ########"

echo "a = $a"      # a =

echo "a = $a"      # a =
# Different result from   a=${param5+xyz}

echo "a = $a"      # a = xyz

   ${parameter?err_msg}, ${parameter:?err_msg}
          If parameter set, use it, else print err_msg and abort the
          script with an exit status of 1.

          Both forms nearly equivalent. The : makes a difference only
          when parameter has been declared and is null, as above.

   Example 10-7. Using parameter substitution and error messages

#  Check some of the system's environmental variables.
#  This is good preventative maintenance.
#  If, for example, $USER, the name of the person at the console, is not set,
#+ the machine will not recognize you.

: ${HOSTNAME?} ${USER?} ${HOME?} ${MAIL?}
  echo "Name of the machine is $HOSTNAME."
  echo "You are $USER."
  echo "Your home directory is $HOME."
  echo "Your mail INBOX is located in $MAIL."
  echo "If you are reading this message,"
  echo "critical environmental variables have been set."

# ------------------------------------------------------

#  The ${variablename?} construction can also check
#+ for variables set within the script.

#  Note, by the way, that string variables may be set
#+ to characters disallowed in their names.
: ${ThisVariable?}
echo "Value of ThisVariable is $ThisVariable".

echo; echo

: ${ZZXy23AB?"ZZXy23AB has not been set."}
#  Since ZZXy23AB has not been set,
#+ then the script terminates with an error message.

# You can specify the error message.
# : ${variablename?"ERROR MESSAGE"}

# Same result with:   dummy_variable=${ZZXy23AB?}
#                     dummy_variable=${ZZXy23AB?"ZXy23AB has not been set."}
#                     echo ${ZZXy23AB?} >/dev/null

#  Compare these methods of checking whether a variable has been set
#+ with "set -u" . . .

echo "You will not see this message, because script already terminated."

exit $HERE   # Will NOT exit here.

# In fact, this script will return an exit status (echo $?) of 1.

   Example 10-8. Parameter substitution and "usage" messages

: ${1?"Usage: $0 ARGUMENT"}
#  Script exits here if command-line parameter absent,
#+ with following error message.
# 1: Usage: ARGUMENT

echo "These two lines echo only if command-line parameter given."
echo "command-line parameter = \"$1\""

exit 0  # Will exit here only if command-line parameter present.

# Check the exit status, both with and without command-line parameter.
# If command-line parameter present, then "$?" is 0.
# If not, then "$?" is 1.

   Parameter substitution and/or expansion. The following expressions
   are the complement to the match in expr string operations (see
   Example 16-9). These particular ones are used mostly in parsing file
   path names.

   Variable length / Substring removal

          String length (number of characters in $var). For an array,
          ${#array} is the length of the first element in the array.


          + ${#*} and ${#@} give the number of positional parameters.
          + For an array, ${#array[*]} and ${#array[@]} give the number
            of elements in the array.

          Example 10-9. Length of a variable



if [ $# -eq 0 ]  # Must have command-line args to demo script.
  echo "Please invoke this script with one or more command-line arguments."
  exit $E_NO_ARGS

echo "var01 = ${var01}"
echo "Length of var01 = ${#var01}"
# Now, let's try embedding a space.
var02="abcd EFGH28ij"
echo "var02 = ${var02}"
echo "Length of var02 = ${#var02}"

echo "Number of command-line arguments passed to script = ${#@}"
echo "Number of command-line arguments passed to script = ${#*}"

exit 0

   ${var#Pattern}, ${var##Pattern}
          ${var#Pattern} Remove from $var the shortest part of $Pattern
          that matches the front end of $var.

          ${var##Pattern} Remove from $var the longest part of $Pattern
          that matches the front end of $var.

          A usage illustration from Example A-7:

# Function from "" example.
# Strips leading zero(s) from argument passed.

strip_leading_zero () #  Strip possible leading zero(s)
{                     #+ from argument passed.
  return=${1#0}       #  The "1" refers to "$1" -- passed arg.
}                     #  The "0" is what to remove from "$1" -- strips zeros.

          Manfred Schwarb's more elaborate variation of the above:

strip_leading_zero2 () # Strip possible leading zero(s), since otherwise
{                      # Bash will interpret such numbers as octal values.
  shopt -s extglob     # Turn on extended globbing.
  local val=${1##+(0)} # Use local variable, longest matching series of 0's.
  shopt -u extglob     # Turn off extended globbing.
                       # If input was 0, return 0 instead of "".

          Another usage illustration:

echo `basename $PWD`        # Basename of current working directory.
echo "${PWD##*/}"           # Basename of current working directory.
echo `basename $0`          # Name of script.
echo $0                     # Name of script.
echo "${0##*/}"             # Name of script.
echo "${filename##*.}"      # data
                            # Extension of filename.

   ${var%Pattern}, ${var%%Pattern}
          ${var%Pattern} Remove from $var the shortest part of $Pattern
          that matches the back end of $var.

          ${var%%Pattern} Remove from $var the longest part of $Pattern
          that matches the back end of $var.

   Version 2 of Bash added additional options.

   Example 10-10. Pattern matching in parameter substitution

# Pattern matching  using the # ## % %% parameter substitution operators.

pattern1=a*c  # * (wild card) matches everything between a - c.

echo "var1 = $var1"           # abcd12345abc6789
echo "var1 = ${var1}"         # abcd12345abc6789
                              # (alternate form)
echo "Number of characters in ${var1} = ${#var1}"

echo "pattern1 = $pattern1"   # a*c  (everything between 'a' and 'c')
echo "--------------"
echo '${var1#$pattern1}  =' "${var1#$pattern1}"    #         d12345abc6789
# Shortest possible match, strips out first 3 characters  abcd12345abc6789
#                                     ^^^^^               |-|
echo '${var1##$pattern1} =' "${var1##$pattern1}"   #                  6789

# Longest possible match, strips out first 12 characters  abcd12345abc6789
#                                    ^^^^^                |----------|

echo; echo; echo

pattern2=b*9            # everything between 'b' and '9'
echo "var1 = $var1"     # Still  abcd12345abc6789
echo "pattern2 = $pattern2"
echo "--------------"
echo '${var1%pattern2}  =' "${var1%$pattern2}"     #     abcd12345a
# Shortest possible match, strips out last 6 characters  abcd12345abc6789
#                                     ^^^^                         |----|
echo '${var1%%pattern2} =' "${var1%%$pattern2}"    #     a
# Longest possible match, strips out last 12 characters  abcd12345abc6789
#                                    ^^^^                 |-------------|

# Remember, # and ## work from the left end (beginning) of string,
#           % and %% work from the right end.


exit 0

   Example 10-11. Renaming file extensions:
# Renaming file extensions.
#         rfe old_extension new_extension
# Example:
# To rename all *.gif files in working directory to *.jpg,
#          rfe gif jpg


case $# in
  0|1)             # The vertical bar means "or" in this context.
  echo "Usage: `basename $0` old_file_suffix new_file_suffix"
  exit $E_BADARGS  # If 0 or 1 arg, then bail out.

for filename in *.$1
# Traverse list of files ending with 1st argument.
  mv $filename ${filename%$1}$2
  #  Strip off part of filename matching 1st argument,
  #+ then append 2nd argument.

exit 0

   Variable expansion / Substring replacement

          These constructs have been adopted from ksh.

          Variable var expanded, starting from offset pos.

          Expansion to a max of len characters of variable var, from
          offset pos. See Example A-13 for an example of the creative
          use of this operator.

          First match of Pattern, within var replaced with Replacement.

          If Replacement is omitted, then the first match of Pattern is
          replaced by nothing, that is, deleted.

          Global replacement. All matches of Pattern, within var
          replaced with Replacement.

          As above, if Replacement is omitted, then all occurrences of
          Pattern are replaced by nothing, that is, deleted.

          Example 10-12. Using pattern matching to parse arbitrary


echo "var1 = $var1"

echo "var1 (with everything, up to and including first - stripped out) = $t"
#  t=${var1#*-}  works just the same,
#+ since # matches the shortest string,
#+ and * matches everything preceding, including an empty string.
# (Thanks, Stephane Chazelas, for pointing this out.)

echo "If var1 contains a \"-\", returns empty string...   var1 = $t"

echo "var1 (with everything from the last - on stripped out) = $t"


# -------------------------------------------
# -------------------------------------------
echo "path_name = $path_name"
echo "path_name, stripped of prefixes = $t"
# Same effect as   t=`basename $path_name` in this particular case.
#  t=${path_name%/}; t=${t##*/}   is a more general solution,
#+ but still fails sometimes.
#  If $path_name ends with a newline, then `basename $path_name` will not work
#+ but the above expression will.
# (Thanks, S.C.)

# Same effect as   t=`dirname $path_name`
echo "path_name, stripped of suffixes = $t"
# These will fail in some cases, such as "../", "/foo////", # "foo/", "/".
#  Removing suffixes, especially when the basename has no suffix,
#+ but the dirname does, also complicates matters.
# (Thanks, S.C.)


echo "$path_name, with first 11 chars stripped off = $t"
echo "$path_name, with first 11 chars stripped off, length 5 = $t"


echo "$path_name with \"bozo\" replaced  by \"clown\" = $t"
echo "$path_name with \"today\" deleted = $t"
echo "$path_name with all o's capitalized = $t"
echo "$path_name with all o's deleted = $t"

exit 0

          If prefix of var matches Pattern, then substitute Replacement
          for Pattern.

          If suffix of var matches Pattern, then substitute Replacement
          for Pattern.

          Example 10-13. Matching patterns at prefix or suffix of string

# Demo of pattern replacement at prefix / suffix of string.

v0=abc1234zip1234abc    # Original variable.
echo "v0 = $v0"         # abc1234zip1234abc

# Match at prefix (beginning) of string.
v1=${v0/#abc/ABCDEF}    # abc1234zip1234abc
                        # |-|
echo "v1 = $v1"         # ABCDEF1234zip1234abc
                        # |----|

# Match at suffix (end) of string.
v2=${v0/%abc/ABCDEF}    # abc1234zip123abc
                        #              |-|
echo "v2 = $v2"         # abc1234zip1234ABCDEF
                        #               |----|


#  ----------------------------------------------------
#  Must match at beginning / end of string,
#+ otherwise no replacement results.
#  ----------------------------------------------------
v3=${v0/#123/000}       # Matches, but not at beginning.
echo "v3 = $v3"         # abc1234zip1234abc
                        # NO REPLACEMENT.
v4=${v0/%123/000}       # Matches, but not at end.
echo "v4 = $v4"         # abc1234zip1234abc
                        # NO REPLACEMENT.

exit 0

   ${!varprefix*}, ${!varprefix@}
          Matches names of all previously declared variables beginning
          with varprefix.

# This is a variation on indirect reference, but with a * or @.
# Bash, version 2.04, adds this feature.


a=${!xyz*}         #  Expands to *names* of declared variables
# ^ ^   ^           + beginning with "xyz".
echo "a = $a"      #  a = xyz23 xyz24
a=${!xyz@}         #  Same as above.
echo "a = $a"      #  a = xyz23 xyz24

echo "---"

echo "b = $b"      #  b = abc23
c=${!b}            #  Now, the more familiar type of indirect reference.
echo $c            #  something_else

Chapter 11. Loops and Branches


   What needs this iteration, woman?

   --Shakespeare, Othello

   Operations on code blocks are the key to structured and organized
   shell scripts. Looping and branching constructs provide the tools for
   accomplishing this.

11.1. Loops

   A loop is a block of code that iterates [50] a list of commands as
   long as the loop control condition is true.

   for loops

   for arg in [list]
          This is the basic looping construct. It differs significantly
          from its C counterpart.

          for arg in [list]


   During each pass through the loop, arg takes on the value of each
   successive variable in the list.

for arg in "$var1" "$var2" "$var3" ... "$varN"
# In pass 1 of the loop, arg = $var1
# In pass 2 of the loop, arg = $var2
# In pass 3 of the loop, arg = $var3
# ...
# In pass N of the loop, arg = $varN

# Arguments in [list] quoted to prevent possible word splitting.

          The argument list may contain wild cards.

          If do is on same line as for, there needs to be a semicolon
          after list.

          for arg in [list] ; do
          Example 11-1. Simple for loops

# Listing the planets.

for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
  echo $planet  # Each planet on a separate line.

echo; echo

for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
    # All planets on same line.
    # Entire 'list' enclosed in quotes creates a single variable.
    # Why? Whitespace incorporated into the variable.
  echo $planet

echo; echo "Whoops! Pluto is no longer a planet!"

exit 0

          Each [list] element may contain multiple parameters. This is
          useful when processing parameters in groups. In such cases,
          use the set command (see Example 15-16) to force parsing of
          each [list] element and assignment of each component to the
          positional parameters.

          Example 11-2. for loop with two parameters in each [list]

# Planets revisited.

# Associate the name of each planet with its distance from the sun.

for planet in "Mercury 36" "Venus 67" "Earth 93"  "Mars 142" "Jupiter 483"
  set -- $planet  #  Parses variable "planet"
                  #+ and sets positional parameters.
  #  The "--" prevents nasty surprises if $planet is null or
  #+ begins with a dash.

  #  May need to save original positional parameters,
  #+ since they get overwritten.
  #  One way of doing this is to use an array,
  #         original_params=("$@")

  echo "$1              $2,000,000 miles from the sun"
  #-------two  tabs---concatenate zeroes onto parameter $2

# (Thanks, S.C., for additional clarification.)

exit 0

          A variable may supply the [list] in a for loop.

          Example 11-3. Fileinfo: operating on a file list contained in
          a variable


/sbin/ypbind"     # List of files you are curious about.
                  # Threw in a dummy file, /usr/bin/fakefile.


for file in $FILES

  if [ ! -e "$file" ]       # Check if file exists.
    echo "$file does not exist."; echo
    continue                # On to next.

  ls -l $file | awk '{ print $8 "         file size: " $5 }'  # Print 2 fields
  whatis `basename $file`   # File info.
  # Note that the whatis database needs to have been set up for this to work.
  # To do this, as root run /usr/bin/makewhatis.

exit 0

          If the [list] in a for loop contains wild cards (* and ?) used
          in filename expansion, then globbing takes place.

          Example 11-4. Operating on files with a for loop

# Generating [list] in a for-loop, using "globbing"


for file in *
#           ^  Bash performs filename expansion
#+             on expressions that globbing recognizes.
  ls -l "$file"  # Lists all files in $PWD (current directory).
  #  Recall that the wild card character "*" matches every filename,
  #+ however, in "globbing," it doesn't match dot-files.

  #  If the pattern matches no file, it is expanded to itself.
  #  To prevent this, set the nullglob option
  #+   (shopt -s nullglob).
  #  Thanks, S.C.

echo; echo

for file in [jx]*
  rm -f $file    # Removes only files beginning with "j" or "x" in $PWD.
  echo "Removed file \"$file\"".


exit 0

          Omitting the in [list] part of a for loop causes the loop to
          operate on $@ -- the positional parameters. A particularly
          clever illustration of this is Example A-15. See also Example

          Example 11-5. Missing in [list] in a for loop


#  Invoke this script both with and without arguments,
#+ and see what happens.

for a
 echo -n "$a "

#  The 'in list' missing, therefore the loop operates on '$@'
#+ (command-line argument list, including whitespace).


exit 0

          It is possible to use command substitution to generate the
          [list] in a for loop. See also Example 16-54, Example 11-10
          and Example 16-48.

          Example 11-6. Generating the [list] in a for loop with command

# for-loop with [list]
#+ generated by command substitution.

NUMBERS="9 7 3 8 37.53"

for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
  echo -n "$number "

exit 0

          Here is a somewhat more complex example of using command
          substitution to create the [list].

          Example 11-7. A grep replacement for binary files

# Locates matching strings in a binary file.

# A "grep" replacement for binary files.
# Similar effect to "grep -a"


if [ $# -ne 2 ]
  echo "Usage: `basename $0` search_string filename"
  exit $E_BADARGS

if [ ! -f "$2" ]
  echo "File \"$2\" does not exist."
  exit $E_NOFILE

IFS=$'\012'       # Per suggestion of Anton Filippov.
                  # was:  IFS="\n"
for word in $( strings "$2" | grep "$1" )
# The "strings" command lists strings in binary files.
# Output then piped to "grep", which tests for desired string.
  echo $word

# As S.C. points out, lines 23 - 30 could be replaced with the simpler
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'

#  Try something like  "./ mem /bin/ls"
#+ to exercise this script.

exit 0

          More of the same.

          Example 11-8. Listing all users on the system


n=1           # User number

for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
# Field separator = :    ^^^^^^
# Print first field              ^^^^^^^^
# Get input from password file               ^^^^^^^^^^^^^^^^^
  echo "USER #$n = $name"
  let "n += 1"

# USER #1 = root
# USER #2 = bin
# USER #3 = daemon
# ...
# USER #30 = bozo

exit 0

#  Exercise:
#  --------
#  How is it that an ordinary user (or a script run by same)
#+ can read /etc/passwd?
#  Isn't this a security hole? Why or why not?

          Yet another example of the [list] resulting from command

          Example 11-9. Checking all the binaries in a directory for

# Find a particular string in the binaries in a specified directory.

fstring="Free Software Foundation"  # See which files come from the FSF.

for file in $( find $directory -type f -name '*' | sort )
  strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
  #  In the "sed" expression,
  #+ it is necessary to substitute for the normal "/" delimiter
  #+ because "/" happens to be one of the characters filtered out.
  #  Failure to do so gives an error message. (Try it.)

exit $?

#  Exercise (easy):
#  ---------------
#  Convert this script to take command-line parameters
#+ for $directory and $fstring.

          A final example of [list] / command substitution, but this
          time the "command" is a function.

generate_list ()
  echo "one two three"

for word in $(generate_list)  # Let "word" grab output of function.
  echo "$word"

# one
# two
# three

          The output of a for loop may be piped to a command or

          Example 11-10. Listing the symbolic links in a directory

# Lists symbolic links in a directory.

#  Defaults to current working directory,
#+ if not otherwise specified.
#  Equivalent to code block below.
# ----------------------------------------------------------
# ARGS=1                 # Expect one command-line argument.
# if [ $# -ne "$ARGS" ]  # If not 1 arg...
# then
#   directory=`pwd`      # current working directory
# else
#   directory=$1
# fi
# ----------------------------------------------------------

echo "symbolic links in directory \"$directory\""

for file in "$( find $directory -type l )"   # -type l = symbolic links
  echo "$file"
done | sort                                  # Otherwise file list is unsorted
#  Strictly speaking, a loop isn't really necessary here,
#+ since the output of the "find" command is expanded into a single word.
#  However, it's easy to understand and illustrative this way.

#  As Dominik 'Aeneas' Schnitzer points out,
#+ failing to quote  $( find $directory -type l )
#+ will choke on filenames with embedded whitespace.
#  containing whitespace.

exit 0

# --------------------------------------------------------
# Jean Helou proposes the following alternative:

echo "symbolic links in directory \"$directory\""
# Backup of the current IFS. One can never be too cautious.

for file in $(find $directory -type l -printf "%p$IFS")
do     #                              ^^^^^^^^^^^^^^^^
       echo "$file"

# And, James "Mike" Conley suggests modifying Helou's code thusly:

IFS='' # Null IFS means no word breaks
for file in $( find $directory -type l )
  echo $file
  done | sort

#  This works in the "pathological" case of a directory name having
#+ an embedded colon.
#  "This also fixes the pathological case of the directory name having
#+  a colon (or space in earlier example) as well."

          The stdout of a loop may be redirected to a file, as this
          slight modification to the previous example shows.

          Example 11-11. Symbolic links in a directory, saved to a file

# Lists symbolic links in a directory.

OUTFILE=symlinks.list                         # save file

#  Defaults to current working directory,
#+ if not otherwise specified.

echo "symbolic links in directory \"$directory\"" > "$OUTFILE"
echo "---------------------------" >> "$OUTFILE"

for file in "$( find $directory -type l )"    # -type l = symbolic links
  echo "$file"
done | sort >> "$OUTFILE"                     # stdout of loop
#           ^^^^^^^^^^^^^                       redirected to save file.

exit 0

          There is an alternative syntax to a for loop that will look
          very familiar to C programmers. This requires double

          Example 11-12. A C-style for loop

# Multiple ways to count up to 10.


# Standard syntax.
for a in 1 2 3 4 5 6 7 8 9 10
  echo -n "$a "

echo; echo

# +==========================================+

# Using "seq" ...
for a in `seq 10`
  echo -n "$a "

echo; echo

# +==========================================+

# Using brace expansion ...
# Bash, version 3+.
for a in {1..10}
  echo -n "$a "

echo; echo

# +==========================================+

# Now, let's do the same, using C-like syntax.


for ((a=1; a <= LIMIT ; a++))  # Double parentheses, and "LIMIT" with no "$".
  echo -n "$a "
done                           # A construct borrowed from 'ksh93'.

echo; echo

# +=========================================================================+

# Let's use the C "comma operator" to increment two variables simultaneously.

for ((a=1, b=1; a <= LIMIT ; a++, b++))
do  # The comma chains together operations.
  echo -n "$a-$b "

echo; echo

exit 0

          See also Example 27-16, Example 27-17, and Example A-6.


          Now, a for loop used in a "real-life" context.

          Example 11-13. Using efax in batch mode

# Faxing (must have 'efax' package installed).

MODEM_PORT="/dev/ttyS2"   # May be different on your machine.
#                ^^^^^      PCMCIA modem card default port.

if [ $# -ne $EXPECTED_ARGS ]
# Check for proper number of command-line args.
   echo "Usage: `basename $0` phone# text-file"
   exit $E_BADARGS

if [ ! -f "$2" ]
  echo "File $2 is not a text file."
  #     File is not a regular file, or does not exist.
  exit $E_BADARGS

fax make $2              #  Create fax-formatted files from text files.

for file in $(ls $2.0*)  #  Concatenate the converted files.
                         #  Uses wild card (filename "globbing")
                         #+ in variable list.
  fil="$fil $file"

efax -d "$MODEM_PORT"  -t "T$1" $fil   # Finally, do the work.
# Trying adding  -o1  if above line fails.

#  As S.C. points out, the for-loop can be eliminated with
#     efax -d /dev/ttyS2 -o1 -t "T$1" $2.0*
#+ but it's not quite as instructive [grin].

exit $?   # Also, efax sends diagnostic messages to stdout.

          This construct tests for a condition at the top of a loop, and
          keeps looping as long as that condition is true (returns a 0
          exit status). In contrast to a for loop, a while loop finds
          use in situations where the number of loop repetitions is not
          known beforehand.

          while [ condition ]

          The bracket construct in a while loop is nothing more than our
          old friend, the test brackets used in an if/then test. In
          fact, a while loop can legally use the more versatile
          double-brackets construct (while [[ condition ]]).

          As is the case with for loops, placing the do on the same line
          as the condition test requires a semicolon.

          while [ condition ] ; do

          Note that the test brackets are not mandatory in a while loop.
          See, for example, the getopts construct.

          Example 11-14. Simple while loop



while [ "$var0" -lt "$LIMIT" ]
#      ^                    ^
# Spaces, because these are "test-brackets" . . .
  echo -n "$var0 "        # -n suppresses newline.
  #             ^           Space, to separate printed out numbers.

  var0=`expr $var0 + 1`   # var0=$(($var0+1))  also works.
                          # var0=$((var0 + 1)) also works.
                          # let "var0 += 1"    also works.
done                      # Various other methods also work.


exit 0

          Example 11-15. Another while loop


                               # Equivalent to:
while [ "$var1" != "end" ]     # while test "$var1" != "end"
  echo "Input variable #1 (end to exit) "
  read var1                    # Not 'read $var1' (why?).
  echo "variable #1 = $var1"   # Need quotes because of "#" . . .
  # If input is 'end', echoes it here.
  # Does not test for termination condition until top of loop.

exit 0

          A while loop may have multiple conditions. Only the final
          condition determines when the loop terminates. This
          necessitates a slightly different loop syntax, however.

          Example 11-16. while loop with multiple conditions



while echo "previous-variable = $previous"
      [ "$var1" != end ] # Keeps track of what $var1 was previously.
      # Four conditions on "while", but only last one controls loop.
      # The *last* exit status is the one that counts.
echo "Input variable #1 (end to exit) "
  read var1
  echo "variable #1 = $var1"

# Try to figure out how this all works.
# It's a wee bit tricky.

exit 0

          As with a for loop, a while loop may employ C-style syntax by
          using the double-parentheses construct (see also Example 8-5).

          Example 11-17. C-style syntax in a while loop

# Count to 10 in a "while" loop.


while [ "$a" -le $LIMIT ]
  echo -n "$a "
  let "a+=1"
done           # No surprises, so far.

echo; echo

# +=================================================================+

# Now, repeat with C-like syntax.

((a = 1))      # a=1
# Double parentheses permit space when setting a variable, as in C.

while (( a <= LIMIT ))   # Double parentheses, and no "$" preceding variables.
  echo -n "$a "
  ((a += 1))   # let "a+=1"
  # Yes, indeed.
  # Double parentheses permit incrementing a variable with C-like syntax.


# C programmers can feel right at home in Bash.

exit 0

          Inside its test brackets, a while loop can call a function.


condition ()

  if [ $t -lt 5 ]
    return 0  # true
    return 1  # false

while condition
#     ^^^^^^^^^
#     Function call -- four loop iterations.
  echo "Still going: t = $t"

# Still going: t = 1
# Still going: t = 2
# Still going: t = 3
# Still going: t = 4

   Similar to the if-test construct, a while loop can omit the test
while condition
   command(s) ...

          By coupling the power of the read command with a while loop,
          we get the handy while read construct, useful for reading and
          parsing files.

cat $filename |   # Supply input from a file.
while read line   # As long as there is another line to read ...

# =========== Snippet from "" example script ========== #

  while read value   # Read one data point at a time.
    rt=$(echo "scale=$SC; $rt + $value" | bc)
    (( ct++ ))

  am=$(echo "scale=$SC; $rt / $ct" | bc)

  echo $am; return $ct   # This function "returns" TWO values!
  #  Caution: This little trick will not work if $ct > 255!
  #  To handle a larger number of data points,
  #+ simply comment out the "return $ct" above.
} <"$datafile"   # Feed in data file.


   A while loop may have its stdin redirected to a file by a < at its
   A while loop may have its stdin supplied by a pipe.

          This construct tests for a condition at the top of a loop, and
          keeps looping as long as that condition is false (opposite of
          while loop).

          until [ condition-is-true ]

          Note that an until loop tests for the terminating condition at
          the top of the loop, differing from a similar construct in
          some programming languages.

          As is the case with for loops, placing the do on the same line
          as the condition test requires a semicolon.

          until [ condition-is-true ] ; do

          Example 11-18. until loop



until [ "$var1" = "$END_CONDITION" ]
# Tests condition here, at top of loop.
  echo "Input variable #1 "
  echo "($END_CONDITION to exit)"
  read var1
  echo "variable #1 = $var1"

# ------------------------------------------- #

#  As with "for" and "while" loops,
#+ an "until" loop permits C-like test constructs.


until (( var > LIMIT ))
do  # ^^ ^     ^     ^^   No brackets, no $ prefixing variables.
  echo -n "$var "
  (( var++ ))
done    # 0 1 2 3 4 5 6 7 8 9 10

exit 0

   How to choose between a for loop or a while loop or until loop? In C,
   you would typically use a for loop when the number of loop iterations
   is known beforehand. With Bash, however, the situation is fuzzier.
   The Bash for loop is more loosely structured and more flexible than
   its equivalent in other languages. Therefore, feel free to use
   whatever type of loop gets the job done in the simplest way.

11.2. Nested Loops

   A nested loop is a loop within a loop, an inner loop within the body
   of an outer one. How this works is that the first pass of the outer
   loop triggers the inner loop, which executes to completion. Then the
   second pass of the outer loop triggers the inner loop again. This
   repeats until the outer loop finishes. Of course, a break within
   either the inner or outer loop would interrupt this process.

   Example 11-19. Nested Loop
# Nested "for" loops.

outer=1             # Set outer loop counter.

# Beginning of outer loop.
for a in 1 2 3 4 5
  echo "Pass $outer in outer loop."
  echo "---------------------"
  inner=1           # Reset inner loop counter.

  # ===============================================
  # Beginning of inner loop.
  for b in 1 2 3 4 5
    echo "Pass $inner in inner loop."
    let "inner+=1"  # Increment inner loop counter.
  # End of inner loop.
  # ===============================================

  let "outer+=1"    # Increment outer loop counter.
  echo              # Space between output blocks in pass of outer loop.
# End of outer loop.

exit 0

   See Example 27-11 for an illustration of nested while loops, and
   Example 27-13 to see a while loop nested inside an until loop.

11.3. Loop Control


   Tournez cent tours, tournez mille tours,

   Tournez souvent et tournez toujours . . .

   --Verlaine, "Chevaux de bois"

   Commands affecting loop behavior

   break, continue
          The break and continue loop control commands [51] correspond
          exactly to their counterparts in other programming languages.
          The break command terminates the loop (breaks out of it),
          while continue causes a jump to the next iteration of the
          loop, skipping all the remaining commands in that particular
          loop cycle.

          Example 11-20. Effects of break and continue in a loop


LIMIT=19  # Upper limit

echo "Printing Numbers 1 through 20 (but not 3 and 11)."


while [ $a -le "$LIMIT" ]

 if [ "$a" -eq 3 ] || [ "$a" -eq 11 ]  # Excludes 3 and 11.
   continue      # Skip rest of this particular loop iteration.

 echo -n "$a "   # This will not execute for 3 and 11.

# Exercise:
# Why does the loop print up to 20?

echo; echo

echo Printing Numbers 1 through 20, but something happens after 2.


# Same loop, but substituting 'break' for 'continue'.


while [ "$a" -le "$LIMIT" ]

 if [ "$a" -gt 2 ]
   break  # Skip entire rest of loop.

 echo -n "$a "

echo; echo; echo

exit 0

          The break command may optionally take a parameter. A plain
          break terminates only the innermost loop in which it is
          embedded, but a break N breaks out of N levels of loop.

          Example 11-21. Breaking out of multiple loop levels

# Breaking out of loops.

# "break N" breaks out of N level loops.

for outerloop in 1 2 3 4 5
  echo -n "Group $outerloop:   "

  # --------------------------------------------------------
  for innerloop in 1 2 3 4 5
    echo -n "$innerloop "

    if [ "$innerloop" -eq 3 ]
      break  # Try   break 2   to see what happens.
             # ("Breaks" out of both inner and outer loops.)
  # --------------------------------------------------------



exit 0

          The continue command, similar to break, optionally takes a
          parameter. A plain continue cuts short the current iteration
          within its loop and begins the next. A continue N terminates
          all remaining iterations at its loop level and continues with
          the next iteration at the loop, N levels above.

          Example 11-22. Continuing at a higher loop level

# The "continue N" command, continuing at the Nth level loop.

for outer in I II III IV V           # outer loop
  echo; echo -n "Group $outer: "

  # --------------------------------------------------------------------
  for inner in 1 2 3 4 5 6 7 8 9 10  # inner loop

    if [[ "$inner" -eq 7 && "$outer" = "III" ]]
      continue 2  # Continue at loop on 2nd level, that is "outer loop".
                  # Replace above line with a simple "continue"
                  # to see normal loop behavior.

    echo -n "$inner "  # 7 8 9 10 will not echo on "Group III."
  # --------------------------------------------------------------------


echo; echo

# Exercise:
# Come up with a meaningful use for "continue N" in a script.

exit 0

          Example 11-23. Using continue N in an actual task

# Albert Reiner gives an example of how to use "continue N":
# ---------------------------------------------------------

#  Suppose I have a large number of jobs that need to be run, with
#+ any data that is to be treated in files of a given name pattern in a
#+ directory. There are several machines that access this directory, and
#+ I want to distribute the work over these different boxen. Then I
#+ usually nohup something like the following on every box:

while true
  for n in .iso.*
    [ "$n" = ".iso.opts" ] && continue
    [ -r .Iso.$beta ] && continue
    [ -r .lock.$beta ] && sleep 10 && continue
    lockfile -r0 .lock.$beta || continue
    echo -n "$beta: " `date`
    run-isotherm $beta
    ls -alF .Iso.$beta
    [ -r .Iso.$beta ] && rm -f .lock.$beta
    continue 2

#  The details, in particular the sleep N, are particular to my
#+ application, but the general pattern is:

while true
  for job in {pattern}
    {job already done or running} && continue
    {mark job as running, do job, mark job as done}
    continue 2
  break        # Or something like `sleep 600' to avoid termination.

#  This way the script will stop only when there are no more jobs to do
#+ (including jobs that were added during runtime). Through the use
#+ of appropriate lockfiles it can be run on several machines
#+ concurrently without duplication of calculations [which run a couple
#+ of hours in my case, so I really want to avoid this]. Also, as search
#+ always starts again from the beginning, one can encode priorities in
#+ the file names. Of course, one could also do this without `continue 2',
#+ but then one would have to actually check whether or not some job
#+ was done (so that we should immediately look for the next job) or not
#+ (in which case we terminate or sleep for a long time before checking
#+ for a new job).


   The continue N construct is difficult to understand and tricky to use
   in any meaningful context. It is probably best avoided.

11.4. Testing and Branching

   The case and select constructs are technically not loops, since they
   do not iterate the execution of a code block. Like loops, however,
   they direct program flow according to conditions at the top or bottom
   of the block.

   Controlling program flow in a code block

   case (in) / esac
          The case construct is the shell scripting analog to switch in
          C/C++. It permits branching to one of a number of code blocks,
          depending on condition tests. It serves as a kind of shorthand
          for multiple if/then/else statements and is an appropriate
          tool for creating menus.

          case "$variable" in
           "$condition1" )
           "$condition2" )


          + Quoting the variables is not mandatory, since word splitting
            does not take place.
          + Each test line ends with a right paren ).
          + Each condition block ends with a double semicolon ;;.
          + If a condition tests true, then the associated commands
            execute and the case block terminates.
          + The entire case block ends with an esac (case spelled

          Example 11-24. Using case

# Testing ranges of characters.

echo; echo "Hit a key, then hit return."
read Keypress

case "$Keypress" in
  [[:lower:]]   ) echo "Lowercase letter";;
  [[:upper:]]   ) echo "Uppercase letter";;
  [0-9]         ) echo "Digit";;
  *             ) echo "Punctuation, whitespace, or other";;
esac      #  Allows ranges of characters in [square brackets],
          #+ or POSIX ranges in [[double square brackets.

#  In the first version of this example,
#+ the tests for lowercase and uppercase characters were
#+ [a-z] and [A-Z].
#  This no longer works in certain locales and/or Linux distros.
#  POSIX is more portable.
#  Thanks to Frank Wang for pointing this out.

#  Exercise:
#  --------
#  As the script stands, it accepts a single keystroke, then terminates.
#  Change the script so it accepts repeated input,
#+ reports on each keystroke, and terminates only when "X" is hit.
#  Hint: enclose everything in a "while" loop.

exit 0

          Example 11-25. Creating menus using case


# Crude address database

clear # Clear the screen.

echo "          Contact List"
echo "          ------- ----"
echo "Choose one of the following persons:"
echo "[E]vans, Roland"
echo "[J]ones, Mildred"
echo "[S]mith, Julie"
echo "[Z]ane, Morris"

read person

case "$person" in
# Note variable is quoted.

  "E" | "e" )
  # Accept upper or lowercase input.
  echo "Roland Evans"
  echo "4321 Flash Dr."
  echo "Hardscrabble, CO 80753"
  echo "(303) 734-9874"
  echo "(303) 734-9892 fax"
  echo ""
  echo "Business partner & old friend"
# Note double semicolon to terminate each option.

  "J" | "j" )
  echo "Mildred Jones"
  echo "249 E. 7th St., Apt. 19"
  echo "New York, NY 10009"
  echo "(212) 533-2814"
  echo "(212) 533-9972 fax"
  echo ""
  echo "Ex-girlfriend"
  echo "Birthday: Feb. 11"

# Add info for Smith & Zane later.

          * )
   # Default option.
   # Empty input (hitting RETURN) fits here, too.
   echo "Not yet in database."



#  Exercise:
#  --------
#  Change the script so it accepts multiple inputs,
#+ instead of terminating after displaying just one address.

exit 0

          An exceptionally clever use of case involves testing for
          command-line parameters.

#! /bin/bash

case "$1" in
  "") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;
                      # No command-line parameters,
                      # or first parameter empty.
# Note that ${0##*/} is ${var##pattern} param substitution.
                      # Net result is $0.

  -*) FILENAME=./$1;;   #  If filename passed as argument ($1)
                      #+ starts with a dash,
                      #+ replace it with ./$1
                      #+ so further commands don't interpret it
                      #+ as an option.

  * ) FILENAME=$1;;     # Otherwise, $1.

          Here is an more straightforward example of command-line
          parameter handling:

#! /bin/bash

while [ $# -gt 0 ]; do    # Until you run out of parameters . . .
  case "$1" in
              # "-d" or "--debug" parameter?
              if [ ! -f $CONFFILE ]; then
                echo "Error: Supplied file doesn't exist!"
                exit $E_CONFFILE     # File not found error.
  shift       # Check next set of parameters.

#  From Stefano Falsetto's "Log2Rot" script,
#+ part of his "rottlog" package.
#  Used with permission.

          Example 11-26. Using command substitution to generate the case

# Using command substitution to generate a "case" variable.

case $( arch ) in   # "arch" returns machine architecture.
                    # Equivalent to 'uname -m' ...
  i386 ) echo "80386-based machine";;
  i486 ) echo "80486-based machine";;
  i586 ) echo "Pentium-based machine";;
  i686 ) echo "Pentium2+-based machine";;
  *    ) echo "Other type of machine";;

exit 0

          A case construct can filter strings for globbing patterns.

          Example 11-27. Simple string matching

# Simple string matching.

match_string ()
{ # Exact string match.
  PARAMS=2     # Function requires 2 arguments.

  [ $# -eq $PARAMS ] || return $E_BAD_PARAMS

  case "$1" in
  "$2") return $MATCH;;
  *   ) return $E_NOMATCH;;



match_string $a     # wrong number of parameters
echo $?             # 91

match_string $a $b  # no match
echo $?             # 90

match_string $b $d  # match
echo $?             # 0

exit 0

          Example 11-28. Checking for alphabetic input

# Using a "case" structure to filter a string.


isalpha ()  # Tests whether *first character* of input string is alphabetic.
if [ -z "$1" ]                # No argument passed?
  return $FAILURE

case "$1" in
  [a-zA-Z]*) return $SUCCESS;;  # Begins with a letter?
  *        ) return $FAILURE;;
}             # Compare this with "isalpha ()" function in C.

isalpha2 ()   # Tests whether *entire string* is alphabetic.
  [ $# -eq 1 ] || return $FAILURE

  case $1 in
  *[!a-zA-Z]*|"") return $FAILURE;;
               *) return $SUCCESS;;

isdigit ()    # Tests whether *entire string* is numerical.
{             # In other words, tests for integer variable.
  [ $# -eq 1 ] || return $FAILURE

  case $1 in
    *[!0-9]*|"") return $FAILURE;;
              *) return $SUCCESS;;

check_var ()  # Front-end to isalpha ().
if isalpha "$@"
  echo "\"$*\" begins with an alpha character."
  if isalpha2 "$@"
  then        # No point in testing if first char is non-alpha.
    echo "\"$*\" contains only alpha characters."
    echo "\"$*\" contains at least one non-alpha character."
  echo "\"$*\" begins with a non-alpha character."
              # Also "non-alpha" if no argument passed.



digit_check ()  # Front-end to isdigit ().
if isdigit "$@"
  echo "\"$*\" contains only digits [0 - 9]."
  echo "\"$*\" has at least one non-digit character."



e=`echo $b`   # Command substitution.

check_var $a
check_var $b
check_var $c
check_var $d
check_var $e
check_var $f
check_var     # No argument passed, so what happens?
digit_check $g
digit_check $h
digit_check $i

exit 0        # Script improved by S.C.

# Exercise:
# --------
#  Write an 'isfloat ()' function that tests for floating point numbers.
#  Hint: The function duplicates 'isdigit ()',
#+ but adds a test for a mandatory decimal point.

          The select construct, adopted from the Korn Shell, is yet
          another tool for building menus.

          select variable [in list]

          This prompts the user to enter one of the choices presented in
          the variable list. Note that select uses the $PS3 prompt (#? )
          by default, but this may be changed.

          Example 11-29. Creating menus using select


PS3='Choose your favorite vegetable: ' # Sets the prompt string.
                                       # Otherwise it defaults to #? .


select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
  echo "Your favorite veggie is $vegetable."
  echo "Yuck!"
  break  # What happens if there is no 'break' here?


# Exercise:
# --------
#  Fix this script to accept user input not specified in
#+ the "select" statement.
#  For example, if the user inputs "peas,"
#+ the script would respond "Sorry. That is not on the menu."

          If in list is omitted, then select uses the list of command
          line arguments ($@) passed to the script or the function
          containing the select construct.

          Compare this to the behavior of a

          for variable [in list]

          construct with the in list omitted.

          Example 11-30. Creating menus using select in a function


PS3='Choose your favorite vegetable: '


select vegetable
# [in list] omitted, so 'select' uses arguments passed to function.
  echo "Your favorite veggie is $vegetable."
  echo "Yuck!"

choice_of beans rice carrots radishes tomatoes spinach
#         $1    $2   $3      $4       $5       $6
#         passed to choice_of() function

exit 0

          See also Example 37-3.

Chapter 12. Command Substitution

   Command substitution reassigns the output of a command [52] or even
   multiple commands; it literally plugs the command output into another
   context. [53]

   The classic form of command substitution uses backquotes (`...`).
   Commands within backquotes (backticks) generate command-line text.
script_name=`basename $0`
echo "The name of this script is $script_name."

   The output of commands can be used as arguments to another command,
   to set a variable, and even for generating the argument list in a for

rm `cat filename`   # "filename" contains a list of files to delete.
# S. C. points out that "arg list too long" error might result.
# Better is              xargs rm -- < filename
# ( -- covers those cases where "filename" begins with a "-" )

textfile_listing=`ls *.txt`
# Variable contains names of all *.txt files in current working directory.
echo $textfile_listing

textfile_listing2=$(ls *.txt)   # The alternative form of command substitution
echo $textfile_listing2
# Same result.

# A possible problem with putting a list of files into a single string
# is that a newline may creep in.
# A safer way to assign a list of files to a parameter is with an array.
#      shopt -s nullglob    # If no match, filename expands to nothing.
#      textfile_listing=( *.txt )
# Thanks, S.C.


   Command substitution invokes a subshell.


   Command substitution may result in word splitting.
COMMAND `echo a b`     # 2 args: a and b

COMMAND "`echo a b`"   # 1 arg: "a b"

COMMAND `echo`         # no arg

COMMAND "`echo`"       # one empty arg

# Thanks, S.C.

   Even when there is no word splitting, command substitution can remove
   trailing newlines.
# cd "`pwd`"  # This should always work.
# However...

mkdir 'dir with trailing newline

cd 'dir with trailing newline

cd "`pwd`"  # Error message:
# bash: cd: /tmp/file with trailing newline: No such file or directory

cd "$PWD"   # Works fine.

old_tty_setting=$(stty -g)   # Save old terminal setting.
echo "Hit a key "
stty -icanon -echo           # Disable "canonical" mode for terminal.
                             # Also, disable *local* echo.
key=$(dd bs=1 count=1 2> /dev/null)   # Using 'dd' to get a keypress.
stty "$old_tty_setting"      # Restore old setting.
echo "You hit ${#key} key."  # ${#variable} = number of characters in $variabl
# Hit any key except RETURN, and the output is "You hit 1 key."
# Hit RETURN, and it's "You hit 0 key."
# The newline gets eaten in the command substitution.

#Code snippet by StИphane Chazelas.


   Using echo to output an unquoted variable set with command
   substitution removes trailing newlines characters from the output of
   the reassigned command(s). This can cause unpleasant surprises.
dir_listing=`ls -l`
echo $dir_listing     # unquoted

# Expecting a nicely ordered directory listing.

# However, what you get is:
# total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo
# bozo 51 May 15 20:57 -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13

# The newlines disappeared.

echo "$dir_listing"   # quoted
# -rw-rw-r--    1 bozo       30 May 13 17:15 1.txt
# -rw-rw-r--    1 bozo       51 May 15 20:57
# -rwxr-xr-x    1 bozo      217 Mar  5 21:13

   Command substitution even permits setting a variable to the contents
   of a file, using either redirection or the cat command.

variable1=`<file1`      #  Set "variable1" to contents of "file1".
variable2=`cat file2`   #  Set "variable2" to contents of "file2".
                        #  This, however, forks a new process,
                        #+ so the line of code executes slower than the above

#  Note that the variables may contain embedded whitespace,
#+ or even (horrors), control characters.

#  It is not necessary to explicitly assign a variable.
echo "` <$0`"           # Echoes the script itself to stdout.

#  Excerpts from system file, /etc/rc.d/rc.sysinit
#+ (on a Red Hat Linux installation)

if [ -f /fsckoptions ]; then
        fsckoptions=`cat /fsckoptions`
if [ -e "/proc/ide/${disk[$device]}/media" ] ; then
             hdmedia=`cat /proc/ide/${disk[$device]}/media`
if [ ! -n "`uname -r | grep -- "-"`" ]; then
       ktag="`cat /proc/version`"
if [ $usb = "1" ]; then
    sleep 5
    mouseoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Pr
    kbdoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot


   Do not set a variable to the contents of a long text file unless you
   have a very good reason for doing so. Do not set a variable to the
   contents of a binary file, even as a joke.

   Example 12-1. Stupid script tricks
# Don't try this at home, folks.
# From "Stupid Script Tricks," Volume I.

dangerous_variable=`cat /boot/vmlinuz`   # The compressed Linux kernel itself.

echo "string-length of \$dangerous_variable = ${#dangerous_variable}"
# string-length of $dangerous_variable = 794151
# (Does not give same count as 'wc -c /boot/vmlinuz'.)

# echo "$dangerous_variable"
# Don't try this! It would hang the script.

#  The document author is aware of no useful applications for
#+ setting a variable to the contents of a binary file.

exit 0

   Notice that a buffer overrun does not occur. This is one instance
   where an interpreted language, such as Bash, provides more protection
   from programmer mistakes than a compiled language.

   Command substitution permits setting a variable to the output of a
   loop. The key to this is grabbing the output of an echo command
   within the loop.

   Example 12-2. Generating a variable from a loop
# Setting a variable to the output of a loop.

variable1=`for i in 1 2 3 4 5
  echo -n "$i"                 #  The 'echo' command is critical
done`                          #+ to command substitution here.

echo "variable1 = $variable1"  # variable1 = 12345

variable2=`while [ "$i" -lt 10 ]
  echo -n "$i"                 # Again, the necessary 'echo'.
  let "i += 1"                 # Increment.

echo "variable2 = $variable2"  # variable2 = 0123456789

#  Demonstrates that it's possible to embed a loop
#+ within a variable declaration.

exit 0

   Command substitution makes it possible to extend the toolset
   available to Bash. It is simply a matter of writing a program or
   script that outputs to stdout (like a well-behaved UNIX tool should)
   and assigning that output to a variable.

#include <stdio.h>

/*  "Hello, world." C program  */

int main()
  printf( "Hello, world.\n" );
  return (0);

bash$ gcc -o hello hello.c


echo $greeting

bash$ sh
Hello, world.


   The $(...) form has superseded backticks for command substitution.

output=$(sed -n /"$1"/p $file)   # From ""        example.

# Setting a variable to the contents of a text file.
File_contents1=$(cat $file1)
File_contents2=$(<$file2)        # Bash permits this also.

   The $(...) form of command substitution treats a double backslash in
   a different way than `...`.

bash$ echo `echo \\`

bash$ echo $(echo \\)

   The $(...) form of command substitution permits nesting. [54]

   word_count=$( wc -w $(echo * | awk '{print $8}') )

   Or, for something a bit more elaborate . . .

   Example 12-3. Finding anagrams
# Example of nested command substitution.

#  Uses "anagram" utility
#+ that is part of the author's "yawl" word list package.


if [ -z "$1" ]
  echo "Usage $0 LETTERSET"
  exit $E_NOARGS         # Script needs a command-line argument.
elif [ ${#1} -lt $MINLEN ]
  echo "Argument must have at least $MINLEN letters."
  exit $E_BADARG

FILTER='.......'         # Must have at least 7 letters.
#       1234567
Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) )
#          $(     $(  nested command sub.    ) )
#        (              array assignment         )

echo "${#Anagrams[*]}  7+ letter anagrams found"
echo ${Anagrams[0]}      # First anagram.
echo ${Anagrams[1]}      # Second anagram.
                         # Etc.

# echo "${Anagrams[*]}"  # To list all the anagrams in a single line . . .

#  Look ahead to the "Arrays" chapter for enlightenment on
#+ what's going on here.

# See also the script for an example of anagram finding.

exit $?

   Examples of command substitution in shell scripts:

    1. Example 11-7
    2. Example 11-26
    3. Example 9-16
    4. Example 16-3
    5. Example 16-22
    6. Example 16-17
    7. Example 16-54
    8. Example 11-13
    9. Example 11-10
   10. Example 16-32
   11. Example 20-8
   12. Example A-16
   13. Example 29-3
   14. Example 16-47
   15. Example 16-48
   16. Example 16-49

Chapter 13. Arithmetic Expansion

   Arithmetic expansion provides a powerful tool for performing
   (integer) arithmetic operations in scripts. Translating a string into
   a numerical expression is relatively straightforward using backticks,
   double parentheses, or let.


   Arithmetic expansion with backticks (often used in conjunction with

z=`expr $z + 3`          # The 'expr' command performs the expansion.

   Arithmetic expansion with double parentheses, and using let
          The use of backticks (backquotes) in arithmetic expansion has
          been superseded by double parentheses -- ((...)) and $((...))
          -- and also by the very convenient let construction.

z=$((z+3))                                  #  Also correct.
                                            #  Within double parentheses,
                                            #+ parameter dereferencing
                                            #+ is optional.

# $((EXPRESSION)) is arithmetic expansion.  #  Not to be confused with
                                            #+ command substitution.

# You may also use operations within double parentheses without assignment.

  echo "n = $n"                             # n = 0

  (( n += 1 ))                              # Increment.
# (( $n += 1 )) is incorrect!
  echo "n = $n"                             # n = 1

let z=z+3
let "z += 3"  #  Quotes permit the use of spaces in variable assignment.
              #  The 'let' operator actually performs arithmetic evaluation,
              #+ rather than expansion.

          Examples of arithmetic expansion in scripts:

    1. Example 16-9
    2. Example 11-14
    3. Example 27-1
    4. Example 27-11
    5. Example A-16

Chapter 14. Recess Time

   This bizarre little intermission gives the reader a chance to relax
   and maybe laugh a bit.

       Fellow Linux user, greetings! You are reading something which
       will bring you luck and good fortune. Just e-mail a copy of
       this document to 10 of your friends. Before making the copies,
       send a 100-line Bash script to the first person on the list
       at the bottom of this letter. Then delete their name and add
       yours to the bottom of the list.
       Don't break the chain! Make the copies within 48 hours.
       Wilfred P. of Brooklyn failed to send out his ten copies and
       woke the next morning to find his job description changed
       to "COBOL programmer." Howard L. of Newport News sent
       out his ten copies and within a month had enough hardware
       to build a 100-node Beowulf cluster dedicated to playing
       Tuxracer. Amelia V. of Chicago laughed at this letter
       and broke the chain. Shortly thereafter, a fire broke out
       in her terminal and she now spends her days writing
       documentation for MS Windows.
       Don't break the chain!  Send out your ten copies today!

   Courtesy 'NIX "fortune cookies", with some alterations and many

Комментариев нет:

Отправить комментарий