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

In other words,

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 in sh.

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 by case 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