Bash Scripting
Introduction
As an SRE, the Linux system sits at the core of our day to day work and so is bash scripting. It’s a scripting language that is run by Linux Bash Interpreter. Until now we have covered a lot of features mostly on a command line, now we will use this command line as an interpreter to write programs that will ease our day to day job as an SRE.
Writing the first bash script:
We will start with a simple program, we will use Vim as the editor during the whole journey.
#!/bin/bash
# This if my first bash script
# Line starting with # is commented
echo "Hello world!"
The first line of the script starting with “#!” is called she-bang. This is simply to let the system which interpreter to use while executing the script.
Any Line starting with “#” (other than #!) is referred to as comments in script and is ignored by the interpreter while executing the script. Line 6 shows the “echo” command that we would be running.
We will save this script as “firstscript.sh” and make the script executable using chmod
.
Next thing is to run the script with the explicit path. We can see the desired “Hello World!” as output.
Taking user input and working with variables:
Taking standard input using the read
command and working with variables in bash.
#!/bin/bash
#We will take standard input
#Will list all files at the path
#We will concate variable and string
echo "Enter the path"
read path
echo "How deep in directory you want to go:"
read depth
echo "All files at path " $path
du -d $depth -all -h $path
We are reading path in variable “path” and variable “depth” to list files and directories up to that depth. We concatenated strings with variables. We always use $
(dollar-sign) to reference the value it contains.
We pass these variables to the du
command to list out all the files and directories in that path upto the desired depth.
Exit status:
Every command and script when it completes executing, returns an integer in the range from 0 to 255 to the system, this is called exit status. “0” denotes success of the command while non-zero return code usually indicates various kinds of errors.
We use $?
special shell variable to get exit status of the last executed script or command.
Command line arguments and understanding If … else branching:
Another way to pass some values to the script is using command line arguments. Usually command line arguments in bash are accessed by $ followed by the index. The 0th index refers to the file itself, $1
to the first argument and so on. We use $#
to check the count of arguments passed to the script.
Making decisions in the programming language is it’s integral part, and to tackle different conditions we use if … else statements or some more nested variant of it.
The below script uses multiple concepts in one script. The aim of the script is to get some properties of the file.
Line 4 to 7 is the standard example of "if statement" in bash. Syntax is as explained below:
If [ condition ]; then
If_block_to_execute
else
else_block_to_execute
fi
fi is to close the if … else block. We are comparing count of argument($#) if it is equal to 1 or not. If not we prompt for only one argument and exit the script with status code 1(not a success). One or more if statements can exist without else statement but vice versa doesn’t make any sense.
Operator -ne is used to compare two integers, read as “integer1 not equal to integer 2”. Other comparison operators are:
Operations | Description |
---|---|
num1 -eq num2 | check if 1st number is equal to 2nd number |
num1 -ge num2 | checks if 1st number is greater than or equal to 2nd number |
num1 -gt num2 | checks if 1st number is greater than 2nd number |
num1 -le num2 | checks if 1st number is less than or equal to 2nd number |
num1 -lt num2 | checks if 1st number is less than 2nd number |
#!/bin/bash
# This script evaluate the status of a file
if [ $# -ne 1 ]; then
echo "Please pass one file name as argument"
exit 1
fi
FILE=$1
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 2
fi
exit 0
There are lots of file expressions to evaluate file,like in bash script “-e” in line 10 returns true if the file passed as argument exist, false otherwise. Below are the some widely used file expressions:
File Operations | Description |
---|---|
-e file | File exists |
-d file | File exists and is directory |
-f file | File exists and is regular file |
-L file | File exists and is symbolic link |
-r file | File exists and has readable permission |
-w file | File exists and has writable permission |
-x file | File exists and has executable permission |
-s file | File exists and size is greater than zero |
-S file | File exists and is a network socket. |
Exit status is 2 when the file is not found. And if the file is found it prints out the properties it holds with exit status 0(success).
Looping over to do a repeated task.
We usually come up with tasks that are mostly repetitive, looping helps us to code those repetitive tasks in a more formal manner. There are different types of loop statement we can use in bash:
Loop | Syntax |
---|---|
while | while [ expression ] do [ while_block_to_execute ] done |
for | for variable in 1,2,3 .. n do [ for_block_to_execute ] done |
until | until [ expression ] do [ until_block_to_execute ] done |
#!/bin/bash
#Script to monitor the server
hosts=`cat host_list`
while true
do
for i in $hosts
do
h="$i"
ping -c 1 -q "$h" &>/dev/null
if [ $? -eq 0 ]
then
echo `date` "server $h alive"
else
echo `date` "server $h is dead"
fi
done
sleep 60
done
Monitoring a server is an important part of being an SRE. The file “host_list” contains the list of host which we want to monitor.
We used an infinite “while” loop that will sleep every 60seconds. And for each host in the host_list we want to ping that host and check if that ping was successful with its exit status, if it’s successful we say server is live or it’s dead.
The output of the script shows it is running every minute with the timestamp.
Function
Developers always try to make their applications/programs in modular fashion so that they don’t have to write the same code every time and everywhere to carry out similar tasks. Functions help us achieve this.
We usually call functions with some arguments and expect result based on that argument.
The backup process we discussed in earlier section, we will try to automate that process using the below script and also get familiar with some more concepts like string comparison, functions and logical AND and OR operations.
In the below code “log_backup” is a function which won’t be executed until it is called.
Line37 will be executed first where we will check the no. of arguments passed to the script.
There are many logical operators like AND,OR, XOR etc.
Logical Operator | Symbol |
---|---|
AND | && |
OR | | |
NOT | ! |
Passing the wrong argument to script “backup.sh” will prompt for correct usage. We have to pass whether we want to have incremental backup of the directory or the full backup along with the path of the directory we want to backup. If we want the incremental backup we will an additional argument as a meta file which is used to store the information of previous backed up files.(usually a metafile is .snar extension).
#!/bin/bash
#Scripts to take incremental and full backup
backup_dir="/mnt/backup/"
time_stamp="`date +%d-%m-%Y-%Hh-%Mm-%Ss`"
log_backup(){
if [ $# -lt 2 ]; then
echo "Usage: ./backup.sh [backup_type] [log_path]"
exit 1;
fi
if [ $1 == "incremental" ]; then
if [ $# -ne 3 ]; then
echo "Usage: ./backup.sh [backup_type] [log_path] [meta_file]"
exit 3;
fi
tar --create --listed-incremental=$3 --verbose --verbose --file="${backup_dir}incremental-${time_stamp}.tar" $2
if [ $? -eq 0 ]; then
echo "Incremental backup succesful at '${backup_dir}incremental-${time_stamp}.tar'"
else
echo "Incremental Backup Failure"
fi
elif [ $1 == "full" ];then
tar cf "${backup_dir}fullbackup-${time_stamp}.tar" $2
if [ $? -eq 0 ];then
echo "Full backup successful at '${backup_dir}fullbackup-${time_stamp}.tar'"
else
echo "Full Backup Failure"
fi
else
echo "Unknown parameter passed"
echo "Usage: ./backup.sh [incremental|full] [log_path]"
exit 2;
fi
}
if [ $# -lt 2 ] || [ $# -gt 3 ];then
echo "Usage: ./backup.sh [incremental|full] [log_path]"
exit 1
elif [ $# -eq 2 ];then
log_backup $1 $2
elif [ $# -eq 3 ];then
log_backup $1 $2 $3
fi
exit 0
Passing all 3 arguments for incremental backup will take incremental backup at “/mnt/backup/” with each archive having timestamp concatenated to each file.
The arguments passed inside the function can be accessed via $
followed by the index. The 0th index refers to the function itself,
$1
to the first argument and so on. We use #$
to check the count of arguments passed to the function.
Once we pass the string “incremental” or “full” it gets compared inside the function and the specific block is executed. Below are some more operations that can be performed over strings.
String Operations | Description |
---|---|
string1 == string2 | Returns true if string1 equals string 2 otherwise false. |
string1 != string2 | Returns true if string NOT equal string 2 otherwise false. |
string1 ~= regex | Returns true if string1 matches the extended regular expression. |
-z string | Returns true if string length is zero otherwise false. |
-n string | Returns true if string length is non-zero otherwise false. |