Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • Python – assert statement
  • Java – assert keyword
  • R – stopifnot statement

Bash does not have built-in error checking, or the equivalent of These languages also have throw/catch exception handling constructs, which lets a program detect and deal with errors – if possible.So .

There are mechanisms in bash that provide some of this functionality (see set -e and set -x decsribed in https://www.turnkeylinux.org/blog/shell-error-handling and http://linuxcommand.org/lc3_wss0150.php, and trap-ing signals in http://linuxcommand.org/lc3_wss0150.php).

Here, though, we're going to explore how to add "roll our own" error handling mechanisms. This will also force us to better understand shells & sub-shells, exit and result codes, and other communication between execution contexts.

Shells and sub-shells

Every bash program has its own execution environment (sub-shell), which is a child process of its calling parent shell. A new sub-shell is created, runs, and returns when:

  • a built-in bash utility (e.g. ls) is run from the command line (or from within script)
  • a custom script is run from the command line (or from within a script)
  • backtick evaluation is used to execute commands (e.g. echo `date`)
  • any set of commands enclosed in parentheses is run, e.g.
    • ( date )

Parentheses evaluation is similar to backtick evaluation except that the standard output of backtick evaluation is automatically connected to the standard input of the caller. To connect the standard output of parentheses evaluation to the standard input of the caller, the the parentheses expression must be "evaluated" with a dollar sign. Consider:

  • today=`date`
  • today=$(date)
    • the date command is run in a sub-shell, writing its data to its standard output
    • date's standard output stream is connected to the calling shell's standard input
    • the caller's standard input text is stored in the today variable

Here are the main communication methods between shell environments:

  • Input to sub-shells
    • program arguments
    • environment variables
    • file or stream data
  • Output from sub-shells
    • exit code
    • standard output
    • file data

Environment variables

In addition to passing arguments to a program, a caller may set environment variables (normal bash shell variables) that can be read in the called environment. However by default, variables in a parent shell are not copied into sub-shells unless they are exported using the export keyword.with a set of functions. Our goals are:

  • To stop execution immediately when an error is detected
  • Have the last line of the log file describe the fatal error (easy to tail)
  • Make it easy to frequently check for errors of different sorts in our code
  • Report the check being done (even when successful) in order to leave "bread crumbs" in the log file for troubleshooting.

The step_04.sh Script

Here's a step_04.sh script that builds on our step_03.sh work. Copy it from ~/workshop/step_04.sh, or copy and paste this text into a new file, ~/step_04.txt in your home directory. Make sure it is executable (chmod +x ~/step_04.txt).

Code Block
languagebash

The Parts

There are a number of error checking functions defined in step_04.sh – and many more that could be added of course – but this is a good start to get the ideas across.

err function

The main error function is just called err, and is called with some descriptive text elsewhere when an error is detected. It outputs that text with some surrounding decoration, and exits with a non-0 error code. If we do our error checking properly, and an error occurs, this will be the last line in the log file. Or the text "...exiting" can also be grep'd for in the log file to see if something bad happened.

Code Block
languagebash
# General function that exits after printing its text
#   in a standard format which can be easily grep'd.
err() {
  echo_se "** ERROR: $@ ...exiting"; exit 255;
}

check_res function

The check_res function checks the result/exit code passed in to it (usually our friend $?)

Code Block
languagebash
# Function to check result code of programs.
# Exits with a standard error normalmessage bashif variablecode is not visible to sub-shells
foo=abc
( echo $foo )

# exported bash variables are visible to sub-shells
export foo
( echo $foo )

export bar="def"
( echo $bar )

Script exit codes and function return values

Unlike most other programming languages, bash functions and scripts can only return a single integer between 0 and 255. By convention a return value of 0 means success, and any other return value is an error code.

A function can return this value using the return keyword (e.g. return 0); the return value is then stored in the special $? variable, which can be checked by the caller. Function callers are always in the same execution environment (sub-shell) as any bash functions they call. Since this not very much information, function return values are not often used or checked. Instead, as we've seen, functions are often called for their standard output, which serves as a return value proxy.

A script can also return an integer value, called the exit code, using the exit keyword (e.g. exit 255). The exit code is returned to the script caller (in the parent shell) in the $? variable. Note that no further code in the current sub-shell is executed after exit is called.

...

non-zero.
# Otherwise displays a completion message.
#   arg 1 - the return code (usually $?)
#   arg 2 - text describing what ran
check_res() {
  if [[ $1 == 0 ]]; then maybe_echo ".. check_res: $2 OK";
  else err "check_res: $2 returned non-0 exit code $1"; fi
}

exercise 1

Test the check_res function in a parentheses sub-shell with one or two arguments.

Expand
titleSolution
Code Block
languagebash
tmux new
source step_04.sh
( check_res 0 )
( check_res 0 testing )
( check_res 4 bad_thing )

exit

file checking functions

Several file/directory checking functions are defined:

  • check_dir
  • check_file
  • check_file_not_empty

Code Block
languagebash
# Function Athat successfulchecks exitif codea isdirectory 0exists lsand echoexits $?if not.
#   arg Any1 non-0 exit code is an error (here the code is 2)
ls not_a_file
echo $?

Note that in the non-0 exit code case, the program may also report error information on standard error (e.g. ls: cannot access not_a_file: No such file or directory above).

Tip
titleTip

The $? return code variable must be checked immediately after the called program or sub-shell completes, because any further actions in the caller will change $?. One way to do this is to save off the value $? of in another variable (e.g. res=$?).

exercise 1

On the command line, call exit with various codes in a parentheses sub-shell and check the result in the caller.

Tip
titleTip
You may want to do this in a new tmux or screen session, since accidentally calling exit at top-level (instead of in a sub-shell) will log you off the server!
- the directory name
#   arg 2 - text describing the directory (optional)
check_dir() {
  if [[ ! -d "$1" ]]; then err "$2 Directory '$1' not found"
  else maybe_echo ".. check_dir: $2 directory '$1' exists"; fi
}
# Function that checks if a file exists
#   arg 1 - the file name
#   arg 2 - text describing the file (optional)
check_file() {
  if [[ ! -e "$1" ]]; then err "$2 File '$1' not found"
  else maybe_echo ".. check_file: $2 file '$1' exists"; fi
}
# Function checks if a file exists & has non-0 length, else exits.
#   arg 1 - the file name
#   arg 2 - text describing the file (optional)
check_file_not_empty() {
  if [[ ! -e "$1" ]]; then err "$2 File '$1' not found"
  elif [[ ! -s "$1" ]]; then err "$2 File '$1' is empty"
  else maybe_echo ".. check_file_not_empty: $2 file '$1' OK"; fi
}

exercise 2

Explore these file checking functions in the safety of a sub-shell.

Expand
titleSolution
Code Block
languagebash
tmux new
source step_04.sh

( exit 0 )
res=$?
echo "exit code: $res" 

( exit 255 )
res=$?
echo "exit code: $res"

x

...

languagebash

...

check_dir ~/ 'my home' )
( check_file_not_empty ~/step_04.sh 'script' )
( check_file not_a_file.txt )

exit