Shell scripts
Scripts are simply files with usually many Linux commands, usually written in Bash, as it is the default shell language in many Linux-based distributions. You can see scripts as programs, and you will execute them as you do with programs.
A script is usually written in a file name.sh
, regardless of the language used in the script, but this isn't mandatory.
β‘οΈ Scripts are simply files with commands. It means you can execute anything here directly inside a shell, and the other way around.
β‘οΈ Some scripts are only defining variables, functions or aliases and are not executed but only sourced (e.g. imported in the current shell):
$ source ~/.bashrc # most well-known example
$ . ~/.bashrc # same as "source"
- Create a script
example.sh
with the contents below
#!/bin/bash
echo "Hello, World!"
- Allow the script to be executed
$ chmod +x example.sh
- Execute the script
$ ./example.sh
Hello, World!
β‘οΈ You can also use bash example.sh
to run a script with bash
.
POSIX standard
The Portable Operating System Interface (POSIX) is a family of standards that defines what we should and shouldn't do to write scripts that work on all POSIX-compliant shells.
Each shell, such as Bash provides syntax that is not POSIX but can make scripts shorter or simpler.
for (( i = 0; i < 10; i++ )); do
# β Bash | Not POSIX-compliant
done
if [[ 5 > 3 || 4 > 3 ]]; then
# β Bash | Not POSIX-compliant
fi
Recap of what isn't allowed by POSIX
- β DO NOT USE
((
,[[
,]]
, and))
- β DO NOT USE
&&
1,||
1 - β DO NOT USE
>
1,<
1... - β DO NOT USE
((i++))
for calculations
1 such operators only do what we expect when used in "[[ ]]
".
Introduction
Shebang
Every script should start with a directive called Shebang π₯ telling the CLI which shell interpreter should be used to run the script.
β‘οΈ Traditional way to request /bin/bash
#!/bin/bash
β‘οΈ Modern way to request bash
#!/usr/bin/env bash
Comments
This is how you write comments:
# this is a comment
Booleans
It's important for you to remember that in shell, 0
means success, anything else, usually 1
, means failure. For conditions:
-
0
meansTRUE
-
NOT 0
meansFALSE
This is important to remember that, because in many other languages, such as in C, if (1)
is TRUE, while in bash, if (1)
is FALSE.
Exit Code
You can use echo $?
use query the exit code of the previously executed command:
$ true # /bin/true is a command returning 0
$ echo $?
0
Variables, and their usage
You can assign a variable with =
, without ANY SPACES. Note: a variable exists even without being assigned, but it's empty (=sort of "").
number=5
text1=Hello
text2="Hello, World"
You can even store the output of a command to work on it
command_output1=`ls -la .`
command_output2=$(ls -la .)
Add $ before a variable name.
echo $number
echo ${number} # same
There is no need to use "quotes" to concatenate
$ echo $text1 $number
Hello 5
$ echo "$text1 $number" # same
$ echo "$text1" $number # same
Branching, and the command test
The usual if, else if (elif), and else.
if test1; then
# code
fi
if test1; then
# code
elif test2; then
# code
fi
if test1; then
# code
else
# code
fi
A test is a command exiting with the code 0 (TRUE), or a number between 1, and 255 (FALSE). This could be expressed as follows
if `exit 1`; then
echo "ok";
else
echo "ko"; # will execute this as '1' is FALSE
fi
While exit 1
could be replaced with false
, as if you followed, false
is a command exiting with 1
.
Fortunately, you got a command called test which is taking a condition, and returning 0 if true, and 1 otherwise. This command has a shortcut: [] which is doing the exact same thing.
if test toto == toto; then
echo "ok";
fi
# same
if [ toto == toto ]; then
echo "ok";
fi
Operators
-
a -lt b
: true if $a \lt b$ (lesser than) -
a -le b
: true if $a \le b$ (lesser equals) -
a -eq b
: true if $a = b$ (equals) -
a -ne b
: true if $a \neq b$ (not equals) -
a -ge b
: true if $a \ge b$ (greater equals) -
a -gt b
: true if $a \gt b$ (greater than) -
-z $variable
: true, if$variable
is empty
Others
-
str1 == str2
: true if "str1" is the same as "str2" -
str1 != str2
: true if "str1" is the different from "str2"
Special conditions (is it a file, does it exist...)
-
-f path
: true, ifpath
leads to a regular file -
-d path
: true, ifpath
leads to a folder -
-a path
: true, ifpath
leads to a system file -
-w path
: true, ifpath
is writable
Chain expressions (AND, OR)
Of course, you can chain expressions, with equivalents of &&
, and !!
# and
$ test toto == toto -a test toto == toto
# or
$ test toto == toto -o test tata == toto
# not
$ test ! toto == tata
$ test toto != tata
Examples
$ path=~/some_folder_that_exists
$ test -f $path; echo $?
1
$ test -d $path; echo $?
0
$ if [ -d $path ]; then echo "Folder+exists."; fi
Folder+exists.
Loops
for i in: this loop is taking values separated by a space
for i in "Hello, World!" word2 word3 ; do
# i will be: Hello, World!
# ...
done
iterative for
# for ((i = 1; i <= 5; i++))
for i in `seq 1 5`; do
# code
done
# for ((i = 1; i <= 5; i+=2))
for i in `seq 1 5 2`; do
# code
done
You can use break to forcefully exit a for/while/until, and you can use continue to forcefully finish the current iteration, and start the next one. You may apply the keyword on more than one loop.
for i in {1..5} ; do
break
break 1 # same, break 1 loop
done
while/until: while is taking a "test" like if.
while test; do
# code
done
until test; do
# code
done
You can merge if/elif/else into a case statement
case $x in
pattern)
# if $x == pattern
;;
pattern1 | pattern2)
# if [ $x == pattern -o $x == pattern ]
;;
*)
# else = default
;;
esac
Command-line arguments
Pass arguments
You can pass arguments to a script in a similar way than commands:
$ ./example arg1 "This is arg2" -o arg4
You can use $#
to get the number of arguments:
echo $#
# 4
Each argument is stored in a variable "$n
"
echo $0 # ./example
echo $1 # arg1
echo $2 # This is arg2
We can use $@
to get the list of arguments:
echo $@
# echo ./example arg1 "This is arg2" -o arg4
There is also $*
in which is all arguments as a single string.
Handle incorrect usages
We often check the number of arguments and display a "usage" message when required arguments are missing:
if [ $# -lt 2 ]; then # $0 $1 $2
echo "Usage: $0 arg1 arg2"
echo "Try '$0 -h' for more information."
exit 1
fi
Proper argument handling
We usually avoid using $n
directly inside the code. We usually store them inside variables and use them instead.
program_name=$0
Iterate arguments
A common code to iterate arguments (β οΈ do not forget quotes):
for i in "$@"; do
echo $i
done
For more complex uses:
while [ $# -gt 0 ]; do
case "$1" in
# handle "-xxx yyy"
-xxx)
xxx_value="$2"
shift 2 # consume 2 args
;;
# ...
esac
done
Handle command arguments
There are multiple commands that call other commands. To pass command arguments, we commonly use --
to indicate that all arguments, even if they start with -
, should be treated as arguments.
# Ex: ./call_command.sh [...] -- -flag arg1 arg2
# if we were using a while/esac
# ...
--)
command_args="$@"
break
;;
# ...
# call the command with some arguments from our script
# and pass it the command arguments too
some_command [...] ${command_args}
Input Field Separator (IPS)
The $IPS
variable to determine what is an argument.
Builtin functions
Builtin functions, are functions that are declared inside your script.
myBuiltin() {
# code
}
Then, you can call the builtin function in your script like this
myBuiltin arg1 arg2
The myBuiltin may take arguments, but the parentheses will always remain empty ()
. In the builtin function, you will access args as you would for command-line arguments!
# myBuiltin arg1 arg2
myBuiltin() {
echo $0 # ./example (not myBuiltin)
echo $1 # arg1
echo $2 # arg2
# ...
}
Exit code
A builtin function may return something, but you CAN NOT use exit
, as it would kill the whole process. Use return
instead.
myBuiltin() {
return $1
}
myBuiltin 5
echo $? # will be "5"
Return value
To return something, simply use echo
.
myBuiltin() {
echo "Hello, World!"
}
text=$(myBuiltin)
echo $text # Hello, World!
Read input from the user
You can use the command read
to read input. This command takes a suite of 1, or more variables, and stores a word in each variable. If there are not enough variables, then the last variable is used to store everything that could not be stored.
$ read x
toto
$ echo $x
toto
$ read -p "Prompt: " x # Prompt for input
$ read x
toto tata
$ echo $x
toto tata
$ read x y
toto tata titi
$ echo $x
toto
$ echo $y
tata titi
Note: handle newlines / "infinite input"...
This code is running indefinitely until read
fails. Each time the user press ENTER, the code will loop, and ask for input again. A user can notify the script that the input is done using CTRL+D, which will make read
fail, and end the loop.
while read x; do
echo $x
done
You can use a nested for
to extract each word that was entered.
while read line; do
for word in $line; do
echo $word
done
done
Test
Note: <CR>
, for carriage return, means that I pressed enter.
$ ./example
toto tata <CR>
toto
tata
titi toto tata <CR>
titi
toto
tata
Read a file, or Write content in a file
We are mainly using redirections to read/write files, as the process is the same as reading input from a user, but this time the input/output source is different.
-
Create an empty file
toto.txt
(note thattouch
DO NOT ensure that the file is empty, so we can't use that)
# create an empty file
echo -n "" > toto.txt
-
Write user input in
toto.txt
while read line; do
echo $line >> toto.txt
done
This can be enhanced by doing the redirection at the end, meaning that every output (every echo...) will be redirected to toto.txt
.
while read line; do
echo $line
done >> toto.txt
-
Read the content of
toto.txt
while read line; do
echo $line
done < toto.txt
- Note 1: DO NOT USE CAT, stop killing kittens πΏπΎ
cat toto.txt | while read line; do
echo $line
done
- Note 2: But, you may use the concept with other commands
head -n 5 toto.txt | while read line; do
echo $line
done
Random
You may use "sleep" to pause your script
$ sleep 50 # sleep for 50 seconds
$ sleep 50s # same
Don't ask why, but if you want a random number in $[10, 20]$.
$ echo $[RANDOM%(20-10+1) + 10]
π» To-do π»
Stuff that I found, but never read/used yet.
-
getopts
-
readonly var=value
-
env X=val ./myScript