You are on page 1of 27

Bash Scripting and Shell Programming

(Linux Command Line)

A simple script example


#!/bin/bash
echo "Scripting is fun!"

$ chmod 755 script.sh


./script.sh
Scripting is fun!
$

Shebang
If a script does not contain a shebang the commands are executed, using your
Shell.
Different shells have slightly varying syntax.

#!/bin/csh
echo "This script uses csh as the interpreter."

#!/bin/ksh
echo "This script uses ksh as the interpreter."

#!/bin/zsh
echo "This script uses zsh as the interpreter."

Also you can use another interpreter for your scripts.


#!/usr/bin/python
print "This is a Python script."

Execution

$ chmod 755 hi.py


$ ./hi.py
This is a Python script.
$

sleepy.sh
#!/bin/bash
sleep 90

$ ./sleepy.sh &
[1] 16796
$ ps -fp 16796
UID PID PPID c STIME TTY TIME CMD
jacks 16796 16725 0 22:50 pts/0 00:00:00
/bin/bash ./sleepy.sh
$

$ /tmp//sleepy.sh &
[1] 16804
$ ps -fp 16804
UID PID PPID c STIME TTY TIME CMD
jacks 16804 16725 0 22:51 pts/0 00:00:00
/bin/bash /tmp/sleepy.sh
$
$ ps -ef | grep 16804 | grep -v grep
jacks 16804 16725 0 22:51 pts/0 00:00:00
/bin/bash /tmp/sleepy.sh
jacks 16804 16725 0 22:51 pts/0 00:00:00
sleep 90
$ pstree -p 16804
sleepy.sh(16804)-----sleep(16805)
$

Variable
Storage locations that have a name
Name-value pairs
Syntax:

VARIABLE_NAME="Value"

Variables are case sensitive


By convention variables are uppercase.

Variable Usage
#!/bin/bash
MY_SHELL="bash"
echo "I like the $MY_SHELL shell."

#!/bin/bash
MY_SHELL="bash"
echo "I like the ${MY_SHELL} shell."

The curly brace syntax is optional, unless you need to immediately precede
or follow the variable with additional data.

Example

Lets say you want to add the letter 'ing' to the value
#!/bin/bash
MY_SHELL="bash"
echo "I like the ${MY_SHELL}ing on my keyboard."

Note
you can't do 'echo "I like the $MY_SHELLing on my keyboard." '
Because the shell will interpret as variable name MY_SHELLing

Assign command output to a variable


To do this, enclose the command in parenthesis

#!/bin/bash
SERVER_NAME=$(hostname)
echo "You are running this script on ${SERVER_NAME}."

Old syntaxe the back ticks

#!/bin/bash
SERVER_NAME=`hostname`
echo "You are running this script on ${SERVER_NAME}."

Variables name
Here some example of valid and invalid for variable names

Note
Also is a convention that variables names be uppercase

Valid:

FIRST3LETTERS="ABC"
FIRST_THREE_LETTERS="ABC"
firstThreeLetters="ABC"

Invalid:

3LETTERS="ABC"
first-three-letters="ABC"
first@Three@Letters="ABC"
TESTS
For test a condition

Syntax:
[ condition-to-test-for]

example:

Test if a file exists


[ -e /etc/passwd]

Others file test operators


Operator Description

-d True if file is a directory

-e True if file exists

-f True if file exists and is a regular file

-r True if file is readable by you

-s True if file exists and is not empty

-w True if file is writable by you

-x True if file is executable by you

'man test' for more information

Strings operators (tests)


Operator Description

-z STRING True if string is empty

-n STRING True if string is not empty

String1=String2 True if the string are equal

String1!=String2 True if the string are not equal


Arithmetic operators (tests)
Operator Description

arg1 -eq arg2 True if arg1 is equal to arg2

arg1 -ne arg2 True if arg1 is not equal to arg2

arg1 -lt arg2 True if arg1 is less than arg2

arg1 -le arg2 True if arg1 is less than or equal to arg2

arg1 -gt arg2 True if arg1 is greater than arg2

arg1 -ge arg2 True if arg1 is greater or equal to arg2

Making Decisions - The If statement

The If statement
Syntax:

if [ condition-is-true ]
then
command 1
command 2
command n
fi

An example:

#!/bin/bash
MY_SHELL="bash"

if [ "$MY_SHELL" = "bash" ]
then
echo "You seem to like the bash shell."
fi

The Else statement


Syntax:
if [ condition-is-true ]
then
command n
else
command n
fi

An example:

#!/bin/bash
MY_SHELL="csh"

if [ "$MY_SHELL" = "bash" ]
then
echo "You seem to like the bash shell."
else
echo "You don't seem to like the bash shell."
fi

The Else/if (elif) statement


Syntax:

if [ condition-is-true ]
then
command n
elif [ condition-is-true ]
then
command n
else
command n
fi

Note
You can also test for mutiple conditions using the 'elif'

An example:
#!/bin/bash
MY_SHELL="csh"

if [ "$MY_SHELL" = "bash" ]
then
echo "You seem to like the bash shell."
elif [ "$MY_SHELL" = "csh" ]
then
echo "You seem to like the csh shell."
else
echo "You don't seem to like the bash or csh shell."
fi

For loop
For iterate N times you can use the for loop

Syntax:

for VARIABLE_NAME in ITEM_1 ITEM_N


do
command 1
command 2
command N
done

An example:

#!/bin/bash
for COLOR in red green blue
do
echo "COLOR: $COLOR"
done

Output:

COLOR:red
COLOR:green
COLOR:blue

Renaming a result of ls command


This script renames all of the files that end in jpg.
#!/bin/bash
PICTURES=$(ls *jpg)
DATE=$(date +%F)

for PICTURE in $PICTURES


do
echo "Renaming ${PICTURE} to ${DATE} -${PICTURE}"
mv ${PICTURE} ${DATE}-${PICTURE}
done

Example

(base) ~/testeimg ./rename-pics.sh


Renaming im1.jpg to 2021-05-15 -im1.jpg
Renaming img2.jpg to 2021-05-15 -img2.jpg
Renaming img3.jpg to 2021-05-15 -img3.jpg

Positional Parameters
The value of parameter can be accessed by special syntax like n, beginwith0 that will contain tha
name of the program, until many parameters was passed

Example

> $ script.sh parameter1 parameter2 parameter3

Will be contain

var value

$0 "script.sh"

$1 "parameter1"

$2 "parameter2"

$3 "parameter3"

Archive home directory


#!/bin/bash

echo "Executing script: $0"


echo "Archiving user: $1"

# Lock the account


passwd -l $1

# Create an archive of the home directory.


tar cf /archives/${1}.tar.gz /home/${1}

Note
You can assign this Positional parameter like
USER=$1

Ther also a special character to access all the positional parameter

$@ accessing the positional parameters


Example:

#!/bin/bash

echo "Executing script: $0"


for USER in $@
do
echo "Archiving user: $USER"
# Lock the account
passwd -l $USER
# Create an archive of the home directory
tar cf /archives/${USER}.tar.gz /home/${USER}
done

Note
Using the  $@  you can pass multiple user, how many needed.

Accepting User Input (STDIN)


The read command accepts STDIN.

Syntax:

read -p "PROMPT" VARIABLE


Archive home directory (STDIN version)
#!/bin/bash

read -p "Enter a user name: " USER


echo "Archiving user: $USER"

# Lock the account


passwd -l $USER

# Create an archive of the home directory


tar cf /archives/${USER}.tar.gz /home/${USER}

Sumary 1
 #!/path/to/interpreter 
 VARIABLE_NAME="Value" 
 $VARIABLE_NAME 
 ${VARIABLE_NAME} 
 VARIABLE_NAME=$(command) 
If

if [ condition-is-true ]
then
commands
elif[ condition-is-true ]
then
commands
else
commands
fi

For loop

for VARIABLE_NAME in ITEM_1 ITEM_N


do
command 1
command 2
command N
done

Positional Parameters:
 $0, $1, $2 ... $9 
 $@ 
Comments  # This will be ignored 
To accept input use read

Exit Status/Return Code


Every command returns an exit Status
Range from 0 to 255
0 = success
Other than 0 = error condition
Uses for error checking
Use man or info to find meaning of exit status

Checking the exit Status


$? contains the return code of the previously executed command.

Example

> $ ls /note/here
echo "$?"

Output:
ls: cannot access '/note/here': No such file or directory
2

Using an exit status (example)


HOST="google.com"
ping -c 1 $HOST
# the -c option is to specify the quantity of packages to sent
# In this case only one package is being send.
if [ "$?" -eq "0" ]
then
echo "$HOST reachable."
else
echo "$HOST unreachable."
fi

# For better readability you can give meaning variables names`


HOST="google.com"
ping -c 1 $HOST
RETURN_CODE=$? # This line has changed <---
# the -c option is to specify the quantity of packages to sent
# In this case only one package is being send.
if [ "$RETURN_CODE" -ne "0" ]
then
echo "$HOST unreachable."
fi

Note this example only check for an error, but you can
implement some error handling.

(And,OR) && and ||


&& = AND

mkdir /tmp/bak && cp test.text /tmp/bak/

Note
If the mkdir fails the cp command will not be executed

|| = OR

cp test.text /tmp/bak/ || cp test.txt /tmp

Note
If the cp test.text fails, the cp test.txt will be executed.
But if the first commands executed with rc=0, the second will NOT be executed.

Example AND:
#!/bin/bash
HOST="google.com"
ping -c 1 $HOST && echo "$HOST reachable."

Note
In this example, if the ping command exits with a 0 exit status, then "google.com reachable" will
be echoed to the screen.

Example OR:

#!/bin/bash
HOST="google.com"
ping -c 1 $HOST || echo "$HOST unreachable."

Note
In this example, if the ping command fails, then "google.com unreachable" will be echoed to the
screen.
If the ping succeeds, the echo will not be executed.

The semicolon
Separate commands with a semicolon to ensure they all get executed. No matter if the previous
command fails, the next will execute.

cp test.txt /tmp/bak ; cp test.txt /tmp

Same as:
cp test.txt /tmp/bak
cp test.txt /tmp/

Exit Command
To explicitly define the return code you can use exit.
The exit allow number from 0 to 255.

exit 0
exit 1
exit 2
exit 255
The default values is that of the last command executed.

An example of controlling the exit status


#!/bin/bash
HOST="google.com"
ping -c 1 $HOST
if [ "$?" -ne "0" ]
then
echo "$HOST unreachable."
exit 1
fi
exit 0

Sumary Exit
All command return an exit status
0-255 is the range allowed for exit status
0= success
Other than 0 = error condition
$? contains the exit StatusDecision making - if, &&,||
exit

Using man
When you type  man <command>  for search for a patter you can:

type  /  slash to start a search


if you press  n  you go to the next occurrence of the - pattern.
Press q to exit the man

Functions (Keep it DRY)


Don't repeat yourself! Don't repeat yourself!
Write once, use many times
Reduces script length.
Single place to edit and troubleshoot.
Easier to maintain.
Note
If you are repeating yourself, use a function.
Must be defined before use
Has parameter support

Creating a function
# Explicitly
function function-name() {
# Code goes here.
}

Another way
# Implicitly is the same but withou the keyword function
function-name() {
# Code goes here.
}

Note
To call or execute a function, you just need to call the name.

#!/bin/bash
function hello() {
echo "Hello!"
}
hello

Be aware that functions can call other functions

#!/bin/bash
function hello() {
echo "Hello!"
now
}
function now() {
echo "It's $(date +%r)"
}
hello

As said before the function should be declared before used.


So this WILL NOT work
# DON'T DO THIS
#!/bin/bash
function hello() {
echo "Hello!"
now
}
hello
function now() {
echo "It's $(date +%r)"
}

Note
you don't need the parenthesis, just te name, if you need to provide
some arguments (parameters), they will follow the name
with a space as separator.

An example with parameters


#!/bin/bash
function hello() {
echo "Hello $1"
}
# Using
hello Jason
# Output:
# Hello Jason

For loop of all parameter you can use the same strategy of the for loop with $@

#!/bin/bash
function hello() {
for NAME in $@
do
echo "Hello $NAME"
done
}
hello Jason Dan Ryan

Variable Scope
By default, variable are global
Variables have to be defined before used.
Note
One attention will be on the declaration point, if you try to use a variable that wasn't declared
before you will get a null point.

my_function
GLOBAL_VAR=1

The variable are global but my_function will not get access To
because was declared after my_function call.

Other case:

#!/bin/bash
my_function() {
GLOBAL_VAR=1
}
# GLOBAL_VAR not available yet
echo $GLOBAL_VAR
my_function
# GLOBAL_VAR is NOW available
echo $GLOBAL_VAR

Note
On the first echo you don't get any value because the execution of the function has not yet take
place.
And is on the execution that declaration will create the GLOBAL_VAR.

Local Variables
Can only be accessed within the function
Create using the  local  keyword.
 local LOCAL_VAR=1 
Only function can have local variables.
Best practice to keep variables local in functions.

Exit Status (Return Codes)


Functions have an exit status
Explicitly
 return <RETURN_CODE> 
Implicitly
The exit status of the last command executed in the function
Valid exit codes range from 0 to 255
0 = success
$? = the exit status

Function Example (backup)


function backup_file() {
if [ -f $1 ]
then
BACK="/tmp/$(basename $(1)).$(date +%F).$$"
echo "Backing up $1 to $(BACK)"
cp $1 $BACK
fi
}
backup_file /etc/hosts
if [ $? -eq 0 ]
then
echo "Backup succeeded!"
fi

With some variation on the if

function backup_file() {
if [ -f $1 ]
then
local BACK="/tmp/$(basename $(1)).$(date +%F).$$"
echo "Backing up $1 to $(BACK)"
# The exit status of the function will
# be the exit status of the cp command.
cp $1 $BACK
else
# The file does not exist.
return 1
fi
}
backup_file /etc/hosts
if [ $? -eq 0 ]
then
echo "Backup succeeded!"
fi

We can also do some other error handling


backup_file /etc/hosts

# Make a decision based on the exit status.


if [ $? -eq 0 ]
then
echo "Backup succeeded!"
else
echo "Backup failed!"
# About the script and return a non-zero exit status.
exit 1
fi

Named Character Classes


You can use predefined named character classes

 [[:alpha:]]  - matches alphabetic letters


 [[:alnum:]]  - matches alphanumeric characters
 [[:digit:]]  - matches any number
 [[:lower:]]  - matches any lowercase letter
 [[:space:]]  - matches any space *including new lines
 [[:upper:]]  - matches any uppercase letter

Loop with wild card


#!/bin/bash
cd /var/www
for FILE in *.html
do
echo "Copying $FILE"
cp $FILE /var/www-just-html
done

Case Statement
Alternative to if statement
if[ "$VAR"="one"]
elif[ "$VAR"="two"]
elif[ "$VAR"="three"]
elif[ "$VAR"="four"]
case "$VAR" in
pattern_1)
# Commands go here.
;;
pattern_N)
# Commands go here
;;
esac

If the pattern matches the commands will be executed until reach a  ;; 

An example:

case "$1" in
start)
/usr/sbin/sshd
;;
stop)
kill $(cat /var/run/sshd.pid)
;;
esac

Note
if you pass START with capital letters nothing will happen Because
The bash is case sensitive.

A modified version will take case about the any other case like a default

case "$1" in
start)
/usr/sbin/sshd
;;
stop)
kill $(cat /var/run/sshd.pid)
;;
*)
echo "Usage: $0 start|stop" ; exit 1
;;
esac

Note
The  *)  will be executed if any other pattern matches.

Another improvement
case "$1" in
start|START)
/usr/sbin/sshd
;;
stop|STOP)
kill $(cat /var/run/sshd.pid)
;;
*)
echo "Usage: $0 start|stop" ; exit 1
;;
esac

The  |  will be a or, so in this case both start and START will be a match for the first, and stop or
STOP also will be a match for the second
doing this we will take care of the lower and upper case.

Asking for
The input is stored in the variable ANSWER
Here we are using the character class to filter

read -p "Enter y or n: " ANSWER


case "$ANSWER" in
[yY]|[yY][eE][sS])
echo "You answered yes"
;;
[nN]|[nN][oO])
echo "You answered no"
;;
*)
echo "Invalid answer"
;;
esac

While Loop
While loop is a loop that repeats a series of commands fo as long
the condition be true, if the condition fails with a exit status
different from 0 the loop will stop.
while [ CONDITION_IS_TRUE ]
do
command 1
command 2
command n
done

Note
If the condition is never true, than the commands inside the while never will be executed too.

Infinite loop:

Note
If the condition never changes from true to false inside the while loop, than you never break the
loop, this will be a infinite loop.
Than you can use  control + C  to interrupt, or kill the process.

If you want a condition that will be always true, you can use  true  keyword.

while true
do
command N
sleep 1
done

Looping N times (example)


Here is an example of how to use a while loop to start and end at specific amount of times.

INDEX=1
while [ $INDEX -lt 6 ]
do
echo "Creating project-${INDEX}"
mkdir /usr/local/project-${INDEX}
((INDEX++))
done

Checking user input (example)


while [ "$CORRECT" != "y" ]
do
read -p "Enter your name: " NAME
read -p "Is ${NAME} correct? " CORRECT
done
Return code of Command (example)
while ping -c 1 app1 >/dev/null
do
echo "app1 still up..."
sleep 5
done

echo "app1 down, continuing."

Note
the  >/dev/null  is for to discard the output, and get only the return code.

Reading a file, line-by-line


LINE_NUM=1
while read LINE
do
echo "${LINE_NUM}: ${LINE}"
((LINE_NUM++))
done < /etc/fstab

Pipe a command

grep xfs /etc/fstab | while read LINE


do
echo "xfs: ${LINE}"
done

More complex

We are assigning a variables to the fields

FS_NUM=1
grep xfs /etc/fstab | while read FS MP REST
do
echo "${FS_NUM}: file system: ${FS}"
echo "${FS_NUM}:mount point: ${MP}"
((FS_NUM++))
done

read: read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars]
[-p prompt] [-t timeout] [-u fd] [name ...]
Read a line from the standard input and split it into fields.

Reads a single line from the standard input, or from file descriptor FD
if the -u option is supplied. The line is split into fields as with word
splitting, and the first word is assigned to the first NAME, the second
word to the second NAME, and so on, with any leftover words assigned to
the last NAME. Only the characters found in $IFS are recognized as word
delimiters.

If no NAMEs are supplied, the line read is stored in the REPLY variable.

Options:
-a array assign the words read to sequential indices of the array
variable ARRAY, starting at zero
-d delim continue until the first character of DELIM is read, rather
than newline
-e use Readline to obtain the line in an interactive shell
-i text use TEXT as the initial text for Readline
-n nchars return after reading NCHARS characters rather than waiting
for a newline, but honor a delimiter if fewer than

NCHARS characters are read before the delimiter


-N nchars return only after reading exactly NCHARS characters, unless
EOF is encountered or read times out, ignoring any
delimiter
-p prompt output the string PROMPT without a trailing newline before
attempting to read
-r do not allow backslashes to escape any characters
-s do not echo input coming from a terminal
-t timeout time out and return failure if a complete line of
input is not read within TIMEOUT seconds. The value of the
TMOUT variable is the default timeout. TIMEOUT may be a
fractional number. If TIMEOUT is 0, read returns
immediately, without trying to read any data, returning
success only if input is available on the specified
file descriptor. The exit status is greater than 128
if the timeout is exceeded
-u fd read from file descriptor FD instead of the standard input

Exit Status:
The return code is zero, unless end-of-file is encountered, read times out
(in which case it's greater than 128), a variable assignment err

A simple menu with while


while true
do
read -p "1: Show disk usage. 2: Show uptime. " CHOICE
case "$CHOICE" in
1)
df -h
;;
2)
uptime
;;
*)
break
;;
esac
done

Continue keyword
mysql -BNe 'show databases' | while read DB
do
db-backed-up-recently $DB
if [ "$?" -eq "0" ]
then
continue
fi
backup $DB
done

Any command that follow the continue statement in the loop will be executed. Execution
continues back at the top of the loop and the
while condition is examined again.
Here we are looping through a list of MySQL databases, the -B option to MySQL disables the
ASCII table output that MySQL normally displays.
The -N option suppresses the column names in the output. Hidding the headers.
The -e option causes MySQL to execute the command that follow it.
MySQL is showing a database per line of output (the show databases)
That is piped to a while loop
the read assigns the input to the DB variable. First we check if the database has been backed up
recently this is a script ( db-backed-up-recently). If return 0, the database is passed to it has
backed up in the las 24 hr, otherwise returns a 1.
we use a if to check the return code of that script
if the database was backed up, we call the continue, restarting the process.
when this if get a false we execute the last of the lines below the continue.

References
reference
bash-scripting course Udemy
https://linuxhint.com/bash_read_command/#:~:text=Read is a bash builtin,taking input from
standard input.

You might also like