Chapter 04Lesson 01~50 minutes

Exit Status as Bash's Fundamental Boolean Signal

Bash does not require a dedicated Boolean type to control most automation. Its fundamental truth signal is the exit status of a command: zero means success, while non-zero means failure or another non-success condition. Understanding that model makes shell control flow much easier to reason about.

BeginnerConditionsHands-on lab

Learning objectives

By the end of this lesson

  • Explain why exit status is Bash's core truth signal.
  • Inspect and preserve command status with $?.
  • Use true, false, !, &&, and || without confusing data output with success.
  • Interpret common multi-status commands such as grep.
  • Design checks that distinguish expected non-matches from operational failures.

1. Bash asks whether commands succeeded

In shell programming, commands themselves participate directly in Boolean logic. Bash evaluates a command's exit status. By convention, status 0 means success; any non-zero value means some form of non-success.

Command status becomes control flow
flowchart LR
  C["Run command"] --> S{"exit status"}
  S -->|"0"| T["true / success branch"]
  S -->|"non-zero"| F["false / failure branch"]
true
printf 'true status=%d\n' "$?"

false
printf 'false status=%d\n' "$?"
Shell truth is inverted from many numeric conventions

In arithmetic, zero is often described as false. For command status, zero means success and therefore acts as true in shell control flow.

2. $? contains the immediately previous status

The special parameter $? expands to the exit status of the most recently completed foreground pipeline. Because every new command can replace it, capture it immediately when you need it later.

grep -q '^root:' /etc/passwd
status=$?

printf 'saved grep status=%d\n' "$status"

# This printf itself now becomes the most recent command.
printf 'current $? after printf will no longer be the grep status\n'

A common bug is to run a diagnostic command before saving $?. The diagnostic succeeds, and the script accidentally records that status instead of the operation being checked.

3. if executes a command list; it does not require [ ]

The grammar of if is built around command status. The condition can be any command or command list.

if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  printf 'inside a Git work tree\n'
else
  printf 'not inside a Git work tree\n'
fi

The [ ... ] syntax you often see is simply another command that calculates a status. Bash does not require it around every condition.

4. && and || are status-driven command lists

cmd1 && cmd2 runs cmd2 only if cmd1 succeeds. cmd1 || cmd2 runs cmd2 only if cmd1 fails.

mkdir -p output &&
printf 'directory ready\n'

test -f config.env ||
printf 'config.env is absent\n' >&2
FormControl behaviorTypical use
A && BRun B only when A succeedsUseful for dependent steps
A || BRun B only when A failsUseful for fallback or explicit error handling
! AInvert A's success/failure resultUseful when absence is the expected condition
Do not turn whole scripts into &&/|| chains

For important branching, if blocks usually communicate intent and error handling more clearly.

5. ! negates a command result

The reserved word ! reverses the status used by shell control flow.

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

Negation changes the logical result. If you also need the original status value for diagnosis, structure the code explicitly rather than relying on $? after negation.

6. Non-zero does not always mean the same thing

Many commands use several non-zero statuses to represent different conditions. grep is a classic example: status 0 means a match was found, 1 means no selected line matched, and a larger failure status indicates an error.

if grep -q 'READY' app.log; then
  printf 'ready marker found\n'
else
  status=$?
  case $status in
    1) printf 'ready marker not found\n' ;;
    *) printf 'grep failed with status %d\n' "$status" >&2 ;;
  esac
fi
Operational distinction

An expected negative result and an operational failure are not necessarily the same thing. Read the command's documented exit-status contract before reducing every non-zero value to one generic error.

7. Exit-status numbers carry conventions, not universal meanings

Shells and programs commonly use small positive integers for failure categories. Status 126 and 127 have conventional shell meanings related to command execution; statuses derived from signals may also appear in ranges based on shell conventions. Application-specific statuses vary.

bash -c 'exit 42'
printf 'custom status=%d\n' "$?"

bash -c 'command_that_does_not_exist'
printf 'not-found status=%d\n' "$?"

Do not invent a global interpretation such as “2 always means configuration error.” Define and document status meanings within your own script or follow the command's documented interface.

8. Functions also return status

A Bash function returns the status of its last command unless you use return N explicitly.

service_exists() {
  local name=$1
  grep -Fxq "$name" services.txt
}

if service_exists api; then
  printf 'api exists\n'
fi

This lets functions become reusable predicates. Prefer predicate functions that are quiet on stdout unless output is part of their documented interface.

9. Validate prerequisites before mutation

One safe DevOps pattern is to perform read-only checks first, then change state only after all mandatory conditions succeed.

require_command() {
  local name=$1
  if ! command -v "$name" >/dev/null 2>&1; then
    printf 'error: required command missing: %s\n' "$name" >&2
    return 1
  fi
}

require_command git || exit 1
require_command bash || exit 1

printf 'prerequisites satisfied\n'

This keeps failures close to their cause and prevents partial work from starting under an invalid environment.

10. Hands-on lab: build a status-aware health check

Create a script that distinguishes a healthy marker, an absent marker, and an unreadable/missing file.

mkdir -p "$HOME/devops-academy/bash/chapter04/lesson01"
cd "$HOME/devops-academy/bash/chapter04/lesson01"

cat > health.sh <<'EOF'
#!/usr/bin/env bash

log_file=${1:-app.log}

if [[ ! -r $log_file ]]; then
  printf 'ERROR unreadable log: %s\n' "$log_file" >&2
  exit 2
fi

if grep -q 'STATUS=READY' "$log_file"; then
  printf 'READY\n'
  exit 0
else
  status=$?
  if (( status == 1 )); then
    printf 'NOT_READY\n'
    exit 1
  fi

  printf 'ERROR grep failed with status %d\n' "$status" >&2
  exit 3
fi
EOF

printf 'STATUS=READY\n' > app.log
bash health.sh app.log; printf 'status=%d\n' "$?"

printf 'STATUS=STARTING\n' > app.log
bash health.sh app.log; printf 'status=%d\n' "$?"

bash health.sh missing.log; printf 'status=%d\n' "$?"

Verification checklist

11. Knowledge check

Question 1. What status value acts as success in Bash control flow?

Question 2. Why should you save $? immediately?

Question 3. Does if require a [ ... ] expression?

Question 4. Why can blindly treating every non-zero grep result as an execution error be wrong?

12. Summary

Bash's Boolean foundation is process status. Commands that return zero succeed; non-zero statuses represent false or failure conditions according to each command's contract. if, &&, ||, and ! all operate on this model. Preserve statuses promptly and distinguish expected negative outcomes from genuine execution errors.

13. Further reading

  • GNU Bash Reference Manual — Exit Status and Conditional Constructs.
  • GNU Bash Reference Manual — Lists of Commands.
  • POSIX Shell Command Language — exit status.
  • GNU grep manual — exit-status meanings.
Next lesson

test, [, and [[ ]] Expressions

Continue Chapter 4 by building the next layer of Bash decision logic.

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.