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
###########################################################################