Chapter 04Lesson 03~65 minutes

File, String, and Numeric Condition Tests

Most automation branches on observable state: Does a configuration file exist? Is it readable? Is an input empty? Is a retry limit within range? Bash provides compact predicates for these questions, but reliable scripts must understand exactly what each test proves—and what it does not.

BeginnerConditionsHands-on lab

Learning objectives

By the end of this lesson

  • Use common file predicates correctly.
  • Test strings for emptiness and equality without accidental expansion.
  • Perform integer comparisons with the appropriate operators.
  • Compare file timestamps and identity where useful.
  • Recognize time-of-check/time-of-use limitations in mutable systems.

1. File tests answer specific filesystem questions

PredicateTestsUse
-e pathPath existsDoes not mean regular file
-f pathRegular fileUseful for ordinary config/artifact files
-d pathDirectoryChecks directory file type
-L pathSymbolic linkBash also accepts -h
-r pathReadable by current process credentialsPermission/access check
-w pathWritable by current process credentialsDoes not guarantee future write success
-x pathExecutable/searchableMeaning depends on file type
-s pathExists and size is greater than zeroUseful for non-empty artifacts
config="/etc/hosts"

if [[ -f $config && -r $config && -s $config ]]; then
  printf 'config is a readable non-empty regular file\n'
fi

3. Permission predicates are observations, not guarantees

-r, -w, and -x test access according to current process credentials and filesystem semantics. Passing a test does not reserve the resource or guarantee the subsequent operation will succeed.

TOCTOU warning

Filesystem state can change between a check and a later operation. Prefer attempting the operation and handling failure when correctness depends on the operation itself.

# Instead of only checking -w and assuming success:
if printf '%s\n' "new content" > "$target"; then
  printf 'write succeeded\n'
else
  printf 'write failed\n' >&2
  exit 1
fi

4. Bash can compare file modification times

Within conditional expressions, -nt means “newer than” and -ot means “older than.”

source_file="source.yaml"
generated_file="rendered.conf"

if [[ $source_file -nt $generated_file ]]; then
  printf 'regeneration is needed\n'
fi

Timestamp comparisons are convenient for local build-like workflows, but distributed systems and generated artifacts may need stronger change detection such as hashes or explicit version metadata.

5. -ef tests whether two paths refer to the same file

The -ef operator is useful when hard links or alternate path spellings could refer to the same underlying file.

if [[ path-a -ef path-b ]]; then
  printf 'both paths identify the same file\n'
fi

This is about file identity, not merely matching pathname text.

6. String tests should make emptiness explicit

PredicateMeaningCaution
-z stringString length is zeroCommon validation for missing/empty input
-n stringString length is non-zeroExplicit presence check
a == bString equality in [[ ]]Right side can be pattern if unquoted
a != bString inequalitySame pattern considerations
a < bLexicographic ordering in [[ ]]Not numeric ordering
token=${API_TOKEN:-}

if [[ -z $token ]]; then
  printf 'API_TOKEN is empty or unset\n' >&2
fi

7. Numeric comparison is not string comparison

Classic test syntax uses integer comparison operators such as -eq, -ne, -lt, -le, -gt, and -ge. In Bash, arithmetic context (( ... )) is often clearer for numeric expressions.

retries=5
limit=3

if [[ $retries -gt $limit ]]; then
  printf 'too many retries\n'
fi

if (( retries > limit )); then
  printf 'same numeric conclusion via arithmetic context\n'
fi
Validate external input

If a value came from a user, environment variable, API, or file, validate its expected numeric syntax before using it in arithmetic.

8. Validate numeric ranges explicitly

Configuration checks often need both syntax and range validation.

port=${1:-}

if [[ ! $port =~ ^[0-9]+$ ]]; then
  printf 'port must contain decimal digits only\n' >&2
  exit 64
fi

port=$((10#$port))

if (( port < 1 || port > 65535 )); then
  printf 'port out of range: %d\n' "$port" >&2
  exit 65
fi

printf 'valid port=%d\n' "$port"

9. Executable discovery is usually better with command -v

Checking a literal path such as [[ -x /usr/bin/jq ]] assumes where the executable is installed. If your real requirement is “can this shell resolve the command?”, use command -v.

if command -v jq >/dev/null 2>&1; then
  printf 'jq is available through command resolution\n'
else
  printf 'jq is unavailable\n' >&2
fi

10. Creating required directories can be more robust than pre-checking

If the desired state is simply “this directory exists,” mkdir -p is naturally idempotent and often clearer than an existence check followed by creation.

workspace="$HOME/devops-academy/work"

if mkdir -p -- "$workspace"; then
  printf 'workspace ready: %s\n' "$workspace"
else
  printf 'could not prepare workspace\n' >&2
  exit 1
fi

This reduces the race window between “does it exist?” and “create it.”

11. Hands-on lab: validate a deployment configuration

Build a validator for a configuration path, replica count, port, and environment name.

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

cat > validate-deploy.sh <<'EOF'
#!/usr/bin/env bash

config=${1:-}
replicas=${2:-}
port=${3:-}
environment=${4:-}

if [[ ! -f $config || ! -r $config || ! -s $config ]]; then
  printf 'invalid config file: %s\n' "$config" >&2
  exit 2
fi

if [[ ! $replicas =~ ^[0-9]+$ || ! $port =~ ^[0-9]+$ ]]; then
  printf 'replicas and port must be decimal integers\n' >&2
  exit 3
fi

replicas=$((10#$replicas))
port=$((10#$port))

if (( replicas < 1 || replicas > 100 )); then
  printf 'replicas out of range\n' >&2
  exit 4
fi

if (( port < 1 || port > 65535 )); then
  printf 'port out of range\n' >&2
  exit 5
fi

if [[ $environment != dev && $environment != staging && $environment != prod ]]; then
  printf 'unsupported environment\n' >&2
  exit 6
fi

printf 'configuration accepted\n'
EOF

printf 'service=api\n' > app.conf
bash validate-deploy.sh app.conf 3 8080 staging
bash validate-deploy.sh app.conf 0 8080 staging || true
bash validate-deploy.sh missing.conf 3 8080 staging || true

Verification checklist

12. Knowledge check

Question 1. Does -f path merely mean the path exists?

Question 2. What does -s path test?

Question 3. Why can a successful -w test still be followed by a failed write?

Question 4. Which comparison is appropriate for numeric 10 greater than 2?

13. Summary

Bash offers focused predicates for file type, access, size, timestamps, strings, and integers. Use the predicate that answers the exact question you care about, validate external numeric syntax before arithmetic, and remember that pre-checks do not make later operations atomic. When possible, attempt idempotent operations directly and handle failure.

14. Further reading

  • GNU Bash Reference Manual — Bash Conditional Expressions.
  • POSIX test utility specification.
  • Linux stat(2), access(2), and symlink(7) manual pages.
  • ShellCheck guidance on file and numeric tests.
Next lesson

if, elif, else, and Compound Conditions

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.