Advanced Bash-Scripting HOWTO

A guide to shell scripting, using Bash

Mendel Cooper

  [email protected]

  v0.2, 30 October 2000

  This is a major update on version 0.1. -- a couple of bugs fixed, plus
  much additional material and more example scripts added.

  This document is both a tutorial and a reference on shell scripting
  with Bash. It assumes no previous knowledge of scripting or
  programming, but progresses rapidly toward an intermediate/advanced
  level of instruction. The exercises and heavily-commented examples
  invite active reader participation. Still, it is a work in progress.
  The intention is to add much supplementary material in future updates
  to this HOWTO, so that it will gradually evolve into an LDP "guide",
  i.e., a complete book.
    _________________________________________________________________

  Table of Contents
  1. [1]Why Shell Programming?
  2. [2]Starting Off With a Sha-Bang

       2.1. [3]Invoking the script
       2.2. [4]Shell wrapper, self-executing script

  3. [5]Tutorial / Reference

       3.1. [6]exit and exit status
       3.2. [7]Special characters used in shell scripts
       3.3. [8]Introduction to Variables and Parameters
       3.4. [9]Quoting
       3.5. [10]Tests
       3.6. [11]Operations and Related Topics
       3.7. [12]Variables Revisited
       3.8. [13]Loops
       3.9. [14]Internal Commands and Builtins
       3.10. [15]External Filters, Programs and Commands
       3.11. [16]System and Administrative Commands
       3.12. [17]Backticks (`...`)
       3.13. [18]I/O Redirection
       3.14. [19]Recess Time
       3.15. [20]Regular Expressions
       3.16. [21]Subshells
       3.17. [22]Process Substitution
       3.18. [23]Functions
       3.19. [24]List Constructs
       3.20. [25]Arrays
       3.21. [26]Files
       3.22. [27]Here Documents
       3.23. [28]Of Zeros and Nulls
       3.24. [29]Debugging
       3.25. [30]Options
       3.26. [31]Gotchas
       3.27. [32]Miscellany
       3.28. [33]Bash, version 2

  4. [34]Credits
  [35]Bibliography
  A. [36]Contributed Scripts
  B. [37]Copyright

  List of Tables
  3-1. [38]bash options

  List of Examples
  2-1. [39]cleanup: A script to clean up the log files in /var/log
  2-2. [40]cleanup: An enhanced and generalized version of above script.
  2-3. [41]shell wrapper
  2-4. [42]A slightly more complex shell wrapper
  3-1. [43]exit / exit status
  3-2. [44]Code blocks and I/O redirection
  3-3. [45]Saving the results of a code block to a file
  3-4. [46]Backup of all files changed in last day
  3-5. [47]Variable assignment and substitution
  3-6. [48]Using param substitution and :
  3-7. [49]Renaming file extensions:
  3-8. [50]Using pattern matching to parse arbitrary strings
  3-9. [51]What is truth?
  3-10. [52]Equivalence of [ ] and test
  3-11. [53]Tests, command chaining, redirection
  3-12. [54]arithmetic and string comparisons
  3-13. [55]zmost
  3-14. [56]Compound Condition Tests Using && and ||
  3-15. [57]Representation of numerical constants:
  3-16. [58]Variable Assignment
  3-17. [59]Variable Assignment, plain and fancy
  3-18. [60]Positional Parameters
  3-19. [61]wh, whois domain name lookup
  3-20. [62]Using shift
  3-21. [63]Using declare to type variables
  3-22. [64]Indirect References
  3-23. [65]Generating random numbers
  3-24. [66]Simple for loops
  3-25. [67]Missing in [list] in a for loop
  3-26. [68]Using efax in batch mode
  3-27. [69]Simple while loop
  3-28. [70]Another while loop
  3-29. [71]until loop
  3-30. [72]Effects of break and continue in a loop
  3-31. [73]Using case
  3-32. [74]Creating menus using case
  3-33. [75]Creating menus using select
  3-34. [76]Creating menus using select in a function
  3-35. [77]Using getopts to read the flags/options passed to a script
  3-36. [78]Using set with positional parameters
  3-37. [79]basename and dirname
  3-38. [80]Variable assignment, using read
  3-39. [81]Changing the current working directory
  3-40. [82]"Including" a data file
  3-41. [83]Waiting for a process to finish before proceeding
  3-42. [84]Using ls to create a table of contents for burning a CDR
         disk

  3-43. [85]Badname, eliminate file names in current directory
         containing bad characters and white space.

  3-44. [86]Log file using xargs to monitor system log
  3-45. [87]copydir, copying files in current directory to another,
         using xargs

  3-46. [88]Showing the effect of eval
  3-47. [89]Forcing a log-off
  3-48. [90]Using expr
  3-49. [91]Letting let do some arithmetic.
  3-50. [92]printf in action
  3-51. [93]Using cpio to move a directory tree
  3-52. [94]toupper: Transforms a file to all uppercase.
  3-53. [95]lowercase: Changes all filenames in working directory to
         lowercase.

  3-54. [96]nl: A self-numbering script.
  3-55. [97]Formatted file listing.
  3-56. [98]Using date
  3-57. [99]uuencoding encoded files
  3-58. [100]Using seq to generate loop arguments
  3-59. [101]Effects of exec
  3-60. [102]killall, from /etc/rc.d/init.d
  3-61. [103]Perl embedded in a bash script
  3-62. [104]Variable scope in a subshell
  3-63. [105]Running parallel processes in subshells
  3-64. [106]Simple function
  3-65. [107]Function Taking Parameters
  3-66. [108]Converting numbers to Roman numerals
  3-67. [109]Local variable visibility
  3-68. [110]Recursion, using a local variable
  3-69. [111]Using an "and list" to test for command-line arguments
  3-70. [112]Using "or lists" in combination with an "and list"
  3-71. [113]Simple array usage
  3-72. [114]Some special properties of arrays
  3-73. [115]An old friend: The Bubble Sort
  3-74. [116]Complex array application: Sieve of Erastosthenes
  3-75. [117]dummyfile: Creates a 2-line dummy file
  3-76. [118]broadcast: Sends message to everyone logged in
  3-77. [119]Multi-line message using cat
  3-78. [120]upload: Uploads a file pair to "Sunsite" incoming directory
  3-79. [121]Setting up a swapfile using /dev/zero
  3-80. [122]test23, a buggy script
  3-81. [123]test24, another buggy script
  3-82. [124]Trapping at exit
  3-83. [125]Cleaning up after Control-C
  3-84. [126]String expansion
  3-85. [127]Indirect variable references - the new way
  3-86. [128]Using arrays and other miscellaneous trickery to deal four
         random hands from a deck of cards

  A-1. [129]manview: A script for viewing formatted man pages
  A-2. [130]manview: A script for uploading to an ftp site, using a
         locally encrypted password

  A-3. [131]behead: A script for removing mail and news message headers
  A-4. [132]ftpget: A script for downloading files via ftp
    _________________________________________________________________

Chapter 1. Why Shell Programming?

  The shell is a command interpreter. It is the insulating layer between
  the operating system kernel and the user. Yet, it is also a fairly
  powerful programming language. A shell program, called a script , is
  an easy-to-use tool for building applications by "gluing" together
  system calls, tools, utilities, and compiled binaries. Virtually the
  entire repertoire of UNIX commands, utilities, and tools is available
  for invocation by a shell script. If that were not enough, internal
  shell commands, such as testing and loop constructs, give additional
  power and flexibility to scripts. Shell scripts lend themselves
  exceptionally well to to administrative system tasks and other routine
  repetitive jobs not requiring the bells and whistles of a full-blown
  tightly structured programming language.

  A working knowledge of shell scripting is essential to everyone
  wishing to become reasonably adept at system administration, even if
  they do not anticipate ever having to actually write a script.
  Consider that as a Linux machine boots up, it executes the shell
  scripts in /etc/rc.d to restore the system configuration and set up
  services. A detailed understanding of these scripts is important for
  analyzing the behavior of a system, and possibly modifying it.

  Writing shell scripts is not hard to learn, since the scripts can be
  built in bite-sized sections and there is only a fairly small set of
  shell-specific operators and options to learn. The syntax is simple
  and straightforward, similar to that of invoking and chaining together
  utilities at the command line, and there are only a few "rules" to
  learn. Most short scripts work right the first time, and debugging
  even the longer ones is straightforward.

  A shell script is a "quick and dirty" method of prototyping a complex
  application. Getting even a limited subset of the functionality to
  work in a shell script, even if slowly, is often a useful first stage
  in project development. This way, the structure of the application can
  be tested and played with, and the major pitfalls found before
  proceeding to the final coding in C, C++, Java, or Perl.

  Shell scripting hearkens back to the classical UNIX philosophy of
  breaking complex projects into simpler subtasks, of chaining together
  components and utilities. Many consider this a better, or at least
  more esthetically pleasing approach to problem solving than using one
  of the new generation of high powered all-in-one languages, such as
  Perl, which attempt to be all things to all people, but at the cost of
  forcing you to alter your thinking processes to fit the tool.

  When not to use shell scripts

    * resource-intensive tasks, especially where speed is a factor
    * complex applications, where structured programming is a necessity
    * file handling (Bash is limited to serial file access, and that
      only in a particularly clumsy and inefficient line-by-line
      fashion)
    * need to generate or manipulate graphics or GUIs
    * need direct access to system hardware
    * need port or socket I/O
    * need to use libraries or interface with legacy code

  If any of the above applies, consider a more powerful scripting
  language, perhaps Perl, Tcl, Python, or even a high-level compiled
  language such as C, C++, or Java. Even then, prototyping the
  application as a shell script might still be a useful development
  step.

  We will be using Bash, an acronym for "Born-Again Shell" and a pun on
  Stephen Bourne's now classic Bourne Shell. Bash has become the de
  facto standard for shell scripting on all flavors of UNIX. Most of the
  principles dealt with in this document apply equally well to scripting
  with other shells, such as the Korn Shell, from which Bash derives
  some of its features, and the C Shell and its variants. (Note that C
  Shell programming is not recommended due to certain inherent problems,
  as pointed out in a [133]news group posting by Tom Christiansen in
  October of 1993).

  The following is a tutorial in shell scripting. It relies heavily on
  examples to illustrate features of the shell. As far as possible, the
  example scripts have been tested, and some of them may actually be
  useful in real life. The reader should cut out and save the examples,
  assign them appropriate names, give them execute permission (chmod u+x
  scriptname), then run them to see what happens. Note that some of the
  scripts below introduce features before they are explained, and this
  may require the reader to temporarily skip ahead for enlightenment.

  Unless otherwise noted, the author of this document wrote the example
  scripts that follow.
    _________________________________________________________________

Chapter 2. Starting Off With a Sha-Bang

  In the simplest case, a script is nothing more than a list of system
  commands stored in a file. At the very least, this saves the effort of
  retyping that particular sequence of commands each time it is invoked.

  Example 2-1. cleanup: A script to clean up the log files in /var/log
# cleanup
# Run as root, of course.

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Logs cleaned up."

  There is nothing unusual here, just a set of commands that could just
  as easily be invoked one by one from the command line on the console
  or in an xterm. The advantages of placing the commands in a script go
  beyond not having to retype them time and again. The script can easily
  be modified, customized, or generalized for a particular application.

  Example 2-2. cleanup: An enhanced and generalized version of above
  script.
#!/bin/bash
# cleanup, version 2
# Run as root, of course.

if [ -n $1 ]
# Test if command line argument present.
then
 lines=$1
else
 lines=50
 # default, if not specified on command line.
fi


cd /var/log
tail -$lines messages > mesg.temp
# Saves last section of message log file.
mv mesg.temp messages

# cat /dev/null > messages
# No longer needed, as the above method is safer.

cat /dev/null > wtmp
echo "Logs cleaned up."

exit 0
# A zero return value from the script upon exit
# indicates success to the shell.

  Since you may not wish to wipe out the entire system log, this variant
  of the first script keeps the last section of the message log intact.
  You will constantly discover ways of refining previously written
  scripts for increased effectiveness.

  The sha-bang ( #!) at the head of a script tells your system that this
  file is a set of commands to be fed to the command interpreter
  indicated. The #! is actually a two byte " magic number", a special
  marker that designates an executable shell script (man magic gives
  more info on this fascinating topic). Immediately following the
  sha-bang is a path name. This is the path to the program that
  interprets the commands in the script, whether it be a shell, a
  programming language, or a utility. This enables the specific commands
  and directives embedded in the shell or program called.

#!/bin/sh
#!/bin/bash #!/bin/awk #!/usr/bin/perl #!/bin/sed
#!/usr/bin/tcl

  Each of the above script header lines calls a different command
  interpreter, be it /bin/sh, the default shell (bash in a Linux system)
  or otherwise. Using #!/bin/sh, the default Bourne Shell in most
  commercial variants of UNIX, makes the script portable to non-Linux
  machines, though you may have to sacrifice a few bash-specific
  features (the script will conform to the POSIX sh standard).

  Note that the path given at the "sha-bang" must be correct, otherwise
  an error message, usually Command not found will be the only result of
  running the script.

  #! can be omitted if the script consists only of a set of generic
  system commands, using no internal shell directives. Example 2, above,
  requires the initial #!, since the variable assignment line, lines=50,
  uses a shell-specific construct. Note that #!/bin/sh invokes the
  default shell interpreter, which defaults to /bin/bash on a Linux
  machine.
    _________________________________________________________________

2.1. Invoking the script

  Having written the script, you can invoke it by sh scriptname, or
  alternately bash scriptname. (Not recommended is using sh <scriptname,
  since this effectively disables reading from input within the script.)
  Much more convenient is to make the script itself directly executable
  by

  Either:
         chmod 755 scriptname (gives everyone execute permission)

  or
         chmod +x scriptname (gives everyone execute permission)

         chmod u+x scriptname (gives only the script owner execute
         permission)

  In this case, you could try calling the script by ./scriptname.

  As a final step, after testing and debugging, you would likely want to
  move it to /usr/local/bin (as root, of course), to make the script
  available to yourself and all other users as a system-wide executable.
  The script could then be invoked by simply typing scriptname [return]
  from the command line.
    _________________________________________________________________

2.2. Shell wrapper, self-executing script

  A sed or awk script would normally be invoked from the command line by
  a sed -e 'commands' or awk -e 'commands'. Embedding such a script in a
  bash script permits calling it more simply, and makes it "reusable".
  This also permits combining the functionality of sed and awk, for
  example piping the output of a set of sed commands to awk. As a saved
  executable file, you can then repeatedly invoke it in its original
  form or modified, without retyping it on the command line.

  Example 2-3. shell wrapper
#!/bin/bash

# This is a simple script
# that removes blank lines
# from a file.
# No argument checking.

# Same as
# sed -e '/^$/d $1' filename
# invoked from the command line.

sed -e /^$/d $1
# '^' is beginning of line,
# '$' is end,
# and 'd' is delete.

  Example 2-4. A slightly more complex shell wrapper
#!/bin/bash

# "subst", a script that substitutes one pattern for
# another in a file,
# i.e., "subst Smith Jones letter.txt".

if [ $# -ne 3 ]
# Test number of arguments to script
# (always a good idea).
then
 echo "Usage: `basename $0` old-pattern new-pattern filename"
 exit 1
fi

old_pattern=$1
new_pattern=$2

if [ -f $3 ]
then
   file_name=$3
else
   echo "File \"$3\" does not exist."
   exit 2
fi

# Here is where the heavy work gets done.
sed -e "s/$old_pattern/$new_pattern/" $file_name
# 's' is, of course, the substitute command in sed,
# and /pattern/ invokes address matching.
# Read the literature on 'sed' for a more
# in-depth explanation.

exit 0
# Successful invocation of the script returns 0.

  Exercise. Write a shell script that performs a simple task.
    _________________________________________________________________

Chapter 3. Tutorial / Reference



  ...there are dark corners in the Bourne shell, and people use all of
  them.

  --Chet Ramey
    _________________________________________________________________

3.1. exit and exit status

  The exit command may be used to terminate a script, just as in a C
  program. It can also return a value, which is available to the shell.

  Every command returns an exit status (sometimes referred to as a
  return status ). A successful command returns a 0, while an
  unsuccessful one returns a non-zero value that usually may be
  interpreted as an error code.

  Likewise, functions within a script and the script itself return an
  exit status. The last command executed in the function or script
  determines the exit status. Within a script, an exit nn command may be
  used to deliver an nn exit status to the shell (nn must be a decimal
  number in the 0 - 255 range).

  $? reads the exit status of script or function.

  Example 3-1. exit / exit status
#!/bin/bash

echo hello
echo $?
# exit status 0 returned
# because command successful.

lskdf
# bad command
echo $?
# non-zero exit status returned.

echo

exit 143
# Will return 143 to shell.
# To verify this, type $? after script terminates.

# By convention, an 'exit 0' shows success,
# while a non-zero exit value indicates an error or anomalous condition.

# It is also appropriate for the script to use the exit status
# to communicate with other processes, as when in a pipe with other scripts.
    _________________________________________________________________

3.2. Special characters used in shell scripts

  #

  Comments. Lines beginning with a # (with the exception of #!) are
  comments.

# This line is a comment.

         Comments may also occur at the end of a command.

echo "A comment will follow." # Comment here.

         Comments may also follow white space at the beginning of a
         line.

       # A tab precedes this comment.

  ;

  Command separator. Permits putting two or more commands on the same
  line

echo hello; echo there

         Note that the ; sometimes needs to be escaped (\).

  .

  "dot" command. Equivalent to source, explained further on

  :

  null command. Exit status 0, alias for true

         Endless loop:

while :
do
  operation-1
  operation-2
  ...
  operation-n
done

         Placeholder in if/then test:

if condition
then :   # Do nothing and branch ahead
else
  take-some-action
fi

         Provides a placeholder where a binary operation is expected,
         see [134]Section 3.3.1.

: ${username=`whoami`}
# ${username=`whoami`}   without the leading : gives an error


         Evaluate string of variables using "parameter substitution",
         see [135]Example 3-6:

: ${HOSTNAME?} ${USER?} ${MAIL?}

         Prints error message if one or more of essential environmental
         variables not set.

  ${}

  Parameter substitution.

         See [136]Section 3.3 for more details.

  ()

  command group.
(a=hello; echo $a)

    Note: A listing of commands within parentheses starts a subshell
    (see [137]Section 3.16).

  {}

  block of code. This, in effect, creates an anonymous function.

         The code block enclosed in braces may have I/O redirected to
         and from it.

  Example 3-2. Code blocks and I/O redirection
#!/bin/bash

{
read fstab
} < /etc/fstab

echo "First line in /etc/fstab is:"
echo "$fstab"

exit 0

  Example 3-3. Saving the results of a code block to a file
#!/bin/bash

#                rpm-check
#                ---------
# Queries an rpm file for description, listing, and whether it can be installed