Shell Scripting 101: If Else in Shell Script

Branching diagram showing bash if else decision diamond splitting into two paths for shell script conditionals

My first bash if else always took the wrong branch, the test looked right and still did the opposite. I ran the broken string test on bash 5.2.21 (Ubuntu 24.04) and watched an unquoted variable flip the result, then fixed it with quoting and [[ ]].

If else in shell script is how you branch a bash script: test a condition, run one block when it is true, and another when it is false. You get copy-paste examples for numbers, strings, and files so the branch you expect is the branch you get.

Why if else still trips bash scripts

Bash does not branch on true or false the way other languages do. It branches on exit status. Zero means success and non-zero means failure.

That is why if [ “$n” -eq 1 ] and if grep -q “hi” file.txt both work. The [ test and the grep command each exit 0 or 1, and if follows that result.

The most common trap is quoting and spacing, not logic. Miss a space around [ or leave “$var” unquoted when it is empty. Bash then parses a different test than you wrote.

FormWhat it doesWhen I use it
if … then … fiRun block only on successSingle guard check
if … then … else … fiChoose one of two blocksSuccess vs fallback
if … then … elif … then … else … fiTest in order, first match winsThree or more branches
Nested if inside elseBranch inside a branchRare, prefer elif

What you need before the first branch

I tested every command here on bash 5.2.21 and your Ubuntu, Debian, or Fedora install ships the same syntax.

  • bash 5.x and a file that starts with #!/bin/bash
  • Execute permission: chmod +x script.sh or run with bash script.sh
  • Quotes around variables that may be empty: use “$var”, never bare $var
  • A habit of checking with shellcheck, see how to run ShellCheck on Linux before you ship the script

Make the script executable once, then re-run it after each edit. I also keep variables in shell scripts handy when you assign the values that your conditions test.

How to write if, elif, else, and fi so the right branch runs

The general form never changes. I use this skeleton and swap only the test inside.

if [ condition ] then
  # runs when condition exits 0
  echo "branch A"
elif [ other_condition ] then
  echo "branch B"
else
  echo "fallback"
fi

Keep then on the same line after a semicolon, or put it on the next line without the semicolon. Both pass shellcheck.

Branch on numbers the portable way

Numbers use integer tests inside [ ]. I ran this exact block and got This is true as expected.

n=1
if [ "$n" -eq 1 ] then
  echo "This is true"
fi

The screenshot below shows that run with n=1. One line prints and the script exits cleanly.

Terminal showing basic bash if statement printing This is true when n equals 1
Basic if with n=1 prints one line and nothing else, the true branch alone.

Add else when the false case must do something. With n=2 the same test took the else branch and printed N is no longer 1 in my run.

n=2
if [ "$n" -eq 1 ] then
  echo "This is true"
else
  echo "N is no longer 1"
fi
Terminal output showing N is no longer 1 when n equals 2
If else flips to the fallback when the equality test fails.
OperatorMeaningExample
-eqequal[ “$a” -eq “$b” ]
-nenot equal[ “$a” -ne 1 ]
-gt / -gegreater than / or equal[ “$n” -gt 5 ]
-lt / -leless than / or equal[ “$n” -lt 10 ]

Branch on strings without the quoting trap

Strings trip scripts when “$str” is unquoted. I reproduced the classic failure: s1=”hi” and s2=”hi” if [ “$s1” == “$s2” ] matches, but if [ $s1 == $s2 ] without quotes breaks when either side is empty.

s1="hi"
s2="hi"
if [ "$s1" = "$s2" ] then
  echo "match"
fi

str=""
if [ -z "$str" ] then
  echo "empty"
fi

For new bash scripts I prefer [[ ]] for strings. It avoids word splitting, understands == and != as literal comparisons, and lets you combine tests safely. For POSIX portability, stick to [ ] with =.

TestUseSafer form
[ “$a” = “$b” ]String equal (portable)[[ “$a” == “$b” ]] in bash
[ -z “$str” ]Empty string is trueSame in both
[ -n “$str” ]Non-empty is trueSame in both

If you store names and paths in variables first, those variables in shell scripts determine whether -z sees empty or not, quote them every time you test.

Branch on files and directories

File tests are the other half of everyday branching. I use them to guard reads, writes, and installs.

path="/etc/hosts"
if [ -f "$path" ] then
  echo "regular file exists"
elif [ -d "$path" ] then
  echo "it is a directory"
else
  echo "missing or special file"
fi

if [ -r "$path" ] && [ -w "$path" ] then
  echo "readable and writable"
fi
FlagTrue when
-epath exists (any type)
-fregular file
-ddirectory
-r / -w / -xreadable / writable / executable
-sexists and size > 0

Chain elif and nest without losing track

elif is else if without extra nesting. I chained three branches on bash 5.2.21. The middle one fired for n=5.

n=5
if [ "$n" -eq 1 ] then
  echo "Variable is 1"
elif [ "$n" -ge 5 ] then
  echo "The variable is greater than 5"
else
  echo "Variable is greater than 1 but less than 5"
fi
Terminal output showing The variable is greater than 5 for n equals 5
Elif picks the first true branch when n equals 5.

Deep nesting makes later edits error prone, so when you need more than two elif branches check whether a switch case in shell scripts expresses the choice more clearly.

Combine conditions with AND and OR

Two conditions can guard one branch. I tested n=3 against 1 or 3 and the first branch printed correctly.

n=3
if [ "$n" -eq 1 ] || [ "$n" -eq 3 ] then
  echo "Variable is 1 or 3"
elif [ "$n" -ge 5 ] then
  echo "The variable is greater than 5"
else
  echo "Variable is greater than 1 but less than 5"
fi

# Bash-only alternative, no second [ needed
if [[ "$n" -eq 1 || "$n" -eq 3 ]] then
  echo "Variable is 1 or 3"
fi
Terminal output showing Variable is 1 or 3 using OR condition
OR lets one branch cover two values.
WantPortable [ ]Bash [[ ]]
Both must be true[ “$a” -eq 1 ] && [ “$b” = “x” ] or [ “$a” -eq 1 -a “$b” = “x” ][[ “$a” -eq 1 && “$b” == “x” ]]
Either may be true[ “$a” -eq 1 ] || [ “$b” = “x” ][[ “$a” -eq 1 || “$b” == “x” ]]
Negate[ ! -f “$file” ][[ ! -f “$file” ]]

Prefer [[ ]] with && and || inside for bash-only scripts. For looping over numbers after the branch works, a for loop in shell scripts or while loop in shell scripts is the next natural step.

When the wrong branch runs, and the one-line fix

I triaged the three fixes below every time a script took the wrong branch on my machine. Each is one edit.

SymptomRoot causeFix
Branch always true[ $var == “x” ] with no quotes, empty $var collapses the test[ “$var” = “x” ] or [[ “$var” == “x” ]]
[: missing ] or command not found: [Missing spaces: [$n -eq 1][ “$n” -eq 1 ] with spaces around brackets
syntax error near elseMissing semicolon or newline before then/elseif [ “$n” -eq 1 ] then or put then on next line
integer expression expectedString tested with -eqUse = for strings, -eq only for integers
File test fails silentlyUnquoted path with spaces[ -f “$path” ]
Two conditions behave oddly[ “$a” = 1 -o “$b” = 2 ] inside one [Separate: [ “$a” = 1 ] || [ “$b” = 2 ] or use [[ ]]

Run bash -x script.sh to see which test bash actually evaluated.

Run shellcheck script.sh to flag missing quotes and spaces before you do.

Echo the variable right before the test: printf ‘n=%q\n’ “$n” shows empty at a glance.

When branching controls an early exit, pair it with break and continue in shell scripts so the loop and the conditional do not fight each other. For logic you extracted into a function, test its exit code directly with functions in shell scripts.

What to remember and what to build into your next script

Use [ ] for portable integer and file tests with quoted variables and [[ ]] for bash string logic.

Chain with elif instead of nesting, and combine with && and || in the form you chose. Add set -u awareness, an unset variable fails fast instead of silently taking the wrong branch.

For the next script, replace the old if [ $var == … ] lines with the quoted forms above. If you lean on arrays later, the same quoting discipline carries over to arrays in shell scripts.

# next-script template I reuse
if [[ -z "$input" ]] then
  echo "input missing" >&2; exit 1
elif [[ "$input" == "prod" ]] then
  echo "prod path"
else
  echo "dev path"
fi
Next script taskDo this
Quote every testChange [ $var = x ] to [ “$var” = x ]
Pick the right bracketUse [[ ]] for bash strings
Chain cleanlyUse elif then consider case

FAQ

Do I write if with [ ] or [[ ]] in bash?

Use [[ ]] for bash-only string logic, it avoids word splitting and lets you write [[ “$a” == “x” && “$b” != “y” ]] safely. Use [ ] with = and -eq when you need POSIX portability or need the script to run under dash or sh.

Why does my if always evaluate to true?

Most often $var was empty and you wrote [ $var = “x” ] without quotes, so bash sees [ = “x” ] and parses it wrong. Quote the variable: [ “$var” = “x” ] or switch to [[ “$var” == “x” ]]. Also check for missing spaces around [ and ].

What is the difference between = and == in shell if?

In [ ] use = for string equality for portability. In [[ ]] both = and == work and == does literal string comparison. For integers, always use -eq, -ne, -gt, -ge, -lt, -le, not = or ==.

How do I check if a file or directory exists in shell script?

Use [ -e “$path” ] for exists, [ -f “$path” ] for regular file, [ -d “$path” ] for directory, [ -r “$path” ] / [ -w “$path” ] for permissions, and [ -s “$path” ] for size greater than zero. Quote “$path” when it may contain spaces.

How do I combine two conditions with AND and OR?

In bash, write [[ “$n” -eq 1 || “$n” -eq 3 ]] or [[ “$a” == “x” && “$b” == “y” ]]. Portably, write [ “$n” -eq 1 ] || [ “$n” -eq 3 ] or [ “$n” -eq 1 ] && [ “$m” -eq 2 ]. Avoid -a and -o inside a single [ ] for new code.