Intermediate Shell Scripting in Linux

Intermediate Shell Scripting in Linux

Powerful way to perform repetitive tasks

Recap

Welcome to my second blog post on shell scripting! Before we dive into more advanced topics, I'd like to take a moment to revisit my first blog post on the basics of shell scripting. In that post, I covered essential concepts such as command-line arguments, variables, and control structures. If you haven't had a chance to read it yet, I highly recommend starting there before diving into this one.

To read my first blog post on shell scripting, click here.

Conditions

The general syntax for using conditions in a shell script is given below:

# if statements
if [ //some condition ]
then
    #If the condition is valid, do something
fi

#if - else statements
if [ //some condition ]
then
    #If the condition is valid, do something
else
    #If the condition fails, do something
fi

# Conditional statements with multiple branches using if-elif-else
if [ //some condition ]
then
    #If the condition is valid, do something
elif [ //some condition ]
then
    #If the condition is valid, do something
elif [ //some condition ]
then
    #If the condition is valid, do something
else
    #If all the condition fails, do something
fi #The 'fi' keyword is used to close the conditional block

Examples

  • Run the command: bash myscript.sh 5

      ans=$1      # Assign the first command line argument to a variable called ans
    
      if [ $ans = 5 ]  # Check if the value of ans is equal to 5
      then             # If the condition is true, execute the following command
          echo "User entered 5"
      else             # If the condition is false, execute the following command
          echo "User didn't enter 5"
      fi               # End the if statement
    
  • Run the command: bash myscript.sh 8

      ans=$1         # Assign the first command-line argument to a variable named "ans"
    
      if [ $ans = 5 ]   # Check if $ans is equal to 5
      then
          echo "User entered 5"    # If so, print this message to the console
      elif [ $ans = 6 ]   # If $ans is not equal to 5, check if it's equal to 6
      then
          echo "User entered 6"    # If so, print this message to the console
      else
          echo "User entered something else"    # If $ans is not equal to 5 or 6, print this message
      fi    # End of if-elif-else statement
    
  •     if [ "abc" = "abc" ]      # Check if "abc" is equal to "abc"
        then
            echo "abc is equal to abc"    # If so, print this message to the console
        else
            echo "abc is not equal to abc"    # If not, print this message to the console
        fi    # End of if-else statement
    
  •     if [ "bbc" != "abc" ]      # Check if "bbc" is not equal to "abc"
        then
            echo "bbc is not equal to abc"    # If so, print this message to the console
        else
            echo "bbc is equal to abc"    # If not, print this message to the console
        fi    # End of if-else statement
    
  •     if [ 5 -eq 4 ] # Check if 5 is equal to 4 then
        then
            echo "5 is equal to 4" # print 5 is equal to 4 to the console
        else
            echo "5 is not equal to 4" #otherwise, print 5 is not equal to 4 to the console
        fi # end the if-else statement
    
  •     # Check if 5 is not equal to 4
        if [ 5 -ne 4 ]
        then
            # If the condition is true, print "5 is not equal to 4"
            echo "5 is not equal to 4"
        else
            # If the condition is false, print "5 is equal to 4"
            echo "5 is equal to 4"
        fi
    
  •     # Check if 5 is greater than 4
        if [ 5 -gt 4 ]
        then
            # If true, print the following message to the console
            echo "5 is greater than 4"
        else
            # If false, print the following message to the console
            echo "5 is not greater than 4"
        fi
    
  •     # Check if 5 is less than 8
        if [ 5 -lt 8 ]
        then
            # If true, print the following message to the console
            echo "5 is less than 8"
        else
            # If false, print the following message to the console
            echo "5 is not less than 8"
        fi
    
  •     # Check if 5 is less than 3 and also less than 10. Both the conditions has to satisfy
        if [[ 5 -gt 3 && 5 -lt 10 ]]
        then
            # If true, print the following message to the console
            echo "5 is greater than 3 and less than 10"
        fi
    
        # Check if 5 is less than 3 or less than 10. If any one of them satisfies then whole condition satisfies
        if [[ 5 -gt 3 || 5 -lt 10 ]]
        then
            # If true, print the following message to the console
            echo "Either 5 is greater than 3 or less than 10"
        fi
    
  •     # This snippet is same as the above one. The use of brackets in the if condition is different here.
        if [ 5 -gt 3 ] && [ 5 -lt 10 ]
        then
            echo "5 is greater than 3 and less than 10"
        fi
    
        if [ 5 -gt 3 ] || [ 5 -lt 10 ]
        then
            echo "Either 5 is greater than 3 or less than 10"
        fi
    

For Loop

Loops are useful when running the same command multiple times with different argument values. Like, creating a bunch of ten files.

Use cases:

  • Execute a command or a set of commands many times.

  • Iterate through files.

  • Iterate through lines within a file.

  • Iterate through the output of a command.

Examples

  • Use case: Execute a command or a set of commands many times.

    Create 5 files: The variable 'file' will be replaced by the file name and the commands that will be executed are 'touch file1', 'touch file2' etc.

      for file in file1 file2 file3 file4 file5
      do
          touch $file
      done
    
  • Use case: Iterate through lines within a file.

    Suppose you have a file in the current working directory that contains these lines:

    Hello World

    From Visual Studio Code

    :)

    When you run this loop it will read each line of the file and will print it. In this program, each line of the file will be stored in a variable 'file' and will be printed.

      for file in $(cat myfile.txt)
      do
          echo $file
      done
    
  • A similar use case for installing a bunch of packages.

      for pkg in $(cat packages.txt)
      do
          sudo apt install $pkg -y
      done
    
  • A similar use case for printing numbers.

      for (( i = 0 ; i < 10 ; i++ ))
      do
          echo $i
      done
    
      for i in {0..9}
      do
          echo $i
      done
    
  • Use case:

    Iterate through the output of a command.

    Iterate through files.

    Print the line count of each file in the current working directory.

      for file in $(ls)
      do
          echo Line count of $file is $(cat $file | wc -l)
      done
    

While Loop

Use Cases:

  • Repetitively run a command or a group of commands until a certain condition is met or becomes true.

  • Create infinite loops.

  • Menu-driven programs.

Examples

  • Use case: Repetitively run a command or a group of commands until a certain condition is met or becomes true.

    Until and unless the user enters 5 the program will not exit.

      while [ true ]                        # start an infinite loop 
      do
          read -p "Enter 5 to exit: " num   # prompt user to enter a number
          if [ $num = 5 ]                   # check if the user entered 5
          then
              break                         # exit the loop if user entered 5
          fi
      done                                  # end of loop
    
  • Use Case: Create infinite loops.

    This program prints Hello :) infinite times. ctrl+c to exit.

      while [ true ]                # start an infinite loop
      do
          echo "Hello :)"           # print "Hello :)" to the console
      done                          # end of loop
    
  • Use Case: Menu-driven programs.

    A basic calculator that exits when the user enters 5.

      # input two numbers and choose between different arithmetic operations.
    
      while [ true ]
      do
          echo "=========="
          echo "Calculator"
          echo "=========="
          read -p "Enter first number: " n1
          read -p "Enter second number: " n2
          echo "Enter 1 for addition"
          echo "Enter 2 for subtraction"
          echo "Enter 3 for multiplication"
          echo "Enter 4 for division"
          echo "Enter 5 to exit"
          read -p "Enter choice: " ch
          if [ $ch -eq 1 ]
          then
              echo $((n1 + n2))    # addition operation
          elif [ $ch -eq 2 ]
          then
              echo $((n1 - n2))    # subtraction operation
          elif [ $ch -eq 3 ]
          then
              echo $((n1 * n2))    # multiplication operation
          elif [ $ch -eq 4 ]
          then
              echo $((n1 / n2))    # division operation
          else
              break               # exit the loop if user enters 5 or any other value
          fi
      done
    
  • Print numbers from 1 to 9

      # Set `i` to 1
      i=1
    
      # Continue running the loop until `i` becomes greater than or equal to 10
      while [ $i -lt 10 ]
      do
          # Echo the current value of `i`
          echo $i
    
          # Subtract 1 from `i` and update the `i` variable
          i=`expr $i - 1`
      done
    

Until Loop

Until Loop executes a set of instructions repeatedly based on some boolean expression. The set of instructions is executed only while the value of the boolean expression or condition is false. As soon as the result of the boolean expression evaluates to true the loop stops. Although similar to a while loop, an until loop executes the set of instructions as long as the condition remains false.

Examples

  • Print numbers from 1 to 10

      # Set `i` to 1
      i=1
    
      # Continue running the loop until `i` becomes greater than 10
      until [ $i -gt 10 ]
      do
          # Echo the current value of `i`
          echo $i
    
          # Add 1 to the current value of `i` and update the `i` variable
          i=`expr $i + 1`
      done
    

Switch Case

This helps to avoid multiple if - else statements.

A simple calculator program designed using a switch case.

# Infinite loop until user chooses to exit (option 5)
while [ true ]
do
    # Displaying the Calculator banner
    echo "=========="
    echo "Calculator"
    echo "=========="

    # Prompts user to enter two numbers
    read -p "Enter first number: " n1
    read -p "Enter second number: " n2

    # Displaying options for the operations
    echo "Enter 1 for addition"
    echo "Enter 2 for subtraction"
    echo "Enter 3 for multiplication"
    echo "Enter 4 for division"
    echo "Enter 5 to exit"

    # Prompts user to choose an option
    read -p "Enter choice: " ch

    # Perform an arithmetic operation based on the user's choice
    case $ch in
        1)  # Addition
            echo $((n1 + n2))
            ;;
        2)  # Subtraction
            echo $((n1 - n2))
            ;;
        3)  # Multiplication
            echo $((n1 * n2))
            ;;
        4)  # Division
            echo $((n1 / n2))
            ;;
        5)  # Exit
            break
            ;;
        *)  # Invalid input
            echo "Invalid choice"
            ;;
    esac
done

Shebang

A shebang statement, also known as a hashbang, is a special line of code that specifies the interpreter to be used when executing a shell script. It is typically included at the beginning of the script and starts with the characters '#!' followed by the path to the interpreter. This allows the shell to automatically select the correct interpreter to execute the script with, avoiding errors caused by executing the script with the wrong interpreter.

Example:

#!/bin/bash
for val in {0..10}
do
    echo $val
done

#!/bin/bash - This shebang statement includes '#!' followed by the interpreter on which the script will be executed. It will be executed in the Bourne-again shell or bash shell.

#!/bin/sh - This shebang statement will execute the script in the bourne shell.

There are many shells in Linux, but they do not work the same way or execute a particular instruction in the same way. Running the script below using the bash shell works perfectly fine and prints numbers from 0 to 10.

for val in {0..10}
do
    echo $val
done

Scripts are interpreted by the shell environment mentioned in the shebang statement, located at the beginning of the script. For instance, the #!/bin/bash shebang indicates that the script will run on the Bourne-again shell or bash shell. If the same script is executed using sh (Bourne shell) which does not support brace expansion, you may encounter errors or unexpected output. For example, if you use the {0..10} expression to generate a sequence of numbers, the Bourne shell will not expand it into a loop and produce the output {0..10} instead. To avoid such issues, always specify the appropriate shebang statement at the top of your scripts. This ensures that the script runs on the intended shell environment, irrespective of the environment you are executing it on.

Keep in mind that if you specify a shell explicitly while running a script, the shell mentioned within the script won't be used.

Run the command:bash myscript.sh

The script will always be executed through the bash shell irrespective of the shell mentioned inside the script.

Use cases

Although it is common to include the executable of a shell in a shebang statement, it is not mandatory. Instead of providing the executable for a shell, you can specify the executable of any other command, such as the 'cat' command. As long as the command is executable and installed on the system, running the script will execute the specified command and produce its output. However, it is necessary to specify an executable of some command.

#!/bin/cat
for val in {0..10}
do
    echo $val
done

IMPORTANT POINTS ON SHEBANG

- There is no space between # and !

- It is good practice not to use space between #! and /bin/bash.

- #! has to be on the first line otherwise, it will be treated as a comment. It must not even contain a blank line before it.

Exit Codes

  • In Linux, every command returns an exit code or a return code indicating whether the command executed successfully or encountered an error.

  • The exit code can be 0 or greater than 0 depending on whether the command ran successfully or not.

  • If the value is 0 the command ran successfully or greater than 0 if it failed.

  • The exit code of the last executed command is stored in the special built-in variable '$?' in Linux.

  • The value of the variable gets updated each time you run a command.

  • We can check the exit code right after running a command to verify the status of the command run.

  • Run the command: ls

    Check the status: echo $? -> 0 (As the previous command ran successfully.)

  • Run the command: mkdirr dir

    Check the status: echo $? -> Greater than 0 (As the previous command didn't ran successfully.)

  • Run the command: echo $? -> 0 (As the previous echo command ran successfully.)

Functions

Functions are generally a block of code that performs a specific task. They make the code much cleaner. Functions also promote the reusability of code. Shell scripting follows the same mechanism when it comes to functions. It's important to declare functions before calling them to ensure their availability.

A general syntax:

function func_name() {
    // do something
}

Examples

  • A simple program that calls a function 'add' and performs the addition of two numbers.

      #!/bin/bash
    
      function add() {
          a=5
          b=6
          echo $(($a + $b))
      }
    
      # Call the function
      add
    
  • This will prompt the user to enter two numbers, add them together, and then print the result to the console.

      function add () {
          read -p "Enter first number: " a
          read -p "Enter second number: " b
          echo $(($a+$b))
      }
      add
    
  • Here 3 and 5 are passed as arguments to the add function. 3 is stored in '$1' and 5 is stored in '$2'

      function add() {
        echo $(($1 + $2))
      }
    
      # Call the add function and pass in 3 and 5 as arguments
      add 3 5
    
  • The return keyword here is used to return a value from the functions. However, the returned value is stored in the '$?' variable which by default stores the exit codes.

      function add() {
        return $(($1 + $2))
      }
    
      # Call the add function and pass in 3 and 5 as arguments
      add 3 5
    
      # Check the return value of the add function
      result=$?
      echo "The result is: $result"
    
  • If you do not want to store the returned value in the '$?' variable then use 'echo' instead of return. This way you can directly assign the returned value in your custom variable.

      function add() {
        echo $(($1 + $2))
      }
    
      # Call the add function and capture its output in the `sum` variable
      sum=$(add 3 5)
    
      # Print the value of the `sum` variable
      echo $sum
    

Arrays

Arrays are used to store a collection of values. Using too many variables can be confusing sometimes. In many programming languages, arrays are typically used to store values of the same type, but Bash arrays can hold values of different types. Additionally, like most programming languages, Bash arrays are zero-indexed, which means that the first element has an index of 0, the second element has an index of 1, and so on.

Initialization

There are multiple ways to initialize values in an array.

a[0]=5
a[1]=6
a[2]=8
a[3]=5

b=(54 87 98 11 236 45)

c=([0]=5 [1]=6 [2]=356 [3]=100)

d=([0]=ubuntu [1]=fedora [2]=centos [3]=linux [4]=1000)

Accessing values

  • Access all elements

      echo ${a[@]} # Output - 5 6 8 5
      echo ${c[*]} # Output - 5 6 356 100
    
  • Access a particular element

      echo ${a[2]} # Output - 8
      echo ${c[3]} # Output - 100
      echo ${b[4]} # Output - 236
    
  • Access all elements from a particular index

      echo ${a[@]:3} # Output - 5
      echo ${b[@]:3} # Output - 11 236 45
      echo ${c[@]:0} # Output - 5 6 356 100
    
  • Accessing in a Range:

    echo ${ARRAYNAME[WHICH_ELEMENT/All_ELEMENTS]:STARTING_INDEX:COUNT_ELEMENT/COUNT_CHARACTERS}

      echo ${a[@]:1:1} # Output - 6 // Consider all elements in the array, start from index 1 and count 1 element.
    
      echo ${b[@]:3:2} # Output - 11 236 // Consider all elements in the array, start from index 3 and count 2 elements.
    
      echo ${c[@]:0:3} # Output - 5 6 356 // Consider all elements in the array, start from index 0 and count 3 elements.
    
      echo ${d[1]:1:2} # Output - ed // Consider element at index 1 in the array, start from index 1 of the element and print 2 characters.
    
      echo ${d[4]:1:2} # Output - 00 // Consider element at index 4 in the array, start from index 1 of the element and print 2 characters.
    
  • Length of the array. '*' can be used in place of '@'

      echo ${#a[@]} # Output - 4  
      echo ${#b[@]} # Output - 6
      echo ${#c[@]} # Output - 4
      echo ${#d[@]} # Output - 5
    
  • Length of an element at a particular index.

      echo ${#a[0]} # Output - 1 (Length OF 5 - 1 Character)
      echo ${#b[2]} # Output - 2 (Length OF 98 - 2 Characters)
      echo ${#c[3]} # Output - 3 (Length OF 100 - 3 Characters)
      echo ${#d[2]} # Output - 6 (Length OF 'centos' - 6 Characters)
    

Conclusion

In conclusion, shell scripting is a powerful tool in the Linux environment that allows you to automate. Through this article, I have covered a range of essential topics such as conditions, loops, switch cases, shebang, exit codes, functions, and arrays. By mastering these concepts, you can create complex shell scripts that can save you time and effort by automating many tasks.

Whether you are a system administrator or a developer, understanding shell scripting is a valuable skill that can improve your productivity and efficiency.

Thanks for reading the blog. Connect with me on Twitter.