Min Yang Jung   Software, Safety, and Medical Robotics

Bash Tips and Code Examples

For the past couple years at work, I have been writing quite a bit of shell scripts for multiple projects. Most of them are to leverage the power of the shell scripts to automate development workflows such as build, packaging, deploy, and software release.

Among many variants of shells, I primarily use Bash whenever possible. Over time, I noticed that there are some code snippets that I have been repeatedly using and certain tips I found useful when writing and debugging bash scripts. This post compiles such code snippets and tips.

Hope you find these useful as well. Enjoy Bash-ing!

Logging

Showing messages while logging to file

Sometimes it is necessary to show messages to the console, i.e., the standard output (stdout) and the standard error (stderr) while dumping those messages to a log file. This can be easily achieved in Bash using the exec command. exec is a built-in Bash command that can redirect the output of the current process. Combined with tee, we can use exec as follows:

log_file="/tmp/log.txt"
exec >  >(tee -ia $log_file)
exec 2> >(tee -ia $log_file >&2)

Colored Output

NC='\033[0m'
BLACK='\033[0;30m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'

fatal()   { echo -e "${RED}ERROR: $1${NC}"; exit 1; }
error()   { echo -e "${RED}ERROR: $1${NC}"; }
warning() { echo -e "${YELLOW}$1${NC}"; }
info()    { echo -e "${GREEN}$1${NC}"; }
message() { echo -e "$1"; }
debug()   { echo -e "${CYAN}$1${NC}"; }

Debugging

Debugging Bash scripts from the Bash Guide for Beginners

run(): Run command with error handling and call stack

One of the recurring patterns with bash scripts is to run a command, check if the command succeeds, and exit immediately with an error message if the command failed. For example, let’s say I want to recursively copy src_dir to dest_dir:

mkdir $dest_dir
cp -R $src_dir $dest_dir

Of course, adding a bit of error handling:

mkdir $dest_dir
if [ $? -ne 0 ]; then
  echo "ERROR: Failed to create $dest_dir"
  exit 1
fi

cp -R $src_dir $dest_dir
if [ $? -ne 0 ]; then
  echo "ERROR: Failed to copy $src_dir to $dest_dir"
  exit 1
fi

This simple error handling may work if this code was part of a single script. If it was, however, one of bash scripts that were sourced or executed by another scripts possibly with multiple depths of caller functions, it is not straightforward to identify the exact spot of a failure based on such a simple error message.

In this situation, wouldn’t it be nice if we could print out a user-defined error message with a call stack that shows file names and line numbers? run() is a small helper function that does exactly this:

#
# How to use:
#
# $ run "<mssage>"\
#       "<command>"\
#       "<error mssage>"
#
# If DEBUG is defined as a non-empty value, <message> and <command>
# are printed out as well.
#
run() {
  local msg=$1      # message to print
  local cmd=$2      # command to run
  local err_msg=$3  # error message to print if cmdn fails

  echo -n "$msg ... "
  if [ -n "$DEBUG" ]; then
    echo "$cmd"
    eval "$cmd"
    result=$?
  else
    eval "$cmd" 2>&1 > /dev/null
    result=$?
  fi

  [ $result -eq 0 ] && { echo "OK"; return; }

  echo "$err_msg"
  echo "-- Failed command: $cmd"
  echo "-- Call stack:"

  count=0
  for src in ${BASH_SOURCE[*]}; do
    [ $count -eq 0 ] && line=$LINENO || line=${BASH_LINENO[count-1]}
    echo_yellow "[$count] $src:$line"
    ((count++))
  done
  [ -n "$BASE_ERROR_CODE" ] && exit $BASE_ERROR_CODE || exit 1
}

A few examples showing how to use it:

run "Executing a single command"\
    "mkdir $dest_dir"\
    "Failed to create dest directory: $dest_dir"

run "Executing multiple commands"\
    "mkdir $dest_dir && \
     cp -R $src_dir $dest_dir"\
    "Failed to run multiple commands"

run "Executing a single command and store result in the variable"\
    "files=$(find . -type f -name '*.png')"\
    "Failed to create dest directory: $dest_dir"
echo "${files[@]}"

Shortcuts

A short list of bash shortcuts that I frequently use:

Navigation Description
Ctrl+a Go to the beginning of the line
Ctrl+e Go to the end of the line
Ctrl+x+x Toggle between the current position and the beginning of the line
Alt+f Move the cursor forward by one word
Alt+b Move the cursor backward by one word
Shell Description
Ctrl+d Exit the current shell
Ctrl+l Clear the current shell screen. Same as clear command.
Editing Description
Ctrl+k Remove the line from the cursor to the end of the line
Ctrl+u Remove the line from the cursor to the beginning of the line

TIP: To use Alt in Mac, turn on Use Option as Meta Key:

Mac-shell-profile-setting

Others

Get path, directory, and name of the current script

SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname "${SCRIPT_PATH}")
SCRIPT_NAME=$(basename "${SCRIPT_PATH}")

If realpath is not available (e.g., on Mac):

SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"

Measure and print elapsed time in human readable format

It is sometimes necessary to measure time to execute a code block and print the elapsed time in a human readable format. The following code snippets could be useful for such a case:

convertsecs() {
  ((h=${1}/3600))
  ((m=(${1}%3600)/60))
  ((s=${1}%60))
  printf "%02d:%02d:%02d\n" $h $m $s
}

tic=$(date +'%s')  # '%s.%N' for nanoseconds if the system supports
#
# <do something here>
#
toc=$(date +'%s')
elapsed=$(($toc - $tic))

echo "Elapsed time: $(convertsecs $elapsed)"
SOFTWARE DEVELOPMENT LINUX BASH