Sign in
← Terminal: bash scripts
5 min · medium · Lesson 4 of 5

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:

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

testtrue if the path
-e pathexists (file, directory, anything)
-f pathis a regular file
-d pathis a directory
-L pathis a symbolic link (symlink)
-s pathis a file and it's not empty
-r, -w, -x pathreadable, 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:

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:

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

What you'll need for the tasks

Practice in your browser

Linux runs right here. Solve the task and we'll save your progress automatically.

01

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.

{ i }

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson terminal-scripts-04-conditional-script