The motivation here is to remember bash script best practices, and also to create user friendly scripts. There is a very nice gist that goes into detail many of the things I mention here. This article is also very helpful. There are 2 examples here:
General script magic Link to heading
So here is a strange script that covers most of the things that would be helpful:
#!/usr/bin/env bash
set -e # Exit immediately if some command has a non-zero exit code
set -u # Exit with error if you use variables that have not been defined
set -o pipefail # Prevent errors in pipeline from being masked
#set -euo pipefail # This is the commands above combined
#set -x # Uncomment for debugging
# Handy way of getting information about the script itself
SCRIPT_DIR="$(
cd "$(dirname "$0")" >/dev/null 2>&1
pwd -P
)"
SCRIPT_NAME=$(basename $0)
SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_NAME"
# Nice logging functions with colorized output
log-lines() { for a in "$@"; do printf "\n $a" 1>&2 ;done ;printf "\n" 1>&2 ;}
log-info() { printf "$(tput setaf 2)INFO$(tput sgr0): $1" 1>&2; log-lines "${@:2}"; }
log-warn() { printf "$(tput setaf 3)WARN$(tput sgr0): $1" 1>&2; log-lines "${@:2}"; }
log-error() { printf "$(tput setaf 1)ERROR$(tput sgr0): $1" 1>&2; log-lines "${@:2}"; exit 1; }
highlight() { d=""; for a in "$@"; do printf "$(tput bold)$(tput setaf 5)$d$a$(tput sgr0)"; d=" "; done; }
# Function to print out error message and usage examples
usage() {
cat 1>&2 <<EOF
Usage: $SCRIPT_NAME NAME AGE
Examples:
$ $SCRIPT_NAME Bob 10
EOF
(log-error "")
echo -n " " 1>&2
}
# Easy way for making arguments mandatory
name=${1:?"NAME <- missing parameter $(usage)"}
age=${2:?"AGE <- missing parameter $(usage)"}
# Example of how to use the logging functions
log-info "Running script with parameters:" "SCRIPT_DIR: $SCRIPT_DIR" "SCRIPT_NAME: $SCRIPT_NAME" "SCRIPT_PATH: $(highlight $SCRIPT_PATH)" "NAME: $name" "AGE: $age"
# How to run some cleanup when the script exits
function finish() {
log-warn "Exiting script"
}
trap finish EXIT
# Creation of arrays
addresses=(
"738 Ferrell Street"
"1237 Mapleview Drive"
"2070 Cambridge Court"
)
# Change internal field seperator to be less eager
IFS=$'\n\t'
for address in ${addresses[@]}; do
log-info "$address"
done
# Overriding argument to the script
set -- echo Override Arguments
# Execute arguments
$@
# Log error and exit
log-error "Example of failure in the script"
# Replace current shell with command. Has the effect that the finish function is never executed
exec "$@"
Now running the script with no parameters will give you an nice error message:
$ ./script.sh
Usage: script.sh NAME AGE
Examples:
$ script.sh Bob 10
ERROR:
./script.sh: line 40: 1: NAME <- missing parameter
And running it with the proper parameters gives you something like this:
$ ./script.sh Bob 10
INFO: Running script with parameters:
SCRIPT_DIR: /home/bob
SCRIPT_NAME: script.sh
SCRIPT_PATH: /home/bob/script.sh
NAME: Bob
AGE: 10
INFO: 738 Ferrell Street
INFO: 1237 Mapleview Drive
INFO: 2070 Cambridge Court
Override Arguments
ERROR: Example of failure in the script
WARN: Exiting script
Sophisticated argument parsing Link to heading
Sometimes you need more fine grained controll over your argument parsing. Then this is a nice approach:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_NAME=$(basename $0)
# Nice logging functions with colorized output
log-lines() { for a in "$@"; do printf "\n $a" 1>&2 ;done ;printf "\n" 1>&2 ;}
log-error() { printf "$(tput setaf 1)ERROR$(tput sgr0): $1" 1>&2; log-lines "${@:2}"; exit 1; }
usage() {
cat 1>&2 <<EOF
Usage: $SCRIPT_NAME [OPTIONS] FILE LINENR
Print out a specific line number in a file.
Options:
-i, --iter= Number of times to print line [default: 1]
-h, --help Print this dialog
Examples:
$ $SCRIPT_NAME /etc/hosts 1
$ $SCRIPT_NAME /etc/hosts 1 -i 12
EOF
if [ ! -z $1 ]; then
echo "" 1>&2
(log-error "")
echo -n " " 1>&2
fi
exit 1
}
# Set default iter to 1
ITER=1
# Iterate over arguments and parse them
while [ "$#" -gt 0 ]; do
case $1 in
-h | --help)
usage
;;
-i)
shift
ITER="$1"
;;
--iter=*)
ITER=$(echo $1 | awk '{split($0,r,"="); print r[2]}')
;;
*)
if [ "${FILE:-unset}" == "unset" ]; then FILE=$1
elif [ "${LINENR:-unset}" == "unset" ]; then LINENR=$1
fi
;;
esac
shift
done
# Make sure required parameters are set
FILE=${FILE:?"<- parameter missing $(usage FILE)"}
LINENR=${LINENR:?"<- parameter missing $(usage LINENR)"}
# Run iterations
for i in $(seq $ITER); do
sed -n ${LINENR}p $FILE
done
Now you have a very nice interface:
$ ./script.sh
Usage: script.sh [OPTIONS] FILE LINENR
Print out a specific line number in a file.
Options:
-i, --iter= Number of times to print line [default: 1]
-h, --help Print this dialog
Examples:
$ script.sh /etc/hosts 1
$ script.sh /etc/hosts 1 -i 12
ERROR:
./script.sh: line 58: FILE: <- parameter missing
And using correct parameters:
$ ./script.sh /etc/hosts 1 -i 4
# Host addresses
# Host addresses
# Host addresses
# Host addresses