Bash shell scripting – Part I

By | 23/11/2012

This tutorial is a three part series as an introduction to Bash Shell Scripting. Part I is mainly basics of shell scripting and is generic, Part II will gradually move towards some more advanced techniques and focus mostly on the bash shell and finally in Part III we will try to use all the techniques to create working program.

A basic understanding of Linux shell is required for this tutorial. Please refer to Introduction to Linux Shell and Introduction to Shell Environment to learn more about Linux shell.

 

Shell Scripting

The Linux shell includes a full-fledged programming language, A high level language that enables the user to execute sequence of commands, parse their output and change it’s flow based on the command’s output. These programs are referred as shell scripts. Unlike most other programming languages like C, the shell programs are not compiled but are run directly by the interpreter which is in this case is the shell.

Uses of Shell Scripts

Shell scripts uses common utilities that come with the system thus reducing the development time and that are why they are good for rapid prototyping. Apart from this they are also good for automation e.g. System update, Backup etc.
Despite their less development time shell scripts aren’t used for deployment work or final product as they tends to be less efficient compared to the corresponding compiled program. Also shell scripts cause the problem of portability. As the shell uses the system utilities they are highly dependent on the system, and their environment, they were created on.

Understanding Shell Commands

To understand shell scripting lets starts with some information on how shell interprets the command you enter at the prompt.  Any input received at the prompt is considered a command. There can be two kind of commands and both are interpreted differently.

Internal Command or Built-in command

These are commands that are built in to shell and are a part of the shell source code. They mostly work to change the shell environment (variables). To make any change to these commands we have to recompile the shell e.g.

$ cd /home

This command changes the current working directory* to /home.
* The current working directory is kept in the environment in PWD.

External Command

External commands are the executable files that are part of the file system. We can simply replace the executable with the new one or remove one without affecting the functionality of the shell e.g.

$ ls program.c

This command tells the shell to execute the executable file ls with arguments program.c. If the input command is not a built-in command the shell looks in all the directories listed in the PATH environment variable in order and when it finds one it executes it with the arguments provided with the command*. These commands can be anything from binary executable to text files called scripts.

* The shell also checks for the executable bit in the file permission.

The command “type” displays information about command type. Try help type for more information.

Executing Shell Scripts

Any Script like other executable must have its executable bit set. It can be executed in two ways.

Executing in current shell

The script can be executed under the current shell by using the dot (.) command or source commands.
e.g.

$ source script_name.sh

or

$ . script_name.sh

The advantage of running script in the context of current shell is that the change in environment is persistent. For this method the script does not need its executable bit to be set.

Executing in a sub shell

To execute a script in a sub shell, enter the script full name on the prompt. In this way a new child shell is created and the script is run in the context of this shell. Another way to run the script is to pass is as an argument to shell.
e.g.

$ bash script_name.sh

When script is running in a sub shell the change in environment variables are not persistent.

e.g. Consider following script named chdir.sh with only one statement.

cd " $1"

If I run the following command the current working directory will not change

$ bash chdir.sh /home

But following command will change the current working directory to /home

$ . chdir.sh /home

Shell Scripting Basics

Comments

To make comments in a shell we use # symbol*. Anything after # is considered a comment and is ignored by the shell except when it is in quote.
e.g. Consider the following script which prints the files in the users home directory

#!/bin/bash
# This is an example of the comment in a script
# cd /home
ls $USERNAME

If we run above scripts it will fail because the third line won’t run and the directory won’t change to /home. Instead this script will run $USERNAME file in the current working directory.
* Not just shell scripts, even config file in Linux mostly uses # for comments.

SHA-BANG

In above script the first line is a special line which in not a comment. It is called SHA-BANG. The symbol #! Tells the shells path to the interpreter which will be used to run the script. Here are some of the examples of other scripts.

#!/bin/perl , #!/bin/tcsh , #!/bin/ksh

Variables

Like other programming languages shell has variables too. But unlike programming languages they do not have any type attached to them. We do not even need to declare the variables. All the variables hold the data in the form of text. e.g.

x=10

The variable x has the value 10 but its not the integer 10 but rather it’s the text 10. To access the variables value use the dollar sign in front of the variables name. e.g. $x or ${x}
e.g. To print the value of x

$ echo $x

Environment Variables

Environment variables or the global variables are the variables that are available to all the shell scripts. They are defined in your shell configuration file like bashrc for bash e.g. PATH, HOME. Command env or printenv prints all the environment variables.

Local Variables

Local variables are confined to the scripts they are defined in, set command without any arguments prints all the global and local variables defined in the context of current shell.

Special Variables

Apart from global and local variables that we defined there are some automatic variables that get their values automatically.

$*

All the command line arguments

$!

PID of last background process

$?

Exit status of last child process

$$

PID of current process

$#

Count of command line arguments
$0 Name of your command

Command Line Arguments

To access command line arguments to your script use $1, $2, $3 …
e.g.

echo $1
echo $2
echo $3

will prints the first, second and third arguments. We can access up to 9 arguments this way. To use more than 9 argument use shift command. Shift command discard the 1st argument and convert 2nd argument to 1st, 3rd to 2nd, 4th to 3rd and so on. e.g.

echo $1
shift
echi $1
shift 
echi $1

This script will show the same results as shown by the first script.
The shift command does not change the 0th argument. 0th argument always holds the name of the script.

Conditional Execution

If command

Like the other programming languages shell programming has if command for conditional execution.

if command0
then
    command1
    command2
fi

The above program works as follows :

  • The if command runs the command0 and if it is successful only then run the commands command1 and command2.
  • We can include as many command before fi .
  • The fi terminates the if block.
  • As you know in Linux any command on successful execution returns 0 and any other value for some error. So if command only tests the output comand0 to 0.

To understand lets create a simple script which will enter in to a directory and delete file specified on the command line.

#!/bin/bash
#remove.sh
# The first argument is name of directory and second and third are file names
dir=$1
filename1=$2
filename2=$3
if cd $dir
then
    if rm $filename1
    then
        echo "$filename1 deleted successfully"
    fi
    if rm $filename2
    then
        echo "$filename1 deleted successfully"
    fi
fi

Let’s analyze the above script. We are first changing in to the directory specified in the first arguments and if the directory exists only then delete the files and print the Success.

test command

test is a command which is used to evaluates expressions and returns 0 if it is true and non-zero if it is false. This command comes in really handy when working with if command. There are different tests that can be done with this command. Some of them are:

Integer tests.
Operator Name Use

Explanation

-eq equal $x –eq $y Returns True if x and y are equal
-ne not equal $x –ne $y Returns True if x and y are not equal
-gt greater than $x –gt $y Returns True if x is greater than y
-ge greater than equal $x –ge $y Returns True if x is greater than equal
-lt less than $x –lt $y Returns True if x is less than y
-le less than equal $x –le $y Returns True if x less than equal to y

 

String test
Operator

Name

Use Explanation
-z zero length -z $x Returns True if the x is zero length string
-n non zero length -n $x Returns True is the x is a non-zero length
= Equal $x = $y Returns True if x and y are same strings
!= not equal $x != $y Returns True is x and y are different strings

 

Lets modify the above script to use test command

#!/bin/bash
# remove.sh
# The first argument is name of directory and second and third are file names
dir=$1
filename1=$2
filename2=$3
cd $dir
if test $? –eq 0 
then
    rm $filename1
    if test $? –eq 0
    then
        echo "$filename1 delete successfully"
    fi
    rm $filename2
    if test $? –eq 0
    then
       echo "$filename1 delete successfully"
    fi
fi

In above script we are first executing the commands and then checking the exit status of command through special variable $?.

File Test

These are some file or directory tests

Operator Name Use Explanation
-a Exists -a $file Returns True is the file exist
-r Readable -r $file Returns True if the file is readable
-w Writable -w $file Returns True if the file is writable
-x Executable -x $file Returns True if the file is executable
-f Regular File -f $file Returns True if the file is a regular file
-d Directory -d $file Returns True if the file is a directory
-c Character File -c $file Returns True if the file is character file
-b Block File -b $file Returns True if the file is block file
-p named pipe -p $file Returns True if the file is named pipe
-s non zero size -s $file Returns True if the file has non zero size

Let’s create a script which will check if the argument is file or directory and delete it using appropriate command.

#!/bin/bash
#remove.sh
# The usage: ./remove.sh filename
filename=$1
if test –d $filename
then
    CMD=rdirm $filename
fi
if test –f $filename
then
    CMD=rm $filename
fi
$CMD
if test $? –eq 0
then
    echo "$filename deleted successfully"
fi

Explanation

  • First assign first argument to variable filename.
  • After that check if it’s a directory if it’s a directory then set CMD to rmdir $filename
  • Check if its regular file if it is then set CMD to rm $filename
  • After that run the CMD and checks its status.
Other Operators

There are some logical operators

Operator Name Use Explanation
-a And Expression1 –a Expression 2 Returns True if both the Expressions are True
-o Or Expression 1 -o Expression2 Returns True ifany one of the Expressions are True
! Negation ! Expression Negate the Expression

 

[] command

[] command is just another way of using the test command. E.g.

If [ $? –eq 0 ] 
then
    command1
    command2
fi

Pay special attention to spaces around the brackets. It is an error to not place a space around brackets.

Else

With If command we can also use the else command which will run if test evaluates to false.
e.g.

if command
then
    command1
else
    command2
fi

Above if command returns successfully then command1 will run otherwise command2 will run.

Elif command

Elif command is a combination of else if in one single command, its use is as follows.

if command0
then
    command1
elif command2
then
    command3
else
    command4
fi

Explanation:

  • Run Command0, if it is successful run command1
  • Else run command2 and if it is successful run command3
  • Else run command4

Loop

The bash shell has number of ways to creates loops.

While Command

While command keep on executing the commands placed between do and done until its arguments returns non zero.

while command0
do
    Command1
    Command2
    Command3
done

Like if command we can also use the test command here. Let’s creates a script which will print its arguments.

#!/bin/bash
#print.sh
# The usage: ./print.sh arg1 arg2 arg3 arg4 ……..
while test –n "$1" 
do
    echo "$1"
    shift
done

Explanation

  • Check if the first argument is non-zero, if this test fails then exit loop.
  • Print the first argument.
  • Shift the arguments, second argument becomes first.
  • Move to step 1

For command

For command is used to traverse over a list.
e.g.
for i in 1 2 3 4 5 6
do
    echo $i
done

The above program will print number from 1 to 6.

Until command

Until command is a compliment of while loop .i.e. it breaks out of loop only when its argument is True.

until command
do
    Commands
done

let’s create a program which will wait for given user to login and print status.

#!/bin/bash
#user_log.sh
# The usage: ./user_log.sh username
if [ -z "$1" ];then
echo "usage: $0 username"
    exit 1
fi 
Username=$1
until who | grep $Username
do
    # user is not logged in sleep for 1 seconds
    sleep 1
done
echo "$Username has loggedin"

break and continue

Break causes to exit out of loop immediately.
Continue cause the loop to jump directly at the next iteration.
Let’s create a script which will prints its argument unto the “last” argument and skip the argument they came after the “skip”

#!/bin/bash
#print.sh
# The usage: ./print.sh arg1 arg2 arg3 arg4 last……..
while [ –n "$1" ] 
do
    if [ "$1" = "last" ];then
        break
    elif [ "$1" = "skip" ];then
        shift;shift
        continue
    else
       echo $1
       shift
    fi
   echo "$1"
   shift
done

Explanation

  • Check if first argument is non-zero length, exit out of loop if it’s not
  • Check if its last arguments, exit out of loop if it is
  • Check if it is skip, if it is then skip this and next arguments and continue to next iteration
  • Else print the arguments

Switch Case

The equivalent of C Switch case in scripting is “case”. It helps in combining the multiple if else statements to write in a more elegant and efficient way. Its use is to check if the argument matches one of the given patterns and execute some commands.

case argument in
   Pattern1)
       Command1
       Command2
   ;;
   Pattern2)
       Command4
       Command5
   ;;
   *)
       Command7
       Command8
   ;;
esac

esac points the end of case statement. Pattern1 and Pattern2 can be simple strings or regular expression. Double semicolon (;;) marks the end of block of that pattern.

I/O and Redirection

Input output can be through terminal or through files. For output we have already seen the use of echo command. Echo command prints its argument on the standard output that is terminal for default and appends a line break after that. To suppress appending of new line character use –n option. Default echo does not interpret the special characters like line break but we can change its behavior by using the –e option.

Run following commands in your shell.

echo -n"Hello World\n" 
echo –en " Hello World\n"

If we need to take input from user we use read command. Read command can be very handy when combined with redirection. Try following commands.

read –p"Enter Value:" x
echo $x

Redirection is a very powerful tool that makes it easy to handle input output. For example when we are using the pipe | we are redirecting the command output to second command. Apart from pipe there are other redirections also.

Symbol Use Explanation
| Comand1 | Command2 Use to redirect output of one command to another
> Command > filename Use to redirect output of command to a file
< Command < filename Use to read from a file
<< << delimiter Use to redirect the stdin of command from the script itself
<<- <<-delimiter Heredoc with suppress tabs

 

Lets Create a script which reads /etc/passwd file and prints the shell and home directory of each user.

#!/bin/bah
# Print Shell and Home directories of user
# user-info.sh 
while read line;do
   username=`echo $line| cut-d":" –f1`
   home=`echo $line| cut-d":" –f6`
   shell=$(echo $line| cut-d":" –f7)
   echo $shell | egrep –e ‘false’
   if [ $? –ne 0 ];
       echo –e "Username\t:\t$username"
       echo –e "Home\t:\t$home"
       echo –e "Shell\t:\t$shell"
   fi
done < /etc/passwd

Explanation:

  • In first line we are reading a input from user and using its return value as a test for while loop.
  • As In the last line we have redirected the input of from file /etc/passwd so read command will read from this file instead of stdin.
  • When it reaches at the end of file read will return exit status other than 0 and while loop will break.
  • In second line we have used special constrict back tick. Its use is to run the commands given in a sub shell and returns the output of these commands.
  • We can replace back tick with $(command) which is simple to use and less error prone.
  • We are running the command in a sub shell and assigning the output to variables.
  • The cut command is use to divide the line in to columns based on the delimiter character given by option –d.
  • In fifth line we are checking if this is a standard user account which has its shell other than /bin/false.
  • Then we are printing the output.

Notice here that we have use the semicolon to combine the command while and do and in the same way if and then, it is not necessary to do so, it is more of a personal choice whether to use a new line for then and do or the same.

When we run the above script using ./user-info.sh  the output is shown on the terminal. Now lets use following command

$ ./user-info.sh > filename

This will cause the output of the script user-info.sh to be redirected to file.

Here Document

Here document or heredoc is a way to specify script input in a literal string in the script itself. To use it we define a delimiter which tells the interpreter the end of the input. e.g.

echo <<END

This is an example of here document. We use to it to redirect the command’s input to some literal string.
END. Here END is delimiter; we can use anything as delimiter. One point to keep in mind is that delimiter must come in a new line and that line must not have anything beside the delimiter.

<<- delimiter syntax helps in suppressing the tabs before each lines which are used to format the code.

Arithmetic Operations

In programs we sometime need to do some arithmetic operations on our input, shell also give us tools to do it.

Let Command

Let command is a very simple command that can do some easy arithmetic operations on in arguments and returns the results. It is a shell built-in command so to get help use help command. e.g.

x=10
let x++
echo $x

The output will be 11. Here is a list of operators that can be used with the Let command. (From the help let command)

id++, id–

variable post-increment, post-decrement
++id, –id variable pre-increment, pre-decrement
-, + unary minus, plus
!, ~ logical and bitwise negation
** exponentiation
*, /, % multiplication, division, remainder
+, - addition, subtraction
<<, >> left and right bitwise shifts
<=, >=, <, > comparison
==, != equality, inequality
& bitwise AND
^ bitwise XOR
| bitwise OR
&& logical AND
|| logical OR
expr ? expr : expr conditional operator
=, *=, /=, %=, +=, -=, <<=, >>=,&=, ^=, |= assignment

The downside of using let command is, it does not output the results, we have to use a variable to store the results. E.g to add two numbers

Let "x=1+2"
echo $x

Lets write script which will work as the seq command ( output sequence)

#!/bin/bah
# output sequence of numbers
# seq.sh <start> <end>
case $# in
    1)
        last=$1
        start=0
    ;;
    2)
        start=$1
        last=$2
    ;;
    *)
        start=0
        last=0
    ;;
esac
if [ $last- lt $start ];then
   echo "usage:$0[ <start>] <end>"
fi
while [ $last –lt $start ];do
echo $start
let start++
done

Expr command

Like Let command expr command helps for handling arithmetic operations but it’s an external command and unlike let command it directly outputs the results. It evaluates the expression given as an argument and prints the output. e.g.

$ expr "1 + 2"
will output 3

The spaces between elements of an expression are critical. The problem you will experience with is multiplication operator, as the shell uses it for wildcard substitution, for multiplication use \* instead. Both let and expr only handles fixed points integer expressions.

Bc command

Bc is a small interactive program which comes with the system utilities, we can also use it to handle our arithmetic expressions using pipe. Unlike let or expr it is capable of handling real numbers. It is very powerful tool which can handle all the arithmetic work for us. It can also be used to convert from one number system to other.
Examples:

echo "2+3" | bc                        :     Add 2 and 3
echo "7*8" | bc                        :     Multiply 7 and 8
echo "obase=16;100" | bc               :     Convert 100 to hexadecimal
echo "scale=2;13/7"  | bc              :     Floating point calculations

Try man bc to learn more about this wonderful tool.

Debugging Scripts

Like any other programming language some time shell scripting also have the debugging capability. But unlike them we do not need any other tool to do that. The same shell which is used run the scripts can be used to debug them. To debug scripts run the script with –x option. For example to debug my script test.sh I will use the command bash –x test.sh.
I can also change the very first statement of my script to include this flag.  e.g

#!/bin/bash –x

With –x option shell will print all the statements it is executing with replacing the variables with their values.

Getting Help

Shell is a very powerful tool and it takes a lot of time and effort to master it. You can not remember all the commands so it comes with various handy utilities for help.

Help Command

Help command displays help information for built in commands.
Try help help to learn more.

Man Command

Man is a tool to read and display man pages which comes with the utilities we install on our system. For external commands use man is the tool to get help. If you are new to Linux man is your friend, not only for commands but also for the system calls and other libraries. Try man man to learn more

Info command

Info command gives more detailed information about the command. It reads the info documents of that utility.
Try man info to learn more.

-h, –help Options

In Linux almost all the commands implement these command line options to display a small text of help for the command. This is command specific so not all command may have this. To use it try command name with –help option or -h option.

Apropos command

All the above commands will help only if you know the command name exactly. Some time we forgot the exact command name but but know what it does,  then apropos is the command that helps us.  Try man apropos for more info.

An example script

Before concluding lets create a script which will run in the background and monitors the disk space usage of all users and mail them if disk usage becomes more then quota.

For this we have to do following:

  • Find users and their home directories reading /etc/passwd
  • Find his disk usage with du command
  • Compare his usage with the quota.
#!/bin/bash
# Users Disk Space Monitor
# Run every friday midnight
USER_QUOTA="10" # GB
# Traverse over all the lines of /etc/passwd
# Read one line 
while read line;do
    # Parse user info
    home_dir=$(echo $line|cut -d":" -f6)
    user=$(echo $line|cut -d":" -f1)
    shell=$(echo $line|cut -d":" -f7)
    echo $shell| grep false >/dev/null 2>&1
    # Check user is real user
    if [ $? -ne 0 ];then
        # Check user home directory exist
        if [ -n "$home_dir" -a -d "$home_dir" ];then
            # Calculate the disk usage
            DISK_USAGE=$(du -sh "$home_dir" | cut -f1)
            # Check the disk usage are in the GB
           if echo $DISK_USAGE | grep "G";then
               # Get the number and remove G
               DISK_USAGE=$(echo $DISK_USAGE | tr -d '[A-Z]')
               # Check if user has crossed his quota
               if [ $DISK_USAGE -gt $USER_QUOTA ];then
                   # mail user
                   # Using Heredoc to redirect mail input
                   # Use - construct to supress tabs
                   mail -s "Disk Usaga" $user <<-END
                   Dear $user,
                   Your disk usage($DISK_USAGE) crossed $USER_QUOTA GB limit.
                   Please delete unnecessary files"
END
               fi
            fi
        fi
    fi
done < /etc/passwd
# Reschedule the command to run at midnight each Friday
at midnight Friday <<!
$0
!
# End of Script

In this part of the series we have learned some basics of shell scripting. In part II we will learn about arrays, functions, file descriptors, networking and async I/O etc.

 

REFERENCES

Bash Guide for Beginners

Advanced Bash-Scripting Guide

Introduction to Linux Shell

Introduction to Shell Environment

7 thoughts on “Bash shell scripting – Part I

  1. Gajender Pathak

    Great yaar………I need to discuss the “Bc” command with you..

    Reply
  2. maedox

    Two things you should mention in the first paragraph:
    1. Always quote variables. Always!
    2. Don’t create all upper case variables. You will overwrite a system env var without knowing it.

    E.g: The script example that removes directories/files; It needs quotes. In its current form you’re teaching extremely bad practice.

    Reply
    1. rajeev

      Thanks maedox for mentioning these important points, I’ll include these in the third article with examples.

      Reply
  3. Pingback: Anonymous

  4. Pingback: script to read path from console and used to change the script execution path

Leave a Reply

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