Bash

  1. How Bash resolves commands
  2. Text Processing / Text Transformation
    2.1 grep command | Searching in files
    2.2 tr command | Replace characters
    2.3 awk command | columns, fields, logs, CSV-like data
    2.4 sed | Search and replace inside files
    2.5 find | Search for files and directories
  3. Simple Bash script
  4. Basic variables
  5. Exit codes
  6. Arguments
  7. User input
  8. Functions
  9. If/else conditions
  10. Case statements
  11. Loops
    11.1 For Loop
    11.2 While loop
  12. Arrays
    12.1 Indexed Arrays
    12.2 Associative Arrays
  13. IFS – Internal Field Separator
  14. Command Substitution result=$(command)
  15. Sourcing Code /.script.sh vs source scripts.sh
  16. Arithmetic expressions
  17. Logical operators
  18. Process Substitution | Work in progress, should practice it more!

Tools

  • ShellChecker – check your scripts with shellchecker. Finding errors and suggests improvements
  • bash -x script.sh
    bash in debugging mode. This is my bash debugger.
    You can put it inside the script
    set -x | enable it
    set +x | dissable it
    bash -n 01-syntax-error | check bash for errors but it doesn’t run the code
    bash -u 02-undefined variables | checks for undefined variables
  • About strict mode on top of the script. Why to use it and why not

How Bash resolves commands

type : type is a shell built-in command that tells you what a command name refers to — for example whether it is an alias, function, built-in command, or executable file. Difference from which is that which only searches $PATH.
Example:

$ type ls
ls is aliased to `ls --color=auto'

$ type cd
cd is a shell builtin

$ type apache2
apache2 is /usr/sbin/apache2

A related command:

# "command" is often used in scripts because it is POSIX-compatible
command -v <command>

$ command -v git
/usr/bin/git

type → best for interactive shell investigation
command -v → best for scripts
which → simple PATH lookup only

Text Processing / Text Transformation

| Command | Purpose                            |
| ------- | ---------------------------------- |
| `tr`    | Translate/delete characters        |
| `sed`   | Stream editing / substitutions     |
| `awk`   | Text processing / field processing |
| `cut`   | Extract columns/fields             |
| `sort`  | Sort text                          |
| `uniq`  | Remove/count duplicates            |
| `grep`  | Search/filter text                 |

grep command | searching in files

This matches lines that start with root

grep '^root' /etc/passwd

Identifies end of a file. All words that ends with root

grep 'root$' /etc/passwd

Useful greps for scripts

## content of file.txt
dave
DAVE
DIMITAR

### show one line after a
grep -A1 a file.txt

### finds both DAVE and dave, which means it is case sensitive
grep -i dave file.txt

## prints only what match the pattern. dot "." here means match any character
grep -o '^d.'

For example:
$ grep -o '^d.' file.txt
da
$ grep -o '^d..' file.txt
dav
$ grep -o '^d...' file.txt
dave

tr command | Replace characters

Replace characters with tr command – transition

Example: replace lowercase a with A
$ echo "banana" | tr 'a' 'A'
output
bAnAnA

Lowercase → uppercase

echo "hello" | tr 'a-z' 'A-Z'

Delete characters

echo "hello123" | tr -d '0-9'

Replace spaces with newlines

echo "one two three" | tr ' ' '\n'

Squeeze repeated characters

echo "aaabbbccc" | tr -s 'a-z'

awk command | columns, fields, logs, CSV-like data

awk is a text processing / data extraction language used in Linux shells. It is especially useful for working with structured text: columns, fields, logs, CSV-like data, command output, etc.
awk reads input line by line:


input line
    |
    v
split into fields
    |
    v
check pattern
    |
    v
run action

Example file: users.txt

john 25 admin
anna 31 user
mark 22 user

$awk '{print $1}' users.txt
Output:
john
anna
mark

$1 = first column
$2 = second column
$3 = third column
$0 = the entire line

Basic syntax

awk 'pattern { action }' file

If column 2 is greater than 25, print column 1

$ awk '$2 > 25 {print $1}' users.txt

Output:
john
anna

Printing columns

server1 10.0.0.1 running
server2 10.0.0.2 stopped

$ awk '{print $1, $3}' servers.txt
Output:
server1 running
server2 stopped

Changing field separator to “,”. Default separator = spaces/tabs

john,25,admin
anna,31,user

$awk -F',' '{print $1}' users.csv

Searching like grep

#Find lines containing "error":
awk '/error/ {print}' logfile

Awk can use fields

10:30 INFO started
10:31 ERROR failed
10:32 INFO done

$awk '$2=="ERROR" {print $0}' app.log

output:
10:31 ERROR failed

Built-in variables

| Variable | Meaning          |
| -------- | ---------------- |
| `$0`     | whole line       |
| `$1`     | first field      |
| `$NF`    | last field       |
| `NR`     | line number      |
| `NF`     | number of fields |

Example:
awk '{print NR, $NF}' file

Condition if

awk '{if ($3 > 1000) print $1}' /etc/passwd

Change field values

echo "hello world" | awk '{$2="Linux"; print}'

Arithmetic

echo "10 20" | awk '{print $1+$2}'

Loops

echo "1 2 3" | awk '{
    for(i=1;i<=NF;i++)
        print $i
}'

Real Linux examples

# Find biggest files
ls -lh | awk '{print $5, $9}'


# Check processes
ps aux | awk '{print $1,$11}'

Simple bash script – Hello Friend

#!/bin/bash
name='Dimitar'
number=34
system=$(uname -a)

echo "Hello Friend, my name is $name and I am $number years old"
echo "Your kernel is: $system"

Sed

sed reads text line by line and transforms it — substitute, delete, insert, print specific lines. Think of it like find and replace but on steroids and scriptable.

sed [options] 'command' file
# or
command | sed [options] 'command'

Most common use — substitution s:
sed 's/old/new/' file
s        → substitute
/old/    → find this
/new/    → replace with this

Substitution examples:

# replace first occurrence per line
echo "hello hello hello" | sed 's/hello/world/'
# prints: world hello hello

# replace ALL occurrences per line (global flag g)
echo "hello hello hello" | sed 's/hello/world/g'
# prints: world world world

# case insensitive (i flag)
echo "Hello HELLO hello" | sed 's/hello/world/gi'
# prints: world world world

# replace on specific line number
sed '3s/old/new/' file      # only line 3

# replace in line range
sed '1,5s/old/new/' file    # lines 1 through 5


Quick reference:
SUBSTITUTION:
sed 's/old/new/'          → replace first occurrence
sed 's/old/new/g'         → replace all occurrences
sed 's/old/new/gi'        → case insensitive replace all
sed 's/old/new/2'         → replace 2nd occurrence only
sed -i 's/old/new/g'      → edit file in place
sed -i.bak 's/old/new/g'  → edit in place with backup

DELETE:
sed '3d'                  → delete line 3
sed '1,5d'                → delete lines 1-5
sed '/pattern/d'          → delete lines matching pattern
sed '/^$/d'               → delete empty lines
sed '/^#/d'               → delete comment lines

PRINT:
sed -n '5p'               → print line 5 only
sed -n '1,5p'             → print lines 1-5
sed -n '/pattern/p'       → print matching lines

INSERT/APPEND:
sed '3i\text'             → insert before line 3
sed '3a\text'             → append after line 3
sed 's/^/PREFIX: /'       → add prefix to every line
sed 's/$/ SUFFIX/'        → add suffix to every line

FLAGS:
-i                        → edit file in place
-i.bak                    → edit in place with backup
-n                        → suppress default output
-e                        → multiple commands

Find

find — Search for files and directories

What it does: find searches for files and directories in a directory tree based on conditions — name, type, size, date, permissions and more. One of the most powerful sysadmin tools.

find [where] [what] [action]

find /var/log -name "*.log" -type f
#    ↑         ↑              ↑
#    where     what           action (optional)

find [where] [what] [action]

find /var/log -name "*.log" -type f
#    ↑         ↑              ↑
#    where     what           action (optional)


# exact name
find /etc -name "sshd_config"

# wildcard
find /var/log -name "*.log"

# case insensitive
find /home -iname "*.txt"

# by extension
find / -name "*.conf"

Search by type:
find /etc -type f       # files only
find /etc -type d       # directories only
find /etc -type l       # symlinks only


Search by permissions:
find / -perm 777            # exactly 777
find / -perm -777           # at least 777
find / -perm /222           # any write permission set
find / -perm 644            # exactly 644


Actions — what to do with results:
# print (default)
find /etc -name "*.conf" -print

# delete found files — BE CAREFUL
find /tmp -name "*.tmp" -delete

# execute command on each result
find /var/log -name "*.log" -exec ls -lh {} \;
#                                          ↑  ↑
#                                          {} = found file
#                                          \; = end of command

# execute with + (faster — batches files)
find /var/log -name "*.log" -exec ls -lh {} +


Quick reference:
BASIC:
find /path -name "file"         → find by exact name
find /path -name "*.log"        → find by wildcard
find /path -iname "*.log"       → case insensitive
find /path -type f              → files only
find /path -type d              → directories only
find /path -type l              → symlinks only

SIZE:
find /path -size +100M          → larger than 100MB
find /path -size -10k           → smaller than 10KB

TIME:
find /path -mtime -1            → modified last 1 day
find /path -mtime +30           → modified 30+ days ago
find /path -mmin -60            → modified last 60 minutes

PERMISSIONS/OWNER:
find /path -perm 777            → exact permissions
find /path -user username       → owned by user
find /path -group groupname     → owned by group
find /path -nouser              → orphaned files

ACTIONS:
find /path -print               → print results (default)
find /path -delete              → delete found files
find /path -exec cmd {} \;      → run command on each file
find /path -exec cmd {} +       → run command batched

COMBINING:
find /path -type f -name "*.log"          → AND
find /path -name "*.log" -o -name "*.txt" → OR
find /path ! -name "*.log"                → NOT

DEPTH:
find /path -maxdepth 1          → current dir only
find /path -maxdepth 2          → max 2 levels deep
find /path -mindepth 2          → skip top levels

Basic Variables

syntax

foo='Hello there'

quotation marks when exporting variables

#single quotes escape spaces. Doube quotes not
$ foo='hello    world'

$ echo $foo
hello world

$ echo "$foo"
hello    world

Remove variable

unset foo

Create environment variable from other environment variable

thing=$(uname -a)

or

thing=`uname -a`
# ` is ~ without shift ;)

Exit codes

Each shell command returns an exit code when it terminates, whether successfully or not.
By convention, an exit code of zero indicates the command completed successfully, and a non-zero value means an error was encountered.
The special variable $? holds the exit status of the last executed command

date &> /dev/null
echo $?
#The date command completed successfully, so the exit code is zero

#If you try to run ls on a nonexistent directory, the exit code will be non-zero:
ls /nonexistent_dir &> /dev/null
echo $?

Exit Code Conventions

CodeMeaning
0Success
1General error
2Misuse of a shell built-in (wrong arguments, etc.)
126Command found but not executable (permission issue)
127Command not found
128+NCommand terminated by signal N (for example, 130 = Ctrl+C and 137 = SIGKILL)

Arguments

#!/bin/bash
count=0
for f in "$1"/*.log; do
  (( count++ ))
done
echo "Found $count .log files in $1"

$1 is an Argument. It reads first argument after bash scripts for example:
./folder_number_of_logs.bash /var/log/apache2 . /var/log/apache2 is an argument.
You can give more than one argument with $2, $3 etc.

VariableMeaning
`$#`Number of arguments
`$1`First argument
`$2`Second argument
`$3`Third argument
`$@`All arguments
`$0`Script name

Read user input

Syntax

read -p 'Enter your name: ' name
echo "Hello $name"

User input with argument. Give argument to the script example: ./name.bash Dimitar. If no argument is provided it will ask for input

#/bin/bash

if [[ -n $1 ]]; then
        name=$1
else
        read -p 'Enter your name: ' name
fi
echo "Hello $name"

Loop with user input

#/bin/bash
## $@ creates array to all in the arguments. Arguments that we give to the script ./scrip.bash ARG1 ARG2 ARG3

for thing in "$@"; do
        echo "thing is $thing"
done

While Loop with user input

#!/bin/bash

while read -r line; do
        echo "we read line: $line"
done

Functions

You already use functions every day without knowing it. Commands like du, echo, cut — those are essentially functions someone wrote for you.
A function is just a named block of commands you can reuse.
The problem functions solve:

Imagine you need to print a separator line in your script 5 times:
bashecho "-------------------"
echo "Task 1"
echo "-------------------"
echo "Task 2"
echo "-------------------"
echo "Task 3"
echo "-------------------"
What if you want to change --- to ===? You edit 4 lines. Now imagine 50 times. That's a nightmare.
With a function:
bashseparator() {
    echo "-------------------"
}
separator
echo "Task 1"
separator
echo "Task 2"
separator
echo "Task 3"
Now to change it you edit one place only.

Syntax

function_name() {
    # commands go here
}

# call it like this:
function_name

Arguments let you pass data into a function:

greet() {
    echo "Hello $1, welcome to the server!"
}

greet "dimitar"   # prints: Hello dimitar, welcome to the server!
greet "root"      # prints: Hello root, welcome to the server!

Argument in functions vs arguments only to bash script itself

When passing to a script:

./myscript.bash Dimitar username
                  ↑        ↑
                 $1       $2       ← available in the whole script


When passing to a function:

greet "Dimitar" "username"
         ↑          ↑
        $1          $2             ← available inside the function only


Same syntax, different scope. Think of it like this:


SCRIPT LEVEL
├── $1 = "Dimitar"   ← what you passed to the script
├── $2 = "username"
│
└── greet "Dimitar"  ← calling function with its own argument
         └── FUNCTION LEVEL
             └── $1 = "Dimitar"   ← only exists inside function

This means $1 can mean two different things in the same script:
#!/bin/bash
# $1 here = what you passed to the SCRIPT

greet() {
    echo "Hello $1"   # $1 here = what you passed to the FUNCTION
}

echo "Script got: $1"      # prints what you gave the script
greet "Dimitar"            # function gets its own $1

Write a function called backup_multiple that takes multiple filenames as arguments and backs up each one with a .bak extension. If a file doesn’t exist, print an error and skip it.

backup_multiple() {

                for file in "$@"; do
                        if [ -f "$file" ]; then
                                cp "$file" "$file.bak"

                        else
                                echo "File with name $file does not exist!"
                        fi

                done



}

backup_multiple testfile.txt testfile2.txt testfile3.txt

Write a function called check_services that takes multiple service names as arguments and prints whether each one is running or not.

check_services() {
    for service in "$@"; do
        if systemctl is-active "$service" &>/dev/null; then
            echo "Service $service is RUNNING"
        else
            echo "Service $service is STOPPED"
        fi
    done
}

check_services snapd.service apache2.service

If/else Conditions

Syntax

#!/usr/bin/env bash

simple syntax

a=2
b=2

if [[ $a == $b ]]; then
     echo 'a and b are the same'
fi

c=2
d=3

if [[ $c != $d  ]]; then
         echo 'c and d are NOT the same'
fi

if [[ -f file.txt]]; then
   echo 'file.txt exists and is a file'
fi


if true; then
   echo "if statement was true"
else
   echo "if statement was false"
fi


You can replace true with whatever command you want!

Case statements

Syntax

case "$variable" in
    pattern1)
        # commands
        ;;
    pattern2)
        # commands
        ;;
    *)
        # default — matches anything
        ;;
esac

;; means "stop here, don't check other patterns"
* is the default case — like else
esac is case spelled backwards — it closes the block

case also supports patterns:

case "$answer" in
    yes|y|YES)       # matches any of these
        echo "You said yes"
        ;;
    no|n|NO)
        echo "You said no"
        ;;
esac

Write a script that takes a day name as argument ($1) and prints whether it is a weekday or weekend.

case "$1" in
        monday)
                echo "Monday is weekday"
                ;;
        tuesday)
                echo "Tuesday is weekday"
                ;;
        wednesday)
                echo "Wednesday is weekday"
                ;;
        thursday)
                echo "Thursday is weekday"
                ;;
        friday)
                echo "Friday is weekday"
                ;;
        saturday)
                echo "Saturday is weekend"
                ;;
        sunday)
                echo "Sunday is weekend"
                ;;
        *)
                echo "This is not a day from the week"
                ;;
esac

## Better Solution
## instead of 5 separate weekday cases:
case "${1,,}" in
    monday|tuesday|wednesday|thursday|friday)
        echo "$1 is a weekday"
        ;;
    saturday|sunday)
        echo "$1 is a weekend"
        ;;
    *)
        echo "$1 is not a valid day"
        ;;
esac

Write a script that takes a service name as argument and an action (start, stop, restart, status). Perform the correct systemctl command based on the action. If the action is unknown print
an error.

case "${2,,}" in
    start)
    systemctl start "$1"
    ;;
    stop)
    systemctl stop "$1"
    ;;
    status)
    systemctl status "$1"
    ;;
    restart)
    systemctl restart "$1"
    ;;
    *)
    echo "Error. Argument is not recognized. Choose from start stop status restart"
    ;;
esac

Menu with user input

#!/bin/bash
## User choses something from menu. Program return result
while true; do

    echo "1) Check disk usage"
    echo "2) Check memory usage"
    echo "3) Check who is logged in"
    echo "4) Exit"

    read choice

    case "$choice" in
        1)
            df -h
            ;;
        2)
            free -h
            ;;
        3)
            who
            ;;
        4)
            break
            ;;
        *)
            echo "You must choose something from 1 .. 4"
            ;;
    esac

done

Write a script called server_health.sh that checks the health of a server and prints a report. It must include:

A function called check_disk that warns if any partition is above 80%
A function called check_memory that warns if memory usage is above 80%
A function called check_services that checks if ssh and cron are running
A menu using case that asks the user what to check:

1) Check disk
2) Check memory
3) Check services
4) Run all checks
5) Exit

#!/bin/bash
## check_service function. Important, when comparing strings we are using [[ ... ]] and the env is with $ and "".

check_service() {

        ssh_check=$(systemctl is-active ssh)
        cron_check=$(systemctl is-active cron)
        if [[ "$ssh_check" == active && "$cron_check" == active ]]; then
                echo -e "\n Both service are up and runnig! sshd is $ssh_check. cron is $cron_check \n"
        else 
                echo -e "\n Bro, you have problem with a service! sshd is $ssh_check. cron is $cron_check \n"

        fi



}

## check memory function
#towa mi dawa kolko memory se polzva v momenta v procenti. free | awk '/Mem:/ {printf("%.2f%%\n", $3/$2*100)}'
#towa bez procenti free | awk '/Mem:/ {printf("%.2f\n", $3/$2*100)}'

check_memory() {

        total_memory_usage=$(free | awk '/Mem:/ {printf("%.f\n", $3/$2*100)}')
        if  (( total_memory_usage > 80 )); then
                echo -e "\n Warning. You are using more than 80% of your memory \n"
        else
                echo -e "\n You are using $total_memory_usage% of your total memory. It's ok \n"

        fi




}

###check disk function
#checker=0 in programming is called  flag variable.

check_disk() {

checker=0

        for partition_percentage in $(df -h --output=source,fstype,size,used,avail,pcent,target -x tmpfs -x devtmpfs | grep ^/dev | awk '{ print $6 }' | tr -d '%'); do
                if (( partition_percentage > 80 )); then
                        checker=1
                        break

                fi
        done
        if (( checker == 0 )); then 
                echo -e "\n All partitions are fine. \n"
        else
                echo -e "\n WARNING!!! One or more partitions are more than 80% full. \n"

      fi

}



### menu part
while true; do
    echo "1) Check disk usage"
    echo "2) Check memory usage"
    echo "3) Check service ssh and cron status"
    echo "4) Run all checks"
    echo "5) Exit"

    read choice
    
    case "$choice" in
         1)
                 check_disk
                    ;;
         2)
                 check_memory
                 ;;
         3)
                 check_service
                 ;;
         4)
                 check_disk; check_memory; check_service
                 ;;
         5)     
                break
                ;;
        *)
                echo "You must choose something from 1 .. 5"
                ;;
esac
                         

done

For Loop

In Bash, the for loop syntax has a few common forms. Loop over a list is the most common.

for var in item1 item2 item3; do
    commands
done

Example:
for name in Alice Bob Charlie; do
    echo "Hello $name"
done

for item in a b c d; do
  echo "$item"
done

item is just a variable — and on each iteration it gets assigned the next a b c d from the list.

iteration 1:  item="a"
iteration 2:  item="b"
 ... and so on

for [variable] in [list of things]; do
       ↑                ↑
       gets assigned     one item at a time
       each item

The "list of things" can be:

numbers → {1..10}
words → a b c d
files → /var/log/*.log
command output → $(cat somefile)


for thing in {a..f}; do
   echo "thing is a $thing"
done


max=5
for (( i = 0; i < max ; i++)); do
  echo "Printing message 5 times in C style"
done

Classic FizzBuzz — numbers 1 to 20. Multiples of 3 → “Fizz”, multiples of 5 → “Buzz”, multiples of both 3 and 5 → “FizzBuzz”, otherwise the number.

for i in {1..20}; do

  if (( i % 3 == 0 && i % 5 != 0)); then
          echo "Fizz"

  elif (( i % 5 == 0 && i % 3 != 0)); then
          echo "Buzz"
          
  elif (( i % 5 == 0 && i % 3 == 0 )); then
          echo "FizzBuzz"

  else 
          echo "$i"
  fi      
done


## more elegant
echo "More elegant sollution \n"

for i in {1..20}; do
  if (( i % 3 == 0 && i % 5 == 0 )); then
    echo "FizzBuzz"
  elif (( i % 3 == 0 )); then
    echo "Fizz"
  elif (( i % 5 == 0 )); then
    echo "Buzz"
  else
    echo "$i"
  fi
done

Loop over all .log files in /var/log and print each filename and its size in a human readable format.

for i in /var/log/*.log; do
  size=$(du -sh "$i" | cut -f1)
  echo "File: $i | Size: $size"
done

Write a script that loops over all users in /etc/passwd and prints only users with a UID greater than 1000 (those are typically real human users on Linux).

for userid in $(cut -d ':' -f3 /etc/passwd); do
        if (( userid >= 1000 && userid < 65534)); then
               echo "The only real human on this linux server is with UID $userid"
        fi
done

While loop

It keeps running as long as the condition is true. When condition becomes false – it stops.

while [ condition ]; do
    # commands
done

3 most common patterns

1. Counter based:
count=0
while (( count < 5 )); do
    echo "count is $count"
    (( count++ ))
done

2. Read a file line by line:
while read line; do
    echo "$line"
done < /etc/passwd

command > file    # output OF command goes INTO file
command < file    # content OF file goes INTO command

3. Infinite loop — runs until you break it:
while true; do
    echo "running..."
    sleep 1
done

Break and continue work exactly like in for

while true; do
    if [ something ]; then
        break      # exit the loop completely
    fi
done

While and until

# while — runs WHILE condition is true
while (( count < 10 )); do

# until — runs UNTIL condition becomes true
until ping -c1 google.com; do

Write a script that counts from 1 to 10 using a while loop and prints each number.

count=1
while (( count < 11 )); do
        echo "counting number $count"
        (( count++ ))
done

Write a script that keeps pinging google.com until it gets a successful response, then prints “Network is up!” and stops.

until ping -c1 google.com &>/dev/null; do
    echo "Network is down, retrying..."
    sleep 2
done
echo "Network is up!"


##this is with while

while ! ping -c1 google.com &>/dev/null; do
    echo "Network is down, retrying..."
    sleep 2
done
echo "Network is up!"

Infinite loop

#!/bin/bash
## infinite loop

while true; do
    echo "running..."
    sleep 1
done

Read a file line by line

while read line; do
    echo "$line"
done < /etc/passwd

Write a script that reads /etc/passwd line by line and prints only the username and their home directory in this format:
User: root | Home: /root
User: dimitar | Home: /home/dimitar

while read line; do
    username=$(echo "$line" | cut -d: -f1)
    homedir=$(echo "$line" | cut -d: -f6)
    echo "User: $username | Home: $homedir"
done < /etc/passwd

Write a function called monitor_disk that keeps checking disk usage of a directory every 3 seconds and warns when it goes above a threshold. It stops when you press Ctrl+C.

monitor_disk() {
    folder="$1"
    threshold="$2"
    
    while true; do
        result=$(df -h "$1" | awk 'NR==2 {print $5}' | tr -d '%')
        if (( result >= threshold )); then
            echo "WARNING: $folder is at ${result}% — above threshold of ${threshold}%"
        else
            echo "OK: $folder is at ${result}% — below threshold of ${threshold}%"
        fi
        sleep 3
    done
}
monitor_disk "$1" "$2"

While with read and If statement

check_disk() {
    checker=0
    while read -r partition percentage; do
        pct=$(echo "$percentage" | tr -d '%')
        if (( pct > 80 )); then
            echo "WARNING: $partition is at ${pct}%"
            checker=1
        fi
    done < <(df -h --output=source,pcent -x tmpfs -x devtmpfs | grep ^/dev)

    if (( checker == 0 )); then
        echo -e "\nAll partitions are fine.\n"
    fi
}
check_disk

For vs. While

ForWhile
Use when: you know how many iterationsUse when: you don’t know how many iterations
Example: loop over 10 filesExample: keep trying until server responds

The rule:
looping over words/files/numbers -> for
reading structured lines with columns -> while read
reading /etc/passwd, logs, df output -> while read

Arrays

================================================================
BASH ARRAYS - REGULAR vs ASSOCIATIVE
================================================================

REGULAR ARRAY
-------------
Declaration:    servers=("web01" "web02" "db01")
Index type:     numbers starting from 0
Access one:     ${servers[0]}
Access all:     ${servers[@]}
Get indexes:    ${!servers[@]}
Count:          ${#servers[@]}
Add element:    servers+=("web04")
Use when:       ordered list of items

Example:
    servers=("web01" "web02" "db01")
    for server in "${servers[@]}"; do
        echo "$server"
    done

----------------------------------------------------------------

ASSOCIATIVE ARRAY
-----------------
Declaration:    declare -A servers
                servers["production"]="web01"

Or all at once: declare -A servers=(
                    ["production"]="web01"
                    ["staging"]="web02"
                )

Index type:     named keys (strings)
Access one:     ${servers["production"]}
Access all:     ${servers[@]}
Get all keys:   ${!servers[@]}
Count:          ${#servers[@]}
Add element:    servers["newkey"]="newvalue"
Use when:       key-value pairs / dictionary-like data

Example:
    declare -A server_ips=(
        ["web01"]="192.168.1.10"
        ["web02"]="192.168.1.11"
        ["db01"]="192.168.1.20"
    )
    for server in "${!server_ips[@]}"; do
        echo "$server → ${server_ips[$server]}"
    done

----------------------------------------------------------------

KEY DIFFERENCES
---------------
| Feature        | Regular          | Associative         |
|----------------|------------------|---------------------|
| Index type     | numbers 0 1 2    | named keys          |
| Declare        | servers=()       | declare -A servers  |
| Preserves order| yes              | no                  |
| Use when       | ordered list     | key-value pairs     |
| Example        | list of servers  | server → IP mapping |

----------------------------------------------------------------

COMMON SYNTAX (works for BOTH types)
-------------------------------------
${array[@]}     → all values
${!array[@]}    → all indexes/keys
${#array[@]}    → count elements
array+=()       → append element

================================================================
The pattern to remember for associative arrays:
for key in "${!array[@]}"; do
    value="${array[$key]}"
    # now you have both key and value
done

Locals in arrays. Important, with locals you can skip some nasty bugs

The problem without local:
'
name="global"

change_name() {
    name="inside function"
}

change_name
echo $name    # prints: "inside function"
'
The function accidentally changed the variable outside itself. In small scripts this is fine, but in big scripts this causes nasty bugs that are hard to find.

local keeps variables inside the function:

`name="global"

change_name() {
    local name="inside function"
}

change_name
echo $name    # prints: "global"
`
The function has its own name — the outside name is untouched.

local with arrays:
my_function() {
    local my_array=("a" "b" "c")    # only exists inside function
    echo "${my_array[@]}"
}

my_function
echo "${my_array[@]}"    # prints nothing — array is gone

For associative arrays:
my_function() {
    local -A my_map=(["key"]="value")    # -A for associative
    echo "${my_map["key"]}"
}


Rules:
local var="value"          → local regular variable
local array=("a" "b")     → local regular array
local -A map=(["k"]="v")  → local associative array

declare -A map             → global associative array (no local)

Indexed Arrays

Syntax

Define:
servers=("web01" "web02" "db01")

Access one element:
echo "${servers[0]}"   # web01
echo "${servers[1]}"   # web02

Access all elements:
echo "${servers[@]}"   # web01 web02 db01

Count elements:
echo "${#servers[@]}"  # 3

Add an element:
servers+=("db02")


Loop over array:
for server in "${servers[@]}"; do
    echo "$server"
done

fruits=("apple" "banana" "cherry")
echo "${fruits[0]}"
echo "${fruits[@]}"
echo "${#fruits[@]}"

Show all keys
echo "${!servers[@]}" #Output will be : 0 1 2 3 4 5

Create an array of 5 server names. Loop over them and print each one with its index number like this:
Server 0: web01
Server 1: web02

servers=("web01" "web02" "web03" "web04" "web05")
        for server_number in "${!servers[@]}"; do
                server_name=${servers[$server_number]}
                echo "Server $server_number : $server_name"


        done

Create an array of services (ssh, cron, ufw, rsyslog). Loop over them, check if each one is running and store the failed ones in a second array called failed_services. At the end print how many services failed and which ones.
Expected output:
Checking ssh… OK
Checking cron… OK
Checking ufw… FAILED
Checking rsyslog… OK

Failed services: 1
Failed: ufw

#Create an array of services
services=("ssh" "cron" "ufw" "rsyslog")

#loop over an array, if service is active show ok, else show failed and create new array, which stores failed services.
for service_name in "${services[@]}"; do
   if systemctl is-active "${service_name}" &> /dev/null; then
          echo "Checking '${service_name}'... OK"
  else

        echo "Checking '${service_name}'... FAILED"
        failed_services+=("$service_name")
        #echo "Checking ${failed_services[@]}... FAILED"

   fi
done

echo ""
echo "Failed services: ${#failed_services[@]}"
echo "Failed : ${failed_services[@]}"

Associative arrays

Syntax

Associative arrays — instead of numeric indexes, you use named keys.
Regular array — numeric index:
servers[0]="web01"
servers[1]="web02"

Associative array — named key:
servers["production"]="web01"  
servers["staging"]="web02"
servers["database"]="db01"

Think of it like a dictionary:
key          → value
"production" → "web01"
"staging"    → "web02"
"database"   → "db01"

syntax:
Define — must use declare -A first:
declare -A servers
servers["production"]="web01"
servers["staging"]="web02"

Or all at once:
declare -A servers=(
    ["production"]="web01"
    ["staging"]="web02"
    ["database"]="db01"
)

Access one value:
echo "${servers["production"]}"   # web01

Get all keys:
echo "${!servers[@]}"   # production staging database

Get all values:
echo "${servers[@]}"    # web01 web02 db01

Loop over keys and values:
for key in "${!servers[@]}"; do
    echo "$key → ${servers[$key]}"
done

Loop associative array:
declare -A server_ips=(
    ["web01"]="192.168.1.10"
    ["web02"]="192.168.1.11"
    ["db01"]="192.168.1.20"
)

for server in "${!server_ips[@]}"; do
    echo "$server → ${server_ips[$server]}"
done

## loop over the keys
for key in "${!arr[@]}"; do
        echo "got $key"
done
  


Create an associative array that maps service names to their config file paths. Loop over it and print whether each config file exists or not.
Example
[“ssh”]=”/etc/ssh/sshd_config”
[“cron”]=”/etc/crontab”
[“rsyslog”]=”/etc/rsyslog.conf”
[“ufw”]=”/etc/ufw/ufw.conf”

expected output:
ssh → /etc/ssh/sshd_config → EXISTS
cron → /etc/crontab → EXISTS
rsyslog → /etc/rsyslog.conf → EXISTS
ufw → /etc/ufw/ufw.conf → EXISTS

declare -A service_name_and_paths=(
    ["ssh"]="/etc/ssh/sshd_config"
    ["cron"]="/etc/crontab"
    ["rsyslog"]="/etc/rsyslog.conf"
    ["ufw"]="/etc/ufw/not_existing.conf"
)

for service in "${!service_name_and_paths[@]}"; do
    path="${service_name_and_paths[$service]}"
    if [ -f "$path" ]; then
        echo "$service → $path → EXISTS"
    else
        echo "$service → $path → DOES NOT EXIST"
    fi
done

Write a script called server_inventory.sh that uses an associative array to store server names and their IPs. The script must have:

An associative array with at least 4 servers and their IPs
A function called ping_servers that loops over the array, pings each server once and stores online servers in a regular array and offline servers in another regular array
A function called print_report that prints a summary like this:

================================
      SERVER INVENTORY REPORT
================================

ONLINE SERVERS: 2
 – web01 (192.168.1.10)
 – web02 (192.168.1.11)

OFFLINE SERVERS: 2
 – db01 (192.168.1.20)
 – db02 (192.168.1.21)


================================

declare -A server_name_and_ip=(
    
       ["google"]="8.8.8.8"
       ["cloudflare"]="1.1.1.1"
       ["slop1"]="192.168.64.100"
       ["slop2"]="192.168.64.200"
       ["slop3"]="192.168.64.201"
)
#explicitly declaring empty arrays at the top is cleaner and safer
online_servers=()
offline_servers=()


ping_servers() {
    
for server_name in "${!server_name_and_ip[@]}"; do

    server_ip="${server_name_and_ip[$server_name]}"

if ping -c1 $server_ip &>/dev/null; then

	#echo "Server with name ${server_name} and IPv4: ${server_ip} is UP and running"
	online_servers+=("$server_name")


else

              #echo "Server with name ${server_name} and IPv4: ${server_ip} is DOWN!"
	      offline_servers+=("$server_name")
fi

done

#echo "Online servers are: ${online_servers[@]}"
#echo "Offline servers are: ${offline_servers[@]}"
}
#ping_servers

print_report() {
##populate ping_servers function first
ping_servers

echo "================================"
echo "       SERVER INVENTORY REPORT  "
echo "================================"

echo "ONLINE SERVERS: ${#online_servers[@]}"

for online_server_name in "${online_servers[@]}"; do

 echo "$online_server_name : ${server_name_and_ip[$online_server_name]}"

done
echo ""
echo "OFFLINE SERVERS: ${#offline_servers[@]}"

for offline_server_name in "${offline_servers[@]}"; do

echo "$offline_server_name : ${server_name_and_ip[$offline_server_name]}"

done
}
print_report

IFS

IFS is a special bash variable that tells bash “how to split text into pieces”. By default IFS contains three characters: space, tab, newline.

line="web01 web02 web03"
for server in $line; do
    echo "$server"
done
# prints:
# web01
# web02
# web03


Changing IFS:
IFS=":"
line="root:x:0:0:root:/root:/bin/bash"
for field in $line; do
    echo "$field"
done
# prints:
# root
# x
# 0
# 0
# root
# /root
# /bin/bash

## using array
array=(
	foo
	bar
	baz
)

#IFS=.
echo "array is ${array[*]}"

Quick reference

IFS                    → Internal Field Separator
default value          → space, tab, newline
IFS=":"                → permanent change — avoid
IFS=":" command        → temporary change — preferred
OLD_IFS=$IFS           → save before changing
IFS=$OLD_IFS           → restore after changing
common use             → while IFS=: read -r col1 col2

Command Substitution

What is command substitution?
It means "run this command and give me its output as a value":

result=$(command)

common uses in sysadmin scripts:

Store command output:

current_date=$(date +%Y-%m-%d)
hostname=$(hostname)
disk_usage=$(df -h / | awk 'NR==2 {print $5}')

Use directly in strings:

echo "Server $(hostname) started at $(date)"
echo "You are logged in as $(whoami)"

Use in conditions:

if [ $(whoami) == "root" ]; then
    echo "You are root"
fi

Use in filenames:

backup="backup_$(date +%Y-%m-%d).tar.gz"
# creates: backup_2024-01-15.tar.gz

Important: Command substitution creates subshell. Can create bug if you do not know it


i=5
my-func() {
        i=6
        echo "hi"

}
thing=$(my-func)
echo "thins is $thing"

echo "i is $i"
#the result of i is 5, even the funchtion changed it 6, because () creates a subshell

Reference:

$(command)                    → capture output of command
$(command1 | command2)        → capture output of pipeline
$(command1 $(command2))       → nested substitution
var=$(command)                → store in variable
echo "text $(command) text"   → inline in string
file_$(date +%Y%m%d).log     → dynamic filenames

#The rule
#subshell can send data UP only through stdout — echo
#subshell CANNOT send variable changes UP to parent

# ALL of these create subshells:
result=$(command)       # command substitution
(command)               # explicit subshell
command | while read    # pipe creates subshell!

Sourcing code

Source reads and executes a file in the current shell — not a subshell. Variables, functions and settings defined in that file become available in your current session.

SYNTAX:
source file.sh          → load file into current shell
. file.sh               → same thing, shorthand

VS EXECUTING:
./script.sh             → runs in subshell, variables die
source script.sh        → runs in current shell, variables survive

COMMON USES:
source functions.sh     → load reusable functions
source config.sh        → load configuration variables
source ~/.bashrc        → reload shell config after editing

BEST PRACTICES:
# always use full path in scripts
source /etc/myapp/config.sh

# check file exists before sourcing
[ -f config.sh ] && source config.sh

# common pattern — config + main script
config.sh               → variables only
functions.sh            → functions only
main.sh                 → sources both, runs logic

FILE NAMING CONVENTIONS:
config.sh               → configuration variables
functions.sh            → reusable functions
.env                    → environment variables (docker style)
lib.sh                  → library of functions


Common use case:
Splitting a big script into files

# functions.sh — just your functions
check_disk() { ... }
check_memory() { ... }
ping_servers() { ... }

# main.sh — your main script
source functions.sh    # load all functions
check_disk             # now available here
ping_servers           # now available here

Arithmetic Expression

Strange behavior when using set -e. Be careful with exit codes when performing arithmetics.

thing=$(( 2 + 2 ))

echo "thing is "$thing""


a=2
b=3

echo $(( a + b ))


p=10

if ((p % 2)); then

        echo "p is even" #chetno
else

        echo "p is odd" #nechetno
fi

#In bash, ((expression)) is an arithmetic evaluation, and its exit status depends on the numeric result:
#If the arithmetic result is non-zero → exit status 0 (true)
#If the arithmetic result is zero → exit status 1 (false)

d=10

if (( d % 2 == 0 )); then
    echo "d number is even"
else
    echo "d number is odd"
fi

##the code below will return i is 0, becayse ((i++)) while doint artithemetics i=0 returns error, so $? = 1  and set -e stops the execution
set -e

i=0

while ((i<10)); do
        echo "i is $i"
        ((i++))
done
~    

Logical Operators

BASH LOGICAL OPERATORS: QUICK REFERENCE
=======================================

1. COMMAND CHAINING (based on exit status)
------------------------------------------
cmd1 && cmd2    AND       run cmd2 only if cmd1 succeeded (exit 0)
cmd1 || cmd2    OR        run cmd2 only if cmd1 failed (non-zero)
! cmd           NOT       invert the exit status of cmd
cmd1 ; cmd2     sequence  run cmd2 always, regardless of result
cmd1 & cmd2     background  cmd1 goes to background, cmd2 starts now
cmd1 | cmd2     pipe      stdout of cmd1 feeds cmd2 (not a logical op)

Examples:
  mkdir -p /tmp/x && cd /tmp/x          # cd only if mkdir worked
  ping -c1 host || echo "host down"     # echo only on failure
  ! grep -q foo file && echo "no foo"   # negate, then AND
  cmd1 && cmd2 || cmd3                  # pseudo if/else (see gotcha)

GOTCHA: in "A && B || C", C also runs if A succeeds but B fails.
Use a real if/else when that matters.

GROUPING:
  { cmd1; cmd2; } && cmd3     runs in the current shell
  ( cmd1; cmd2 ) && cmd3      runs in a subshell