Bash scripts are sets of the same commands that can be entered from the keyboard, but compiled into a single file and united by some common purpose. This approach allows you to automate a lot of routine tasks such as building projects or installing new programs. Bash is easy to learn and use, flexible and somehow present in the vast majority of Linux distributions.
Bash scripts have an extension .sh
:
$ touch script.sh
It is good practice to specify the path to your terminal at the beginning of each script:
#! /bin/bash
This technique is called shebang, you can read more about it here.
A list of available terminals on your system can be viewed with this command:
$ cat /etc/shells
- Bash scripts by example
- Content
- Hello world
- Comments
- Variables
- User input
- Passing arguments
- Conditions if else
- Comparison operators
- Logical operators
- Arithmetic operators
- Switch case
- Arrays
- While loop
- Until loop
- For loop
- Select loop
- Break and continue keywords
- Functions
- Local variables
- Keyword readonly
- Signal processing
- Debugging scripts
- Additional materials
#! /bin/bash
echo "Hello world"
Running a script:
$ bash script.sh
The script can be made into an executable file and run without the bash
command:
$ chmod +x script.sh
$ ./script.sh
One line comments:
# This is comment
# And that too
echo "Hello from bash" # this command outputs a line into the console
Multiline comments:
: 'Multi-line comments are very handy
to describe your scripts in detail.
Use them wisely!'
MY_STRING="bash is cool"
echo $MY_STRING # Output the value of a variable
The variable name must not begin with a number
The read
command reads user input and writes it to the specified variable:
echo "Input your name:"
read NAME
echo "Hello, $NAME!"
If no variable is specified, the
read
command will by default save all data to theREPLY
variable
You can write multiple variables. To do this, when entering from the terminal, the values must be separated by a space:
read V1 V2 V3
echo "1st var: $V1"
echo "2nd var: $V2"
echo "3rd var: $V3"
$ bash script.sh
$ hello world some other text
1st var: hello
2nd var: world
3rd var: some other text
The -a
flag allows you to create an array in which user input lines separated by spaces will be written:
read -a NAMES
echo "Array of names: ${NAMES[0]}, ${NAMES[1]}, ${NAMES[2]}"
$ bash script.sh
Alex Mike John
Array of names: Alex, Mike, John
The -p
flag allows you not to carry user input to the next line.
The -s
flag allows you to hide the characters you enter (as you do when entering a password).
read -p "Enter your login: " LOGIN
read -sp "Enter your password: " PASSWD
$ bash script.sh
Enter your login: bash_hacker
Enter your password: ******
Arguments are simply values that can be specified when running the script.
All passed arguments are assigned a unique name equal to their ordinal number:
echo "Argument 1 - $1; argument 1 - $2; argument 1 - $3."
$ bash script.sh hello test 1337
Argument 1 - hello; argument 1 - test; argument 1 - 1337.
The null argument is always the name of the script file:
echo "You have run the file $0"
$ bash script.sh
You have run the file script.sh
All arguments can be put into a named array:
args=("$@")
echo "Arguments received: ${args[0]}, ${args[1]}, ${args[2]}."
$ bash script.sh some values 123
Arguments received: some, values, 123
@
- is the name of the default array that stores all the arguments (except for the null one)
The number of arguments passed (with the exception of zero) is stored in the #
variable:
echo "Total arguments received: $#"
Conditions always start with the keyword if
and end with fi
:
echo "Enter your age:"
read AGE
if (($AGE >= 18))
then
echo "Access allowed"
else
echo "Access denied"
fi
$ bash script.sh
Enter your age:
19
Access allowed
$ bash script.sh
Enter your age:
16
Access denied
There can be as many conditions as you want, for this purpose the construct elif
is used, which also as if
can check conditions:
read COMMAND
if [ $COMMAND = "help" ]
then
echo "Available commands:"
echo "ping - will return the PONG string"
echo "version - returns the version of the program"
elif [ $COMMAND = "ping" ]
then
echo "PONG"
elif [ $COMMAND = "version" ]
then
echo "v1.0.0"
else
echo "The command is not defined. Use the 'help' command for help."
fi
Note that the
if
andelif
constructions are always followed by a line with the keywordthen
.
Also remember to separate conditions with spaces inside the curly braces ->[ condition ]
.
Different comparison operators can be used for numbers and strings. Their complete lists with examples are given in the tables below.
Note that different operators are used with certain brackets
Operator | Description | Example |
---|---|---|
-eq | is equal to | if [ $age -eq 18 ] |
-ne | does not equal | if [ $age -ne 18 ] |
-gt | more than | if [ $age -gt 18 ] |
-ge | greater than or equal to | if [ $age -ge 18 ] |
-lt | less than | if [ $age -lt 18 ] |
-le | less than or equal to | if [ $age -le 18 ] |
> | more than | if (($age > 18)) |
< | less than | if (($age < 18)) |
=> | greater than or equal to | if (($age => 18)) |
<= | less than or equal to | if (($age <= 18)) |
Operator | Description | Example |
---|---|---|
= | equality check | if [ $str = "hello" ] |
== | equality check | if [ $str == "hello" ] |
!= | check for NOT equality | if [ $str != "hello" ] |
< | comparison less than ASCII character code | if [[$str < "hello"]] |
> | comparison of more than the ASCII character code | if [[$str > "hello"]] |
-z | check if the string is empty | if [ -z $str ] |
-n | check if there is at least one character in the string | if [ -n $str ] |
There are also operators for checking various conditions on files:
Operator | Description | Example |
---|---|---|
-e | checks if a file exists | if [ -e $file ] |
-s | checks if a file is empty | if [ -s $file ] |
-f | checks if the file is a normal file and not a directory or a special file | if [ -f $file ] |
-d | checks if the file is a directory | if [ -d $file ] |
-r | checks if the file is readable | if [ -r $file ] |
-w | checks if the file is writable | if [ -w $file ] |
-x | checks if the file is executable | if [ -x $file ] |
Conditions with the "AND" operator return true only when all conditions are true.
There are several options for writing conditions with logical operators
if [ $age -ge 18 ] && [ $age -le ]
if [ $age -ge 18 -a $age -le ]
if [[ $age -ge 18 && $age -le ]]
Conditions with an OR operator return true when at least one condition is true.
if [ -r $file ] || [ -w $file ]
if [ -r $file -o -w $file ]
if [[ -r $file || -w $file ]]
num1=10
num2=5
# Addition
echo $((num1 + num2)) # 15
echo $(expr $num1 + $num2) # 15
# Subtraction
echo $((num1 - num2)) # 5
echo $(expr $num1 - $num2) # 5
# Multiplication
echo $((num1 * num2)) # 50
echo $(expr $num1 \* $num2) # 50
# Division
echo $((num1 / num2)) # 2
echo $(expr $num1 / $num2) # 2
# Residue from division
echo $((num1 % num2)) # 0
echo $(expr $num1 % $num2) # 0
Note that when using multiplication with the
expr
keyword you must use a slash.
It is not always convenient to use if/elif constructions for a large number of conditions. The case construct is better suited for this purpose:
read COMMAND
case $COMMAND in
"/help" )
echo "You opened the help menu" ;;
"/ping" )
echo "PONG" ;;
"/version" )
echo "Version: 1.0.0" ;;
* )
echo "There is no such command :(" ;;
esac
The case with the asterisk * will only work if none of the conditions above fit.
Arrays allow you to store an entire collection of data in a single variable. This variable can be conveniently and easily interacted with.
array=('aaa' 'bbb' 'ccc' 'ddd')
echo "Array elements: ${array[@]}"
echo "First element of the array: ${array[0]}"
echo "Array element indexes: ${!array[@]}"
array_length=${#array[@]}
echo "Array length: ${array_length}"
echo "The last element of the array: ${array[$((array_length - 1))]}"
$ bash script.sh
Array elements: aaa bbb ccc ddd
First element of the array: aaa
Array element indexes: 0 1 2 3
Array length: 4
The last element of the array: ddd
Note that the array elements are separated by a space without a comma.
Array elements can be added/rewritten/deleted as the script runs:
array=('a' 'b' 'c')
array[3]='d'
echo ${array[@]} # a b c d
array[0]='x'
echo ${array[@]} # x b c d
array[0]='x'
echo ${array[@]} # x b c d
unset array[2]
echo ${array[@]} # x b d
The while loop repeats the execution of the block of code described between the do
- done
keywords until the given condition is true.
i=0
while (( $i < 5 ))
do
i=$((i + 1))
echo "Iteration number $i"
done
$ bash script.sh
Iteration number 1
Iteration number 2
Iteration number 3
Iteration number 4
Iteration number 5
The operation of increasing a number by 1 unit is called an increment, and there is a special notation for it:
(( i++ )) # post increment
(( ++i )) # pre increment
The opposite operation is decrement:
(( i-- )) # post decrement
(( --i )) # pre decrement
With while cycles you can read different files line by line. There are several ways to do this:
echo "Reading a file line by line:"
while read line
do
echo $line
done < text.txt
echo "Reading a file line by line:"
cat text.txt | while read line
do
echo $line
done
echo "Reading a file line by line:"
while IFS='' read -r line
do
echo $line
done < text.txt
The until loop is opposite to the while loop in that it executes the block of code described between the do
- done
keywords when the given condition returns false:
i=5
until (( $i == 0 )) # will be executed until i equals 0
do
echo "Value of the variable i = $i"
(( i-- ))
done
$ bash script.sh
Value of the variable i = 5
Value of the variable i = 4
Value of the variable i = 3
Value of the variable i = 2
Value of the variable i = 1
The most classic cycle.
for (( i=1; i<=10; i++ ))
do
echo $i
done
In newer versions of Bash there is a more convenient way to write using the in
operator:
for i in {1..10}
do
echo $i
done
The condition after the in
keyword generally looks like this:
{START..END..INCREMENT}
START - which number to start the cycle with;
END - up to which number to continue the cycle;
INCREMENT - by how much to increment the start number after each iteration (1 β by default).
The for loop can be used to run a set of commands sequentially:
for command in ls pwd date # List of commands to run
do
echo "---Running a command $command---"
$command
echo "------------------------"
done
$ bash script.sh
---Running a command ls---
script.sh text.txt
------------------------
---Running a command pwd---
/home/user/bash
------------------------
---Running a command date---
Sun Jan 22 10:35:51 AM +03 2023
------------------------
Extremely handy loop for creating a menu of option selections.
select color in "Red" "Green" "Blue" "White"
do
echo "You have selected $color color..."
done
$ bash script.sh
1) Red
2) Green
3) Blue
4) White
#? 1
You have selected Red color...
#? 2
You have selected Green color...
#? 3
You have selected Blue color...
#? 4
You have selected White color...
The select
loop combines very well with the case
selection operator. This way you can very easily create interactive console applications with a lot of branching:
echo "---Welcome to the menu---"
select cmd in "Run" "Settings" "About" "Exit"
do
case $cmd in
"Run")
echo "The program is running"
echo "Enter the number:"
read input
echo "$input squared = $(( input * input ))" ;;
"Settings")
echo "Program Settings" ;;
"About")
echo "Version 1.0.0" ;;
"Exit")
echo "Exiting the program..."
break ;;
esac
done
The keyword break
is used to force an exit from the loops:
count=1
while (($count)) # always returns the truth
do
if (($count > 10))
then
break # forced exit in spite of the condition after while
else
echo $count
((count++))
fi
done
The continue
keyword is used to skip the current iteration of the loop and go to the next one:
for (( i=5; i>0; i-- ))
do
if ((i % 2 == 0))
then
continue
fi
echo $i
done
$ bash script.sh
5
3
1
Functions are named code sections that can be reused an unlimited number of times.
hello() {
echo "Hello World!"
}
# Call the function 3 times:
hello
hello
hello
$ bash script.sh
Hello World!
Hello World!
Hello World!
Functions, just like scripts themselves, can accept arguments. They have the same names, but function arguments are only visible inside the function to which they have been passed:
echo "$1" # argument passed when running the script
calc () {
echo "$1 + $2 = $(($1 + $2))"
}
# passing two arguments to the calc function
calc 42 17
$ bash script.sh hello
hello
42 + 17 = 59
If we declare a variable and then declare another one with the same name, but inside the function, we have an overwrite:
VALUE="hello"
test() {
VALUE="linux"
}
test
echo $VALUE
$ bash script.sh
linux
To prevent this behavior, the local
keyword is used in front of the variable name that is declared inside a function:
VALUE="hello"
test() {
local VALUE="linux"
echo "Variable inside a function: $VALUE"
}
test
echo "Global variable: $VALUE"
$ bash script.sh
Variable inside a function: linux
Global variable: hello
By default, every variable created in Bash can subsequently be overwritten. To protect a variable from changes you can use the readonly
keyword:
readonly PI=3.14
PI=100
echo "PI = $PI"
$ bash script.sh
script.sh: line 2: PI: readonly variable
PI = 3.14
readonly
can be used not only at the time the variable is declared, but also afterwards:
VALUE=123
VALUE=$(($VALUE * 1000))
readonly VALUE
VALUE=555
echo $VALUE
$ bash script.sh
script.sh: line 4: VALUE: readonly variable
123000
The same is true for functions. They can also be overridden, so you can protect them with readonly
with the -f
flag:
test() {
echo "This is test function"
}
readonly -f test
test() {
echo "Hello World!"
}
test
$ bash script.sh
script.sh: line 9: test: readonly function
This is test function
During the execution of scripts, unexpected actions may occur. For example, the user may interrupt the execution of the script with a combination of Ctrl + C
, or may accidentally close the terminal or some error may occur in the script itself and so on...
In POSIX-systems, there are special signals - process notifications of some event. Their list is defined in the table below:
Signal | Code | Action | Description |
---|---|---|---|
SIGHUP | 1 | Terminate | Closing the terminal |
SIGINT | 2 | Terminate | Interrupt signal (Ctrl-C) from the terminal |
SIGQUIT | 3 | Terminate (core dump) | The "Quit" signal from the terminal (Ctrl-) |
SIGILL | 4 | Terminate (core dump) | Invalid CPU instruction |
SIGABRT | 6 | Terminate (core dump) | Signal sent by abort() |
SIGFPE | 8 | Terminate (core dump) | Erroneous arithmetic operation |
SIGKILL | 9 | Terminate | Process killed |
SIGSEGV | 11 | Terminate (core dump) | Violation when accessing memory |
SIGPIPE | 13 | Terminate | Writing to a broken connection (pipe, socket) |
SIGALRM | 14 | Terminate | Alarm expires when the time set by alarm() expires |
SIGTERM | 15 | Terminate | End signal (the default signal for the kill utility) |
SIGUSR1 | 30/10/16 | Terminate | User-defined signal β 1 |
SIGUSR2 | 31/12/17 | Terminate | User-defined signal β 2 |
SIGCHLD | 20/17/18 | Ignore | Subsidiary process completed or stopped |
SIGCONT | 19/18/25 | Continue | Continue executing the previously stopped process |
SIGSTOP | 17/19/23 | Stop | Stopping the process execution |
SIGTSTP | 18/20/24 | Stop | Stop signal from the terminal (Ctrl-Z) |
SIGTTIN | 21/21/26 | Stop | Attempting to read from the terminal by a background process |
SIGTTOU | 22/22/27 | Stop | Attempting to write to the terminal by a background process |
Bash has a keyword trap
which can be used to catch different signals and provide for certain commands:
trap <COMMAND> <SIGNAL>
Under the signal you can use its name (Signal column in the table), or its code (Code column in the table). You can specify several signals, separating their names or codes with a space.
Exceptions: SIGKILL (9) and SIGSTOP (17/19/23) signals are impossible to catch, so there is no point in specifying them.
trap "echo Program execution interrupted...; exit" SIGINT
for i in {1..10}
do
sleep 1
echo $i
done
$ bash script.sh
1
2
3
4
^Program execution interrupted...
Running the script with the -x
parameter will show its step-by-step execution, which will be useful for debugging and searching for errors:
$ bash -x script.sh