---
title: 'Conditions and Tests'
source: 'https://academia.sh/en/courses/shell-programming/conditions-and-tests'
course: 'Shell Programming'
language: en
updated: '2026-08-17T18:10:01+00:00'
license: 'CC BY-SA 4.0'
---

# Conditions and Tests

A condition being an exit code, file/string/number tests, the parsing difference between single and double brackets, pattern matching, and multi-way selection.

The script assumes that the log given to it exists and is readable. Testing
this assumption requires a condition. In the Programming Fundamentals
course, a condition was a logical expression, and it reduced to a true or
false value.

There is no such type in the shell. **A condition is a command that gets
executed; its result is the exit code.** The `if` construct does not
evaluate an expression, it runs a command and executes the body if its
code is 0. This single difference explains all the syntax oddities of
shell conditions.

## `if` Takes a Command

```sh
if command; then
  ...
elif other_command; then
  ...
else
  ...
fi
```

Any command can be a condition:

```sh
if grep -q '404' access.log; then
  echo "the log has a 404"
fi
```

The `[ ... ]` form used for conditions is not a syntax element, it is a
second name for the command called `test`. This is why a space between `[`
and the first argument is mandatory, and the closing `]` is this
command's last argument. Writing `[-f file]` looks for a command named
`[-f` and does not find it.

## File Tests

`test` defines a family of operators related to the file system.

| Operator | True when |
|---|---|
| `-e` | the path exists (regardless of type) |
| `-f` | regular file |
| `-d` | directory |
| `-r`, `-w`, `-x` | read / write / execute permission exists |
| `-s` | the file exists and its size is greater than zero |
| `-L` | symbolic link |
| `a -nt b` | `a` was modified more recently than `b` |

```sh
[ -f access.log ]; echo "regular file -> $?"
[ -d access.log ]; echo "directory    -> $?"
[ -x access.log ]; echo "executable   -> $?"
```

```
regular file -> 0
directory    -> 1
executable   -> 1
```

Permission tests do not check the file's permission bits, they check
**whether the calling process can perform that operation on that file**;
the result can come out different for the superuser. The permission model
established in the Introduction to Linux course turns into behavior here.

The distinction between `-e` and `-f` must not be neglected: a directory
also passes the `-e` test, but it cannot be read with `cut`.

## String and Number Tests

String tests: `-z` is it empty, `-n` is it non-empty, `=` are they equal,
`!=` are they different.

Number tests use separate operators: `-eq`, `-ne`, `-lt`, `-le`, `-gt`,
`-ge`.

The two families need to be separate, because in the shell every value is
a character string, and "equality" carries two different meanings:

```sh
[ "007" =   "7" ]; echo "string equality -> $?"
[ "007" -eq "7" ]; echo "number equality -> $?"
```

```
string equality -> 1
number equality -> 0
```

`007` and `7` are different strings but the same number. It is the one
writing the code who decides which meaning is wanted; the choice of
operator is the declaration of that decision.

Number operators work only with integers. If a decimal value is given,
`test` reports an error. If a decimal comparison is needed, a calculation
tool is used instead; this is taken up in the Arithmetic and Command
Substitution lesson.

## The Single Bracket's Parsing Problem

Because `[` is an ordinary command, its arguments are first expanded by
the shell and split into words. The result of splitting can change the
argument count `test` expects.

```sh
empty=""
[ $empty = "x" ]
```

```
bash: [: =: unary operator expected
```

After expansion, only the arguments `=`, `x`, `]` reached the `[`
command; `test` took this as an expression with a single operand. The
same problem shows up in the opposite direction with a value containing
a space:

```sh
d="two words"
[ $d = "two words" ]
```

```
bash: [: too many arguments
```

Both errors are fixed with quoting:

```sh
[ "$empty" = "x" ]; echo "code=$?"
```

```
code=1
```

In older scripts, a pattern of the form `[ "x$a" = "xb" ]` is seen. Its
purpose is to prevent the variable's value from being confused with an
operator; since quoting is enough, this pattern is unnecessary now, but
it should be recognized for what it does when encountered.

## Double Brackets

Bash and ksh define a separate syntax element called `[[ ... ]]`. This is
not a command; the shell recognizes it at the parsing stage and **does
not apply word splitting or pathname expansion** to the expansions
inside it.

```sh
empty=""; d="two words"
[[ $empty = "x" ]]; echo "empty  -> $?"
[[ $d = "two words" ]]; echo "spaced -> $?"
```

```
empty  -> 1
spaced -> 0
```

The unquoted forms did not error. This means quotes are not required
inside `[[ ]]` — but writing them does no harm either, and can be
preferred so as not to break the habit.

Double brackets have three additional capabilities.

**Pattern matching.** If the right side of the `=` or `==` operator is
unquoted, it is interpreted as a pathname pattern:

```sh
path="/api/data"
[[ $path == /api/* ]]   && echo "under api"
[[ $path == "/api/*" ]] || echo "quoted pattern is literal"
```

```
under api
quoted pattern is literal
```

Quoting the right side turns the pattern into an ordinary string. This
is the control needed in situations where pattern matching is not
wanted.

**Regular expression matching.** The `=~` operator treats the right side
as an extended regular expression:

```sh
code="404"
[[ $code =~ ^[45][0-9][0-9]$ ]] && echo "error-class code"
```

```
error-class code
```

Regular expression syntax will be taken up in detail in the Text
Processing topic. What matters here is that the pattern is written
unquoted: a quoted right side looks for a literal match.

**Logical operators.** `&&` and `||` combine conditions inside the
brackets:

```sh
[[ -f access.log && -r access.log ]] && echo "readable file"
```

```
readable file
```

Inside `[ ]`, the `-a` and `-o` operators exist for the same job, but
they are not recommended because they produce parsing ambiguity. In
portable code, two separate tests are chained with `&&`:

```sh
[ -f access.log ] && [ -r access.log ] && echo "readable file"
```

**`[[ ]]` is a bash extension; it does not exist in the POSIX shell
language.** It cannot be used in a script declaring `#!/bin/sh`.
Decision rule: if bash is declared, `[[ ]]` is preferred — it is immune
to quoting mistakes; if POSIX is required, `[ ]` and careful quoting are
used.

## Multi-Way Selection

Instead of `if`–`elif` chains that test the same value against many
possibilities, `case` is written. The patterns use pathname pattern
syntax.

```sh
for code in 200 301 404 500 xyz; do
  case "$code" in
    2??) class="successful" ;;
    3??) class="redirect" ;;
    4??) class="client error" ;;
    5??) class="server error" ;;
    *)   class="unknown" ;;
  esac
  printf '%-4s -> %s\n' "$code" "$class"
done
```

```
200  -> successful
301  -> redirect
404  -> client error
500  -> server error
xyz  -> unknown
```

`case` runs the first matching branch and stops; there is no fall-through
between branches. Multiple options are added to a pattern with `|`:
`200|201|204)`. The `*)` written as the last branch catches values that
match no pattern, and it should be present in every `case` construct —
if it is missing, an unrecognized value is silently ignored.

`case` is POSIX and provides pattern matching without requiring `[[ ]]`;
it is the correct tool for pattern testing in portable scripts.

## Applying It to the Script

`report.sh` is extended to validate its inputs. Giving separate codes to
error classes continues the contract from the previous lesson.

```sh
log_file="${1:-access.log}"

if ! [[ "$count" =~ ^[0-9]+$ ]] || [ "$count" -lt 1 ]; then
  echo "error: -n expects a positive integer: $count" >&2
  exit 2
fi

if [ ! -e "$log_file" ]; then
  echo "error: log does not exist: $log_file" >&2
  exit 3
elif [ ! -f "$log_file" ]; then
  echo "error: not a regular file: $log_file" >&2
  exit 3
elif [ ! -r "$log_file" ]; then
  echo "error: no read permission: $log_file" >&2
  exit 3
elif [ ! -s "$log_file" ]; then
  echo "warning: log is empty: $log_file" >&2
fi
```

The reason the regular-expression test runs before the
`[ "$count" -lt 1 ]` test in the number check is order: if `count` were
not a number, `-lt` would error out. Because `||` short-circuits, the
second test is reached only when the first one passes.

Four calls follow four paths:

```sh
./report.sh missing.log;  echo "code=$?"
./report.sh -n abc;   echo "code=$?"
./report.sh /etc;     echo "code=$?"
```

```
error: log does not exist: missing.log
code=3
error: -n expects a positive integer: abc
code=2
error: not a regular file: /etc
code=3
```

## Summary

- In the shell, a condition is a command; `if` looks at the exit code.
  `[ ]` is a second name for the `test` command, and the spaces around
  it are mandatory.
- String equality is tested with `=`, number equality with `-eq`; `007`
  and `7` are different as strings, the same as numbers.
- Because expansions inside `[ ]` undergo word splitting, quoting is
  mandatory; an empty or space-containing value otherwise produces a
  parsing error.
- `[[ ]]` is recognized at the parsing level, applies no splitting, and
  offers pattern and regular expression matching along with logical
  operators; it is a bash extension.
- `case` runs the first matching branch, there is no fall-through, and
  the `*)` branch should always be written.

## Next Step

The report currently produces a single list. When a separate count is
wanted for each status-code class, the same pipeline has to be
duplicated by hand. The next lesson takes up iteration: loops that run
over a list, counted loops, and the correct form of reading a file line
by line — which is anything but obvious in the shell.
