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.
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
-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 artifactsconfig="/etc/hosts"
if [[ -f $config && -r $config && -s $config ]]; then
printf 'config is a readable non-empty regular file\n'
fi2. Existence and symlink checks have edge cases
A dangling symbolic link can exist as a directory entry even though its target does not exist. A test focused on the target and a test focused on the link answer different questions.
target="missing-target"
link="demo-link"
ln -s "$target" "$link"
[[ -L $link ]] && printf 'symbolic link itself exists\n'
[[ -e $link ]] || printf 'resolved target does not exist\n'
rm -f "$link"When automation manages links, decide whether you care about the link object, the resolved target, or both.
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.
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
fi4. 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'
fiTimestamp 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'
fiThis is about file identity, not merely matching pathname text.
6. String tests should make emptiness explicit
-z stringString length is zeroCommon validation for missing/empty input-n stringString length is non-zeroExplicit presence checka == bString equality in [[ ]]Right side can be pattern if unquoteda != bString inequalitySame pattern considerationsa < bLexicographic ordering in [[ ]]Not numeric orderingtoken=${API_TOKEN:-}
if [[ -z $token ]]; then
printf 'API_TOKEN is empty or unset\n' >&2
fi7. 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'
fiIf 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
fi10. 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
fiThis 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 || trueVerification 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?
-gt or arithmetic context (( 10 > 2 )), not lexicographic string ordering.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
testutility specification. - Linux
stat(2),access(2), andsymlink(7)manual pages. - ShellCheck guidance on file and numeric tests.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.