﻿# Shell scripts: variables, conditions, loops and regular expressions

> How a bash script is built and run: variables and quoting, arguments, if and the test command, loops, functions, regular expressions and redirection. Ends with a robust script skeleton – check the input, send errors to stderr, exit with a non-zero code.

Source: https://laszlonemes.com/university/computer-systems/shell-scripts · Author: László Nemes

Session 3 · Computer Systems

How a bash script is built and run: variables and quoting, arguments, if and the test command, loops, functions, regular expressions and redirection. Ends with a robust script skeleton – check the input, send errors to stderr, exit with a non-zero code.

22 min read `Running a script: ./, bash, source` `Quoting and expansion` `Arguments: $1, $#, "$@"` `if, test and exit codes` `Regular expressions with grep` `stdout, stderr and redirection`

A **shell script** is a text file holding the commands you would otherwise type into the terminal. Bash reads it top to bottom and runs one line after the other. That is the whole idea – and it is enough to turn a task you repeat every day into one command that takes parameters, checks its input and can be reused by other scripts.

This session covers the language: variables, arguments, conditions, loops, functions, regular expressions and redirection. The next one, [Shell scripting in practice](https://laszlonemes.com/university/computer-systems/shell-practice), combines all of it into real tasks, and your shell-script assignment is built from exactly these pieces.

After this session you can

- write, make executable and run a script, and explain the difference between ./script, bash script and source script
- predict what the shell does to a command line before it runs it – and quote variables so it does not surprise you
- read script arguments, check them, and stop with a usage message on stderr and a non-zero exit code
- write if, while, for and functions, knowing that if branches on an exit code
- read and write regular expressions for grep and [[ =~ ]]

> **The one thing to remember:** bash is a language of *commands*. A condition is a command whose exit code is 0 or not, a function returns a status and prints its result, and before any command runs, the shell rewrites the line you wrote. Most surprising behaviour follows from these three facts.

## Your first script

hello.sh

```shell
#!/bin/bash
# A comment: everything after # is ignored
echo "Hello world"
```

- **`#!/bin/bash`** – The **shebang**. Always the very first line: it tells the system which interpreter runs the file.
- **`# …`** – A comment. The shell ignores everything after the # on that line.
- **`echo`** – Prints its arguments to the standard output – the terminal, unless redirected.

A new file is not executable. Give it the **x** (execute) permission once, then run it. After `chmod +x`, `ls -l hello.sh` shows `-rwxr-xr-x` in the first column – the permission bits from [Session 1](https://laszlonemes.com/university/computer-systems/unix-basics).

```shell
chmod +x hello.sh     # once per file: give it the execute (x) permission
./hello.sh            # run it from the current directory
```

### Three ways to run a script

```shell
./hello.sh            # needs x; new process, interpreter from the shebang
bash hello.sh         # no x needed; new process, bash chosen by you
source hello.sh       # no x needed; runs in YOUR shell (same as: . hello.sh)
```

The first two start a **new process**; `source` (or its short form, a single dot) runs the lines inside the shell you are typing into (Fig. 1). That difference shows up as soon as a script sets a variable or changes directory: in a child process the change disappears when the script ends, with `source` it stays.

> **Fig. 1** · ./ and bash both start a child process – whatever the script changes dies with it. source runs the lines in your own shell.
>
> *Diagram:* Three ways to run a script. ./hello.sh needs the execute permission and starts a new bash process chosen by the shebang; bash hello.sh needs no permission and also starts a new process. Variables set in a child process vanish when it exits. source hello.sh runs the lines inside your current shell, so the variables it sets stay.

Good to know

**Why ./ and not just hello.sh?**

When you type a command name, the shell looks for it only in the directories listed in the `PATH` variable, and the current directory is not one of them (on purpose – otherwise a file called `ls` in someone's folder could hijack your `ls`). `./` means "the file in this directory", so no search is needed.

Common mistake

**bad interpreter or $'\r': command not found**

If you wrote the script on Windows, its lines end in CRLF and bash chokes on the invisible `\r`. Fix it with `dos2unix script.sh` or `sed -i 's/\r$//' script.sh`, and set your editor to LF. The full story is in [Session 5](https://laszlonemes.com/university/computer-systems/files-line-endings).

## Variables and quoting

```shell
name="student"           # no spaces around =, no $ on the left
echo "$name"             # $ to read the value
echo "${name}_backup"    # braces mark where the name ends
today=$(date +%F)        # command substitution: the output becomes the value
sum=$(( 3 + 4 ))         # arithmetic
unset name               # from here on, "$name" is an empty string
```

- No spaces around `=`. `name = "x"` runs a command called `name` with two arguments.
- No `$` when assigning, `$` when reading.
- Names use letters, digits and `_`, and do not start with a digit.
- An undefined variable is silently an **empty string** – no error. A typo in a name is a bug bash will not report.

### What the shell does to a line before running it

This is the key to quoting. When you press Enter, the command does not get your text. The shell first substitutes variables, then command substitutions, then splits the line into words at spaces, then expands patterns like `*.txt` into matching file names (Fig. 2). Only the finished words reach the command.

> **Fig. 2** · Four rewrites happen before echo starts. Quotes decide which ones apply: double quotes allow substitution but keep the result one word.
>
> *Diagram:* Before a command runs, the shell rewrites the line: it substitutes variables, then command substitutions, then splits the line into words, then expands glob patterns like *.txt into file names. Only then does echo start, with the finished words as its arguments. Double quotes keep Hi student as one word; single quotes would stop every step.

```shell
echo "Hello $USER"       # Hello student
echo 'Hello $USER'       # Hello $USER
echo "Today: $(date +%F)"  # Today: YYYY-MM-DD
```

| Form | What it does |
| --- | --- |
| `"…"` | Variables and $( ) are substituted, but the result stays one word and is not globbed. |
| `'…'` | Everything is literal. Nothing is substituted. |
| `$(…)` | Command substitution: runs the command and puts its output in its place. |

The rule that follows: **almost always write variables in double quotes**, `"$file"`. Without them a file name with a space becomes two arguments, and an empty value disappears entirely – which turns `[ $a == x ]` into the syntax error `[ == x ]`.

### Environment variables

| Variable | Holds |
| --- | --- |
| `HOME` | the absolute path of your home directory |
| `PWD` | the absolute path of the current directory |
| `PATH` | the directories searched for commands, separated by : |
| `USER` | your user name |
| `SHELL` | your default shell |
| `RANDOM` | a new random number from 0 to 32767 on every read (a bash variable) |
| `PS1` | the look of your prompt (a bash variable) |

```shell
echo "User: $USER, home: $HOME"
echo "Dice: $(( RANDOM % 6 + 1 ))"
env                      # list every environment variable
```

## Script arguments

Words after the script name arrive as numbered variables (Fig. 3). Quotes on the command line decide what counts as one argument – the same word splitting as before.

```shell
./args.sh "Ada Lovelace" 1815 London
```

> **Fig. 3** · The quoted "Ada Lovelace" is one argument. $# counts only the arguments, not the script name.
>
> *Diagram:* The command line ./args.sh "Ada Lovelace" 1815 London becomes $0 = the script name, $1 = Ada Lovelace (the quotes keep it one argument), $2 = 1815 and $3 = London. $# is 3 because the script name does not count. "$@" gives the three arguments as separate words, "$*" glues them into one string.

| Variable | Meaning |
| --- | --- |
| `$0` | the script name, as it was started |
| `$1 … $9` | the first … ninth argument |
| `${10}` | from the tenth on, braces are required ($10 is $1 followed by a 0) |
| `$#` | the number of arguments |
| `"$@"` | all arguments, each as a separate word – the one to loop over |
| `$*` | all arguments glued into one string |
| `$?` | the exit code of the last command (0 = success) |
| `$$` | the PID of the running shell – handy for unique temporary file names |

args.sh – the interesting lines

```shell
echo "Script name: $0"
echo "First: $1"
echo "Count: $#"
for arg in "$@"; do      # "$@" keeps every argument as one word
    echo "Argument: $arg"
done
```

```text
Script name: ./args.sh
First: Ada Lovelace
Count: 3
Argument: Ada Lovelace
Argument: 1815
Argument: London
```

## Conditions: if and test

```shell
if [ "$n" -lt 10 ]; then
    echo "one digit"
elif [ "$n" -lt 100 ]; then
    echo "two digits"
else
    echo "three or more digits"
fi
```

The spaces inside `[ … ]` are not style. `[` is a **command** – another name for `test` – and everything up to `]` are its arguments. Like every command it finishes with an exit code, and that code is all `if` looks at (Fig. 4).

> **Fig. 4** · if runs a command. Exit code 0 means success and picks then; anything else picks else.
>
> *Diagram:* if does not evaluate a boolean, it runs a command and looks at its exit code. [ "$n" -lt 10 ] is a command (another name for test). Exit code 0 means success and selects the then branch; any code from 1 to 255 selects the else branch.

```shell
[ 5 -lt 10 ]; echo $?     # 0  -> true
[ 5 -gt 10 ]; echo $?     # 1  -> false
test 5 -lt 10; echo $?    # 0  -> [ is just another name for test
```

So any command can be a condition. `grep -q` prints nothing and only reports through its exit code – exactly what an `if` needs:

```shell
if grep -q "error" log.txt; then
    echo "The log contains an error"
fi
```

### Operators

| Numbers | Strings | Files – true if … |
| --- | --- | --- |
| `-eq` equal | `==` equal | `-e` the path exists |
| `-ne` not equal | `!=` different | `-f` it is a regular file |
| `-lt` less than | `-z` empty string | `-d` it is a directory |
| `-le` less or equal | `-n` not empty | `-s` it is not empty |
| `-gt` greater than | `=~` matches a regex (only in [[ ]]) | `-r` `-w` `-x` readable, writable, executable |
| `-ge` greater or equal |  |  |

Combine conditions with `&&` (and), `||` (or) and `!` (not) inside `[[ ]]`, for example `[[ ! -f "$file" ]]` or `[[ $ext == "jpg" || $ext == "png" ]]`.

### Which brackets?

| Syntax | Use it for | Example |
| --- | --- | --- |
| `[ ]` | simple tests, numbers with -eq, -lt; works in every POSIX shell | `[ "$n" -lt 10 ]` |
| `[[ ]]` | strings, && and \|\| inside, regex with =~; bash only, safer with unquoted variables | `[[ $a == x && $b != y ]]` |
| `(( ))` | arithmetic with the usual < > == operators, no $ needed | `(( n % 2 == 0 ))` |
| `( )` | not a test: runs commands in a subshell | `(cd /tmp && ls)` |

```shell
if (( n % 2 == 0 )); then     # inside (( )) no $ is needed and < > work
    echo "$n is even"
fi
```

In a `#!/bin/bash` script, `[[ ]]` is the comfortable default. Learn `[ ]` too – most existing scripts use it.

### Reading input

```shell
read -r -p "Enter username: " username
read -r -s -p "Enter password: " password   # -s: typing is not shown
echo                                        # -s also hides the Enter, so break the line
```

`-r` keeps backslashes as typed (use it every time), `-p` prints a prompt, `-s` hides what is typed. Arguments are better for anything another script might pass in; `read` is for interactive questions.

## Working with strings

Bash can cut and change strings itself, without calling another program. All of these are forms of `${…}`:

```shell
str="ELTE IK University"
echo "${#str}"          # 18                  length
echo "${str:5:2}"       # IK                  2 characters from index 5
echo "${str:8}"         # University          from index 8 to the end
echo "${str: -10}"      # University          last 10 (the space before - matters)
echo "${str/IK/TTK}"    # ELTE TTK University first match replaced
echo "${str//E/e}"      # eLTe IK University  every match replaced
echo "${str^^}"         # ELTE IK UNIVERSITY  upper case
echo "${str,,}"         # elte ik university  lower case
```

> **Fig. 5** · Positions start at 0, and spaces count. ${str:start:length} – leave out the length to go to the end.
>
> *Diagram:* The characters of ELTE IK University are numbered from 0. ${str:5:2} starts at index 5 and takes 2 characters, giving IK. ${str:8} starts at index 8 and runs to the end, giving University.

```shell
first="ELTE "
second="IK"
both="${first}${second}"   # ELTE IK
both+=" University"        # ELTE IK University
```

Braces matter when text follows the name: `"$file_backup"` reads a variable called `file_backup`, `"${file}_backup"` appends `_backup` to `$file`. The default-value form is the shortest way to make an argument optional:

```shell
n=${1:-10}        # the first argument, or 10 if there is none
```

Good to know

**Case conversion on older bash**

`${str^^}` and `${str,,}` do not exist in older bash versions, such as the one macOS ships – you get "bad substitution". `tr '[:lower:]' '[:upper:]'` works everywhere. On the course server both work.

## Loops

| Loop | Repeats … |
| --- | --- |
| `while` | as long as the condition is true |
| `until` | as long as the condition is false |
| `for … in …` | once for every item of a list: words, a range, files |
| `for (( … ))` | with a counter, C style |

while

```shell
count=1
while [ "$count" -le 5 ]; do
    echo "$count"
    ((count++))
done
```

until

```shell
count=1
until [ "$count" -gt 3 ]; do     # runs while the condition is FALSE
    echo "$count"
    ((count++))
done
```

for – list, range, files

```shell
for fruit in apple pear banana; do echo "$fruit"; done   # a list of words
for i in {1..5}; do echo "$i"; done                      # a range
for file in *.sh; do echo "Script: $file"; done          # files (globbing)
```

for – C style

```shell
for (( i=10; i>0; i-- )); do
    echo -n "$i "      # -n: no newline
done
echo
```

break and continue

```shell
for i in {1..10}; do
    if (( i == 3 )); then continue; fi   # skip the rest of this round
    if (( i == 6 )); then break; fi      # leave the loop
    echo -n "$i "
done                                     # prints: 1 2 4 5
```

One loop you will write in almost every task processes a file line by line. The redirection after `done` feeds the file into the loop, and each `read` takes the next line:

```shell
while read -r line; do
    echo "Line: $line"
done < numbers.txt
```

[Session 4](https://laszlonemes.com/university/computer-systems/shell-practice) takes this loop apart – splitting lines into fields, the last line without a newline, and why piping into it loses your variables.

Common mistake

**while [ $valid ] is always true**

`[ $valid ]` only checks that the string is not empty, and `false` is a non-empty string. Compare explicitly – `[ "$valid" == "true" ]` – or use the commands `true` and `false` themselves: `while true; do … done`.

## Functions

```shell
rectangle_area() {
    local area=$(( $1 * $2 ))   # local: exists only inside the function
    echo "Area: $area"
}

rectangle_area 10 20            # call without parentheses: Area: 200
```

- Define a function before the line that calls it.
- Call it by name, without parentheses; arguments follow, separated by spaces.
- Inside, the function reads its own arguments as `$1`, `$2`, `$#`, `"$@"` – the script's arguments are hidden while it runs.
- Without `local`, every variable is global: the rest of the script sees it and can overwrite it.

The surprise is how a function gives something back. It has two separate channels (Fig. 6): what it **prints**, which the caller captures with `$( )`, and its **exit status**, a number from 0 to 255 that `if` can branch on.

> **Fig. 6** · Results travel through stdout and $( ); return only sets a success/failure status.
>
> *Diagram:* A bash function has two separate outputs. The result is printed to stdout with echo and captured by the caller with $( ), for example result=$(square 7). The status is a number from 0 to 255 set by return or by the last command, and if branches on it directly.

```shell
square() {
    echo $(( $1 * $1 ))         # the RESULT goes to stdout
}
is_even() {
    (( $1 % 2 == 0 ))           # the STATUS: 0 if true, 1 if false
}

result=$(square 7)              # 49
if is_even 4; then echo "4 is even"; fi
```

Common mistake

**return is not return**

In Java or Python, `return x` hands back a value. In bash it sets the exit status. Use `echo` + `$( )` for numbers and text, `return 0` / `return 1` for yes and no.

## Regular expressions

A regular expression describes the **shape** of text you are looking for. `grep`, `sed`, `awk` and bash's `[[ … =~ … ]]` all use them. Most characters match themselves; the special ones (`. * [ ( ? + ^ $ \`) need a backslash to be taken literally – `\.` is a real dot. Try patterns on [regex101](https://regex101.com/), which explains every piece.

### Characters and classes

| Pattern | Matches | Example |
| --- | --- | --- |
| `.` | any single character | `a.a` → aba, a1a |
| `[abc]` | one of the listed characters | `[0-9]` → one digit |
| `[a-d]` | one character from a range | `[a-z]` → one lowercase letter |
| `[^abc]` | one character NOT in the list | `[^0-9]` → not a digit |
| `\w` | a letter, digit or _ | `\w+` → a word |
| `[[:digit:]]` | a digit (POSIX class); also [[:alpha:]], [[:space:]] | same as [0-9] |

Inside `[ ]` most special characters lose their meaning: `[.,]` is a dot or a comma, no backslash needed. `grep -E` does not know `\d` from other languages – write `[0-9]`.

### Anchors and repetition

| Pattern | Meaning | Example |
| --- | --- | --- |
| `^` | start of the line | `^apple` → lines starting with apple |
| `$` | end of the line | `^apple$` → lines that are exactly apple |
| `\b` | a word boundary | `\bpear\b` → pear as a whole word |
| `?` | the previous item 0 or 1 times | `-?[0-9]` → 5, -5 |
| `*` | 0 or more times | `ab*` → a, ab, abbb |
| `+` | 1 or more times | `[0-9]+` → 7, 2026 |
| `{n}` | exactly n times | `[0-9]{4}` → a four-digit number |
| `{n,m}` | n to m times; {n,} at least n, {,m} at most m | `[0-9]{2,4}` → 12, 1234 |

### Groups and alternatives

Parentheses make a group, so a repetition applies to all of it: `(ab)+` matches ab, abab. The bar chooses: `(apple|pear|plum)`. A group also remembers what it matched, and `sed` can reuse it as `\1`, `\2` – here to swap two words:

```shell
echo "apple pear" | sed -E 's/(\w+) (\w+)/\2 \1/'    # pear apple
```

### Reading a real pattern

The example script `line_counter.sh` counts the lines of a file that hold a single number ending in 1, with an optional sign and an optional decimal part. Read the pattern left to right, one piece at a time (Fig. 7).

```shell
grep -cE '^[+-]?([0-9]+[.,])?[0-9]*1$' "$1"
```

> **Fig. 7** · Six pieces, read left to right. The anchors ^ and $ are what make it a whole-line check.
>
> *Diagram:* The pattern ^[+-]?([0-9]+[.,])?[0-9]*1$ read piece by piece: start of line, an optional sign, an optional whole part followed by a dot or comma, any number of digits, a final 1, end of line. So 12.1, -13.321, 321 and 11 match; 123, asd and 1.15 do not.

### grep options you will use

| Option | Effect |
| --- | --- |
| `-E` | extended regex: + ? \| ( ) { } work without backslashes |
| `-c` | print the number of matching lines instead of the lines |
| `-i` | ignore case |
| `-v` | print the lines that do NOT match |
| `-o` | print only the matching part, one per line |
| `-n` | prefix each match with its line number |
| `-x` | the whole line must match |

```shell
grep -E '^[0-9]+$' numbers.txt      # lines that are only digits
grep -cE '^[0-9]+$' numbers.txt     # how many such lines
grep -iv 'error' log.txt            # lines WITHOUT "error", any case
```

Common mistake

**Single quotes around the pattern**

Write `grep -E '^[0-9]+$' f.txt`, never without quotes. Unquoted, the shell sees `$`, `*` and `?` first and expands them before grep ever gets the pattern (Fig. 2).

In bash's own test it is the other way round: the pattern after `=~` must **not** be quoted, or bash compares it as plain text.

```shell
if [[ $times =~ ^[0-9]+$ ]]; then     # pattern NOT quoted
    echo "a non-negative integer"
fi
```

## Standard streams and redirection

Every process starts with three open streams, each identified by a number, its **file descriptor**: 0 is **stdin** (input), 1 is **stdout** (normal output), 2 is **stderr** (error messages). On a terminal 1 and 2 both end up on the screen, so they look the same – until you redirect one of them (Fig. 8).

> **Fig. 8** · Each redirection changes where one descriptor points. 2>&1 copies where 1 points right now – so it goes after > file.
>
> *Diagram:* Where file descriptors 1 (stdout) and 2 (stderr) point for each redirection. > and >> send stdout to a file, 2> sends stderr, 2>/dev/null throws errors away, 2>&1 makes stderr a copy of stdout, >&2 sends normal output to stderr. Without the ampersand, >2 writes to a file called 2.

```shell
ls > out.txt              # stdout to a file (overwrite)
ls >> out.txt             # stdout appended
sort < names.txt          # stdin from a file
ls nope 2> err.txt        # stderr to a file
ls nope 2>/dev/null       # stderr thrown away
ls . nope > all.txt 2>&1  # both into the same file (order matters)
```

### Why error messages go to stderr

```shell
echo "File not found: $1" >&2   # an error message: stderr, not stdout
exit 1                          # and a non-zero exit code
```

`>&2` means "send this echo's stdout to where descriptor 2 points". The `&` is essential: `>2` would create a file called `2`. The payoff shows when someone saves your script's output:

```text
$ ./line_counter.sh nope.txt > result.txt
File not found: nope.txt        <- on the screen; result.txt stays clean
```

The same holds for pipes and `$( )`: `count=$(./line_counter.sh nope.txt)` captures only stdout, so an error message never ends up in `count` looking like a result. Pipes themselves – `cmd1 | cmd2`, stdout of one into stdin of the next – are covered in [Session 2](https://laszlonemes.com/university/computer-systems/processes-filters).

## Putting it together: a robust script

The example scripts of this session share a shape worth copying: check everything at the top, stop at the first problem with a message on stderr and a non-zero exit code, then do the work knowing the input is valid. Here is the head of `line_writer.sh`, which prints a text a given number of times:

line_writer.sh – the checks

```shell
if [ "$#" -ne 2 ]; then
    echo "Usage: $0 <number of times> <text>" >&2
    exit 1
fi
times=$1
text=$2
if [[ ! $times =~ ^[0-9]+$ ]]; then
    echo "Error: '$times' is not a non-negative integer" >&2
    exit 1
fi
```

`dir_tester.sh` does the same with input from `read`: it refuses an empty name, then creates the directory only if it does not exist yet (`mkdir -p` also creates missing parent directories).

dir_tester.sh – the core

```shell
read -r -p "Enter directory name: " dir
if [ -z "$dir" ]; then
    echo "Directory name cannot be empty" >&2
    exit 1
fi
if [ -d "$dir" ]; then echo "Directory exists"; else mkdir -p "$dir"; fi
```

### Exit codes

Every command and script ends with an exit code: `0` for success, `1`–`255` for some kind of failure. Your scripts should follow the same convention, because other scripts – and `if` – rely on it.

```shell
./line_writer.sh 1 test; echo $?    # 0
./line_writer.sh;        echo $?    # 1
```

In your assignment

**Validate first, then work**

The shell-script assignment is tested with wrong input as well as right input. At the top of your script, check the number of arguments (`$#`), that files exist (`-f`, `-d`), and that numbers are numbers (`[[ $n =~ ^[0-9]+$ ]]`). On any problem print a `Usage: …` line to stderr with `>&2` and `exit 1`. Quote every variable – the tester will use a file name with a space in it sooner or later.

## Common mistakes and debugging

| Wrong | Right | Why |
| --- | --- | --- |
| `var = "x"` | `var="x"` | no spaces around = |
| `$var="x"` | `var="x"` | no $ when assigning |
| `if [$n -lt 10]` | `if [ "$n" -lt 10 ]` | [ is a command: it needs spaces around it |
| `[ $a == $b ]` | `[ "$a" == "$b" ]` | an empty or spaced value breaks the test |
| `[ "$n" < 10 ]` | `[ "$n" -lt 10 ]` | inside [ ], < is a redirection, not a comparison |
| `grep ^[0-9]+$ f.txt` | `grep -E '^[0-9]+$' f.txt` | the shell expands the unquoted pattern first |
| `cat f \| grep x \| wc -l` | `grep -c x f` | grep reads files and counts by itself |
| `my_func()` | `my_func` | no parentheses when calling a function |
| `echo "msg" >2` | `echo "msg" >&2` | >2 writes a file named 2 |

### When a script misbehaves

```shell
bash -n script.sh      # syntax check only, nothing runs
bash -x script.sh      # print every command, with the values filled in
set -x                 # inside a script: tracing on ...
set +x                 # ... and off again
```

`bash -x` is the quickest way to see what the shell really ran – every line after expansion, prefixed with `+`:

```text
$ bash -x line_writer.sh 2 hi
+ '[' 2 -ne 2 ']'
+ times=2
+ text=hi
+ [[ 2 =~ ^[0-9]+$ ]]
+ (( i=0 ))
+ (( i<times ))
+ echo hi
hi
```

For a second opinion, paste the script into [ShellCheck](https://www.shellcheck.net/). It finds missing quotes, unused variables and most of the table above, and explains each warning.

## Practice

Write each one as a script with a shebang, run it with good and bad input, and check `$?` after a failure. Try first, open the hint when stuck.

Task 1

**Greeting**

The script takes a name as its argument and prints `Hello <name>!`. Without an argument it greets the logged-in user.

**Approach:**

No `if` needed – a default value does it in one line:

```shell
name=${1:-$USER}
```

Task 2

**Calculator**

The script takes two integers and prints their sum, difference and product. With the wrong number of arguments, or a non-integer, it prints a usage message and exits with an error.

```text
$ ./calc.sh 7 3
Sum:        10
Difference: 4
Product:    21
```

**Approach:**

Two checks before any arithmetic: the count, then the format (an optional sign, then digits). The arithmetic itself is `$(( a + b ))` and friends.

```shell
if [ "$#" -ne 2 ]; then
    echo "Usage: $0 <number1> <number2>" >&2
    exit 1
fi
if [[ ! $1 =~ ^[+-]?[0-9]+$ || ! $2 =~ ^[+-]?[0-9]+$ ]]; then ...
```

Task 3

**File, directory or nothing?**

The script takes a path and reports whether it is a regular file, a directory, or does not exist.

**Approach:**

An `if … elif … else` chain with file tests. Order matters: `-e` is true for files and directories alike, so put the specific tests first.

```shell
if [ -f "$path" ]; then ...
elif [ -d "$path" ]; then ...
else ...
```

Task 4

**Even numbers**

The script takes a number `n` and prints the even numbers from 1 to `n` on one line, e.g. `2 4 6 8 10` for 10.

**Approach:**

Either loop over every number and keep those where `(( i % 2 == 0 ))`, or start at 2 and step by two – then no condition is needed:

```shell
for (( i=2; i<=n; i+=2 )); do
    echo -n "$i "
done
```

Task 5

**Factorial in a function**

Write a function that computes the factorial of its argument, and call it with the script's first argument: `./factorial.sh 5` prints `5! = 120`.

**Approach:**

The function prints the result; the caller captures it with `$( )`. Keep the loop variables `local`. Bash integers are 64-bit, so 20! is the largest result that is still correct.

```shell
factorial() {
    local result=1 i
    for (( i=2; i<=$1; i++ )); do result=$(( result * i )); done
    echo "$result"
}
result=$(factorial "$1")
```

Task 6

**Negative integers only**

Add some lines to a `numbers.txt` and write a regular expression that matches only the lines holding a negative integer.

```text
12.1
-13.321
123
321
asd
something
11
```

**Approach:**

A required minus, at least one digit, and anchors at both ends so nothing else fits on the line:

```shell
grep -E '^-[0-9]+$' numbers.txt
```

```text
-7          match
-42         match
-13.321     no – the dot is not a digit
-0.5        no
+5          no – the sign must be a minus
12          no
```

Task 7

**Sum until zero**

The script keeps asking for numbers until the user enters `0`, then prints the sum. A non-number gets an error message and another chance; `Ctrl+D` must not cause an endless loop.

**Approach:**

An endless `while true` loop that you leave with `break`; `continue` skips invalid input. A failed `read` (end of input) is a reason to leave, too:

```shell
while true; do
    if ! read -r -p "Number (0 to finish): " number; then break; fi
    if [[ ! $number =~ ^[+-]?[0-9]+$ ]]; then echo "Not an integer" >&2; continue; fi
    (( number == 0 )) && break
    # ... add the number to the running sum
done
```

## Summary

- Start every script with `#!/bin/bash`, make it executable once with `chmod +x`, run it with `./`. `source` runs it in your own shell instead of a child process.
- Before a command runs, the shell substitutes variables and `$( )`, splits words and expands globs. Double-quote your variables.
- Arguments are `$1 … $9`, `${10}`, their count is `$#`, all of them `"$@"`.
- `if` runs a command and branches on its exit code; `[` is the `test` command, so it needs spaces. 0 means true.
- Functions print their result (capture it with `$( )`) and `return` only a status.
- Quote regex patterns for grep in single quotes; leave the pattern after `=~` unquoted.
- Errors go to stderr with `>&2`, followed by `exit 1`. Results go to stdout.
