Shell scripts

bashscripting introductiontobashscripting

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, 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"
  1. Create a script with the contents below

echo "Hello, World!"
  1. Allow the script to be executed
$ chmod +x
  1. Execute the script
$ ./
Hello, World!

➑️ You can also use bash 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

if [[ 5 > 3 || 4 > 3 ]]; then
  # ❌ Bash | Not POSIX-compliant

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 "[[ ]]".



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


➑️ Modern way to request bash

#!/usr/bin/env bash


This is how you write comments:

# this is a comment


It's important for you to remember that in shell, 0 means success, anything else, usually 1, means failure. For conditions:

  • 0 means TRUE
  • NOT 0 means FALSE

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 $?

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 "").

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

if test1; then
    # code
elif test2; then
    # code

if test1; then
    # code
    # code

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";
  echo "ko"; # will execute this as '1' is FALSE

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";
# same
if [ toto == toto ]; then 
  echo "ok";
  • 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


  • 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, if path leads to a regular file
  • -d path: true, if path leads to a folder
  • -a path: true, if path leads to a system file
  • -w path: true, if path 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


$ path=~/some_folder_that_exists
$ test -f $path; echo $?
$ test -d $path; echo $?
$ if [ -d $path ]; then echo "Folder+exists."; fi


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!
    # ...

iterative for

# for ((i = 1; i <= 5; i++))
for i in `seq 1 5`; do
    # code
# for ((i = 1; i <= 5; i+=2))
for i in `seq 1 5 2`; do
    # code

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 1 # same, break 1 loop

while/until: while is taking a "test" like if.

while test; do
    # code
until test; do
    # code

You can merge if/elif/else into a case statement

case $x in
  # if $x == pattern
pattern1 | pattern2)
  # if [ $x == pattern -o $x == pattern ]
  # else = default

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

Proper argument handling

We usually avoid using $n directly inside the code. We usually store them inside variables and use them instead.


Iterate arguments

A common code to iterate arguments (⚠️ do not forget quotes):

for i in "$@"; do
  echo $i

For more complex uses:

while [ $# -gt 0 ]; do
  case "$1" in
    # handle "-xxx yyy"
      shift 2 # consume 2 args
    # ...

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: ./ [...] -- -flag arg1 arg2
# if we were using a while/esac
# ...
# ...
# 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!"

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
$ echo $x
$ read -p "Prompt: " x  # Prompt for input
$ read x
toto tata
$ echo $x
toto tata
$ read x y
toto tata titi
$ echo $x
$ 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

You can use a nested for to extract each word that was entered.

while read line; do
  for word in $line; do
    echo $word

Note: <CR>, for carriage return, means that I pressed enter.

$ ./example
toto tata <CR>
titi toto tata <CR>

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 that touch 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

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
  • Note 2: But, you may use the concept with other commands
head -n 5 toto.txt | while read line; do
    echo $line


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