One place for hosting & domains

      How to Use the Shebang in Bash and Python


      A
      Shebang directive, which always begins with the sequence #!, can sometimes be found on the first line of a Bash or Python script. In a Linux environment, the Shebang functions as an interpreter directive. This guide explains what a Shebang is and what advantages it provides. It also describes how to use a Shebang inside a Bash or Python script.

      What is a Shebang?

      The Shebang #! symbol indicates the interpreter, or which version of an interpreter, to use when executing a script. It is also known as the “sharp-exclamation”, “sha-bang”, “hash-bang”, or “pound-bang”. The name is believed to have originated as a partial contraction of either “SHarp bang” or “haSH bang”.

      A Shebang is always the first line of a script. Because it begins with the # symbol, the interpreter does not process the line containing the Shebang. When a Linux system executes a text file, it treats the Shebang as an interpreter directive. It locates the correct interpreter and runs it, passing the name of the file to the interpreter as input. For example, executing a file named ~/scripts/shebang that begins with the Shebang #!/bin/sh is functionally equivalent to running the command /bin/sh ~/scripts/shebang. The text file must be executable for proper processing to occur.

      The Shebang directive has the following advantages:

      • Permits users to treat scripts and files as commands.
      • Hides certain implementation details from users, such as the name of the interpreter.
      • Does not require the user to know the absolute path to the interpreter or how to use the env command.
      • Allows a particular version of an interpreter to be used, for example, python2 versus python3.
      • Allows the interpreter to be changed while maintaining the same user behavior and command.
      • Can automatically pass mandatory options through to the interpreter.

      One potential drawback can occur if the path to the interpreter is hard coded. If the location of the interpreter changes, the Shebang directive must be updated at the same time. Otherwise, the script might stop working.

      The Shebang directive follows this format.

      1
      
      #!interpreter [options]

      Here is an actual example of a Shebang instruction. This Shebang mandates the use of the sh Bourne shell to run the script. This example uses an absolute path to define the interpreter.

      The env utility can help find the path to the interpreter. In this case, the Shebang instructs the system to use /usr/bin/env to discover the path to the python2 interpreter. This technique is more robust because it continues to work if the path changes.

      To effectively implement a Shebang, keep in mind the following rules.

      • The directive must always begin with the #! character combination.
      • To work properly, a Shebang must occur on the first line of the file. If it is found in any other place, it is treated as a comment.
      • Either specify the full absolute path to the interpreter or use env to find the correct path.
      • Place any interpreter options after the name of the interpreter. Implementation details for compiler options vary between different systems. However, all major operating systems support at least one option.
      • One or more spaces between the #! character combo and the name of the interpreter are allowed, but not required. For example, the directives #!interpreter and #! interpreter are both valid and functionally equivalent.
      • Linux permits a second script to serve as the interpreter for the first script, but this is not the case for all operating systems.

      The directive #!/bin/false is a special Shebang. It immediately exits and returns a failure status. It prevents certain system files from being executed outside of their correct context.

      Before You Begin

      1. If you have not already done so, create a Linode account and Compute Instance. See our
        Getting Started with Linode and
        Creating a Compute Instance guides.

      2. Follow our
        Setting Up and Securing a Compute Instance guide to update your system. You may also wish to set the timezone, configure your hostname, create a limited user account, and harden SSH access.

      Note

      This guide is written for a non-root user. Commands that require elevated privileges are prefixed with sudo. If you are not familiar with the sudo command, see the
      Users and Groups guide.

      How to Use a Shebang in a Bash Script?

      The most common use of the Shebang is to specify the correct shell to use for a shell script. If a Shebang is not specified, the system uses the default interpreter belonging to the current shell. For example, in the Bash shell, the default interpreter is bash.

      To ensure the sh interpreter always processes a script, no matter what shell is active, a Shebang must be used. A Shebang such as #!/bin/sh or #!/usr/bin/env sh must be added. The system launches the sh interpreter, appending the name of the file as an argument. The interpreter treats the Shebang like a comment, which avoids an infinite loop, and interprets the remainder of the file.

      Using a Shebang with an Absolute Path

      One common method to use a Shebang is to specify the full path to the interpreter on the first line of the file.

      Note

      In this program, the line ps h -p $$ -o args="" prints out the name of the interpreter along with any arguments passed to it.

      To use a Shebang to define a mandatory interpreter for a shell script, follow these steps.

      1. Create a file named shebang_absolute with the following contents.

        File: shebang_absolute
        1
        2
        3
        4
        
        #!/bin/sh
        
        echo "Interpreter test. The interpreter and arguments are:"
        ps h -p $$ -o args=''
      2. Ensure the file is executable.

        chmod +x shebang_absolute
      3. Execute the file from the same directory. The sh interpreter is shown in the output.

        Interpreter test. The interpreter and arguments are:
        /bin/sh ./shebang_absolute
      4. Change the first line to #!/bin/bash and run the program again. The output now shows bash as the interpreter.

        Interpreter test. The interpreter and arguments are:
        /bin/bash ./shebang_absolute

      Using a Shebang with env

      For a more robust script implementation, use the env utility to determine the path to the interpreter. env uses the $PATH variable to search for the interpreter. It always returns the first match it finds.

      To use env in a Shebang, follow these steps.

      1. Create a new file named shebang_env. Add the following instructions.

        File: shebang_env
        1
        2
        3
        4
        
        #!/usr/bin/env sh
        
            echo "Interpreter test. The interpreter and arguments are:"
            ps h -p $$ -o args=''
      2. Change the file attributes so the file is executable.

      3. Execute the file. Run the command from the same directory as the new file.

        Interpreter test. The interpreter and arguments are:
        sh ./shebang_env

      Passing Options to the Interpreter

      A Shebang can pass through interpreter options. The directive #!/bin/sh -v runs the interpreter using the -v/verbose option. This option echoes each command to the screen upon execution. This example appends the -v option to the Shebang in shebang_absolute.

      #!/bin/bash -v
      
      echo "Interpreter test. The interpreter and arguments are:"
      Interpreter test. The interpreter and arguments are:
      ps h -p $$ -o args=''
      /bin/bash -v ./shebang_absolute

      Note

      If the Shebang uses env, do not declare the option within the Shebang. Instead, use the declaration set -v to set the option on the next line.

      How to Use a Shebang in a Python Script?

      In Python, the Shebang identifies which version of the Python interpreter to use. This feature might be necessary for older programs that cannot run on newer interpreters. Any script that requires Python version 2 can be redirected to the python2 interpreter. The Shebang #!/usr/bin/env python2 sets the Python interpreter to python2. Always use the env utility to set the path to the Python interpreter because it is sometimes installed in a non-standard directory.

      To use a Shebang with a Python script, follow these steps.

      1. Create the py_v3.py Python file. Add the following commands. The sys.version method displays the active version of the Python interpreter. Use import sys to import the necessary package.

        File: py_v3.py
        1
        2
        3
        4
        5
        
        #!/usr/bin/env python3
        
        import sys
        print("This version of Python is:")
        print(sys.version)
      2. Set the execute permission on the file.

      3. Run the executable file, but do not use the python3 command. The correct Python interpreter is selected at runtime based on the Shebang.

        This version of Python is:
        3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0]
      4. To confirm Python version 2 can be substituted in place of Python 3, create a new file py_v2.py and enter the following instructions.

        File: py_v2.py
        1
        2
        3
        4
        5
        
        #!/usr/bin/env python2
        
        import sys
        print("This version of Python is:")
        print(sys.version)
      5. Set the executable attribute and run the file. The program now displays information about the python2 interpreter.

        chmod +x py_v2.py
        ./py_v2.py
        This version of Python is:
        2.7.18 (default, Jul  1 2022, 10:30:50)
        [GCC 11.2.0]

      Overriding the Shebang Directive

      Even if a file contains a Shebang, it’s still possible to override it from the command line. One reason to do this might be to test how a script behaves with a different interpreter. From the command line, enter the name of the interpreter to use, followed by the name of the file. This tells the system to ignore the Shebang statement.

      In the following example, py_v2.py contains the Shebang directive #!/usr/bin/env python2. But if the command python3 ./py_v2.py is entered, the python3 interpreter is used instead.

      This version of Python is:
      3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0]

      Conclusion

      A Shebang indicates which Bash or Python interpreter to use to interpret an executable file. Shebangs are supported on Linux and many other operating systems. A Shebang begins with the characters #! and only occurs on the first line of the file. The interpreter can be specified using an absolute path or through the env program. It is possible to use a Shebang to pass additional parameters to the interpreter or to override a Shebang on the command line.

      More Information

      You may wish to consult the following resources for additional information
      on this topic. While these are provided in the hope that they will be
      useful, please note that we cannot vouch for the accuracy or timeliness of
      externally hosted materials.



      Source link

      Solving Real World Problems With Bash Scripts – A Tutorial


      Updated by Linode Contributed by Mihalis Tsoukalos

      Introduction

      This guide presents some of the advanced capabilities of the bash shell by showing practical and fully functional bash scripts. It also illustrates how you can work with dates and times in bash scripts and how to write and use functions in bash.

      In This Guide

      In this guide, you will find the following information about bash scripts:

      Note

      This guide is written for a non-root user. Depending on your configuration, some commands might require the help of sudo in order to properly execute. If you are not familiar with the sudo command, see the Users and Groups guide.

      Functions in bash shell

      The bash scripting language has support for functions. The parameters of a function can be accessed as $1, $2, etc. and you can have as many parameters as you want. If you are interested in finding out the name of the function, you can use the FUNCNAME variable. Functions are illustrated in functions.sh, which is as follows:

      functions.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      
      #!/bin/bash
      
      function f1 {
          echo Hello from $FUNCNAME!
          VAR="123"
      }
      
      f2() {
          p1=$1
          p2=$2
          sum=$((${p1} + ${p2}))
          echo "${sum}"
      }
      
      f1
      echo ${VAR}
      
      mySum="$(f2 1 2)"
      echo mySum = $mySum
      
      mySum="$(f2 10 -2)"
      echo mySum = $mySum

      Run the script with the following command:

      ./functions.sh
      

      The output will look like this:

        
      Hello from f1!
      123
      mySum = 3
      mySum = 8
      
      

      Note

      If you want to check whether a function parameter exists or not, you can use the statement:

      if [ -z "$1" ]
      

      Using bash Functions as Shell Commands

      This is a trick that allows you to use bash functions as shell commands. You can execute the above code as

      . ./functions.sh
      

      Notice the dot in front of the text file. After that you can use f1 as a regular command in the terminal where you executed . ./my_function.sh. You will also be able to use the f2 command with two integers of your choice to quickly calculate a sum. If you want that function to be globally available, you can put its implementation to a bash configuration file that is automatically executed by bash each time a new bash session begins. A good place to put that function implementation would be ~/.bash_profile.

      Working with Dates and Times

      Bash allows you to work with dates and times using traditional UNIX utilities such as date(1). The main difficulty many programmers run into when working with dates and times is getting or using the correct format. This is a matter of using date(1) with the correct parameters and has nothing to do with bash scripting per se. Using date(1) as date +[something] means that we want to use a custom format – this is signified by the use of + in the command line argument of date(1).

      A good way to create unique filenames is to use UNIX epoch time or, if you want your filename to be more descriptive, a date-time combination. The unique nature of the filename is derived from a focus on a higher level of detail in defining your output. If done correctly, you will never have the exact same time value even if you execute the script multiple times on the same UNIX machine.

      The example that follows will shed some light on the use of date(1).

      Using Dates and Times in bash scripts

      The code of dateTime.sh is the following:

      dateTime.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      
      #!/bin/bash
      
      # Print default output
      echo `date`
      
      # Print current date without the time
      echo `date +"%m-%d-%y"`
      
      # Use 4 digits for year
      echo `date +"%m-%d-%Y"`
      
      # Display time only
      echo `date +"%T"`
      
      # Display 12 hour time
      echo `date +"%r"`
      
      # Time without seconds
      echo `date +"%H:%M"`
      
      # Print full date
      echo `date +"%A %d %b %Y %H:%M:%S"`
      
      # Nanoseconds
      echo Nanoseconds: `date +"%s-%N"`
      
      # Different timezone by name
      echo Timezone: `TZ=":US/Eastern" date +"%T"`
      echo Timezone: `TZ=":Europe/UK" date +"%T"`
      
      # Print epoch time - convenient for filenames
      echo `date +"%s"`
      
      # Print week number
      echo Week number: `date +"%V"`
      
      # Create unique filename
      f=`date +"%s"`
      touch $f
      ls -l $f
      rm $f
      
      # Add epoch time to existing file
      f="/tmp/test"
      touch $f
      mv $f $f.`date +"%s"`
      ls -l "$f".*
      rm "$f".*

      If you want an even more unique filename, you can also use nanoseconds when defining the behaviour of your script.

      Run the dateTime script:

      ./dateTime.sh
      

      The output of dateTime.sh will resemble the following:

        
      Fri Aug 30 13:05:09 EST 2019
      08-30-19
      08-30-2019
      13:05:09
      01:05:09 PM
      13:05
      Friday 30 Aug 2019 13:05:09
      Nanoseconds: 1567159562-373152585
      Timezone: 06:05:09
      Timezone: 10:05:09
      1567159509
      Week number: 35
      -rw-r--r--  1 mtsouk  staff  0 Aug 30 13:05 1567159509
      -rw-r--r--  1 mtsouk  wheel  0 Aug 30 13:05 /tmp/test.1567159509
      
      

      Bash scripts for Administrators

      This section will present some bash scripts that are generally helpful for UNIX system administrators and power users.

      Watching Free Disk Space

      The bash script that follows watches the free space of your hard disks and warns you when that free space drops below a given threshold – the value of the threshold is given by the user as a command line argument. Notice that if the program gets no command line argument, a default value is used as the threshold.

      freeDisk.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      
      #!/bin/bash
      
      # default value to use if none specified
      PERCENT=30
      
      # test for command line arguement is present
      if [[ $# -le 0 ]]
      then
          printf "Using default value for threshold!n"
      # test if argument is an integer
      # if it is, use that as percent, if not use default
      else
          if [[ $1 =~ ^-?[0-9]+([0-9]+)?$ ]]
          then
              PERCENT=$1
          fi
      fi
      
      let "PERCENT += 0"
      printf "Threshold = %dn" $PERCENT
      
      df -Ph | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{ print $5,$1 }' | while read data;
      do
          used=$(echo $data | awk '{print $1}' | sed s/%//g)
          p=$(echo $data | awk '{print $2}')
          if [ $used -ge $PERCENT ]
          then
              echo "WARNING: The partition "$p" has used $used% of total available space - Date: $(date)"
          fi
      done
      • The sed s/%//g command is used for omitting the percent sign from the output of df -Ph.
      • df is the command to report file system disk space usage, while the options -Ph specify POSIX output and human-readable, meaning, print sizes in powers of 1024.
      • awk(1) is used for extracting the desired fields from output of the df(1) command.

      Run ./freeDisk.sh with this command:

      ./freeDisk.sh
      

      The output of freeDisk.sh will resemble the following:

        
      Using default value for threshold!
      Threshold = 30
      WARNING: The partition "/dev/root" has used 61% of total available space - Date: Wed Aug 28 21:14:51 EEST 2019
      
      

      Note

      This script and others like it can be easily executed as cron jobs and automate tasks the UNIX way.

      Notice that the code of freeDisk.sh looks relatively complex. This is because bash is not good at the conversion between strings and numeric values – more than half of the code is for initializing the PERCENT variable correctly.

      Rotating Log Files

      The presented bash script will help you to rotate a log file after exceeding a defined file size. If the log file is connected to a server process, you might need to stop the process before the rotation and start it again after the log rotation is complete – this is not the case with rotate.sh.

      rotate.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      #!/bin/bash
      
      f="/home/mtsouk/connections.data"
      
      if [ ! -f $f ]
      then
        echo $f does not exist!
        exit
      fi
      
      touch ${f}
      MAXSIZE=$((4096*1024))
      
      size=`du -b ${f} | tr -s 't' ' ' | cut -d' ' -f1`
      if [ ${size} -gt ${MAXSIZE} ]
      then
          echo Rotating!
          timestamp=`date +%s`
          mv ${f} ${f}.$timestamp
          touch ${f}
      fi
      • Note that the path to the log file /home/mtsouk/connections.data will not exist by default. You’ll need to either use a log file that already exists like kern.log on some Linux systems, or replace it with a new one.

      • Additionally, the value of MAXSIZE can be a value of your choice, and the script can be edited to suit the needs of your own configuration – you can even make changes to the existing code and provide the MAXSIZE value as a command line argument to the program.

      • The du command is used to estimate the file space usage. It’s use to track the files and directories that are consuming excessive space on the hard disk. The -b option tells this command to print the size in bytes.

      Run the rotate script with the following command:

      ./rotate.sh
      

      The output of rotate.sh when it has reached the threshold defined by MAXSIZE will resemble the following:

        
      Rotating!
      
      

      After running, two files will be created on the system. You can see them with this command:

      ls -l connections.data*
      
        
      -rw-r--r-- 1 mtsouk mtsouk       0 Aug 28 20:18 connections.data
      -rw-r--r-- 1 mtsouk mtsouk 2118655 Aug 28 20:18 connections.data.1567012710
      
      

      If you want to make rotate.sh more generic, you can provide the name of the log file as a command line argument to the bash script.

      Monitoring the Number of TCP Connections

      The presented bash script calculates the number of TCP connections on the current machine and prints that on the screen along with date and time related information.

      tcpConnect.sh
      1
      2
      3
      4
      5
      6
      
      #!/bin/bash
      
      C=$(/bin/netstat -nt | tail -n +3 | grep ESTABLISHED | wc -l)
      D=$(date +"%m %d")
      T=$(date +"%H %M")
      printf "%s %s %sn" "$C" "$D" "$T"
      • The main reason for using the full path of netstat(1) when calling it is to make the script as secure as possible.
      • If you do not provide the full path then the script will search all the directories of the PATH variable to find that executable file.
      • Apart from the number of established connections (defined by the C variable), the script prints the month, day of the month, hour of the day, and minutes of the hour. If you want, you can also print the year and seconds.

      Execute the tcpConnect script with the following command:

      ./tcpConnect.sh
      

      The output will be similar to the following:

        
      8 08 28 16 22
      
      

      tcpConnect.sh can be easily executed as a cron(8) by adding the following to your cron file:

      */4 * * * * /home/mtsouk/bin/tcpConnect.sh >> ~/connections.data
      

      The previous cron(8) job executes tcpConnect.sh every 4 minutes, every hour of each day and appends the results to ~/connections.data in order to be able to watch or visualize them at any time.

      Additional Examples

      Sorting in bash

      The presented example will show how you can sort integer values in bash using the sort(1) utility:

      sort.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      #!/bin/bash
      
      # test that at least one argument was passed
      if [[ $# -le 0 ]]
      then
          printf "Not enough arguments!n"
          exit
      fi
      
      count=1
      
      for arg in "[email protected]"
      do
          if [[ $arg =~ ^-?[0-9]+([0-9]+)?$ ]]
          then
              n[$count]=${arg}
              let "count += 1"
          else
              echo "$arg is not a valid integer!"
          fi
      done
      
      sort -n <(printf "%sn" "${n[@]}")
      • The presented technique uses an array to store all integer values before sorting them.
      • All numeric values are given as command line arguments to the script.
      • The script tests whether each command line argument is a valid integer before adding it to the n array.
      • The sorting part is done using sort -n, which sorts the array numerically. If you want to deal with strings, then you should omit the -n option.
      • The printf command, after sort -n, prints every element of the array in a separate line whereas the < character tells sort -n to use the output of printf as input.

      Run the sort script with the following command:

      ./sort.sh 100 a 1.1 1 2 3 -1
      

      The output of sort.sh will resemble the following:

        
      a is not a valid integer!
      1.1 is not a valid integer!
      -1
      1
      2
      3
      100
      
      

      A Game Written in bash

      This section will present a simple guessing game written in bash(1). The logic of the game is based on a random number generator that produces random numbers between 1 and 20 and expects from the user to guess them.

      guess.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      #!/bin/bash
      NUMGUESS=0
      
      echo "$0 - Guess a number between 1 and 20"
      
      (( secret = RANDOM % 20 + 1 ))
      
      while [[ guess -ne secret ]]
      do
          (( NUMGUESS = NUMGUESS + 1 ))
          read -p "Enter guess: " guess
      
          if (( guess < $secret )); then
              echo "Try higher..."
          elif (( $guess > $secret )); then
              echo "Try lower..."
          fi
      done
      
      printf "Yes! You guessed it in $NUMGUESS guesses.n"

      Run the guess script:

      ./guess.sh
      

      The output of guess.sh will resemble the following:

        
      ./guess.sh - Guess a number between 1 and 20
      Enter guess: 1
      Try higher...
      Enter guess: 5
      Try higher...
      Enter guess: 7
      Try lower...
      Enter guess: 6
      Yes! You guessed it in 4 guesses.
      
      

      Calculating Letter Frequencies

      The following bash script will calculate the number of times each letter appears on a file.

      freqL.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      #!/bin/bash
      
      if [ -z "$1" ]; then
          echo "Usage: $0 filename."
          exit 1
      fi
      
      filename=$1
      
      while read -n 1 c
      do
          echo "$c"
      done < "$filename" | grep '[[:alpha:]]' | sort | uniq -c | sort -nr
      • The script reads the input file character by character, prints each character, and processes the output using the grep, sort, and uniq commands to count the frequency of each character.
      • The [:alpha:] pattern used by grep(1) matches all alphabetic characters and is equivalent to A-Za-z.
      • If you also want to include numeric characters in the output, you should use [:alnum:] instead.
      • Additionally, if you want the output to be sorted alphabetically instead of numerically, you can execute freqL.sh and then process its output using the sort -k2,2 command.

      Run the freqL script:

      ./freqL.sh text.txt
      

      The output of freqL.sh will resemble the following:

        
         2 b
         1 s
         1 n
         1 i
         1 h
         1 a
      
      

      Note

      The file text.txt will not exist by default. You can use a pre-existing text file to test this script, or you can create the text.txt file using a text editor of your choice.

      Timing Out read Operations

      The read builtin command supports the -t timeout option that allows you to time out a read operation after a given time, which can be very convenient when you are expecting user input that takes too long. The technique is illustrated in timeOut.sh.

      timeOut.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      
      #!/bin/bash
      
      if [[ $# -le 0 ]]
      then
          printf "Not enough arguments!n"
          exit
      fi
      
      TIMEOUT=$1
      VARIABLE=0
      
      while :
      do
        ((VARIABLE = VARIABLE + 1))
        read -t $TIMEOUT -p "Do you want to Quit(Y/N): "
        if [ $VARIABLE -gt $TIMEOUT ]; then
          echo "Timing out - user response took too long!"
          break
        fi
      
        case $REPLY in
        [yY]*)
          echo "Quitting!"
          break
          ;;
        [nN]*)
          echo "Do not quit!"
          ;;
        *) echo "Please choose Y or N!"
           ;;
        esac
      done
      • The timeout of the read operation is given as a command line argument to the script, an integer representing the number of seconds that will pass before the script will “time out” and exit.
      • The case block is what handles the available options.
      • Notice that what you are going to do in each case is up to you – the presented code uses simple commands to illustrate the technique.

      Run the timeOut script:

      ./timeOut.sh 10
      

      The output of timeOut.sh will resemble the following:

        
      Do you want to Quit(Y/N): Please choose Y or N!
      Do you want to Quit(Y/N): Y
      Quitting!
      
      

      Alternatively, you can wait the full ten seconds for your script to time out:

        
      Do you want to Quit(Y/N):
      Timing out - user response took too long!
      
      

      Converting tabs to spaces

      The presented utility, which is named t2s.sh, will read a text file and convert each tab to the specified number of space characters. Notice that the presented script replaces each tab character with 4 spaces but you can change that value in the code or even get it as command line argument.

      tabs2spaces.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      #!/bin/bash
      
      for f in "[email protected]"
      do
          if [ ! -f $f ]
          then
            echo $f does not exist!
            continue
          fi
          echo "Converting $f.";
          newFile=$(expand -t 4 "$f");
          echo "$newFile" > "$f";
      done
      • The script uses the expand(1) utility that does the job of converting tabs to spaces for us.
      • expand(1) writes its results to standard output – the script saves that output and replaces the current file with the new output, which means that the original file will change.
      • Although tabs2spaces.sh does not use any fancy techniques or code, it does the job pretty well.

      Run the tabs2spaces script:

      ./tabs2spaces.sh textfile.txt
      

      The output of tabs2spaces.sh will resemble the following:

        
      Converting textfile.txt.
      
      

      Note

      The file textfile.txt will not exist by default. You can use a pre-existing text file to test this script, or you can create the textfile.txt file using a text editor of your choice.

      Counting files

      The following script will look into a predefined list of directories and count the number of files that exist in each directory and its subdirectories. If that number is above a threshold, then the script will generate a warning message.

      ./countFiles.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      
      #!/bin/bash
      
      DIRECTORIES="/bin:/home/mtsouk/code:/srv/www/www.mtsoukalos.eu/logs:/notThere"
      
      # Count the number of arguments passed in
      if [[ $# -le 0 ]]
      then
          echo "Using default value for COUNT!"
      else
          if [[ $1 =~ ^-?[0-9]+([0-9]+)?$ ]]
          then
              COUNT=$1
          fi
      fi
      
      while read -d ':' dir; do
          if [ ! -d "$dir" ]
          then
              echo "**" Skipping $dir
              continue
          fi
          files=`find $dir -type f | wc -l`
          if [ $files -lt $COUNT ]
          then
              echo "Everything is fine in $dir: $files"
          else
              echo "WARNING: Large number of files in $dir: $files!"
          fi
      done <<< "$DIRECTORIES:"

      The counting of the files is done with the find $dir -type f | wc -l command. You can read more about the find command in our guide.

      Run the countFiles script:

      ./countFiles.sh 100
      

      The output of countFiles.sh will resemble the following:

        
      WARNING: Large number of files in /bin: 118!
      Everything is fine in /home/mtsouk/code: 81
      WARNING: Large number of files in /srv/www/www.mtsoukalos.eu/logs: 106!
      ** Skipping /notThere
      
      

      Summary

      The bash scripting language is a powerful programming language that can save you time and energy when applied effectively. If you have a lot of useful bash scripts, then you can automate things by creating cron jobs that execute your bash scripts. It is up to the developer to decide whether they prefer to use bash or a different scripting language such as perl, ruby, or python.

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link

      An Intermediate Guide to Bash Scripting


      Updated by Linode Contributed by Mihalis Tsoukalos

      In the previous guide of this series, Getting Started with Bash Scripting, you learned Bash basics, like creating and using variables, getting user input, using environment variables, and more. In this guide, you will build off what you have already learned and put together more complex Bash scripts for common operations used by Linux system administrators like creating interactive Bash scripts with menu options, scripts that generate formatted output of your data, and scripts that work with files and directories. Each section will provide a brief introduction to each concept and commands with a few examples that you can run to better understand its function.

      In this guide, you will learn about:

      Before You Begin

      1. All example scripts in this guide are run from the bin directory in the user’s home directory, i.e. /home/username/bin/. If you do not have a bin directory in your home directory, create one and move into the directory:

        cd ~ && mkdir bin && cd bin
        
      2. Verify that the bin directory is in your system PATH (i.e. /home/username/bin):

        echo $PATH
        
      3. If it is not, add the new bin directory to your system’s PATH:

        PATH=$PATH:$HOME/bin/
        

      Note

      Ensure all scripts throughout this guide are executable. To add execute permissions to a file, issue the following command:

      chmod +x my-script.sh
      

      Standard Streams

      A standard stream is a communication mechanism used between a computer program and its environment. Every UNIX operating system contains three types of standard streams, standard input (stdin), standard output (stdout), and standard error (stderr). These three streams are represented by three files, /dev/stdin, /dev/stdout and /dev/stderr. Since these three files are always open, you can redirect their stream to another location. Redirection is when you use the output from one source, a file, program, script, or command and feed it as input to another source. In the context of Bash scripting, you can access stdin, stdout, and stderr using file descriptors 0, 1, and 2, respectively.

      Reading from Standard Input

      Bash scripts very often make use of standard input. The example script input.sh gets its input from a file, but if the file is not available in the expected location, it tries to read standard input (/dev/stdin):

      input.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      #!/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 a 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
      1. Using your preferred text editor create an example file for the input.sh script to read:

        echo -e 'Ultimately, literature is nothing but carpentry. nWith both you are working with reality, a material just as hard as wood.' > marquez.txt
        
      2. Run the script and pass marquez.txt as a command line argument:

        ./input.sh marquez.txt
        
          
        Ultimately, literature is nothing but carpentry.
        With both you are working with reality, a material just as hard as wood.
            
        
      3. Run the script without a command line argument:

        ./input.sh
        

        Enter some text after the prompt followed by enter and you will see it echoed back to you in the terminal. Use CTRL+D to end the script.

      You can use the select statement to create menu systems in your bash scripts that users can interact with. When you combine select with the case statement you can create more sophisticated menu options. This section will provide three examples that use select to create menus. If you are not familiar with the case statement, you can refer to our Getting Started with Bash Shell Scripting guide.

      The general format for the select statement is the following:

      bash
      1
      2
      3
      
      select WORD [in LIST];
      do COMMANDS;
      done

      The simple-menu.sh script expands on the skeleton example to create a basic menu that will prompt the user for their favorite color, print out the value of any valid menu selection, and then break out of the select statement:

      simple-menu.sh
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      #!/bin/bash
      
      echo "Enter the number corresponding to your favorite color:"
      
      select COLOR in blue yellow red green
      do
          echo "Your selection is: $COLOR"
          break
      done
      1. Copy and paste the contents of simple-menu.sh into a new file and save it.

      2. Run the script:

        ./simple-menu.sh
        

        Your output will resemble the following, but may vary depending on the menu selection you make:

          
        Enter the number corresponding to your favorite color:
        1) blue
        2) yellow
        3) red
        4) green
        #? 2
        Your selection is: yellow
            
        

      The second example script, computing-terms.sh, improves on the previous example script by using the case statement and by explicitly providing a way for the user to exit the script. By adding a case for each selection, the script can execute separate tasks based on what the user selects. The reserved Bash variable PS3 is reserved for use with select statements to provide a custom prompt to the user. This script will prompt you to select one of a series of cloud related terms and return its corresponding definition when selected.

      computing-terms.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      
      #!/bin/bash
      
      echo "This script shows you how to create select menus in your Bash scripts"
      echo "Enter a number corresponding to the term whose definition you'd like to view"
      PS3="My selection is:"
      
      select TERM in cloud-computing virtual-machine object-storage exit;
      do
          case $TERM in
              cloud-computing)
                  echo "Cloud Computing: A combined system of remote servers, hosted on the internet, to store, manage, and process data."
                  ;;
              virtual-machine)
                  echo "Virtual Machine: The emulating of a computer system, such that a single piece of hardware can deploy and manage a number of host environments, by providing the functionality of physical hardware."
                  ;;
              object-storage)
                  echo "Object Storage: stores data, called objects, in containers, called buckets, and each object is given a unique identifier with which it is accessed."
                  ;;
              exit)
                  echo "You are now exiting this script."
                      break
                      ;;
              *)
                  echo "Please make a selection from the provided options."
          esac
      done
      1. Copy and paste the contents of computing-terms.sh into a new file and save it.

      2. Run the script:

        ./computing-terms.sh
        

        Your output will resemble the following, but may vary depending on the menu selection you make:

          
        This script shows you how to create select menus in your Bash scripts
        Enter a number corresponding to the term whose definition you'd like to view
        1) cloud-computing
        2) virtual-machine
        3) object-storage
        4) exit
        My selection is:3
        Object Storage: stores data, called objects, in containers, called buckets, and each object is given a unique identifier with which it is accessed.
        My selection is:4
        You are now exiting this script.
            
        

      The third example, submenu.sh, uses all the previously covered concepts and enhances them by adding a submenu with a new series of options for the user to select. The script will read all files in the current working directory and display them to the user as selectable options. Once the user selects a file, a submenu will appear prompting the user to select an action to perform on the previously selected file. The submenu allows a user to delete a file, to display the file’s contents, or to simply exit the script.

      submenu.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      
      #!/bin/bash
      
      echo "Use this script to manipulate files in your current working directory:"
      echo "----------------------------------------------------------------------"
      echo "Here is a list of all your files. Select a file to access all"
      echo "available file actions:"
      
      select FILE in * exit;
      do
          case $FILE in
          exit)
              echo "Exiting script ..."
              break
              ;;
          *)
              select ACTION in delete view exit;
              do
                  case $ACTION in
                  delete)
                      echo "You've chose to delete your file" "$FILE"
                      rm -i "$FILE"
                      echo "File ""$FILE" "has been deleted"
                      echo "Exiting script ..."
                      break
                      ;;
                  view)
                      echo "Your selected file's contents will be printed to the terminal:"
                      cat "$FILE"
                      echo "------------------------"
                      echo "Exiting script ..."
                      break
                      ;;
                  exit)
                      echo "Exiting script ..."
                      break
                      ;;
                  esac
              done
              break
              ;;
          esac
      done
      1. Copy and paste the contents of submenu.sh into a new file and save it.

      2. Run the script:

        ./submenu.sh
        

        Note

        Ensure that the directory you are executing your script from contains at least one file in order to run through the full demo of the submenu.sh script.

        Your output will resemble the following, but may vary depending on the menu selection you make:

          
        Use this script to manipulate files in your current working directory:
        ----------------------------------------------------------------------
        Here is a list of all your files. Select a file to access all
        available file actions:
        1) example-file-1.txt
        2) example-file-2.txt
        3) exit
        #? 2
        1) delete
        2) view
        3) exit
        #? 2
        Your selected file's contents will be printed to the terminal:
        Lorem ipsum lorem ipsum
        ------------------------
        Script is exiting ...
            
        

      Introduction to the printf Command

      The bash scripting language supports the printf command, which allows you to customize the terminal output of your scripts. Its roots are in the C programming language. You can read about C’s printf function by accessing your operating system’s manual pages with the following command: man 3 printf.

      The general syntax for printf is a format string followed by arguments that will be first modified by the defined format and then inserted into the final output. A format string specifies where and how data will be output by printf.

      printf FORMAT [ARGUMENT]...
      

      You can use variables as arguments to your printf commands. This is a powerful way to write dynamic scripts that will display varied output based on variable values. For example, the following printf command will format your output by adding line breaks, defining an output color, and replacing part of the format string with the argument’s variable value. $PWD is an environment variable that stores your current working directory.

      printf "Your current working directory is: x1b[32mn %sn" $PWD
      
        
      Your current working directory is:
       /home/user
        
      

      Format Strings

      Format strings accept regular characters, which are unchanged in their output and format specifiers, which define where and how a string will be presented in the output.

      Below is a list of common format specifiers:

      • %s: formatting an argument as a string

        printf "%sn" $OSTYPE
        
          
        linux-gnu
            
        
      • %d: printing a value as an integer

        printf "%dn" "0xF9"
        
          
        249
            
        
      • %x: printing a value as a hexadecimal number with lower case a-f. You could similarly use an upper case X to print the hexadecimal value with upper case A-F

        printf "%xn" "2000000"
        
          
        1e8480
            
        
      • %f: printing floating point values

        printf "%fn" "0.01" "0.99"
        
          
        0.010000
        0.990000
            
        

      Note

      The -v var option causes the output of printf to be assigned to a variable instead of being printed to the standard output. In the example below, the result of the printf format specifier and argument will be stored in a variable named myvar. To view the result, the example echoes the value of $myvar.

      printf -v myvar "%dn" "0xF9"
      echo $myvar
      
        
      249
        
      

      Use printf in a Script

      The example script below makes use of printf to create a readable and nicely formatted report of various sequences of numbers. A for loop is used with the seq command to generate the number sequence, while each printf statement uses different format specifiers to provide slightly varying information from each number sequence. Below is a list of the format specifiers used in the script that have not yet been covered:

      1. Copy and paste the contents of printf.sh into a new file and save it.

      2. Run the script:

        ./printf.sh
        

        The output of printf.sh will resemble the following:

          
        0001	0002	0003	0004	0005	0006	0007	0008	0009	0010
        1	2	3	4	5	6	7	8	9	a
        1	2	3	4	5	6	7	8	9	A
        0010	 is A	 in HEX.
        0011	 is B	 in HEX.
        0012	 is C	 in HEX.
        0013	 is D	 in HEX.
        0014	 is E	 in HEX.
        0015	 is F	 in HEX.
        5......... is 5 in HEX.
        6......... is 6 in HEX.
        7......... is 7 in HEX.
        8......... is 8 in HEX.
        9......... is 9 in HEX.
        10........ is A in HEX.
            
        

      File and Directory Test Operators

      Bash offers file and directory test operators that return a boolean value based on each operator’s specific test criteria. These operators can be used in your Bash scripts to present different behaviors depending on the state of a file or directory. A list of all test operators is included in the expandable note, “File and Directory Test Operators” below.

      The general format for file and directory test operators is the following:

      test -[OPERATOR] [FILE]
      

      The example below tests if your /etc/passwd file exists. If the file exists, you will see "Yes, it exists!" printed as output. If the file does not exist, the first part of the command, test -a /etc/passwd, will return an exit status of 1 (the exit value will not print as output) and the second part of the command, echo "Yes, it exists!", will not execute.

      test -a /etc/passwd && echo "Yes, it exists!"
      

      File and Directory Test Operators

      OperatorDescription
      -aFile exists.
      -bFile exists and is a block special file.
      -cFile exists and is a character special file.
      -dFile exists and is a directory.
      -eFile exists and is a file of any type (node, directory, socket, etc.).
      -fFile exists and is a regular file (not a directory or a device file).
      -GFile exists and has the same group as the active user running the bash script.
      -hFiles exists and is a symbolic link.
      -gFiles exists and has the set group ID flag set.
      -kFile exists and has a sticky bit flag set.
      -LFile exists and is a symbolic link.
      -NFile exists and has been modified since it was last read.
      -OFile exists and is owned by the effective user id.
      -pFile exists and is a pipe.
      -rFile exists and is readable.
      -SFile exists and is socket.
      -sFile exists and has a nonzero size.
      -uFile exists and its set user ID flag is set.
      -wFile exists and is writable by the current user.
      -xFile exists and is executable by the current user.

      Use File and Directory Test Operators in a Script

      The example script, file-operator.sh, takes file or directory locations as arguments and returns information about each type of file that is passed to it. The script makes use of file and directory test operators to generate this information. The first if statement tests to ensure you have passed the script arguments. The for loop then goes on to test if the arguments are files that actually exist and then continues through a series of statements to test the file or directory for other criteria.

      Note

      You can use [] and [[]] commands instead of using the if conditional statement to create file conditions. The script makes use of this format on lines 26 – 40.

      file-operator.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      
      #!/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 "[email protected]"
      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
      1. Copy and paste the contents of file-operator.sh into a new file and save it.

      2. Run the script and pass it a file location as an argument:

        ./file-operator.sh /dev/fd/2
        

        Your output will resemble the following:

          
        * /dev/fd/2 is not a regular file!
        * /dev/fd/2 is not a directory.
        * /dev/fd/2 is not executable.
        * /dev/fd/2 is not a symbolic link.
        * /dev/fd/2 has zero size.
            
        
      3. Run the script and pass it a directory location as an argument:

        ./file-operator.sh /var/log
        

        Your output will resemble the following:

          
        * /var/log is not a regular file!
        * /var/log is a directory.
        * /var/log is executable.
        * /var/log is not a symbolic link.
        * /var/log has nonzero size.
        * /var/log is a readable directory.
            
        

      Read Files and Searching Directories

      This section will present a few utility scripts that can be adopted and expanded on to perform common operations on files and directories, like reading the contents of a text file by line, word, or character. These scripts make use of several of the concepts and techniques covered in this guide and in the Getting Started with Bash Shell Scripting guide.

      Read a File Line by Line

      The example file, line-by-line.sh, expects a file passed to it as an argument. It will then read the contents of the file line by line. The IFS variable (internal field separator) is a built-in Bash variable that defines how Bash recognizes word boundaries when splitting words. The script sets IFS to the null string to preserve leading and trailing white space within your text file.

      line-by-line.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      #!/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
      
      file=$1
      
      if [ ! -f "$file" ]
      then
          echo "File does not exist!"
      fi
      
      while IFS='' read -r line || [[ -n "$line" ]]; do
          echo "$line"
      done < "${file}"
      1. Copy and paste the contents of line-by-line.sh into a new file and save it.

      2. Create an example file for the line-by-line.sh script to read. The leading whitespace in the example file is intentional.

        echo -e '     Ultimately, literature is nothing but carpentry. With both you are working with reality, a material just as hard as wood.' > marquez.txt
        
      3. Run the script and pass it a file location as an argument:

        ./line-by-line.sh marquez.txt
        

        Your output will resemble the following:

          
             Ultimately, literature is nothing but carpentry. With both you are working with reality, a material just as hard as wood.
            
        

      Read a File Word by Word

      The example bash script, word-by-word.sh expects a file to be passed as an argument. It will run checks to ensure an argument has been passed to the script and that it is a file. It then uses a for loop with the cat command to echo each word in the file to your output. The default value of the IFS variable separates a line into words, so in this case there is no need to change its value.

      word-by-word.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      #!/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
      
      file=$1
      
      if [ ! -f "$file" ]
      then
          echo "File does not exist!"
      fi
      
      for word in $(cat "${file}")
      do
          echo "$word"
      done
      1. Copy and paste the contents of word-by-word.sh into a new file and save it.

      2. Create an example file for the word-by-word.sh script to read.

        echo -e 'Ultimately, literature is nothing but carpentry. With both you are working with reality, a material just as hard as wood.' > marquez.txt
        
      3. Run the script and pass it a file location as an argument:

        ./word-by-word.sh marquez.txt
        

        Your output will resemble the following:

          
        Ultimately,
        literature
        is
        nothing
        but
        carpentry.
        With
        both
        you
        are
        working
        with
        reality,
        a
        material
        just
        as
        hard
        as
        wood.
            
        

      Read a File Character by Character

      The example bash script, char-by-char.sh expects a file to be passed as an argument. It will run checks to ensure an argument has been passed to the script and that it is a file. It then uses a while loop with the read command to echo each character in the file to your shell’s output. The -n1 flag is added to the standard read command in order to specify the number of characters to read at a time, which in this case is 1.

      char-by-char.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      #!/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
      
      file=$1
      
      if [ ! -f "$file" ]
      then
          echo "File does not exist!"
      fi
      
      while read -r -n1 char; do
          echo "$char"
      done < "${file}"
      1. Copy and paste the contents of char-by-char.sh into a new file and save it.

      2. Create an example file for the char-by-char.sh script to read. The leading whitespace in the example file is intentional.

        echo -e 'Linode' > linode.txt
        
      3. Run the script and pass it a file location as an argument:

        ./char-by-char.sh linode.txt
        

        Your output will resemble the following:

          
        L
        i
        n
        o
        d
        e
            
        

      Search Directories

      The bash script, search.sh will search a directory for files and directories that begin with the string passed as a command line argument. All matching regular files and directories will be presented as output. The script expects the search string as the first argument and a directory location as the second argument. The script uses the find UNIX command for searching a directory and looks for everything that begins with matched regular expression, $string*.

      search.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      
      #!/bin/bash
      
      if [[ $# -le 1 ]]
      then
          echo "You did not pass any files as arguments to the script."
          echo "Usage:" "$1" "my-file"
          exit
      fi
      
      dir=$2
      string=$1
      
      if [ ! -d "$dir" ]
      then
          echo "Directory" "$dir" "does not exist!"
          exit
      fi
      
      for i in $(find "$dir" -name "$string*");
      do
          if [ -d "$i" ]
          then
             echo "$i" "[Directory]"
          elif [ -f "$i" ]
          then
              echo "$i" "[File]"
          fi
      done
      1. Copy and paste the contents of search.sh into a new file and save it.

      2. Move to your home directory and create an example file and directory for the search.sh script to find.

        cd ~ && echo -e 'Ultimately, literature is nothing but carpentry.' > marquez.txt && mkdir marketing
        
      3. Run the script and pass it a string and directory location as arguments. Ensure you replace the value of /home/user/ with your own home directory.

        ./bin/search.sh mar /home/user/
        

        Your output will resemble the following:

          
        /home/user/marketing [Directory]
        /home/user/marquez.txt [File]
            
        

      Bash Exit Codes

      An exit code is the code returned to a parent process after executing a command or a program. Using exit codes in your Bash scripts allows the script to modify its behavior based on the success or failure of your script’s commands. Exit codes range between 0 - 255. An exit code of 0 indicates success, while any non-zero value indicates failure. This section will provide an introduction to Bash exit codes and a few examples on how to use them in your scripts.

      Exit Codes

      The table lists and describes reserved exit codes. You should not use any of the reserved exit codes in your Bash scripts.

      CodeDescription
      0Successful execution
      1General failure
      2Incorrect usage of Bash built-in commands, by using invalid options, or missing arguments
      126Command found, but is not executable
      127Command not found
      128+nCommand terminated on a fatal signal n. The final exit code will be 128 plus the corresponding termination signal number. For example, a script that is terminated using the kill signal will have an exit code of 137 (128+9).
      130Execution terminated by CTRL-C
      255Exit status out of range

      Learning the Exit Code of a Shell Command

      You can understand whether a bash command was executed successfully or not by accessing the exit code of the command. The built-in Bash variable $? stores the exit (return) status of the previously executed command. The example below issues the long format list files (ls -l) command against your /tmp directory and redirects standard output and standard error to /dev/null in order to suppress any output. Without any direct output there is no way of knowing if the command executed successfully or failed. To circumvent this scenario you can echo the value of the $? variable to view the command’s exit status.

      1. Execute the following example command. You should not see any output, however, the command should have executed successfully.

        ls -l /tmp 2>/dev/null 1>/dev/null
        
      2. Find the value of $? to determine if your command executed successfully or not.

        echo "$?"
        

        The exit code status should output 0 if the command was successful:

          
        0
              
        
      3. Issue the long form list files command against a directory that does not exist

        ls -l /doesNotExist 2>/dev/null 1>/dev/null
        
      4. Find the value of $? to determine if your command executed successfully or not.

        echo "$?"
        

        The exit code status should output 1 if the command failed:

          
        1
              
        

        Note

        After you execute echo $?, the value of $? will always be 0 because echo $? was successfully executed.

      Using set -e

      The set command is used to set or unset different shell options or positional parameters. A very useful option that can be set with this command is the -e option, which causes a bash script to exit if any command or statement generates a non-zero exit code. This option is useful, because it works globally on all commands contained in a script, so you don’t have to test the return status of each command that is executed.

      The example script, set-example.sh, tries to create a file at the specified path. If the file cannot be written to and created, the script will immediately exit and none of the remaining commands will be executed. In the case of a non-zero exit code, you should not expect to see the last line execute the echo "Script is exiting" command.

      set-example.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
      #!/bin/bash
      
      set -e
      
      if [[ $# -le 0 ]]
      then
          echo "You did not pass any file paths as arguments to the script."
          echo "Usage:" "$0" "my-new-file-path"
          exit
      fi
      
      fpath=$1
      
      echo "About to create file: " "$fpath"
      
      if [ -e "$fpath" ]
      then
          echo "${fpath}" "already exists!"
          exit
      fi
      
      echo "Creating and writing to the file: " "$fpath"
      echo "Test" >> "$fpath"
      
      echo "Script is exiting"
      1. Copy and paste the contents of set-example.sh into a new file and save it.

      2. Run the script and pass it a file location as an argument.

        ./set-example.sh /tmp/new-file
        

        Creating a file in this location should be successful and your output will resemble the following:

          
        About to create file:  /tmp/new-file
        About to create and write to the file:  /tmp/new-file
        Script is exiting
            
        
      3. Now, run the script and pass it a file location that you likely do not have elevated enough permissions to write to.

        ./set-example.sh /dev/new-file
        

        Creating a file in this location should not be successful and your script will exit prior to executing the final echo command:

          
        About to create file:  /dev/new-file
        About to create and write to the file:  /dev/new-file
        ./set-e.sh: line 23: /dev/new-file: Permission denied
            
        

      Using set -x

      Another handy way to use the set command is by enabling the -x option. This option displays commands and arguments before they’re executed, which makes this a great option for debugging scripts.

      Note

      Any output generated by the set -x execution trace will be preceded by a + character. This value is stored in the built-in variable, PS4.

      The example script below, debug-set-example.sh, contains identical code to the example in the previous section, however, it makes use of set -x in order to print out all commands before they’re executed.

      debug-set-example.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
      #!/bin/bash
      
      set -xe
      
      if [[ $# -le 0 ]]
      then
          echo "You did not pass any file paths as arguments to the script."
          echo "Usage:" "$0" "my-new-file-path"
          exit
      fi
      
      fpath=$1
      
      echo "About to create file: " "$fpath"
      
      if [ -e "$fpath" ]
      then
          echo "${fpath}" "already exists!"
          exit
      fi
      
      echo "Creating and writing to the file: " "$fpath"
      echo "Test" >> "$fpath"
      
      echo "Script is exiting"
      1. Copy and paste the contents of debug-set-example.sh into a new file and save it.

      2. Run the script and pass it a file location as an argument.

        ./debug-set-example.sh /dev/new-file
        

        Creating a file in this location should not be successful and your script will exit prior to executing the final echo command. However, since you also have the set -x option enabled, you will be able to clearly see on which command the script exited.

          
        + [[ 1 -le 0 ]]
        + fpath=/dev/new-file
        + echo 'About to create file: ' /dev/new-file
        About to create file:  /dev/new-file
        + '[' -e /dev/new-file ']'
        + echo 'About to create and write to the file: ' /dev/new-file
        About to create and write to the file:  /dev/new-file
        + echo Test
        ./set-e.sh: line 23: /dev/new-file: Permission denied
            
        

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link