Chapter 14Lesson 02~65 minutes

Tests, Conditions, case, and Boolean Logic

Turn command exit status, file and string tests, arithmetic conditions, pattern matching, and case dispatch into explicit control flow without hiding failures.

Conditional executioncase patternsValidator lab

Learning objectives

By the end of this lesson

  • Use command success and failure directly as Bash conditions.
  • Choose among test, [ ], [[ ]], and (( )).
  • Combine conditions with !, &&, and || without losing error meaning.
  • Dispatch modes and patterns with case.
  • Build a configuration validator with precise diagnostics and exit status.

1. Bash conditions are commands and exit statuses

In Bash, truth is operational. A command that returns status zero is successful and therefore true in an if, while, or until condition. A nonzero status is false. The shell does not require a Boolean object; it evaluates command results.

How Bash evaluates an if statement
flowchart TD
  C["Run condition command"] --> S{"Exit status is 0?"}
  S -- yes --> T["Execute then branch"]
  S -- no --> E{"elif condition exists?"}
  E -- yes --> C2["Run next condition command"]
  E -- no --> F["Execute else branch if present"]
  C2 --> S2{"Exit status is 0?"}
  S2 -- yes --> T2["Execute elif branch"]
  S2 -- no --> F
if systemctl is-active --quiet ssh.service; then
  printf 'SSH service is active\n'
else
  status=$?
  printf 'SSH service is not active; status=%d\n' "$status" >&2
fi

# Prefer testing the command directly over running it and then examining $?.
if grep -q '^PermitRootLogin no$' /etc/ssh/sshd_config 2>/dev/null; then
  printf 'Explicit root-login policy found\n'
fi

$? is overwritten by the next command. Capture it immediately only when you need the numeric value. For ordinary branching, put the command directly in the conditional so the relationship remains visible.

2. Choose the conditional form for the data model

FormBest useImportant behavior
test expressionPortable file, string, and integer testsCommand syntax; arguments must be separated correctly
[ expression ]Portable shell tests with familiar punctuation[ is a command; the closing ] is an argument
[[ expression ]]Bash string logic, patterns, regex, and compound testsNo word splitting or pathname expansion on ordinary parameter expansions
(( expression ))Integer arithmetic conditionsTrue when the arithmetic result is nonzero
config=/etc/ssh/sshd_config
service_name='api-worker'
replicas=3

if [[ -r $config && -s $config ]]; then
  printf 'Readable, non-empty configuration: %s\n' "$config"
fi

if [[ $service_name == api-* ]]; then
  printf 'API service family\n'
fi

if [[ $service_name =~ ^[a-z][a-z0-9-]{2,31}$ ]]; then
  printf 'Valid service identifier\n'
fi

if (( replicas >= 2 && replicas <= 10 )); then
  printf 'Replica count is in policy range\n'
fi

Inside [[ ]], an unquoted right-hand side of == is a pattern. Quoting it requests literal comparison. The right-hand side of =~ is a regular expression; store complex expressions in a variable rather than fighting multiple parsing layers.

3. File and string tests answer specific questions

Tests should correspond to the property the script actually needs. Existence alone does not prove readability, regular-file type, non-emptiness, ownership, or safe permissions. Likewise, a non-empty string is not necessarily a valid identifier, path, number, URL, or environment name.

path=${1:-}

if [[ -z $path ]]; then
  printf 'A path argument is required\n' >&2
elif [[ ! -e $path ]]; then
  printf 'Path does not exist: %s\n' "$path" >&2
elif [[ -L $path ]]; then
  printf 'Symbolic link: %s -> %s\n' "$path" "$(readlink -- "$path")"
elif [[ -f $path && -r $path ]]; then
  printf 'Readable regular file: %s\n' "$path"
elif [[ -d $path && -x $path ]]; then
  printf 'Searchable directory: %s\n' "$path"
else
  printf 'Path exists but does not satisfy the required policy: %s\n' "$path" >&2
fi

Time-of-check/time-of-use races can still occur between a test and a later operation. Tests improve diagnostics and policy, but the operation itself must handle failure. Do not use pre-checks as a substitute for checking the actual command result.

4. Boolean operators also control command execution

&& executes the next command only after success; || executes it only after failure; ! reverses a command’s truth value. These are command-list operators, not merely punctuation inside expressions.

mkdir -p -- "$HOME/devops-academy/cache" &&
  printf 'Cache directory is ready\n'

if ! command -v shellcheck >/dev/null 2>&1; then
  printf 'shellcheck is not installed\n' >&2
fi

# Group recovery so the intended failure handling is clear.
if ! cp -- source.conf destination.conf; then
  printf 'Copy failed; destination was not updated\n' >&2
  exit 1
fi

# Avoid using "cmd1 && cmd2 || fallback" as a general if/else replacement.
# fallback also runs when cmd2 fails, even if cmd1 succeeded.
Preserve the failing operation

When a failure matters, use an explicit if ! command; then ... fi block. It gives you room to capture context, choose an exit status, and distinguish expected absence from operational failure.

5. case expresses multi-way dispatch and shell patterns

case compares one word against ordered shell patterns. It is ideal for modes, file extensions, environment names, and compact option-like commands. Patterns are not regular expressions: *, ?, bracket expressions, and alternation with | follow shell pattern rules.

environment=${1:-}

case $environment in
  development|dev)
    log_level=debug
    replicas=1
    ;;
  staging|stage)
    log_level=info
    replicas=2
    ;;
  production|prod)
    log_level=warn
    replicas=4
    ;;
  '')
    printf 'Environment is required\n' >&2
    exit 2
    ;;
  *)
    printf 'Unsupported environment: %s\n' "$environment" >&2
    exit 2
    ;;
esac

printf 'environment=%s log_level=%s replicas=%d\n' \
  "$environment" "$log_level" "$replicas"

6. Hands-on lab: build a deployment-input validator

The validator accepts an environment, service name, replica count, and configuration file. It writes no system state and returns a distinct usage or validation status.

lab="$HOME/devops-academy/linux/chapter14/lesson02"
mkdir -p "$lab"
cd "$lab"

cat > validate-deployment.sh <<'SCRIPT'
#!/usr/bin/env bash

usage() {
  printf 'Usage: %s ENVIRONMENT SERVICE REPLICAS CONFIG_FILE\n' "${0##*/}" >&2
}

main() {
  if (( $# != 4 )); then
    usage
    return 2
  fi

  local environment=$1 service=$2 replicas=$3 config_file=$4
  local name_pattern='^[a-z][a-z0-9-]{2,31}$'

  case $environment in
    development|staging|production) ;;
    *) printf 'Invalid environment: %s\n' "$environment" >&2; return 3 ;;
  esac

  if [[ ! $service =~ $name_pattern ]]; then
    printf 'Invalid service name: %s\n' "$service" >&2
    return 3
  fi

  if [[ ! $replicas =~ ^[0-9]+$ ]] || (( replicas < 1 || replicas > 20 )); then
    printf 'Replicas must be an integer from 1 through 20: %s\n' "$replicas" >&2
    return 3
  fi

  if [[ ! -f $config_file || ! -r $config_file || ! -s $config_file ]]; then
    printf 'Configuration must be a readable, non-empty regular file: %s\n' "$config_file" >&2
    return 3
  fi

  printf 'VALID env=%s service=%s replicas=%s config=%s\n' \
    "$environment" "$service" "$replicas" "$config_file"
}

main "$@"
SCRIPT

chmod u+x validate-deployment.sh
printf 'listen=127.0.0.1\n' > 'service config.conf'

./validate-deployment.sh staging payments-api 3 'service config.conf'
./validate-deployment.sh prod 'Bad Name' 3 'service config.conf' || printf 'rejected as expected\n'
./validate-deployment.sh production payments-api 0 'service config.conf' || printf 'rejected as expected\n'
bash -n validate-deployment.sh

Verification checklist

7. Common conditional mistakes

“Square brackets are punctuation.”

[ is a command form. Missing spaces change its arguments and therefore its syntax.

“Existence means usable.”

Test the properties the operation needs, such as regular-file type, readability, size, or directory search permission.

“cmd1 && cmd2 || fallback is always if/else.”

The fallback also runs when cmd2 fails, which may conceal a partial success.

“Regex and glob patterns are interchangeable.”

case and [[ == ]] use shell patterns; [[ =~ ]] uses regular expressions.

8. Knowledge check

Question 1. Why can a command appear directly after if?

Question 2. When is (( expression )) preferable to [ "$n" -gt 3 ]?

Question 3. What is the key semantic difference between [[ $x == api-* ]] and [[ $x == "api-*" ]]?

9. Summary

Bash control flow is built on command status. Use direct command conditions, select the test form that matches the data, distinguish patterns from regular expressions, and keep failure handling explicit. Conditions should improve policy and diagnostics while the real operation still handles races and runtime errors.

10. Further reading

Next lesson

Loops, Functions, Arguments, and Exit Codes

Compose reusable operations and repeat them over structured argument lists with documented status contracts.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.