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 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"
  1. Create a script example.sh with the contents below
#!/bin/bash

echo "Hello, World!"
  1. Allow the script to be executed
$ chmod +x example.sh
  1. 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 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 $?
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, 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

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