# Lab 5: Bash scripting

## Exercise 1 - Bash basics

### Task 1 - Bash scripting basics

- Use the `echo` command to display a line of text. echo has three important options `-n`, `-e`, and `-E`. Read the echo manual.
  Create a shell script `lab_5.sh` and add the following to see the output when you use the various echo options.

      #!/bin/bash
      echo -E "Printing text with newline. This is the default option."
      echo -n "What happens when we print text without new line"
      echo -e "\nEscaping \t characters \t to print\nnew lines for example"

  Execute the script.

      $ bash lab_5.sh

- Single line comments begin with `#`. Append the following lines to `lab_5.sh`.

      # Adding comments that do nothing
      echo "Testing single line comments"

  Execute the script to see what happens.
- You can use multi line comments. The format is to start with a colon `:` followed by the comments enclosed in single quotes `'`. For example, append the following to `lab_5.sh`.

      : '
      This is a multi line comment
      Nothing happens in this section
      '
      echo "Back to executable commands"

  Execute the script to see what happens.
- Double brackets are used to do arithmetic tasks. For example, append the following to `lab_5.sh`

      # Add two numeric values
      ((sum = 12 + 24))

      # Print the sum
      echo $sum

- Get user input with the `read` command. This is normally used in combination with echo to print a prompt to the user. Append the following to `lab_5.sh`.

      echo "What is your favorite fruit?"
      read fruit
      echo "Hey! I like $fruit too."

### Task 2 - Bash loops and conditions

- Create a simple while loop. Save the following script as `while_loop.sh`.

      #!/bin/bash

      counter=1
      while [ $counter -le 10 ]
      do
        echo $counter
        ((counter++))
      done
      echo All done

  > `-le` is the same as `<=`.

- Create an until loop. The until loop will execute the commands within it until the condition becomes true. Save the following script as `until_loop.sh`.

      #!/bin/bash

      counter=1
      until [ $counter -gt 10 ]
      do
        echo $counter
        ((counter++))
      done
      echo All done

  > `-gt` is the same as `>`.

- Create a simple for loop that iterates through the item in a list and prints each of them. Save the following script as `for_loop.sh`.

      #!/bin/bash

      names='Liliya Peter Emili'
      for name in $names
      do
        echo $name
      done
      echo All done

- Bash `for` loop can be written in “C-Style”. For example save the following script as `uptime.sh`.

      #!/bin/bash

      # Loop of ten iterations to print system uptime every 2 seconds.
      for ((i = 1 ; i <= 10 ; i++)); do

        echo -e "$i.\t"$(uptime)
        sleep 2
      done

  Execute the script and analyze the output.

- The break statement tells bash to exit the loop. An example is shown in `break.sh` below.

      #!/bin/bash

      counter=1
      while [ $counter -le 10 ]
      do
        if [ $counter -eq 5 ]
        then
          echo Script encountered the value $counter
          break
        fi
        echo $counter
        ((counter++))
      done
      echo All done

  > `-eq` is the same as `==`.

- The continue statement skips the cuttent iteration of the loop. Save the following as `continue.sh` and run it.

      #!/bin/bash

      counter=0
      while [ $counter -lt 10 ]
      do
        ((counter++))
        if [ $counter -eq 6 ]
        then
          echo Number 6 encountered, skipping to the next iteration
          continue
        fi
        echo $counter
      done
      echo All done

  > `-lt` is the same as `<`.

### Task 3 - If statements

- If statements allow us to make decisions in our script. This is usually utilized with a comparison operator. Save the following as `if_statement.sh`.

      #!/bin/bash

      echo -n "Enter the number: "
      read number
      if [ $number -gt 100 ]
      then
        echo That\'s a large number.
      elif [ $number -gt 50 ]
      then
        echo Not so much.
      else
        echo The number is way too small.
      fi

### Task 4 - Bash functions

- Create a simple function

      $ fun () { echo "This is a function"; echo; }

- To view the content of all functions defined in the shell

      $ declare -f

- To list all functions by its name

      $ declare -F

- To view the content of the one your function

      $ declare -f fun

- To remove the function

      $ unset fun

- Now lets create a script with a 2 functions inside it

      $ vim funky.sh

- Inside vim type the following

      #!/bin/bash

      JUST_A_SECOND=3
      funky ()
      { # This is about as simple as functions get.

          echo "This is a funky function."
          echo "Now exiting funky function."
      } # Function declaration must precede call

      fun ()
      { # A somewhat more complex function.
          i=0
          REPEATS=30
          echo
          echo "And now the fun really begins."
          echo
          sleep $JUST_A_SECOND
          while [ $i -lt $REPEATS ] #use as (<,>,=) or (-lt, -gt, -eq)
              do
              echo   "----------FUNCTIONS---------->"
              echo   "<------------ARE-------------"
              echo   "<------------FUN------------>"
              echo
              let "i+=1"
              done
      }

      add_fun()
      {
      # A function just to add numbers
      echo $((2+2))
      }
      #Now, call the functions
      funky
      fun
      echo The return value of add_fun is: $(add_fun)
      echo exit $? #check your exit status of the last function/command: if 0-success, otherwise is not

- Run the script

      $ bash funky.sh

- Notice how the functions are only available within the one shell (once you exit it is gone for the parent shell)

      $ declare -F

## Exercise 2 - Working with files and directories

### Task 1 - File and directory test operators

There are several options in bash to check the type of file you are interacting with. In many cases, the options are also used to check for the existence of a specified file or directory. The example below shows the options that can be used.

- Create a script `file_checker.sh` and add the following code:

      #!/bin/bash

      if [[ $# -le 0 ]]
      then
        echo "You did not pass any files as arguments to the script."
        echo "Usage:" "$0" "my-file-1 my-file-2"
        exit
      fi

      for arg in "$@"
      do
        # Does it actually exist?
        if [[ ! -e "$arg" ]]
        then
            echo "* Skipping ${arg}"
            continue
        fi

        # Is it a regular file?
        if [ -f "$arg" ]
        then
            echo "* $arg is a regular file!"
        else
            echo "* $arg is not a regular file!"
        fi

        [ -b "$arg" ] && echo "* $arg is a block device."
        [ -d "$arg" ] && echo "* $arg is a directory."
        [ ! -d "$arg" ] && echo "* $arg is not a directory."

        [ -x "$arg" ] && echo "* $arg is executable."
        [ ! -x "$arg" ] && echo "* $arg is not executable."

        [[ -h "$arg" ]] && echo "* $arg is a symbolic link."
        [ ! -h "$arg" ] && echo "* $arg is not a symbolic link."

        [[ -s "$arg" ]] && echo "* $arg has nonzero size."
        [ ! -s "$arg" ] && echo "* $arg has zero size."

        [[ -r "$arg" && -d "$arg" ]] && echo "* $arg is a readable directory."
        [[ -r "$arg" && -f "$arg" ]] && echo "* $arg is a readable regular file."
      done

- Run the script and specify the files you want to check as arguments.

      # bash file_checker.sh /bin/i386 /etc/passwd

  You should get output similar to the following:

      * /bin/i386 is a regular file!
      * /bin/i386 is not a directory.
      * /bin/i386 is executable.
      * /bin/i386 is a symbolic link.
      * /bin/i386 has nonzero size.
      * /bin/i386 is a readable regular file.
      * /etc/passwd is a regular file!
      * /etc/passwd is not a directory.
      * /etc/passwd is not executable.
      * /etc/passwd is not a symbolic link.
      * /etc/passwd has nonzero size.
      * /etc/passwd is a readable regular file.

### Task 2 - Directory and file manipulation

- You can create a new directory in bash with the `mkdir` command. Save the following script as `mkdir_bash.sh`.

      #!/bin/bash
      echo "Enter directory name"
      read newdir
      `mkdir $newdir`

- You can check for the existence of a directory before proceeding to create it. Update `mkdir_bash.sh` to look like the script shown below.

      #!/bin/bash
      echo "Enter directory name"
      read newdir
      if [ -d "$newdir" ]
      then
        echo "Directory exist"
      else
        `mkdir $newdir`
        echo "Directory created"
      fi

- Create a bash script to read every line of a specified file. The file name is passed as a command line argument. Save the following script as `file_reader.sh`.

      #!/bin/bash

      file=$1

      if [[ "$file" == "" || (! -f "$file") ]]
      then
        echo Using standard input!
        file="/dev/stdin"
      fi

      while read -r line
      do
        echo "$line"
      done < "${file}"

  > The script reads the first value passed as a command line argument, represented by $1. If a text file is passed, the script will read and output each line of text.
  > If no command line argument is passed or if the file does not exist, standard input (/dev/stdin) is used instead. This will prompt you to enter text and will output to the terminal screen what is received as input. To signal the end of your stdin input type CTRL+D

  Use this script to read `/etc/passwd`.

      $ bash file_reader.sh /etc/passwd

- The Internal Field Separator (IFS) is used to recognize word boundaries. The default value for IFS consists of whitespace characters. Whitespace characters are space, tab and newline. Add the following script to the file `ifs_test.sh`.

      #!/bin/bash

      mystring="foo:bar baz rab"
      for word in $mystring; do
        echo "Word: $word"
      done

  Run the script.

- The default value for the IFS can be changed. Modify `ifs_test.sh` to contain the following.

      #!/bin/bash

      IFS=:
      mystring="foo:bar baz rab"
      for word in $mystring; do
        echo "Word: $word"
      done

- Let’s do this on a larger scale. Read /etc/passwd word by word while using `:` as the IFS. Save the following as `ifs_word.sh`.

      #!/bin/bash

      if [[ $# -le 0 ]]
      then
        echo "You did not pass any files as arguments to the script."
        echo "Usage:" "$0" "my-file"
        exit
      fi

      IFS=:
      file=$1

      if [ ! -f "$file" ]
      then
        echo "File does not exist!"
      fi

      for word in $(cat "${file}")
      do
        echo "$word"
      done

  Run the script specifying `/etc/passwd` as an argument.

      $ bash ifs_word.sh /etc/passwd

### Task 3 - Jump directories

Sometimes it is difficult to navigate directories with the possibly infinite number of parent directories we need to provide. For example `cd ../../../../../`. Let’s create a script that will help us jump to a specified directory without executing `cd ../`.

- Create the script `jump_dir.sh`.

      # !/bin/bash

      # A simple bash script to move up to desired directory level directly

      function jump()
      {
        # original value of Internal Field Separator
        OLDIFS=$IFS

        # setting field separator to "/"
        IFS=/

        # converting working path into array of directories in path
        # eg. /my/path/is/like/this
        # into [, my, path, is, like, this]
        path_arr=($PWD)

        # setting IFS to original value
        IFS=$OLDIFS

        local pos=-1

        # ${path_arr[@]} gives all the values in path_arr
        for dir in "${path_arr[@]}"
        do
          # find the number of directories to move up to
          # reach at target directory
          pos=$[$pos+1]
          if [ "$1" = "$dir" ];then

        	# length of the path_arr
        	dir_in_path=${#path_arr[@]}

        	#current working directory
        	cwd=$PWD
        	limit=$[$dir_in_path-$pos-1]
        	for ((i=0; i<limit; i++))
        	do
        	  cwd=$cwd/..
        	done
        	cd $cwd
            break
          fi
        done
      }

- Make the script executable

      $ chmod +x jump_dir.sh

- Add it to the .bashrc file to make it available on every terminal session.

      echo "source ~/jump_dir.sh">> ~/.bashrc

- Open a new terminal and try jumping.

      $ jump directory_name

## Exercise 3 - Hash tables and more bash usage

### Task 1 - Hash tables in bash

A dictionary, or a hashmap, or an associative array is a data structure used to store a collection of things. A dictionary consists of a collection of key-value pairs. Each key is mapped to its associated value.

- To declare a dictionary variable in bash, use the `declare` statement with the `-A` option which means associative array. For example:

      $ declare -A newDictionary

- We have declared a variable called `newDictionary`. The following syntax can be used to add key-value pairs to the dictionary:

      $ newDictionary[key]=value

- Let’s add a key-value pair to the dictionary using the syntax above:

      $ newDictionary[1]=SNA
      $ newDictionary[2]=IML

- To retrive the value of a dictionary key, we have to add `$` with braces `{}` to the dictionary variable we defined.

      $ echo ${newDictionary[1]}

- To update the value of a key, we simply overwrite the existing value by writing to the key again. For example, to update `newDictionary[1]`, we do the following:

      $ newDictionary[1]=NEWSNA

  Retrieve the value of they key `[1]` to verify the update.

      $ echo ${newDictionary[1]}
      NEWSNA

- Use the `unset` command to remove a key-value pair from the dictionary.

      $ unset newDictionary[1]

  Verify that the pair has been removed from the dictionary.

      $ echo ${newDictionary[1]}

- You can iterate through the dictionary by creating a `for` loop. An example is shown in the script below:

      #!/bin/bash
      declare -A newDictionary
      newDictionary[1]=SNA
      newDictionary[2]=IML
      newDictionary[3]=SSN
      for key in "${!newDictionary[@]}"; do
        echo "$key ${newDictionary[$key]}"
      done

  You should get an output similar to the following when you run the script:

      3 SSN
      2 IML
      1 SNA

- You can also declare and instantiate a dictionary in one line.

      $ declare -A innoCourses=([1]=SNA [2]=IML [3]=SSN)

  Check the content of the dictionary.

      echo ${innoCourses[3]}

### Task 2 - Use sed in bash

- Use sed to strip all HTML tags in a HTML page. Save the following code as `sed_bash.sh`.

      #!/bin/bash

      strip-tags ()
      {
        sed -e 's/<[^>]*.//g' -
      }
      curl https://example.com/ --silent | strip-tags

  Run the bash script and analyse the output. There is no HTML tag in the results.

### Task 3 - Execute Python commands in bash

- Python can run one-liners from an operating system command line using option `-c`. An example is shown with the command below:

      $ python3 -c "a = 2; b = 4; sum = a + b; print('Sum =', sum)"
      or
      $ python -c "a = 2; b = 4; sum = a + b; print('Sum =', sum)"

  The following bash script executes the python command above.

      #!/bin/bash

      python3 -c "a = 2; b = 4; sum = a + b; print('Sum =', sum)"

- Alternatively, you scan split your python code into multi-lines by declaring a variable in the following fashion:

      #!/bin/bash

      py_script="
      a = 2
      b = 4
      sum = a + b
      print('Sum =', sum)
      "

      python3 -c "$py_script"

## Exercise 4 - Debugging bash scripts

### Task 1 - Command exit code

You can verify whether a bash command executed successfully by viewing the exit status code of the command. The exit status of the previously executed command is stored in the `$?` variable. A successful command returns a `0`, while an unsuccessful one returns a non-zero value that usually can be interpreted as an error code.

- Run the command `$ ls -lah`.
- View the exit status of the previous command with `$ echo "$?"`.
- Now run a command that will fail. For example: `$ ls -lah /directorythatdoesnotexist`
- Run `$ echo "$?"` again to view the exit status. You should get a value that is not `0`.

### Task 2 - Using `set -xe`

When there is an error that stops the execution of a bash script, the bash interpreter usually displays the line number that triggered the error. However, in some cases, it might be necessary to trace the flow of execution of the script. This provides more insight into the conditions that are met, and the state of the loops.

- The `set` command is used to set or unset shell options or positional parameters.
- We can use the `set -e` option to exit the bash script if any command or statement generates a non-zero exit code. This is defined at the start of the script and it applies globally to all commands in the script.
- Additionally, we can also use the `set -x` option to display commands and arguments before they are executed. With this option, we can see every line of command that is executing in the script.
- The `set -e` option and `-x` options can be combined to become useful debugging tools. The option `-e` exits the script as soon as an error is encountered, and the option `-x` shows the command that was running when the error was encountered.
- Create a bash script `loop_debug.sh` and add the following to it:

      #!/bin/bash

      set -xe

      # This script prints all user profiles.

      FILE=".bashrc"  #  File containing user profile,
                  #+ was ".profile" in original script.

      for home in `awk -F: '{print $6}' /etc/passwd`
      do
        [ -d "$home" ] || continue    # If no home directory, go to next.
        [ -r "$home" ] || continue    # If not readable, go to next.
        (cd $home; [ -e $FILE ] && less $FILE)
      done

      exit 0

- Run the script and analyze the output. Did the script stop prematurely?
- Remove the option `-e` and run the script again. Did you notice any difference? Why is it different?
- Now completely remove `set -xe` to see how the program executes without these options.

## Questions to answer

> - Upload the shell script files to moodle.
> - Test every feature in your script and show screenshots of the test in your report.

1. Write a bash script that displays the following details of the logged-in user from the environment variables:

   - Login username
   - Home directory
   - Shell
   - The hostname of the system
   - The script should extract the IP address of the system from the `ifconfig` or `ip` command. Save the IP address to the `ipaddress` variable and display it as output.

   Sample output:

       Username: user1
       Home Directory: /home/user1
       Shell: /bin/bash
       Hostname: ubuntuvm
       IP address: 10.1.1.1

2. Backups are important in system administration. Create a script that will backup your home directory.

   - The backup file should be compressed to `tar.gz`.
   - All files and directory permissions should be preserved in the backup.
   - The backup destination directory is `/var/backups/`
   - The script should create the destination directory if it doesn’t already exist.
   - The backup file name should take the format `home_backup_month_day_year_hour_minute_second.tar.gz`.
     For example `home_backup_Feb_18_2023_02_30_02.tar.gz`

   > A typical real life scenario is keeping backups of websites. Administrators are usually interested in backing up `/var/www/html`.

3. Write a bash script that checks various artifacts on the system. The script mainly checks for system information, and OS components. Your script should do the following:

   - Print the OS kernel name and kernel version.
   - Print the system architecture.
   - Print all currently logged in users (show the date or time which the users logged in, and show the command line of the users’ current process).
   - Verify that EFI is enabled and print the relevant output.
   - List all connected block devices (Bonus: Identify the devices that have the GPT partition by adding an `*` to them in the output).
   - List the first boot device on your system. This should be done according to the boot order in the NVRAM.

   > Ensure that the output of your script is neatly formatted and easy to read.
   > Bonus points if you create and use at least three functions.

### Bonus

4. Write a bash script that scans the entire system for files that contain the string “/bin/bash”. The script should print only the matches that the currently logged in user has execute permission on.