6. Shell Scripts#
What actually makes a shell script:
Put a group of commands in a file, then make the file executable.
In the executable file, apply the shell syntax to add variables, conditionals, iterations.
Create functions for the frequently used blocks.
Utilize
awk,grep, andsedlanguages with regular expressions for string parsing and substitution.
6.1. Shell Scripting Exercises#
Create a directory for shell scripting exercises.
A shell script and executable permissions.
ifandcasestatements.Loops with
whileanduntilconditional statements.forloop.Functions in shell scripts.
awkprogramming language.grepline search filter and regular expressions.String editor
sed.Advantages and disadvantages of shell scripts.
References: for details, consult the Bash Guide for Beginners
6.2. Creating a Shell Script#
SSH into your LXC container.
Create a new directory named bash_scripts where you will run the shell scripting exercises:
mkdir bash_scripts
cd bash_scripts
Note: It is recommended to create a new script file for every exercise, make them executable, and give them names ending with the .sh extension (this is a convention rather than a strict requirement). The first line of a Bash script starts with a shebang:
#!/bin/bash
All non-executable comments in a script are prepended with a hash symbol (#):
File content:
# For example, we list all the files in the current directory
ls -la
ls -l /etc # Comment. Here, we list files in the /etc directory
6.3. Variables: Read, Echo, Assignment, and Substitution#
Shell scripts contain variables. Variables are created using an assignment operator, for example:
DIR=/home/hostadm
Variable values are referenced using the $ sign:
echo $DIR
Variables can be updated via the assignment operator:
DIR=$DIR/KVM
echo $DIR
Variables can also be read from standard output (or keyboard input) by using the read command.
For example, the script scr1.sh acts like a calculator:
File content:
#!/bin/bash
echo "I will work out X*Y"
echo "Enter X"
read X
echo "Enter Y"
read Y
echo "X*Y = $X*$Y = $[X*Y]"
Make the script executable and run it:
chmod 755 scr1.sh
./scr1.sh
6.4. Variable $1#
The variables $1, $2, $3, and so on, capture the first, second, third, and subsequent input parameters passed to the script. For example:
script.sh --help
Here, $1 captures the string --help as a value inside script.sh.
6.5. Variables with Commands#
We can assign a command to a variable and then execute it within the shell.
For example, define the variable cmd and execute it:
cmd="ls /etc"
eval $cmd
Example of a more complex statement: tar the /etc directory and copy the resulting tar file into a new directory named DIR1:
my_dir=etc
cmd="tar -cf ${my_dir}.tar /${my_dir}; mkdir DIR1; cp ${my_dir}.tar DIR1"
eval $cmd
6.6. if Conditional Statements#
The test square brackets
[ ]evaluate whether a statement is true or false.
File content:
if [ condition ]
then
perform a task if condition = true
else
perform a task if condition = false
fi
For example:
File content:
#!/bin/bash
echo 'Enter the parameter value:'
read parameter
if [ "$parameter" == 'this' ]
then
echo $parameter
else
echo 'Does not match'
fi
Another example using nested conditions:
File content:
#!/bin/bash
X=10
Y=5
if [ "$X" -gt "$Y" ]; then
echo "$X is greater than $Y"
elif [ "$X" -lt "$Y" ]; then
echo "$X is less than $Y"
else
echo "$X is equal to $Y"
fi
There are various conditional tests available in Bash. For example, to check if the directory DIR1 exists:
if [ -d "DIR1" ]
To check if the file ttx.txt exists:
if [ -e "ttx.txt" ]
You can run the man command to view various predefined test options:
man test
6.7. Conditional Command Sequences#
Execute command2 only if command1 completes successfully:
command1 && command2
Example:
mkdir testdir
ls testdir && echo "testdir exists"
Alternatively:
[ -d testdir ] && echo "testdir exists"
Conversely, execute command2 only if command1 fails:
command1 || command2
Example:
ls testdir_2 || echo "testdir_2 doesn't exist"
Alternatively:
[ -d testdir_2 ] || echo "testdir_2 doesn't exist"
6.8. case Conditional Statements#
File content:
case $variable in
regex1)
commands1
;;
regex2)
commands2
;;
*)
commands3
;;
esac
Where regex is a regular expression used to match the variable’s value. Use *) at the end to catch all remaining unmatched strings.
File content:
#!/bin/bash
case $1 in
--test|-t)
echo "you used the --test option"
exit 0
;;
--help|-h)
echo "Usage:"
echo " myprog.sh [--test|--help|--version]"
exit 0
;;
--version|-v)
echo "myprog.sh version 0.0.1"
exit 0
;;
*)
echo "No such option $1"
echo "Usage:"
echo " myprog.sh [--test|--help|--version]"
exit 1
;;
esac
echo "You typed "$1" on the command-line"
Note: Always pay close attention to the correct syntax of case statements.
6.9. Looping with while and until Statements#
Script scr2.sh:
File content:
#!/bin/bash
N=1
while [ "$N" -le "10" ]
do
echo "Number $N"
N=$[N+1]
done
Script scr3.sh:
File content:
#!/bin/bash
N=1
until [ "$N" -gt "10" ]
do
echo "Number $N"
N=$[N+1]
done
Note: Common mistakes in shell scripting are usually due to incorrect syntax. For instance, there must be no spaces before and after the = assignment operator:
File content:
N=1 # correct
N =1 # error
N= 1 # error
N=$[N+1] # correct
N =$[N+1] # error
N= $[N+1] # error
6.10. Looping with the for Statement#
Script scr4.sh:
File content:
#!/bin/bash
for i in red white blue
do
echo "$i is a color"
done
Script backup-lots.sh:
File content:
#!/bin/bash
for i in 0 1 2 3 4 5 6 7 8 9 ; do
cp $1 $1.BAK-$i
done
The enumeration in the loop can be represented more concisely using brace expansion, {0..9}:
File content:
#!/bin/bash
for i in {0..9} ; do
cp $1 $1.BAK-$i
done
Now create a file named important_data with some numbers in it, and then run:
./backup-lots.sh important_data
This will copy the file 10 times with 10 different extensions. As you can see, the variable $1 has a special meaning: it represents the first argument passed on the command line. Watch out for correct syntax:
File content:
for i in {0..9}
do
....
done
Another correct alternative:
File content:
for i in {0..9} ; do
....
done
Now modify the script:
File content:
#!/bin/bash
if [ "$1" = "" ] ; then
echo "Usage: backup-lots.sh <filename>"
exit
fi
for i in {0..9} ; do
NEW_FILE=$1.BAK-$i
if [ -e $NEW_FILE ]; then
echo "backup-lots.sh: **warning** $NEW_FILE"
echo " already exists - skipping"
else
cp $1 $NEW_FILE
fi
done
Looping over glob expressions:
File content:
#!/bin/bash
for i in *.txt ; do
echo "found a file:" $i
done
Note that the script above loops over all .txt files in the current directory, so you will need to create several files with a .txt extension to test this exercise.
File content:
#!/bin/bash
for i in /usr/share/*/*.txt ; do
echo "found a file:" $i
done
Breaking out of loops and continuing:
File content:
#!/bin/bash
for i in {0..9} ; do
NEW_FILE=$1.BAK-$i
if [ -e $NEW_FILE ]; then
echo "backup-lots.sh: **error** $NEW_FILE"
echo " already exists - exiting"
break
else
cp $1 $NEW_FILE
fi
done
The break command causes program execution to continue on the line immediately following the done statement.
The continue statement is useful for terminating the current iteration of a loop and proceeding to the next:
File content:
#!/bin/bash
for i in {0..9} ; do
NEW_FILE=$1.BAK-$i
if [ -e $NEW_FILE ] ; then
echo "backup-lots.sh: **warning** $NEW_FILE"
echo " already exists - skipping"
continue
fi
cp $1 $NEW_FILE
done
6.11. Functions#
Function definitions provide a way to group statement blocks into a single reusable unit:
File content:
function usage ()
{
command1
command2; command3
......
}
The word function is optional. The following syntax works as well:
File content:
usage ()
{
command1
command2; command3
......
}
For example, create a script named myprog.sh with the following content:
File content:
#!/bin/bash
function usage ()
{
echo "Usage:"
echo " myprog.sh [--test|--help|--version]"
}
case $1 in
--test|-t)
echo "you used the --test option"
exit 0
;;
--help|-h)
usage
;;
--version|-v)
echo "myprog.sh version 0.0.2"
exit 0
;;
-*)
echo "Error: no such option $1"
usage
exit 1
;;
esac
echo "You typed "$1" on the command-line"
6.12. Using Quotes#
Single forward quotes (') protect the enclosed text from shell interpretation:
echo 'error $?'
echo 'shell name $0'
Double quotes (") allow all shell interpretations and variable expansions to take place:
echo "error $?" # outputs the error code of the last command
echo "shell name $0" # outputs the current shell name
Command substitution:
X=`expr 100 + 50 '*' 3`
echo $X
Assigning command output to a variable:
FSIZE=`wc -l /etc/profile`
which is equivalent to:
FSIZE=$(wc -l /etc/profile)
6.13. Introduction to awk#
The basic function of awk is to search files for lines or text units containing one or more patterns. When a line matches a pattern, specified actions are performed on it.
awk is very frequently used to extract specific columns from text based on matching criteria.
awk reads data from a file or a pipe using the following syntax:
awk 'EXPRESSION { PROGRAM }' file(s)
or
COMMAND | awk 'EXPRESSION { PROGRAM }'
To display usernames from /etc/passwd (field 1):
awk -F: '{ print $1 }' /etc/passwd
Where -F: specifies the field separator in the passwd file. The fields are separated by colons (:).
The default field separator in awk is a space or tab. awk scans the input file and splits each input line into fields accordingly.
Similarly:
cat /etc/passwd | awk -F: '{ print $1 }'
To display usernames, home directories, and login shells (fields 1, 6, and 7) and store them in a separate file named users.txt:
awk -F: '{ print $1, $6, $7 }' /etc/passwd > users.txt
or
cat /etc/passwd | awk -F: '{ print $1, $6, $7 }' > users.txt
Since the default field separator becomes an empty space when reading users.txt, we can print usernames from it using:
awk '{ print $1 }' users.txt
To extract only the columns of lines containing a specific pattern, such as hostadm:
awk '/hostadm/ { print $1 }' users.txt
We can use regular expressions inside the selection blocks (/ /). For example, to view permissions on all files in the /etc directory with a .conf extension:
ls -l /etc | awk '/.*\.conf/ { print $1, $9 }'
In the print command block, we can include custom strings for formatted output:
ls -l /etc | awk '/.*\.conf/ { print "Permission:", $1, "File:", $9 }'
Adding a conditional statement, such as printing files larger than 5000 bytes:
ls -l /etc | awk '/.*\.conf/ { if ( $5 > 5000 ) print "Permission:", $1, "Size", $5, "File:", $9 }'
BEGIN and END blocks allow you to print statements before and after the main awk processing output:
ls -l /etc | awk 'BEGIN { print "Conf Files > 5K found:\n" } /.*\.conf/ { if ( $5 > 5000 ) print "Permission:", $1, "Size", $5, "File:", $9 } END { print "\n ---- The end -------" }'
Recommended tutorial: The GNU awk programming language
6.14. Introduction to grep#
grep is used to search files or standard input for lines containing required patterns.
We will work with a text file named list.txt containing the following content:
File content:
Check the inode list today
reboot the machine tomorrow
Reboot it again in a week
Call Tech support in case of emergency.
tel: 834
Oop 0
Oops 1
Oopss 12
Oopsss 123
Oopssss 1234
End
To find lines containing the string “inode” in list.txt:
grep inode list.txt
To find lines containing “inode lis “ in list.txt:
grep "inode lis " list.txt
This will return nothing because that exact string does not exist in the file.
To search for lines containing “inode list” across all files in the current directory:
grep "inode list" *
The syntax for grep is:
grep [options] regex [files]
where regex represents regular expressions.
6.14.1. Using Regular Expressions#
Regular expressions consist of literals (plain text) and metacharacters (special control characters). They allow you to specify three core concepts: position anchors, groups and ranges, and quantity modifiers.
Anchors:
^ - matches at the beginning of a line
$ - matches at the end of a line
grep '^Call' list.txt
grep '^ Reboot' list.txt
grep 'today$' list.txt
To count the number of empty lines:
grep -c '^$' list.txt
To display all lines containing only the word “End” by itself:
grep '^End$' list.txt
Groups and ranges:
[abc] - matches any single character from a, b, or c
[a-e] - matches any single character within the range a to e
[^abc] - inverse match; matches any single character except a, b, or c
[^a-e] - inverse match; matches any single character outside the range a to e
\<word\> - matches the exact word (option -w can also be used)
. - matches any single character except a newline
\ - turns off the special meaning of the following character
grep '[Rr]eboot' list.txt
grep '\<[Rr]eboot\>' list.txt
Display all lines from list.txt containing three adjacent digits:
grep '[0-9][0-9][0-9]' list.txt
Display lines containing four or more consecutive characters:
grep '....' list.txt
Display all non-blank lines from list.txt:
grep '.' list.txt
Display all lines containing a literal period:
grep '\.' list.txt
Modifiers:
* - matches zero or more instances of the preceding single character
? - matches zero or one instance of the preceding regex (implies the 'grep -E' option)
+ - matches one or more instances of the preceding regex (implies the 'grep -E' option)
\{n,m\} - matches a range of occurrences; \{n\} matches exactly n occurrences; \{n,\} matches at least n occurrences
| - matches either the regex specified before or after the vertical bar (implies the 'grep -E' option)
Display all lines from list.txt containing “Oop”, “Oops”, “Oopss”, etc.:
grep 'Oops*' list.txt
Display all lines containing “Oops”, “Oopss”, etc.:
grep 'Oopss*' list.txt
Display lines containing two or more adjacent digits:
grep '[0-9][0-9][0-9]*' list.txt
Display lines containing the number ‘3’ or the combination ‘34’:
grep -E '34?' list.txt
Display lines containing at least one digit:
grep -E '[0-9]+' list.txt
Display lines containing “sss” and “ssss”:
grep 's\{3,4\}' list.txt
Display lines containing any 3, 4, or 5-digit numbers:
grep '[0-9]\{3,5\}' list.txt
Display lines containing “Reboot”, “reboot”, or “support”:
grep -E '[Rr]eboot|support' list.txt
Display lines containing any letter (no empty lines):
grep '[A-Za-z]' list.txt
Display lines containing any non-alphanumeric and non-space symbol:
grep '[^ 0-9A-Za-z]' list.txt
Display lines containing an uppercase letter followed by zero or more lowercase letters:
grep '[A-Z][a-z]*' list.txt
Display lines containing a 3-digit telephone number:
grep 'tel: [0-9]\{3\}' list.txt
Recommended tutorial on Regular expressions and grep
6.15. Introduction to sed#
The stream editor, sed, is used for editing lines in a file or a stream. Its output goes to standard output and can be redirected to a new file.
Syntax:
sed [options] 'command1' [files]
sed [options] -e 'command1' [-e command2 ...] [files]
sed [options] -f script [files]
To print lines 3 through 5 from list.txt:
sed -n '3,5p' list.txt
To delete lines 3 through 5 from list.txt:
sed '3,5d' list.txt
We can use regular expressions enclosed in / / to target specific blocks for commands.
For example, to delete lines starting with “O”:
sed '/^O/d' list.txt
To delete empty lines:
sed '/^$/d' list.txt
To translate capital letters C, R, and O into lowercase c, r, and o:
sed 'y/CRO/cro/' list.txt
To replace the first occurrence of “Oop” with “Wee” on each line:
sed 's/Oop/Wee/' list.txt
To remove the string “ss” (replace with an empty entry) for the first occurrence on a line:
sed 's/ss//' list.txt
To remove all occurrences of “ss” on a line:
sed 's/ss//g' list.txt
To substitute a single space for any sequence of multiple spaces on a line:
sed 's/ */ /g' list.txt
To substitute an underscore for any sequence of multiple spaces:
sed 's/ */_/g' list.txt
To make sed modify the original file content directly, use the -i (in-place) option.
For example, to apply changes to list2.txt:
cp list.txt list2.txt
sed -i 's/ */_/g' list2.txt
6.16. Printing Lines Containing a Matching Pattern Using grep, awk, and sed#
The following three commands all print out the exact same matching line from list.txt:
grep Reboot list.txt
awk '/Reboot/ { print $0 }' list.txt
sed -n '/Reboot/p' list.txt