Giving your scripts the ability to make decisions: the if/elif/else/fi structure based on exit status, the difference between test [ ] (POSIX) and [[ ]] (bash), test operators for strings, numbers, and files, plus && and || short-circuiting along with ready-to-use monitoring script patterns.

In episode 8 we covered arithmetic operations — computing with $(( )) and bc. In this episode we close out this foundational layer with the structure that makes all that computation meaningful: conditionals. Arithmetic without decisions is just numbers; decisions without arithmetic are just assumptions. Combine the two, and your scripts evolve from merely executing commands to thinking.
Since episode 5, you've already seen glimpses of many idioms we'll dissect now: [ $# -ne 2 ] for argument validation, [ -z "$nama" ] for checking empty input, (( total >= 1000000 )) for thresholds. This episode ties them all together: understanding why bash's if structure works on exit status, why [[ ]] is safer than [ ], and how test operators for strings, numbers, and files can catch almost every real-world condition.
if, elif, else, fi — Decisions Based on Exit StatusBash's if structure differs from other programming languages. In Python or JavaScript, if expects a boolean value. In bash, if expects a command — and it makes its decision based on that command's exit status: 0 means "true", anything non-zero means "false".
#!/usr/bin/env bash
if command_yang_dijalankan; then
echo "Berhasil (exit 0)"
elif perintah_lain; then
echo "Kondisi kedua terpenuhi"
else
echo "Semua kondisi gagal"
fiNotice the complete structure: if ... then ... elif ... then ... else ... fi. fi is the reverse of if — the block closer. The then keyword is required after the condition. And since the foundation is exit status, anything that returns an exit status can be a condition — not just a test. The most concrete example:
#!/usr/bin/env bash
if grep -q "error" /var/log/app.log; then
echo "ADA baris error di log — perlu investigasi."
else
echo "Tidak ada error ditemukan."
figrep -q prints nothing (the -q flag = quiet), and returns 0 if the pattern is found, 1 if not. Since if only reads the exit status, you don't need to write if [ $(grep ...) ]; then — just hand over the command directly. This is what makes bash so elegant: a condition is a command, not a data type.
if [ ... ]; then Form?If if is command-based, then what is [ ... ]? The answer: [ is a command — another name for the test program. It evaluates an expression and returns an exit status. So if [ "$a" = "b" ]; then reads as: run the test command with arguments "$a", =, "b", then use its exit status. This explains many of bash's syntax quirks — like why spaces inside [ ] are mandatory:
[ "$a" = "b" ] # benar — argumen terpisah: [, "$a", =, "b", ]
[ "$a" = "b"] # salah — "b]" menjadi satu argumen, error "unexpected token"
[ "$a"="b" ] # salah — seluruh ekspresi menjadi satu argumenBecause [ is a command, every part of the expression must be a separate argument separated by spaces. This is also why test and [ are interchangeable: test "$a" = "b" is exactly the same as [ "$a" = "b" ]. The final ] is just a closing argument that makes the syntax feel "like parentheses".
[ ] vs [[ ]]: Two Different WorldsThis is one of the most important design decisions you'll make as a bash script writer: use [ ] (POSIX test) or [[ ]] (bash extension). They look similar, but their behavior differs fundamentally.
[ ] — POSIX, Portable, But Fragile[ ] is an external/builtin command that follows the POSIX standard — meaning it's portable: it works in sh, dash, and every shell. But this portability comes at a price:
&& and || are not recognized inside it (the shell splits them off as command operators).[[ ]] — Bash Extension, More Powerful and Safer[[ ]] is a bash keyword — not an external command. Because of that, it understands syntax in a "smarter" way:
&& and || work inside as expression logical operators.=~ — you've seen a glimpse of this in episode 6.Compare:
#!/usr/bin/env bash
var="" # variabel kosong
# [ ] — Wajib kutip; tanpa kutip, error "unary operator expected"
if [ "$var" == "x" ]; then echo "x"; fi
# [[ ]] — Lebih toleran dan lebih aman
if [[ $var == "x" ]]; then echo "x"; fibash: [: x: unary operator expectedWhen $var is empty and written without quotes, [ $var == "x" ] becomes [ == "x" ] — and test is confused because its first argument is the == operator, not a value. Meanwhile [[ $var == "x" ]] treats $var as a single token no matter what it contains — without needing quotes. This is why professional scripting teams set the rule: use [[ ]] in bash scripts and save [ ] only for scripts that must be portable to sh.
Important
Choose [[ ]] for bash scripts (shebang #!/usr/bin/env bash) — it's safer (no word splitting), supports regex =~, and allows &&/|| operators inside. Use [ ] only if you're writing scripts that truly must run in a pure POSIX shell (sh). This consistency eliminates a whole class of bugs that most often plague beginners.
&& and || Inside [[ ]]Because [[ ]] understands logical operators, combining multiple conditions becomes natural:
#!/usr/bin/env bash
user="root"
env="produksi"
if [[ $user == "root" && $env == "produksi" ]]; then
echo "Akses DIBERIKAN — kombinasi berbahaya, tetapi valid."
elif [[ $user == "root" || $env == "staging" ]]; then
echo "Akses dengan peringatan."
else
echo "Akses ditolak."
fiAkses DIBERIKAN — kombinasi berbahaya, tetapi valid.With [ ], the same expression requires chained writing and extra quoting: [ "$user" = "root" ] && [ "$env" = "produksi" ]. Both work, but [[ ]] is far more readable — and readability is a feature, not a luxury.
Now we get to the hardware of conditionals: the test operators. All three work in both [ ] and [[ ]] (with the small differences already discussed).
#!/usr/bin/env bash
nama="Arman"
if [[ -z "$nama" ]]; then echo "String kosong."; fi
if [[ -n "$nama" ]]; then echo "String tidak kosong."; fi
if [[ "$nama" == "Arman" ]]; then echo "Sama persis."; fi
if [[ "$nama" != "Budi" ]]; then echo "Berbeda dengan 'Budi'."; fiString tidak kosong.
Sama persis.
Berbeda dengan 'Budi'.| Operator | True if... | Counter-intuitive? |
|---|---|---|
-z "$var" | the string is empty (zero-length) | Yes — -z = "zero length", not "has a value" |
-n "$var" | the string is not empty | Yes — the intuitive opposite of -z |
$a == $b | two strings are equal | No |
$a != $b | two strings are different | No |
For numbers, don't use > or < inside [ ] — they're shell redirection operators, not comparisons! Bash provides special word operators:
#!/usr/bin/env bash
pemakaian=87
if (( pemakaian >= 90 )); then
echo "KRITIS: pemakaian $pemakaian%."
elif (( pemakaian >= 80 )); then
echo "PERINGATAN: pemakaian $pemakaian%."
else
echo "Normal: pemakaian $pemakaian%."
fi| Operator | Meaning |
|---|---|
-eq | equal — sama dengan |
-ne | not equal — tidak sama |
-gt | greater than — lebih besar |
-ge | greater or equal — lebih besar atau sama |
-lt | less than — lebih kecil |
-le | less or equal — lebih kecil atau sama |
Notice the example above uses (( )) from episode 8 — when only comparing integers, (( )) is a cleaner choice than [ "$pemakaian" -ge 90 ]. But you'll often encounter the [ "$x" -ge 90 ] form in other scripts, so mastering both syntaxes matters. And when the value can be fractional (from bc), remember episode 8's lesson: let bc do the comparing.
This is what makes bash great for automation: bash can query file properties directly, without needing to call another program.
#!/usr/bin/env bash
file="/etc/nginx/nginx.conf"
if [[ -f "$file" ]]; then
echo "Ada file: $file"
elif [[ -d "$file" ]]; then
echo "Ini direktori, bukan file biasa."
fi
if [[ -r "$file" ]]; then
echo "File bisa dibaca."
fi| Operator | True if... | Real-World Scenario |
|---|---|---|
-e path | the path exists (file, dir, symlink, anything) | Check existence before access |
-f path | the path is a regular file | -f = "file" — configuration keys |
-d path | the path is a directory | Check before cd or a glob loop |
-r path | the file can be read | Validate permissions before reading |
-w path | the file can be written | Check before writing logs |
-x path | the file can be executed | Check a binary/script before running |
-s path | the file exists and is not empty | -s = "size > 0" — empty logs |
Tip
Many beginners use -e and -f interchangeably — but they're different. -e is correct for any path (including symlinks and directories); -f is specifically for regular files. When you write if [ -f "$dir" ] for a variable that's actually a directory, that condition will always be false. Ask yourself: what exactly do I want to verify? then pick the matching operator — this precision is what makes scripts trustworthy.
&& and ||Besides if, bash has a compact way to make decisions: short-circuit evaluation. In a pipeline, cmd1 && cmd2 runs cmd2 only if cmd1 succeeds; cmd1 || cmd2 runs cmd2 only if cmd1 fails.
#!/usr/bin/env bash
mkdir -p /tmp/backup && echo "Direktori dibuat."
[ -d /tmp/backup ] || echo "Direktori tidak ada — buat dulu!"Direktori dibuat.
Direktori tidak ada — buat dulu!Why is this called short-circuit? Because bash stops evaluating once the result is already determined. In A && B, if A fails, bash doesn't need to run B (the result is already known to be false). In A || B, if A succeeds, B is skipped (the result is already known to be true). This is why the idiom is efficient — and why operand order matters so much.
&& / || and When if?Both styles can rewrite each other, but each has its best context:
#!/usr/bin/env bash
# Ringkas — untuk aksi satu baris: "kalau berhasil, lakukan ini"
systemctl start nginx && echo "nginx berjalan"
# Eksplisit — untuk logika bercabang yang butuh penjelasan
if systemctl start nginx; then
echo "nginx berjalan"
else
echo "Gagal memulai nginx" >&2
exit 1
fiThe rule of thumb: &&/|| for short, clear sequences of actions; if for logic that branches, comments, or behaves differently on failure. Production scripts more often need if — because when a failure happens, we almost always need to do more than print a single line (for example, exit 1 and logging). But there's one mandatory exception you must always remember — see pitfall number 4 below.
Time to combine all the test operators into a realistic monitoring script — a blend of episode 5 (arguments), episode 8 (arithmetic), and this episode (conditionals):
#!/usr/bin/env bash
set -euo pipefail
threshold="${1:-80}"
log_file="/var/log/cek_disk.log"
if ! [[ "$threshold" =~ ^[0-9]+$ ]]; then
echo "Error: ambang batas harus angka." >&2
exit 1
fi
pemakaian=$(df --output=pcent / | tail -1 | tr -d ' %')
if [[ -f "$log_file" ]]; then
if [[ -w "$log_file" ]]; then
echo "[$(date +%F)] pemakaian=$pemakaian%" >> "$log_file"
else
echo "PERINGATAN: $log_file tidak bisa ditulis." >&2
fi
fi
if (( pemakaian >= threshold )); then
echo "KRITIS: pemakaian disk ${pemakaian}% melebihi ambang ${threshold}%."
exit 2
else
echo "OK: pemakaian disk ${pemakaian}% di bawah ambang ${threshold}%."
exit 0
fi./cek_disk.sh 90OK: pemakaian disk 37% di bawah ambang 90%.Let's break down how each type of test contributes:
| Line | Test Used | Purpose |
|---|---|---|
${1:-80} | Parameter expansion with a default | Script runs without arguments, default threshold 80 |
[[ "$threshold" =~ ^[0-9]+$ ]] | String regex with ! (negation) | Reject thresholds that aren't pure numbers |
[[ -f "$log_file" ]] | File: whether the log file exists | Don't append to a file that doesn't exist yet |
[[ -w "$log_file" ]] | File: whether it's writable | Warn instead of failing silently |
(( pemakaian >= threshold )) | Number with (( )) | The main decision: past the threshold or not |
Notice how all three test types (string, file, number) work side by side for a single goal. This script can be installed in cron to run every 5 minutes — and because exit 0/exit 2 are used, callers (for example, a monitoring system) can distinguish "all good" from "there's a problem" just from the exit status. This is the power of conditionals designed with full awareness.
[ ][ "$a" = "b" ] is correct; [ "$a" = "b"] errors with "missing ]"; [ "$a"="b" ] errors with "unary operator expected". Because [ is a command, every token must be space-separated. This isn't just style — it's syntax.
> / < as numeric comparisonInside [ ] and [[ ]], > and < are string comparisons (alphabetical order), not numeric! [[ "9" > "10" ]] evaluates to true because "9" is lexicographically greater than "1". For numbers, use -gt, -lt, etc. (or (( ))).
[ ][ $var == "x" ] with empty $var becomes [ == "x" ] → "unary operator expected" error. Inside [ ], always quote. Inside [[ ]], quoting becomes optional (but still recommended).
cmd1 && cmd2 || cmd3 — the dangerous three-way patternThis pattern is tempting but dangerous: if cmd2 itself fails, bash will also run cmd3 — because cmd1 && cmd2 yields "false" when cmd2 fails, then || cmd3 fires too. The result: the "success" path can still trigger the error branch. Don't write three-way logic with &&/|| — use if/else, which is unambiguous.
[ $var == "x" ] vs [[ $var == "x" ]] — real differencesBesides the quoting issue, there's another subtle trap: in [[ ]], == and = are both string comparisons. In POSIX [ ], the official operator is = — == might work in bash, but in sh/dash its behavior isn't guaranteed. If the script must be portable, write [ "$var" = "x" ]. If bash-only, use [[ ]].
; or then on the same lineif [ ... ] then — without ; before then — errors. Two valid styles: if [ ... ]; then (one line) or if [ ... ] then a new line with then. Pick one and stay consistent.
In this episode 9, we've completed the decision-making foundation: the if/elif/else/fi structure that works on a command's exit status; the fundamental difference between [ ] (a POSIX command fragile against word splitting) vs [[ ]] (a safer bash keyword that supports regex and logical operators); a catalog of test operators for strings (-z, -n, ==, !=), numbers (-eq, -ne, -gt, -ge, -lt, -le), and files (-f, -d, -e, -r, -w, -x, -s); plus &&/|| short-circuiting — complete with a disk monitoring script that blends it all.
Key takeaways:
if reads exit status, not booleans — a condition is a command.[[ ]] for bash scripts: safer, regex, &&/|| inside.-z -n == !=; Numbers: -eq -ne -gt -ge -lt -le; Files: -f -d -e -r -w -x -s.>/< for numbers — those are string comparisons.[ ]; avoid three-way &&/|| patterns.You now have four core bash foundations: data (arguments & input), file lists (globbing), calculation (arithmetic), and decisions (conditionals). Their combination can already build real automation scripts. The next step is refining the decision-making itself. In the next episode, episode 10, we'll cover Advanced Conditionals (Case Statement & Regex) — matching a single variable against many patterns at once with case ... esac, and validating inputs like email, IP addresses, and numbers using regular expressions with the =~ operator. See you in the next episode!