This is a note on this blog regarding a basic template to create secure and functional bash scripts. The author is Maciej with github and linkedin][(https://www.linkedin.com/in/mradzikowski/).
As I have no previous background for bash script programming, this may serve as a friendly entry level introduction for others with zero experience.
Refer to bash documentation for reference.
Bash is a unix shell and command language replacing Bourne shell. It is the default login shell for nowt linux distributions. It is a command processor where the users type commands to trigger actions, or read and execute commands from a file, called a shell script.
Looks like bash script and shell script are largely the same thing: bash is one of the most popular shell.
It’s name is a pun of ‘Burne Again SHell’
To help us go through the template let’s create an example script to which we migrate the template.
cd ./files
cat input_file.txt > output_file.txt
echo "Copied from input file." >> output_file.txt
With this file and the input/output files created, try
chmod +x example.sh
./example.sh
#!/usr/bin/env bash
Often times there are four shells installed on a linux distro:
Here instead of directly specifying /bin/bash, we use the bash provided in the user environment configuration. You can check the content by running:
/usr/bin/env | grep shell
which should yield SHELL=/bin/bash or something alike. This conduct might provide better compatibility. It is explained here that this helps in the case where bash might not be in /bin (What about the path for env then, are we so sure about it? Seems more sure than bash ). Also, this allows you to adopt an alternative version of bash, or perhaps other shells if desired.
set -Eeuo pipefail
To check the meaning of the settings, try:
set --help | grep -A 3 -- '-E\|-e\|-u\|-o'
trap "{ echo 'Interrupted from user'; exit 0 }" SIGINT
Notice the space next to curly brackets must be preserved.
This trap statement displays a message upon Crtl+C and exit with zero state. Otherwise, the manual interrupt/manual kill will give some other exit state. Check the bash exit states here.
The -e options ensures that the script is stopped once an error is encountered, which helps avoid certain undesired mistakes. (For example, in our previous script, if the input file is not found, the script will stop instead of putting the “Copy” message line into the output file.)
The usage for other options are self-explanatory.
TODO: there are also arguments against such conduct of error handling.
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
This ensures that the working directory can be set to the path of the bash script regardless of where the script is executed from, e.g
/home/user/project/script.sh
Executing a bash script is different from source a bash script, in the sense that execution launches its own bash environment whereas source executes the script in the current environment.
. /home/user/project/script.sh
source /home/user/project/script.sh
BASH_SOURCE is a bash maintained variable, which is an array of source file path names. Each entry corresponds to the script path where the corresponding function in FUNCNAME variable is defined
FUNCNAME is a dynamic variable managed by bash as function stack tracing: An array variable containing the names of all shell functions currently in the execution call stack. The element with index 0 is the name of any currently-executing shell function. The bottom-most element (the one with the highest index) is “main”. This variable exists only when a shell function is executing. Assignments to FUNCNAME have no effect. If FUNCNAME is unset, it loses its special properties, even if it is subsequently reset.
“&> is a redirecting operator. As we know in linux everything is taken as a file, including the input, output and error message. They can be handled with descriptors 0, 1, and 2, corresponding to /dev/input, /dev/stdout and /dev/stderr. These files are then symbolic links to further process/device files. Often times we are interested in loading input from a file, or redirecting output and/or error message towards other files. &> redirects both stdout and stderr. In this case, I suppose they are redirected to /dev/null to avoid collision.”
pwd -P print physical address and avoids all symbolic links
trap cleanup SIGINT SIGTERM ERR EXIT
cleanup () {
trap - SIGINT SIGTERM ERR EXIT
}
Define a function to do some cleanup, e.g. remove temporary files. The trap statement ensures it can be automatically triggered before exit, but it can still be used anywhere desired.
trap - SIG restores the default trap behavior, whereas trap “” SIG ignores it. This is to avoid a loop between cleanup and trap
usage() {
cat <<EOF
Help script
EOF
exit
}
The << identifier … identifier is a here document. Here document is a way to define a string in a shell script or other programming languages. The syntax ‘<< identifier’ tells the leading command to read until it finds the identifier, and treat all contents as a string input. The format and indent of the here document is preserved so that multiple line display can be easily formatted.
EOF, end-of-file is not a char, but rather a condition in a operating system where no more contents can be read from the file. In this case where we use EOF as the identifier of here document, notice that it must be at the beginning of the line and must not have trailing chars in the same line. Alternatively, use -EOF as the identifier allows the final identifier to be indented by tab only.
TODO: understand case switch syntax and $1 describer to implement --help functionality
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
msg() {
echo >&2 -e "${1-}"
}
[[ … ]] is a bash syntax feature and is enhanced compare to [ … ]. It evaluates the expression quoted and returns a 0/1 result. Some advantages include better handling of spaces inside the brackets, and interpreting “&&” and “||” as logical operators.
Check bash conditional expression. The above expression evaluates to:
parse_params() {
# default values of variables set from params
flag=0
param=''
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) set -x ;;
--no-color) NO_COLOR=1 ;;
-f | --flag) flag=1 ;; # example flag
-p | --param) # example named parameter
param="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
# check required params and arguments
[[ -z "${param-}" ]] && die "Missing required parameter: param"
[[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"
return 0
}
The parser handles three types of CLI paramters: flags, named parameters and positional arguments:
Bash dependency management is tricky, so better just copy-paste it. You may now tune the template to your taste:
usage()
cleanup()
TODO
A Bigger Template
Command Line Interface Guidelines
12 Factor CLI Apps
Command line arguments anatomy explained with examples