Skip to content
Shell & TerminalDeep Dive Published Updated 7 min readViews unavailable

cmp Exit Status in Shell: Equal Files, Different Files, and Errors

Handle cmp's three outcomes correctly when comparing artifacts, preserving difference-versus-error semantics in conditionals and pipelines.

cmp compares two files byte by byte. Its status is intentionally three-way: POSIX specifies zero for identical files, one for different files, and a value greater than one for an error. GNU cmp uses status 2 for trouble, but scripts should branch on the portable categories rather than depend on one exact error number. Treating every nonzero status as “files differ” can hide unreadable input or I/O error; treating status one as an ordinary command failure can break comparison logic under set -e.

This pattern is explicit:

case $left in -*) left=./$left ;; esac
case $right in -*) right=./$right ;; esac

if cmp -s "$left" "$right"; then
    printf '%s\n' 'identical'
else
    status=$?
    case $status in
        1) printf '%s\n' 'different' ;;
        *) printf 'comparison failed (status %s)\n' "$status" >&2; exit "$status" ;;
    esac
fi

POSIX requires cmp to follow the Utility Syntax Guidelines, including accepting -- to end option processing before a pathname that begins with a hyphen. A bare operand - is separately defined to mean standard input, however, so the example prefixes relative hyphen-leading names with ./; that handles both option-like names and a file literally named -. Quiet mode suppresses the normal difference report, not the status distinction. If the differing byte matters, use ordinary output or capture a structured comparison result.

Difference is a valid result

Many test workflows use cmp to decide whether to regenerate an artifact. Under set -e, invoke it in an if condition or another deliberate tested context so an ordinary difference does not terminate the script prematurely. Avoid suppressing all errors with a success fallback; that also masks missing files and read errors.

Status values are an API between utility and caller. Map the categories into the application’s own result: same, changed, and unable to compare. POSIX does not require a particular integer above one for an error. Also, with -s, POSIX leaves it unspecified whether an error diagnostic is written to standard error; if a human-readable diagnostic is required, use normal mode and capture it deliberately. Include filenames in any caller-generated report with safe quoting.

An if condition is a deliberate context for a command whose status 1 can be an expected result. This is especially useful in scripts that enable set -e: status 1 means “different,” not necessarily “the workflow failed.” Avoid hiding the status with constructs such as cmp ... || true, because that collapses a real read error into apparent success. If a caller needs its own stable API, wrap cmp and document that the wrapper returns 0 for identical, 1 for different, and a value greater than 1 for inability to compare.

Do not use a command substitution to capture the files’ contents just to compare them. Shell variables cannot represent arbitrary NUL bytes, and command substitution removes trailing newlines. Pass paths as quoted operands, keep the files on disk, and let cmp stream bytes. If paths come from a manifest, preserve each path as one shell argument; whitespace-safe quoting is necessary even though it does not protect against option-like names by itself.

Choose the comparison that matches the question

Use cmp when byte-for-byte identity is the requirement. It does not canonicalize a format, decode text, normalize line endings, ignore whitespace, or identify semantically equivalent JSON. A textual diff is better when a human needs to review line changes. A parser is better when the contract is structural, such as JSON object equality independent of key order. Make that policy explicit rather than treating one tool’s “equal” result as a universal notion of equivalence.

A digest is convenient when the expected value is distributed separately or when a pipeline needs a compact artifact identifier. But a digest comparison answers a different question: it compares fixed-size hash outputs and relies on the hash’s collision resistance and on the expected digest coming from a trusted channel. If both candidate files are already available locally and exact equality is the requirement, cmp avoids the need to define a hashing and manifest process. For downloaded artifacts, verify an authenticated checksum before using the file; do not treat a checksum fetched from the same compromised location as proof of publisher authenticity.

cmp’s ordinary output identifies a first difference, not every difference. The output’s byte and line positions are useful triage hints, but they are not a machine-readable format across locales and implementations. Use -l only when a byte-by-byte report is actually needed and check that implementation’s option semantics; large files can produce a very large output stream. For concise status-only checks, keep -s and branch on the exit category.

Binary comparison versus textual comparison

cmp compares bytes and is suitable when exact artifact identity is the question. It does not normalize line endings, ignore whitespace, decode a character encoding, or compare parsed structures. If text-formatting differences should be ignored, choose a diff mode that expresses that policy. If files are JSON or YAML, parse them and compare normalized data when semantic equivalence is required.

A digest can help with large or remote artifacts, but a hash comparison has collision assumptions and requires an authenticated expected digest. cmp can compare local files directly without inventing text encoding for names or contents. For very large datasets, benchmark I/O rather than assume hashing is always faster.

Files can change during comparison

cmp opens paths and reads data over time. A concurrent writer can modify a file during comparison, so the result may not describe a stable snapshot. If inputs may change, copy or snapshot them first, use immutable artifacts, or coordinate with a lock. A successful comparison only describes what was observed during those reads.

Opening symlinks normally compares target contents, not link text. If comparing link objects, inspect target text with a separate link-aware operation. Permissions, file types, special files, and blocking behavior belong in test policy; comparing a device or FIFO may not behave like comparing regular files.

For a release gate, require regular, finalized artifacts and publish them only after the comparison and every upstream producer have succeeded. cmp cannot prove that a producer wrote a complete file, that two names referred to the same snapshot, or that later writers will not mutate a validated path. Where concurrent publication is possible, use immutable versioned paths or filesystem-specific snapshot/locking facilities, then compare the stable objects. Avoid a check-then-copy sequence that reopens a pathname after comparison and can select different contents.

Pipelines and status preservation

A command such as producer piped to cmp can obscure producer failure in a shell whose pipeline status reports only the final command. Even with pipefail, status one can mean data differed rather than processing failed. If completeness matters, stage producer output, verify producer status, then compare staged bytes.

For artifact publication, compare before replacing previous output, and keep a known-good version until the new one is validated. A difference can be an expected update or an unexpected regression; cmp cannot decide which. Pair it with a trusted expected artifact, version metadata, or semantic tests.

Test equal files, different first byte, different length, missing file, unreadable path, symlink target, special-file handling, and pipeline producer errors. Verify exact statuses under the target platform. A robust comparison has an explicit policy for equality, change, and inability to prove either.

Keep those fixtures small and deterministic. In particular, test a file whose name begins with -, a file named exactly -, a path containing spaces, an input removed between selection and comparison, and a producer that exits unsuccessfully after emitting partial data. Do not test a FIFO by running a comparison that can block without a timeout or a controlled writer. Record the command implementation and platform when a script depends on non-POSIX flags or exact diagnostic wording.

Related:

Sources:

Comments