Check if shell script/function is being piped into without false positives

Summary

Detecting whether a script or function is receiving data from a pipe requires more than testing if stdin is a terminal. The reliable pattern is to inspect the file descriptor 0 with stat/test -p or [[ -p /dev/stdin ]]. This works for both standalone scripts and functions sourced into another shell.

Root Cause

  • [[ -t 0 ]] only tells you “is stdin a tty?” – it does not differentiate between:
    • a pipe,
    • a redirected file,
    • a here‑document, or
    • a terminal.
  • When a script is sourced, the calling shell’s stdin state is inherited, so the test reflects the caller’s context, not the function’s own usage.

Why This Happens in Real Systems

  • Production pipelines often chain many utilities (cmd1 | cmd2 | cmd3).
  • Individual components are sometimes reused as both standalone commands and library functions.
  • Developers rely on [[ -t 0 ]] because it’s short, but in a complex orchestration it yields false positives and masks bugs (e.g., a function assumes interactive mode when it is actually receiving streamed data).

Real-World Impact

  • Incorrect mode selection – a script may skip essential parsing logic, leading to malformed output.
  • Data loss – scripts that default to interactive prompts when fed a pipe can deadlock.
  • Hard‑to‑reproduce bugs in CI/CD pipelines where the same code runs both interactively (local dev) and non‑interactively (automation).

Example or Code (if necessary and relevant)

# Detect pipe or redirection in a portable way
if [ -p /dev/stdin ] || [ -f /dev/stdin ] && [ "$(stat -c %F /dev/stdin)" = "fifo" ]; then
    echo "Piped input detected"
else
    echo "No pipe (interactive or no stdin)"
fi

# Function version (works when sourced)
is_piped() {
    [ -p /dev/stdin ] && return 0
    return 1
}

# Usage
if is_piped; then
    echo "Function received pipe"
else
    echo "Function not piped"
fi

How Senior Engineers Fix It

  • Prefer /dev/stdin checks (-p, -S) over -t.
  • Wrap the logic in a reusable function/library to avoid duplication.
  • When a script must support both interactive and non‑interactive modes, explicitly test for a pipe and for a regular file redirect:
    case "$(stat -c %F /dev/stdin)" in
        FIFO|character special) pipe=true ;;
        regular) pipe=true ;;   # from a file redirect
        *) pipe=false ;;
    esac
  • Document the expected behaviour in the script’s help output, so callers know when interactive prompts will be suppressed.
  • Add unit tests that invoke the script with:
    • no stdin,
    • a pipe (echo data | script),
    • a file redirect (script < file).

Why Juniors Miss It

  • They equate “not a terminal” with “being piped”, overlooking other stdin sources.
  • They seldom test scripts in non‑interactive CI environments, so the bug stays hidden.
  • Lack of familiarity with /dev/stdin and stat utilities leads them to rely on the shorter but inaccurate [[ -t 0 ]].

Leave a Comment