if/else: checking whether a file exists
A script that behaves differently depending on whether a file exists.
Almost every real-world script has to decide what to do next at some
point. If there's no config, fall back to the default one. If a lock file
exists, don't start a second time. If the backup directory is missing,
create it. It's all the same construct: if, a condition, a "yes"
branch, a "no" branch.
Where to practice
The examples below work in an ubuntu:24.04 container. You can spin up
the same one yourself:
docker run --rm -it ubuntu:24.04 bash
On Linux, macOS, and in WSL2 the commands also work in your own terminal.
We create script files with cat > file <<'EOF', same as in the
arguments lesson.
Your first script with if
The script checks whether the file /tmp/secret exists:
cat > /tmp/has.sh <<'EOF'
#!/bin/bash
if [ -e /tmp/secret ]; then
echo exists
else
echo missing
fi
EOF
chmod +x /tmp/has.sh
/tmp/has.sh
missing
Create the file and run it again:
touch /tmp/secret
/tmp/has.sh
exists
What the construct consists of:
if COMMAND; thenruns the command and looks at its exit code. Code 0 means "yes", and the branch afterthenruns.elsestarts the "no" branch. It's optional: without it, if the condition is false, the script just moves on.ficlosesif(it'sifspelled backwards).- The
;beforethenis needed whenthensits on the same line. You can movethento the next line and skip the;.
A condition is a command
[ is a command; it has a second name, test. It checks a condition,
prints nothing, and returns an exit code: 0 if the condition is true, 1
if not. You can see the code through $?:
[ -e /etc/passwd ]; echo "rc=$?"
[ -e /nope ]; echo "rc=$?"
rc=0
rc=1
Since if only looks at the exit code, any command can follow it, not
just [. For example, grep -q prints nothing and returns 0 if it found
the line:
if grep -q root /etc/passwd; then echo "root found"; fi
root found
Hence the main rule for writing it. [ is a command name, and -e, the
path, and ] are its arguments, so spaces between them are mandatory.
Without a space, the whole condition breaks:
if [-e /etc/passwd ]; then echo yes; else echo no; fi
bash: [-e: command not found
no
if [ -e /etc/passwd]; then echo yes; else echo no; fi
bash: [: missing `]'
no
The file exists, but the script went to else. The failing command
returned something other than 0, and if honestly counted that as "no".
The error message goes to stderr; if the script runs from cron, or its
stderr is redirected to /dev/null, nobody will see it, and the script
will silently take the wrong branch.
File tests
| test | true if the path |
|---|---|
-e path | exists (file, directory, anything) |
-f path | is a regular file |
-d path | is a directory |
-L path | is a symbolic link (symlink) |
-s path | is a file and it's not empty |
-r, -w, -x path | readable, writable, executable |
How they behave on different paths (y means exit code 0):
/etc/passwd e=y f=y d=n L=n
/tmp/d e=y f=n d=y L=n
/tmp/good e=y f=y d=n L=y
/tmp/broken e=n f=n d=n L=y
/nope e=n f=n d=n L=n
Here /tmp/d is a directory, /tmp/good is a symlink to /etc/passwd,
and /tmp/broken is a symlink to a path that doesn't exist. Two details
follow from this:
-eand-ffollow the symlink to wherever it points. A broken symlink doesn't exist as far as-eis concerned, even thoughlsshows it. Only-Lsees the symlink itself.-eis also true for a directory. If you need a file specifically, use-f.
Be careful with -r, -w, -x under root: root can read and write any
file, so [ -r file ] is true even for a file with 000 permissions.
Negate with !, with spaces around it:
[ ! -e /tmp/secret ] && echo "no file"
Quoting variables
A path is rarely written directly in the condition; it usually sits in a
variable. Without quotes, the shell splits its value on spaces, and [
gets the wrong arguments:
f="/tmp/my file"
[ -e $f ]; echo "rc=$?"
[ -e "$f" ]; echo "rc=$?"
bash: [: /tmp/my: binary operator expected
rc=2
rc=0
With an empty variable it's even worse. [ -e $f ] with f="" turns
into [ -e ], and [ with a single argument only checks "the string is
not empty". The string -e is not empty, so the condition is true for a
path that doesn't exist:
f=""
[ -e $f ]; echo "rc=$?"
[ -e "$f" ]; echo "rc=$?"
rc=0
rc=1
The rule is the same as with arguments: a variable in a condition always
goes in double quotes, "$f".
Several branches: elif
When there are more than two options, put elif between if and else.
Conditions are checked top to bottom, and the first true one wins:
cat > /tmp/kind.sh <<'EOF'
#!/bin/bash
p=$1
if [ -L "$p" ]; then
echo "$p: symlink"
elif [ -d "$p" ]; then
echo "$p: directory"
elif [ -f "$p" ]; then
echo "$p: file"
else
echo "$p: no such path"
fi
EOF
bash /tmp/kind.sh /etc; bash /tmp/kind.sh /tmp/good; bash /tmp/kind.sh /nope
/etc: directory
/tmp/good: symlink
/nope: no such path
Order matters: -L comes first, because a symlink to a file also passes
-f.
Shorthand with && and ||
Instead of if, people often write it on one line:
[ -e /etc/passwd ] && echo exists || echo missing
exists
It looks like if/else, but it works differently. || fires if anything
on the left fails, not just the test. If the "yes" branch itself fails,
the "no" branch runs too. Here the file exists, but echo couldn't write
its output:
[ -e /etc/passwd ] && echo exists >/dev/full || echo missing
bash: echo: write error: No space left on device
missing
Save the shorthand for one-branch cases ([ -d "$dir" ] || mkdir "$dir").
For two branches, write a full if.
Exiting with a code from a branch
Often the "no" branch needs to stop the script so the caller (another
script or CI) notices. That's what exit with a nonzero code is for. A
bare exit with no number returns the exit code of the last command, and
after echo that's 0:
bash -c 'echo blocked; exit'; echo "rc=$?"
bash -c 'echo blocked; exit 1'; echo "rc=$?"
blocked
rc=0
blocked
rc=1
Not just files
[ also compares strings and numbers. This will come in handy in later
lessons:
[ -z "$x" ]is true if the string is empty;[ -n "$x" ]if it's not.[ "$a" = "$b" ]means the strings are equal,!=means they aren't.[ "$n" -gt 9 ]means the number is greater. There's also-eq,-ne,-lt,-le,-ge.
Compare numbers with -gt and friends. > inside [ compares strings
alphabetically, and without a \ it turns into a file redirect entirely.
[ and [[
bash also has [[ ... ]]. It's more forgiving with unquoted variables
and supports patterns, but it's a bash extension. In Ubuntu /bin/sh is
dash, and it doesn't know [[:
sh -c '[[ -e /etc/passwd ]] && echo y'
sh: 1: [[: not found
If the script might run via sh script.sh, use [. This whole lesson
uses [.
Common mistakes
- No space after
[or before]: an error goes to stderr, and the script goes toelse. - Unquoted variable: a path with a space causes an error, an empty variable gives "true".
-ewhere you need-f: a directory with the same name passes the check.cond && A || Binstead ofif:Balso runs whenAfails.exitwith no code in the error branch: the script prints "error" but exits with 0.[[in a script run viash:[[: not found.- Forgot
fiorthen:syntax error: unexpected end of file.
What you'll need for the tasks
- Write
if [ -e "path" ]; then ...; else ...; fiwith spaces around[and]. - Choose the right test:
-e,-f,-d. - Test the script on both branches: with the file and without it.
- Exit the error branch with
exit 1. - Create the script file with
cat > file <<'EOF'and make it executable.
Practice in your browser
Linux runs right here. Solve the task and we'll save your progress automatically.
exists or missing
Create /root/check.sh, which checks the file /tmp/secret. If the file exists, it prints exists; if not, missing. Right now /tmp/secret doesn't exist; the script should work in both cases.
0%
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson terminal-scripts-04-conditional-script
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress