Bash Scripting Idiosyncrasies
Shell scripting in bash
isn't walk-in-the-park, even for those experienced in C, Java or Python. It's largely due to the quirks we need to accommodate for Bash's "simple" approach to interpreting the context.
Assignments (=
) mustn't have space
The space in assigments are treated as "metacharacter", which separates commands from its arguments (as in a terminal console).
status = on
echo "status: $status"
## output ##
on: command not found
status=on
echo "status: $status"
## output ##
status: on
NUM2= 4 # runs "4" as a command, with the environment variable NUM2 set to an empty string
NUM2 =4 # runs "NUM2" as a command, with "=4" as its argument
NUM2 = 4 # runs "NUM2" as a command, with "=" as its first argument, and "4" as another
All variables are global
The default lifetime of variables is "global", meaning all variables are accessible throughout the script.
example() {
age=10
echo "Age within the function: $age"
}
example
echo "Age outside the function: $age"
## output ##
Age within the function: 10
Age outside the function: 10
To restrict the scope of variables to the function, we need to use "local" like so: local age=10
Spaces around "[" in conditionals
The [
is an operator (which [
), and everything that follows are its arguments. It's also a shell builtin (help [
), that requires ]
as its last argument. It's another way spelling test
command.
In the following snippet for instance, the "[$age" will make the shell treat it as a command and search it in $PATH.
age=10
if [ "$age" -lt 18 ]; then
echo "$age is not eligible to vote"
fi
## output ##
10 is not eligible to vote
if ["$age" -lt 18]; then
...
## output ##
[10: command not found
[ condition ]
is the traditional form, and can't handle strings that are empty, or begin with a hiphen.[[ condition ]]
is bash extension and overcomes many of the shortcomings of the older operator. Caveat being it's not POSIX, i.e., not availalbe insh
.
String Vs. Numeric comparison
Strings are compared using ==
and !=
, and numbers using -eq
, -ne
, -lt
, -gt
. The !
returns true if the expression is false.
age=21
name="Rahul"
if [[ "$name" == "Rahul" && "$age" -gt 18 ]]; then
echo "$name being $age, is eligible to vote"
fi
## output ##
Rahul being 21, is eligible to vote
Program continues even after errors
a) Unbound/undefined variables
Bash treats every undefined variables as if its value is empty string. If we mistype a variable, the statements following it still works, and introduce subtle bugs.
name="Rahul"
echo "$naem is the current voter" #<< incorrect variable name
echo "Current time: $(date)"
## output ##
is the current voter
Current time: Sun Dec 24 06:09:09 AM UTC 2023
set -u
need to be added to disable this behavior, and ask Bash to fail upon encountering undefined variables.
set -u
name="Rahul"
echo "$naem the current voter"
echo "Current time: $(date)"
## output ##
naem: unbound variable
set -e
bails out should any of the lines returns non-zero exit code (i.e., fails).
b) Errors in pipeline
set -e
doesn’t account for the errors that can happen in the middle of a pipeline.
set -e
echo "Hello!"
ls /some/nonexistent/directory | cat -
echo "Date: $(date)"
## output ##
Hello!
ls: cannot access '/some/nonexistent/directory': No such file or directory
Date: Sun Dec 24 06:25:52 AM UTC 2023
The set -o pipefail
needs to be used like so:
set -o pipefail
echo "Hello!"
ls /some/nonexistent/directory | cat -
echo "Date: $(date)"
## output ##
Hello!
ls: cannot access '/some/nonexistent/directory': No such file or directory
set -ueo pipefail
is commonly seen at the start of a bash script.
";" Vs. ";;"
;
as meta-character separates words, and is equivalent to newline.;
as control-character terminates the commands started by-exe
.;;
is always a control-character. And is required bycase
as the last command in each pattern block.
echo -e "\nCurrent Date:";date;
## output ##
Current Date:
Sun Dec 24 06:42:15 AM UTC 2023
wheels=2
case $wheels in
2)
echo "Scooter"
;;
4)
echo "Car"
;;
*)
echo "Invalid"
;;
esac
## output ##
Scooter