Skip to main content
Techlogia — AI and Web Development Berlin

Learn to write Bash scripts (with exercises)

A Bash script bundles commands you'd otherwise type one by one into a repeatable file. That's exactly what makes admins fast. Here you write your first script step by step – and run it directly on a real VM.

Shebang and executability

Every script starts with #!/bin/bash – the shebang that tells the system what runs the file. chmod +x makes it executable. This is exactly where Bash connects to file permissions.

Variables and arguments

You store values in variables (name="value"); inputs reach the script via $1, $2. That turns a rigid sequence into a flexible tool.

Loops and conditions

With for/while loops and if conditions you automate repetition and make decisions – the basis of any real automation.

Why a script fails differently from a command

A script runs without an audience. What you see instantly in a terminal – an error, a prompt – vanishes without trace in a cron run. That is why set -euo pipefail belongs at the top: -e aborts on an error, -u on an unset variable, pipefail also when only one part of a pipe fails. Without that line a broken script cheerfully continues and reports success at the end.

Quotes are not cosmetic

rm $file and rm "$file" are two different commands as soon as the filename contains a space: without quotes the shell splits the value into several arguments. The same rule applies to $@ versus "$@". Almost every data loss caused by a bash script comes down to a missing quote.

Separate the output: stdout versus stderr

A script has two output channels. Results belong on stdout, progress messages on stderr – written with echo "…" >&2. The difference matters as soon as someone processes the output: result=$(my_script) captures only stdout, and your progress notes do not end up in the middle of the data.

Clean up, even when it goes wrong

If your script creates a temporary file, it must disappear on abort too. trap 'rm -f "$tmp"' EXIT attaches a cleanup to the end of the script – whether it finishes normally, aborts on set -e, or is killed with Ctrl + C. Without trap, a regularly scheduled script quietly fills /tmp over months.

Exit codes: how a script signals success

Every command leaves an exit code: 0 means success, anything else failure. $? holds the last one. && and || build on it, and so does whoever calls your script – a cron job, a monitor, a delivery pipeline. A script that returns 0 despite an error reports success for something that did not happen. That is why a deliberate exit 0, or a fitting value, belongs at the end.

What usually goes wrong

The most common mistake is a script that only works in its own directory: relative paths break the moment cron starts it from /. The second is a pipe whose first element fails – without pipefail only the last one counts, and false | echo ok passes as success. The third is carelessness when deleting: rm -rf "$dir/"* with an empty variable points at the root directory.

From one-liner to reusable tool

A script becomes useful once it can explain itself. A short usage() function, printed on missing arguments or on -h, saves you from reading your own source in three months. Speaking variable names and functions instead of copy-and-paste belong to it: whoever has the same block three times in a script has to change it three times on the next fix and will forget one place. That is not a matter of style but the difference between a script that survives in operation and one that gets replaced after the first change.

Commands to try

Create a script and make it executable

$ nano backup.sh$ chmod +x backup.sh$ ./backup.sh /tmp/data

Without chmod +x the shell answers Permission denied even though the file is readable – the execute bit is separate.

The skeleton that makes every failure visible

$ #!/usr/bin/env bash$ set -euo pipefail$ target="${1:?give a path}"$ echo "backing up to $target"

${1:?…} aborts with a readable message when the argument is missing – better than an empty path that quietly points at the root directory.

Loop over files without tripping on spaces

$ for f in *.log; do$   [ -e "$f" ] || continue$   echo "$f: $(wc -l < "$f") lines"$ done

The second line catches the case where no file matches – otherwise the loop runs once with the literal pattern *.log as the name.

Temporary file with guaranteed cleanup

$ tmp=$(mktemp)$ trap 'rm -f "$tmp"' EXIT$ sort input.txt > "$tmp"$ mv "$tmp" output.txt

mktemp creates a collision-free name. The closing mv is atomic – either the complete file is there or the old one, never half of it.

Common error messages

bash: ./backup.sh: Permission denied
Cause: The file lacks the execute bit – reading it is still allowed.
Fix: chmod +x backup.sh. Alternatively bash backup.sh, which starts the interpreter on the file directly.
./backup.sh: line 3: $'\r': command not found
Cause: The file has Windows line endings (CRLF). The trailing carriage return is read as part of the command.
Fix: Convert with sed -i 's/\r$//' backup.sh or set the editor to LF.
backup.sh: line 7: target: unbound variable
Cause: set -u is doing its job: the variable was used but never set – usually a typo in the name.
Fix: Check the name or set a default: target="${1:-/tmp}".
syntax error near unexpected token `fi'
Cause: An if without a closing then on the same or next line, or a missing semicolon before then.
Fix: Check with bash -n script.sh without running it. The reported line number points at the CLOSING word; the mistake is usually above it.

In five steps

  1. 01Create the file and start with #!/usr/bin/env bash – not the hard-coded /bin/bash.
  2. 02Put set -euo pipefail on the second line, before anything happens.
  3. 03Read one argument and guard it against being forgotten with ${1:?…}.
  4. 04Make it executable with chmod +x and run it once without an argument – the error must be readable.
  5. 05Check the syntax with bash -n script.sh without running it; that belongs before every real run.

What you practise

Shebang, chmod +xVariablen & $1/$2for / while / ifset -euo pipefail

Have a project?

Let's bring your idea to life together. We're happy to advise you with no obligation.

Get in Touch

How do you like this page?