June 8, 2025
Bash Shell Scripting: A Comprehensive Guide for Beginners

Bash Shell Scripting: A Comprehensive Guide for Beginners

The Bash shell, short for “Bourne Again SHell,” is a command-line interface widely used in Unix-based operating systems. Whether you’re a seasoned developer or a tech novice, understanding Bash can greatly enhance your productivity and streamline your workflow. In this article, we’ll explore the essentials of Bash, its features, and some practical tips to help you master this powerful tool.

What is Bash?

Bash is both a command interpreter and a scripting language. It allows users to interact with the operating system by executing commands, running scripts, and automating tasks. Originally developed by Brian Fox for the GNU Project in 1987, Bash has since become the default shell for most Linux distributions and macOS.

Key Features of Bash

  1. Command Line Interface (CLI)
    Bash provides a text-based interface for executing commands. This allows for quick navigation and management of files and processes. Learning the CLI can significantly speed up tasks that might take longer through a graphical user interface (GUI).
  2. Scripting Capabilities
    Bash scripting allows users to write sequences of commands in a text file, which can be executed as a single program. This is particularly useful for automating repetitive tasks, such as backups, system updates, or batch processing of files.
  3. Job Control
    Bash supports job control, enabling users to run multiple processes simultaneously. You can start a process in the background, suspend it, and bring it back to the foreground, making multitasking easier.
  4. Command History
    Bash maintains a history of commands executed during the session. Users can easily recall previous commands, which boosts efficiency. The history command displays a list of executed commands, while the arrow keys allow for quick navigation through past commands.
  5. Variables and Parameters
    Bash allows the use of variables to store data and parameters, making scripts more dynamic and reusable. You can create environment variables, which can be accessed globally throughout your session, or local variables, which are limited to the script’s scope.

Essential Bash Commands

Here are some fundamental Bash commands that every user should know:

  • ls: Lists files and directories in the current directory.
  • cd: Changes the current directory.
  • cp: Copies files or directories.
  • mv: Moves or renames files or directories.
  • rm: Removes files or directories (use with caution).
  • mkdir: Creates a new directory.
  • touch: Creates an empty file or updates the timestamp of an existing file.
  • echo: Displays a message or variable value to the terminal.
  • man: Displays the manual for a command, providing detailed information on its usage.

Bash Shell Scripting basic syntax

Bash shell scripting is a powerful way to automate tasks and manage system operations in Unix-based environments. In this article, we will delve into the essentials of Bash scripting, focusing on variables, input parameter parsing, and the various types of loops you can use to control the flow of your scripts.

Echo command

The echo command in Bash is used to display text or variables to the terminal.

#!/bin/bash

# Example 1: Simple text
echo "Hello, World!"

# Example 2: Displaying a variable
name="Alice"
echo "My name is $name."

# Example 3: Using escape sequences
echo -e "Line 1\nLine 2\nLine 3"  # -e enables interpretation of backslash escapes

# Example 4: Displaying the current date
echo "Today's date is: $(date)"

# Example 5: Redirecting output to a file
echo "This will be saved to a file." > output.txt

# taking input from user  and print it
echo -n "Enter some text > "
read text
echo "You entered: $text"

Variables in Bash

Variables in Bash are used to store data that can be referenced later in the script. Here’s how to work with variables:

Declaring and Accessing Variables

You can declare a variable simply by assigning a value to it, without spaces around the equals sign and To access the value of a variable, prefix it with a dollar sign $

my_var="Hello, World!"
echo $my_var

Read-Only Variables

In Bash, there are no built-in constants like you might find in some other programming languages (e.g., const in C or Java) . By convention, you can define variables in uppercase to indicate that they should be treated as constants. While this doesn’t prevent reassignment, it serves as a signal to other developers (and yourself) that these variables should not change. You can make a variable read-only using the readonly which prevents it from being modified after its initial assignment. Use the readonly command for this

#!/bin/bash

# Define a read-only variable
readonly CONSTANT_VALUE=42

# Attempt to change the constant (this will cause an error)
# CONSTANT_VALUE=100  # Uncommenting this line will result in an error

echo "The constant value is: $CONSTANT_VALUE"

Functions as Constants

You can also create functions that act like constants by always returning a fixed value.

#!/bin/bash

# Function to return a constant value
constant_value() {
    echo 42
}

echo "The constant value is: $(constant_value)"

Environment Variables

You can export a variable to make it available to child processes:

export my_env_var="I am an environment variable"

# check if variable is set or not
if [ -z "$my_env_var" ]; then
        echo "my_env_var is not set."
else
        echo "my_env_var is set to: $my_env_var"
fi

Arithmetic operation on variable

This is the most common method for arithmetic operations. You can use standard operators inside (( )).

Operators:

  • +: Addition
  • -: Subtraction
  • *: Multiplication
  • / : Division
  • % : Modulus (remainder)
  • ** : Exponentiation (power)

#!/bin/bash

a=10.5
b=2.5

result=$(echo "$a + $b" | bc)
echo "Addition: $result"

result=$(echo "$a - $b" | bc)
echo "Subtraction: $result"

result=$(echo "$a * $b" | bc)
echo "Multiplication: $result"

result=$(echo "$a / $b" | bc)
echo "Division: $result"

Incrementing /decrementing variable

count = 45

# incrementing variable
((count = count + 14))
((count += 14))
let count=count+14
let count+=14
count=$(expr $count + 14)

Input Parameters

You can pass parameters to your bash script which can be accessed using $<number> syntax

Here’s a simple script that echoes the parameters passed to it:

#!/bin/bash
echo "Script name: $0"
echo "First parameter: $1"
echo "Second parameter: $2"
echo "Total parameters: $#"

# print all the passed parameters
for i in "$@"; do
    echo $i
done

You can run this script as follows:

./script.sh Hello World

Detecting Command Line Arguments:

#!/bin/bash

if [ "$1" != "" ]; then
    echo "Positional parameter 1 contains something"
else
    echo "Positional parameter 1 is empty"
fi

#!/bin/bash

if [ $# -gt 0 ]; then
    echo "Your command line contains $# arguments"
else
    echo "Your command line contains no arguments"
fi

shift command

After each iteration of the loop, shift is used to move to the next command-line argument, effectively discarding the current one that has been processed.

#!/bin/bash

# Default values
operation=""
num1=0
num2=0

# Function to display usage information
usage() {
    echo "Usage: $0 -o | --operation <add|subtract|multiply|divide> -n1 <number1> -n2 <number2>"
    echo "Options:"
    echo "  -o, --operation   Specify the operation to perform (add, subtract, multiply, divide)"
    echo "  -n1               First number"
    echo "  -n2               Second number"
    echo "  -h, --help        Show this help message"
}

# Parse command-line arguments
while [ "$1" != "" ]; do
    case $1 in
        -o | --operation ) shift
                           operation=$1
                           ;;
        -n1 )              shift
                           num1=$1
                           ;;
        -n2 )              shift
                           num2=$1
                           ;;
        -h | --help )      usage
                           exit
                           ;;
        * )                usage
                           exit 1
    esac
    shift
done

# Check if required arguments are provided
if [ -z "$operation" ] || [ -z "$num1" ] || [ -z "$num2" ]; then
    usage
    exit 1
fi

# Perform the operation
case $operation in
    add )
        result=$((num1 + num2))
        echo "Result: $num1 + $num2 = $result"
        ;;
    subtract )
        result=$((num1 - num2))
        echo "Result: $num1 - $num2 = $result"
        ;;
    multiply )
        result=$((num1 * num2))
        echo "Result: $num1 * $num2 = $result"
        ;;
    divide )
        if [ "$num2" -eq 0 ]; then
            echo "Error: Division by zero!"
            exit 1
        fi
        result=$((num1 / num2))
        echo "Result: $num1 / $num2 = $result"
        ;;
    * )
        echo "Error: Invalid operation '$operation'"
        usage
        exit 1
        ;;
esac

To test this you can pass the following parameters:

./simple_calculator.sh -o add -n1 5 -n2 3
./simple_calculator.sh -o subtract -n1 5 -n2 3
./simple_calculator.sh -o multiply -n1 5 -n2 3
./simple_calculator.sh -o divide -n1 6 -n2 2
./simple_calculator.sh -h

Setting the default value to variable if no parameter is passed

#!/bin/bash

name=${1:-"User"}  # Default to "User" if no parameter is provided
echo "Hello, $name!"

Flow Control

Flow control statements allow you to control the execution flow of your scripts based on conditions. The primary flow control constructs in Bash are if, case, break, and continue.

If Statement

The if statement allows you to execute a block of code conditionally:

#!/bin/bash
number=10

if [ $number -gt 5 ]; then
  echo "$number is greater than 5"
elif [ $number -eq 5 ]; then
  echo "$number is equal to 5"
else
  echo "$number is less than 5"
fi

Case Statement

The case statement is a convenient way to handle multiple conditions:

#!/bin/bash
color="red"

case $color in
  red)
    echo "Color is red"
    ;;
  green)
    echo "Color is green"
    ;;
  blue)
    echo "Color is blue"
    ;;
  *)
    echo "Unknown color"
    ;;
esac

# case also suuport patterns 

character = 12
case $character in
                                # Check for letters
    [[:lower:]] | [[:upper:]] ) echo "You typed the letter $character"
                                ;;

                                # Check for digits
    [0-9] )                     echo "You typed the digit $character"
                                ;;

                                # Check for anything else
    * )                         echo "You did not type a letter or a digit"
esac

Loops

Loops allow you to execute a block of code multiple times. Bash supports several types of loops: for, while, and until.

For Loop

The for loop iterates over a list of items. Here’s an example that prints numbers 1 to 5:

for i in {1..5}; do
  echo "Number: $i"
done

You can also iterate over a list of strings:

for fruit in apple banana cherry; do
  echo "Fruit: $fruit"
done

While Loop

The while loop continues executing as long as a specified condition is true. Here’s a simple countdown example:

count=1
until [ $count -gt 5 ]; do
  echo "Number: $count"
  ((count++))  # Increment count
done

# infinite loop

while true; do
  : # Do nothing
done

operators supported by while loop :

  • lt : less than
  • le : less than or equal to
  • gt : greater than
  • ge : greater than or equal to
  • eq : equal to
  • ne : not equal to

alternative syntax :

You can also use (( )) for arithmetic comparisons, which is often cleaner syntax/style:

#!/bin/bash

count=1

while (( count < 5 )); do
    echo "Count is: $count"
    ((count++))  # Increment count
done

Nested Loops

You can also nest loops for more complex iterations. For instance, a loop within another loop can be used to create a multiplication table:

for i in {1..5}; do
  for j in {1..5}; do
    echo "$i * $j = $((i * j))"
  done
done

Break and Continue

  • Break: Use break to exit a loop prematurely
  • Continue: Use continue to skip the current iteration and proceed to the next one:
for i in {1..10}; do
  if [ $i -eq 5 ]; then
    break  # Exit loop when i is 5
  fi
  echo "Number: $i"
done

for i in {1..5}; do
  if [ $i -eq 3 ]; then
    continue  # Skip when i is 3
  fi
  echo "Number: $i"
done

Command Substitution

Command substitution in Bash allows you to capture the output of a command and use it as an argument in another command or assign it to a variable. This is typically done using backticks (`) or the more modern syntax $(…). The latter is preferred for readability and nesting.

# Using backticks 
my_var=`command`

# alternatively you can use $() systax [preffered way]
my_var=$(command)

# Examples 

right_now="$(date +"%x %r %Z")"

home_dir=$(echo "My home directory is: $(echo $HOME)")

file_count=$(ls | wc -l)
echo "There are $file_count files in the current directory."

echo "Number of logged-in users: $(who | wc -l)"

Exit status

Commands, including scripts and shell functions, return a value to the system upon termination, known as the exit status. This value, which is an integer between 0 and 255, signifies whether the command was executed successfully or encountered an error. By convention, an exit status of zero indicates success, while any other value indicates failure. The shell offers a parameter that allows us to check the exit status, as demonstrated below:

#!/bin/bash

# Run a command
mkdir my_directory

# Check the exit status of the command
if [ $? -eq 0 ]; then
    echo "Directory created successfully."
else
    echo "Failed to create directory."
fi

# Run another command
rm my_directory

# Check the exit status of the second command
if [ $? -eq 0 ]; then
    echo "Directory removed successfully."
else
    echo "Failed to remove directory."
fi

Test command

In Bash, the test command (or its shorthand [ ]) allows you to check various conditions using specific options

#!/bin/bash

# Define variables
file="sample.txt"

# Test if a file exists
if test -e "$file"; then
    echo "$file exists."
else
    echo "$file does not exist."
fi

alternative syntax

#!/bin/bash

if [ -f .bashrc ]; then
    echo "You have a .bash_rc. Things are fine."
else
    echo "Yikes! You have no .bash_rc!"
fi

Partial list of file operators supported by test command

  • -a FILE True if file exists.
  • -b FILE True if file is block special.
  • -c FILE True if file is character special.
  • -d FILE True if file is a directory.
  • -e FILE True if file exists.
  • -f FILE True if file exists and is a regular file.
  • -h FILE True if file is a symbolic link.
  • -L FILE True if file is a symbolic link.
  • -k FILE True if file has its `sticky’ bit set.
  • -r FILE True if file is readable by you.
  • -s FILE True if file exists and is not empty.
  • -w FILE True if the file is writable by you.
  • -x FILE True if the file is executable by you.
  • -N FILE True if the file has been modified since it was last read.
  • FILE1 -nt FILE2 True if file1 is newer than file2 (according to (modification date).
  • FILE1 -ot FILE2 True if file1 is older than file2.
  • FILE1 -ef FILE2 True if file1 is a hard link to file2.

#!/bin/bash

# Create some test files for demonstration
touch file1.txt         # Regular file
touch file2.txt         # Another regular file
echo "Hello, World!" > file3.txt  # Non-empty file
touch -d "1 day ago" file4.txt     # Older file
ln file1.txt file5.txt  # Create a hard link

mkdir test_directory      # Create a directory
mkdir empty_directory      # Create an empty directory


# Check if file1.txt exists
if [ -e "file1.txt" ]; then
    echo "file1.txt exists."
else
    echo "file1.txt does not exist."
fi

# Check if file2.txt is a regular file
if [ -f "file2.txt" ]; then
    echo "file2.txt is a regular file."
fi

# Check if file3.txt is not empty
if [ -s "file3.txt" ]; then
    echo "file3.txt is not empty."
fi

# Check if file4.txt is older than file1.txt
if [ "file4.txt" -ot "file1.txt" ]; then
    echo "file4.txt is older than file1.txt."
fi

# Check if file1.txt is newer than file4.txt
if [ "file1.txt" -nt "file4.txt" ]; then
    echo "file1.txt is newer than file4.txt."
fi

# Check if file1.txt is a symbolic link
if [ -h "file1.txt" ]; then
    echo "file1.txt is a symbolic link."
else
    echo "file1.txt is not a symbolic link."
fi

# Check if file5.txt is a hard link to file1.txt
if [ "file5.txt" -ef "file1.txt" ]; then
    echo "file5.txt is a hard link to file1.txt."
fi

# Check if test_directory is a directory
if [ -d "test_directory" ]; then
    echo "test_directory is a directory."
else
    echo "test_directory is not a directory."
fi

# Check if empty_directory is a directory
if [ -d "empty_directory" ]; then
    echo "empty_directory is a directory."
fi

# Check if file1.txt is a directory
if [ -d "file1.txt" ]; then
    echo "file1.txt is a directory."
else
    echo "file1.txt is not a directory."
fi

Partial list of string operators supported by test command

  • -z STRING ,True if string is empty
  • -n STRING , STRING True if string is not empty.
  • STRING1 = STRING2 , True if the strings are equal
  • STRING1 != STRING2 , True if the strings are not equal.

#!/bin/bash

# Sample strings
string1=""
string2="Hello"
string3="Hello"

# Check if string1 is empty
if [ -z "$string1" ]; then
    echo "string1 is empty."
else
    echo "string1 is not empty."
fi

# Check if string2 is not empty
if [ -n "$string2" ]; then
    echo "string2 is not empty."
else
    echo "string2 is empty."
fi

# Check if string2 and string3 are equal
if [ "$string2" = "$string3" ]; then
    echo "string2 and string3 are equal."
else
    echo "string2 and string3 are not equal."
fi

# Check if string2 and string1 are not equal
if [ "$string2" != "$string1" ]; then
    echo "string2 and string1 are not equal."
else
    echo "string2 and string1 are equal."
fi

Other operators:

  • -v VAR True if the shell variable VAR is set.
  • EXPR1 -a EXPR2 True if both expr1 AND expr2 are true.
  • EXPR1 -o EXPR2 True if either expr1 OR expr2 is true.
  • eq, -ne,-lt, -le, -gt, -ge

#!/bin/bash

# Declare some variables
var1="Hello"
var2=""
var3="World"

# Check if var1 is set
if [ -v var1 ]; then
    echo "var1 is set."
else
    echo "var1 is not set."
fi

# Check if var2 is set
if [ -v var2 ]; then
    echo "var2 is set."
else
    echo "var2 is not set."
fi

# Example using -a (AND)
if [ -v var1 -a -n "$var3" ]; then
    echo "Both var1 is set and var3 is not empty."
else
    echo "Either var1 is not set or var3 is empty."
fi

# Example using -o (OR)
if [ -v var2 -o -v var3 ]; then
    echo "Either var2 is set or var3 is set."
else
    echo "Neither var2 nor var3 is set."
fi

Taking input from the user

#!/bin/bash

# using if else statement 
 
read -p "Enter a number> " character
if [ "$character" = "1" ]; then
    echo "You entered one."
elif [ "$character" = "2" ]; then
    echo "You entered two."
elif [ "$character" = "3" ]; then
    echo "You entered three."
else
    echo "You did not enter a number between 1 and 3."
fi

# using case statement 
read -p "Enter a number> " character

case $character in
    1 ) echo "You entered one."
        ;;
    2 ) echo "You entered two."
        ;;
    3 ) echo "You entered three."
        ;;
    * ) echo "You did not enter a number between 1 and 3."
esac


# with timeout 


echo -n "Type something within 3 seconds > "
if read -t 3 response; then
    echo "Great"
else
    echo "you are too slow!"
fi


# withtimeout && hidden user input 

echo -n "Type something within 3 seconds > "
if read -ts 3 response; then
    echo "Great"
else
    echo "you are too slow!"
fi

explanation

  • read: This command is used to read a line of input from the user.
  • -p: This option allows you to specify a prompt that will be displayed to the user. In this case, the prompt is “Enter a number between 1 and 3 inclusive > “.
  • character: This is the variable name where the input will be stored. After the user enters a number and hits Enter, the value they entered will be assigned to this variable.
  • -t: creates a timeout
  • -s: hides the user input

Heredoc in bash

The cat <<- EOF construct in Bash is used to create a here document (often abbreviated as heredoc). This allows you to define a block of text that can be treated as standard input to a command, in this case, cat

cat <<EOF syntax

Syntax Breakdown :

  • cat: This command concatenates and displays the content passed to it. In this context, it’s used to output the heredoc content.
  • <<-: The << operator tells Bash to start a here document. The – allows for leading tabs in the text, which can help with indentation.
  • EOF: This is a delimiter that indicates the start and end of the here document. You can use any string as the delimiter, but it’s common to use EOF, EOF, or similar.

# The use of <<- allows for indentation with tabs. If you used just <<, you would need to match the indentation exactly when closing the heredoc.
cat <<- EOF
This is a heredoc example.
You can include variables like USER: $USER
And it will print their values.
EOF


# Assign multi-line string to a shell variable
sql=$(cat <<EOF
SELECT foo, bar FROM db
WHERE foo='baz'
EOF
)

# Pass multi-line string to a file in Bash

cat <<EOF > print.sh
#!/bin/bash
echo \$PWD
echo $PWD
EOF

# Pass multi-line string to a pipe in Bash

cat <<EOF | grep 'b' | tee b.txt
foo
bar
baz
EOF

String concatenation

Using += Operator :

# Initial variable
my_var="Hello"

# Append a string
my_var+=" World!"

# Display the result
echo "$my_var"  # Output: Hello World!

Using printf :

# Initial variable
my_var="Hello"

# Append a string using printf
printf -v my_var "%s World!" "$my_var"

# Display the result
echo "$my_var"  # Output: Hello World!

Using echo and Command Substitution :

# Initial variable
my_var="Hello"

# Append a string
my_var="$(echo "$my_var World!")"

# Display the result
echo "$my_var"  # Output: Hello World!

Trap command

The trap command allows us to execute a command when our script receives a signal. It works like this:

trap arg signals

#!/bin/bash

trap " echo 'i am going'; exit" SIGHUP SIGINT SIGTERM
read -p "Print file? [y/n]: "


# other examples
# it is most used for clean up functions like this

#!/bin/bash
clean_up() {

  # Perform program exit housekeeping
  exit
}

trap clean_up SIGHUP SIGINT SIGTERM

read -p "Print file? [y/n]: "
# doing something
clean_up

Functions

Bash also have support for functions .

#!/bin/bash


#Exmaple 1 with single parametre

greet() {
    echo "Hello, $1!"
}
greet "Alice"
greet "Bob"

#Exmaple 2 with 2 input parameter

add() {
    local sum=$(( $1 + $2 ))
    echo "$sum"  # Print the sum (can be captured)
}

result=$(add 5 3)
echo "The sum is: $result"

# Example 3 with defaut parameter 
greet() {
    local name=${1:-"World"}  # Default to "World" if no argument
    echo "Hello, $name!"
}
greet "Alice"
greet  # Will use the default

# Example 4 with return statement

is_even() {
    if [ $(( $1 % 2 )) -eq 0 ]; then
        return 0  # Success (true)
    else
        return 1  # Failure (false)
    fi
}

if is_even 4; then
    echo "4 is even."
else
    echo "4 is odd."
fi

if is_even 5; then
    echo "5 is even."
else
    echo "5 is odd."
fi

# Example 5 with Array arguments

print_array() {
    local arr=("$@")  # Capture all arguments as an array
    for element in "${arr[@]}"; do
        echo "$element"
    done
}

# Function call with array
my_array=("apple" "banana" "cherry")
print_array "${my_array[@]}"

Conclusion

Bash shell scripting is a powerful skill that allows you to automate tasks and streamline workflows. By mastering variables, input parameter parsing, and loops, you can create flexible and efficient scripts. Whether you’re managing system processes or automating repetitive tasks, these scripting fundamentals will serve you well in your tech journey.

So, start experimenting with your own scripts, and take your Bash skills to the next level! Happy scripting!

Leave a Reply

Your email address will not be published. Required fields are marked *