Bash Tips and Code Examples
Min Yang Jung | 18 Jan 2021For 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
:
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)"