• Let us begin by talking about how you actually get a script to run, and how you pass arguments to it.


  • There are two basic ways of running a bash script.


  • You could have any file with any name.


  • Let's say, it's "my_script.sh", which contains a sequence of commands you want to run as if you were typing them at the command line.


  • And then, you type "bash", and then the name of the script.


  • The second way to do it is to have in the script the first line contain pound [#], exclamation point [!], and then the name of the interpreter.


  • Here it's "/bin/sh". It could be "/bin/bash", or if it's a Perl script, you will replace "sh" with "perl", etc.


  • And then, you have to make the script executable by giving "chmod +x" and then the name of the file, and then you can just run that file.


  • Just make sure it's in your path and you can just type "./my_script.sh", if it's in your current directory, etc.


  • If you need to pass arguments to the script, so they know what files or numbers or strings to work on.


  • There are special environment variables you can pass to the script.


  • "$0" is always the command name, "$1 $2 $3", etc., are any arguments that are given on the command line.


  • "$*" is all of them.


  • "$@" symbol is a little bit different, but it also represents all the arguments.


  • And "$#" gives the number of arguments.


  • You have to be careful about putting these variables and double quotes sometimes, though not always.


  • Especially if any of the strings that you use are empty.


  • If foobar.sh is:


  •  
    #!/bin/bash
    echo 0 = $0
    echo 1 = $1
    echo ’*’ = $*
    
  • the output of ./foobar.sh a b c d e is:


  •  
    0 = ./foobar
    1=a 
    *=abcde
    
    
  • Inside the script, the command shift n shifts the arguments n times (to the left).


  • There are two ways to include a script file inside another script: . file source file.


  • There are a number of options that can be used for debugging purposes:


  •  
        set -n (bash -n) 
    
  • just checks for syntax


  •  
         set -x (bash -x) 
    
  • echos all commands after running them


  •  set -v (bash -v) 
    
  • echos all commands before running them


  •  set -u (bash -u) 
    
  • causes the shell to treat using unset variables as an error


  •  set -e (bash -e) 
    
  • causes the script to exit immediately upon any non-zero exit status


  • where the set command is used inside the script (with a + sign behavior is reversed) and the second form, giving an option to bash, is invoked when running the script from the command line.


  • bash permits control structures like:


  •  
    if condition
    then
       statements
    else
       statements
    fi
    
    
  • There is also an elif statement.


  • Note that the variable $? holds the exit status of the previous command. The condition can be expressed in several equivalent ways:


  •  
    if [[ -f file.c ]] ; then ... ; fi
    if [-ffile.c] ;then...;fi 
    if test -f file.c ; then ... ; fi
    
    
  • Remember to put spaces around the [ ] brackets.


  • The first form with double brackets is preferred over the second form with single brackets, which is now considered deprecated. For example, a statement such as:


  •  
    if [ $VAR == "" ]
    
    
  • will produce a syntax error if VAR is empty, so you have to do:


  •  
    if [ "$VAR" == "" ]
    
    
  • to avoid this.


  • The test form is also deprecated for the same reason and it is more clumsy as well. However, it is common to see these older conditional forms in many legacy scripts.


  • You will often see the && and || operators (AND and OR, as in C) used in a compact shorthand:


  •  
    $ make && make modules_install && make install
    $ [[ -f /etc/foo.conf ]] || echo ’default config’ >/etc/foo.conf
    
    
  • The &&s (ANDs) in the first statement say stop as soon as one of the commands fails; it is often preferable to using the ; operator. The ||’ (ORs) in the second statement says stop as soon as one of the commands succeeds.


  • The && operator can be used to do compact conditionals; e.g. the statement:


  •  
    [[ $STRING == mystring ]] && echo mystring is "$STRING"
    
    
  • is equivalent to:


  •  
    if [[ $STRING == mystring ]] ; then
        echo mystring is "$STRING"
    fi
    
    
  • File Conditionals There are many conditionals which can be used for various logical tests. Doing man 1 test will enumerate these tests. Grouped by category we find:


  • Test Meaning
    -e file file exists?
    -d file file is a directory?
    -f file file is a regular file?
    -s file file has non-zero size?
    -g file file has sgid set?
    -u file file has suid set?
    -r file file is readable?
    -w file file is writeable?
    -x file file is executable?
  • String Comparisons If you use single square brackets or the test syntax, be sure to enclose environment variables in quotes in the following:


  • Test Meaning
    string string not empty?
    string1 == string2 string1 and string2 same?
    string1 != string2 string1 and string2 differ?
    -n string string not null?
    -z string string null?
  • Arithmetic Comparisons These take the form:


  •  
    exp1 -op exp2
    
    
  • where the operation (-op) can be:


  •  
    -eq, -ne, -gt, -ge, -lt, -le
    
    
  • Note that any condition can be negated by use of !.


  • case This construct is similar to the switch construct used in C code. For example:


  •  
    #!/bin/sh
    echo "Do you want to destroy your entire file system?"
    read response
    
    case "$response" in
       "yes")              echo "I hope you know what you are doing!" ;;
       "no" )              echo "You have some comon sense!" ;;
       "y" | "Y" | "YES" ) echo "I hope you know what you are doing!" ;
                           echo ’I am going to type: " rm -rf /"’;;
       "n" | "N" | "NO" )  echo "You have some comon sense!" ;;
       *   )               echo "You have to give an answer!" ;;
    esac
    exit 0
    
    
  • Note the use of the read command, which reads user input into an environment variable.


  • bash permits several looping structures. They are best illustrated by examples.


  • for


  •  
    for file in $(find . -name "*.o")
    do
        echo "I am removing file: $file"
        rm -f "$file"
    done
    
    
  • which is equivalent to:


  •  
    find . -name "*.o" -exec rm {} ’;’
    
    
  • or


  •  
    find . -name "*.o" | xargs rm
    
    
  • showing use of the xargs utility.


  • while


  •  
    #!/bin/sh
    
    ntry_max=4 ; ntry=0 ; password=’ ’
    
    while [[ $ntry -lt $ntry_max ]] ; do
            
       ntry=$(( $ntry + 1 ))
       echo -n ’Give password:  ’
       read password
       if  [[ $password == "linux" ]] ; then
           echo "Congratulations: You gave the right password on try $ntry!"
           exit 0 
       fi
       echo "You failed on try $ntry; try again!"
    done
    
    echo "you failed $ntry_max times; giving up"
    exit -1
    
    
  • until


  •  
    #!/bin/sh
    
    ntry_max=4 ; ntry=0 ; password=’ ’
    
    until [[ $ntry -ge $ntry_max ]] ; do
            
       ntry=$(( $ntry + 1 ))
       echo -n ’Give password:  ’
       read password
       if [[ $password == "linux" ]] ; then
           echo "Congratulations: You gave the right password on try $ntry!"
           exit 0 
       fi
       echo "You failed on try $ntry; try again!"
    
    done
    
    echo "you failed $ntry_max times; giving up"
    exit -1
    
    
  • One often needs to do the same operation many times in a script.


  • This is where the use of functions or subprograms comes in. You could have a script call another script multiple times, but that's unwieldy, it can make passing information more complex.


  • You have more than one file to maintain and they have to be able to find each other at runtime. So, it's much simpler to use a function.


  • Now, the basic form of a function looks like a function in the C language. You just declare a function, you have some parentheses which contain the statements that would be executed.


  • Note that you never put any arguments within the parentheses where you would in a C program.


  • Now, a function always has to be defined before it's used.


  • A bash script is not a compiled computer language, it just executes as it goes.


  • So everything has to be defined before it's used.


  • You also have to be careful about using variables inside the function that have the same name as variables which are used globally.


  • If you need to do that, it's important to label them as "local" within the function.


  • Example:


  •  
    #!/bin/sh
    
    test_fun1(){
        var=FUN_VAR
        shift
        echo "   PARS after fun shift:   $0 $1 $2 $3 $4 $5"
    }
    
    var=MAIN_VAR
    echo ’ ’
    echo "BEFORE FUN MAIN, VAR=$var"
    echo "   PARS starting in main:  $0 $1 $2 $3 $4 $5"
         
    test_fun1 "$@"
    echo "   PARS after fun in main: $0 $1 $2 $3 $4 $5"
    echo "AFTER FUN MAIN, VAR=$var"
    
    exit 0
    
    
  • Sometimes you will see an (older) method of declaring functions, which explicitly includes a function keyword, as in:


  •  
    function fun_foobar(){
       statements
    }
    
    
  • or


  •  
    function fun_foobar{
       statements
    }
    
    
  • without the parentheses.


  • This syntax will work fine in bash scripts, but is not designed for the original Bourne shell, sh.


  • In the case where a function name is used which collides with an alias, this method will still work.


  • In most cases, use of the function keyword is not often used in new scripts.


  • Create the following file in your favorite text editor and call it nproc:


  •  
    #!/bin/sh
    nproc=$(ps | wc -l)
    echo "You are running $nproc processes"
    exit 0
    
    
  • Note we have used the following commands:


  • wc counts the number of lines, words, and characters in a file ps gives information about running processes.


  • Now make it executable with:


  •  
    chmod +x nproc
    
    
  • and then run it with:


  •  
    ./nproc
    
    
  • Type ps at the command line. You will notice there is one extra line of headings; thus, we have over-counted by one. Edit the script to correct this.


  • Hint: You can use either of these two forms:


  •  
    nproc=$(($nproc - 1 ))
    nproc=$(expr $nproc - 1)
    
    
  • Can you do this exercise without using any variables (i.e. do it in one line)? Hint: You will find it easier with the $(...) construct than with the ‘....‘ construct.


  • Solution You can see a solution for this exercise here:


  •  
    #!/bin/bash
    #/* **************** Coursera **************** */
    #/*
    # * The code herein is: Copyright the Linux Foundation, 2018
    # *
    # * This Copyright is retained for the purpose of protecting free
    # * redistribution of source.
    # *
    # *     URL:    http://training.linuxfoundation.org
    # *     email:  [email protected]
    # *
    # * This code is distributed under Version 2 of the GNU General Public
    # * License, which you should have received with the source.
    # *
    # */
    #!/bin/sh
    
    set -x
    
    ########################################################*****
    nproc=$(ps | wc -l)
    
    nproc=$(($nproc - 1 ))
    # or nproc=$(expr $nproc - 1)
    
    echo "You are running $nproc processes"
    #exit 0
    ########################################################*****
    
    #one line, method 1
    echo "You are running $( expr $(ps | wc -l) - 1 )  processes" 
    
    ########################################################*****
    
    #one line, method 2
    echo "You are running $((     $(ps | wc -l) - 1 )) processes" 
    
    ########################################################*****
    
    
  • Construct a shell script that works as a basic backup utility. It should be invoked as:


  •  
    Backup Source Target
    
    
  • For each directory under Source, a directory should be created under Target. Each directory in Target should get a file named BACKUP.tar.gz which contains the compressed contents of the directory.


  • You shouldnot need permission to write in the Source directory area, but obviously you will need permission to write in Target.


  • A good way to test it might be to use /var as the source.


  • Note: Functions can be called recursively, but you do not have to do so. Some of the utilities and commands you might need are: tar, gzip, find, pushd, popd, cp, echo, mkdir...


  • Challenge: Try making it an incremental backup; only do those directories which have changed since the last backup. This can be made very complicated, but just consider cases where there are files newer than when the backup was made.


  • Solution You can see a solution for this exercise here:


  •  
    #!/bin/bash
    #/* **************** Coursera **************** */
    #/*
    # * The code herein is: Copyright the Linux Foundation, 2018
    # *
    # * This Copyright is retained for the purpose of protecting free
    # * redistribution of source.
    # *
    # *     URL:    http://training.linuxfoundation.org
    # *     email:  [email protected]
    # *
    # * This code is distributed under Version 2 of the GNU General Public
    # * License, which you should have received with the source.
    # *
    # */
    #!/bin/sh
    
    usage="Usage: Backup Source Target"
    
    if [[ $# -lt 2 ]] ; then
        echo -e '\n'    $usage '\n'
        exit 1 
    fi
    
    if ! [[ -d $1 ]] ; then
        echo -e '\n' ERROR: First argument must be a Directory that exists: quitting'\n'
        exit 1 
    fi
    
    SOURCE=$1
    TARGET=$2
    
    DIRLIST=$(cd $SOURCE ; find . -type d )
    # echo DIRLIST= $DIRLIST
    
    for NAMES in $DIRLIST 
    do
    	SOURCE_DIR=$SOURCE/$NAMES
    	TARGET_DIR=$TARGET/$NAMES
    	echo "SOURCE= $SOURCE_DIR      TARGET=$TARGET_DIR"
    	FILELIST=$( (cd $SOURCE_DIR ; find . -maxdepth 1 ! -type d ) )
    	mkdir -p $TARGET_DIR
    	OLDIFS=$IFS
    IFS='
    '
    	tar -zcvf $TARGET_DIR/Backup.tar.gz  -C $SOURCE_DIR $FILELIST
    	IFS=$OLDIFS
    done
    
    
     	
    #!/bin/bash
    #/* **************** Coursera **************** */
    #/*
    # * The code herein is: Copyright the Linux Foundation, 2018
    # *
    # * This Copyright is retained for the purpose of protecting free
    # * redistribution of source.
    # *
    # *     URL:    http://training.linuxfoundation.org
    # *     email:  [email protected]
    # *
    # * This code is distributed under Version 2 of the GNU General Public
    # * License, which you should have received with the source.
    # *
    # */
    #!/bin/sh
    
    StripDotSlash(){
    result=""
    for names in $1 ; do 
           result="$result $(basename $names)"
           done
        echo "$result"
    }
        
    GetFileNames(){
        StripDotSlash "$(find . -maxdepth 1 -not -type d )"
    }
    
    GetDirNames(){
         StripDotSlash "$(find . -maxdepth 1 -mindepth 1 -type d)"
    }
    
    DoDir(){
        local dirnames filenames R_SOURCE R_TARGET
        DIRNO=$(( $DIRNO + 1))
        cd $1
        echo  "DIRNO=$DIRNO    SOURCEDIR= $1    TARGETDIR = $2"
        dirnames=$(GetDirNames)
        filenames=$(GetFileNames) ;
        if [[ -n $filenames ]] ; then
    	tar -zcf $2/Backup.tar.gz $filenames
        fi
    
        for R_SOURCE in $dirnames ; do
            R_TARGET=$2/$R_SOURCE
            mkdir -p $R_TARGET
            DoDir $1/$R_SOURCE $R_TARGET
        done
    }
    
    ###########################################################################
    
    SOURCE=$1
    TARGET=$2
    
    # Make sure the target directory is a full path name
    
    #if ! [[ $(echo "$TARGET" | grep -q ^\/) ]] ; then 
    if ! [[ $(echo "$TARGET" | grep ^\/) ]] ; then 
        echo -n TARGET was $TARGET: AFTER ADDING FULL PATH: 
        TARGET=$(pwd)\/$TARGET
        echo TARGET now is $TARGET
    fi
    
    
    DIRNO=0
    usage="Usage: Backup Source Target"
    
    if [[ $# -lt 2 ]] ; then
        echo -e '\n'    $usage '\n'
        exit 1 
    fi
    
    if ! [[ -d $1 ]] ; then
        echo -e '\n' ERROR: First argument must be a Directory that exists: quitting'\n'
        exit 1 
    fi
    
    # Make sure the target directory exists
    mkdir -p $TARGET
    
    DoDir $SOURCE $TARGET
    
    echo "Backup Successfully Done"
    exit 0
    ###########################################################################