skip to content

In bash, what does the `$(<file)` form of command substitution do differently from `$(cat file)`, and when would you not use it?

level: middleimportance: nice to knowfreq 24%

answer

  1. there is no command in the parentheses
  2. same output, one fewer process
  3. matters per iteration, not per call
  4. ksh heritage, not in POSIX
  5. useless the moment you need a filter

basics

~20 s

$(<file) makes bash read the file's contents itself instead of forking and running an external cat, so it is faster and substitutes the same text. It is a bash and ksh extension rather than POSIX, so a strict /bin/sh script keeps $(cat file).

solid answer

~40 s

Both produce the file's contents with trailing newlines stripped, but `$(<file)` is handled specially: bash reads the file directly rather than forking a subshell to exec `cat`, which the bash manual documents as the faster equivalent. In a loop over thousands of files, that saved process per iteration is real. Two caveats. First, it is a bash and ksh extension, not POSIX, so a script with `#!/bin/sh` targeting dash or busybox needs `$(cat file)`. Second, it only reads a plain file — the moment you want `head -1`, a decompression, or any filtering, you are back to a real command. Everything else is identical: trailing newlines are still stripped, and the result is still word-split unless you quote it.

code

bash · 10 lines
bash
#!/usr/bin/env bash
printf '1.4.2\n' > VERSION

version=$(<VERSION)         # bash reads the file itself
cat_version=$(cat VERSION)  # forks and execs /bin/cat
[[ $version == "$cat_version" ]] && echo "identical results"

# process-free read of the first line only
IFS= read -r first_line < VERSION
echo "$first_line"

go deeper

for a junior

Just recognise the form when you read it — $(<file) means the contents of that file. Knowing it exists is enough; nobody will fail you for reaching for $(cat file) first.

for a middle

Explain that bash reads the file itself instead of forking cat, and name the two limits: it is not POSIX, and it only reads a whole plain file. Note that trailing-newline stripping is unchanged.

for a senior

Show that you apply it where it pays — inside loops and hot paths — and that you do not churn readable one-off code for a single fork. Flag the shebang in review: the same line is correct in a bash script and wrong in a /bin/sh one.

for a principal

Own the threshold judgment: when a script's cost is dominated by per-iteration process creation, the answer is often to stop scripting the loop in bash rather than to micro-optimise each read. Set the portability baseline so this choice is not made ad hoc.

## What the form is `$(< file)` — the redirection-only spelling of command substitution — substitutes the contents of `file`. There is no command inside the parentheses at all, just an input redirection. ```bash version=$(<VERSION) pid=$(</var/run/app.pid) ``` The result is exactly what `$(cat file)` would have produced: the whole file, with **all** trailing newlines stripped, subject to word splitting and globbing if you leave it unquoted. ## The difference: no external process `$(cat file)` is a genuine command substitution around a genuine external command. Bash sets up a subshell and runs `/bin/cat`, which means a `fork` plus an `exec` plus process teardown per invocation. `$(<file)` is recognised by bash as a special case, and bash reads the file itself rather than launching `cat`. The bash manual describes it as the equivalent but faster form for exactly this reason. On a single read the difference is invisible — a fraction of a millisecond. It becomes measurable in the shape where shell scripts usually hurt: a loop. ```bash # thousands of external processes for f in /proc/[0-9]*/comm; do name=$(cat "$f"); done # no external process for the read for f in /proc/[0-9]*/comm; do name=$(<"$f"); done ``` At a few thousand iterations this is the difference between a script that feels instant and one that takes a noticeable pause — the same class of win as replacing `echo "$x" | grep pat` with `[[ $x == *pat* ]]`. ## When not to use it **Portability.** `$(<file)` comes from ksh and is implemented by bash and zsh. It is **not** in POSIX. A script whose shebang is `#!/bin/sh` may be running under dash on Debian or Ubuntu, or busybox ash in an Alpine container, and there it is a surprise rather than a fast read. If the script declares `#!/usr/bin/env bash`, use it freely; if it declares `#!/bin/sh`, write `$(cat file)`. **Anything but a whole plain file.** The form takes a redirection, not a pipeline. As soon as you need the first line only, a decompression, a filter, or a command's output rather than a file's contents, you need a real command again: ```bash first=$(head -n1 file) # cannot be expressed as $(<file) body=$(zcat file.gz) # likewise ``` For "first line only", the shell-native alternative avoids the external process too: ```bash IFS= read -r first < file ``` **Very large files.** Both forms slurp the whole file into a single shell string in memory. `$(<huge.log)` is not more memory-efficient than `$(cat huge.log)` — it is only cheaper in processes. Past a few megabytes, stream it instead of capturing it. **Special files that block.** Reading a FIFO or a character device with either form blocks until the writer closes; the substitution form does not make that safer. ## What is unchanged Everything about the *result* is the same, and forgetting that is the usual mistake: - All trailing newlines are stripped, so a file ending in a blank line loses it. - The result is word-split and glob-expanded unless quoted, so `"$(<file)"` keeps the quotes. - Reading a file that does not exist prints an error to stderr and yields an empty value; the script must handle the empty case rather than assume content. ## The short answer to give "Same result, one fewer process — bash reads the file itself instead of exec'ing `cat`. Use it in bash scripts, especially inside loops; fall back to `$(cat file)` in `#!/bin/sh` scripts and any time you actually need a command rather than a file's raw contents."

  • Does `$(<file)` also strip trailing newlines?
    Yes — it is command substitution, so the same rule applies: every trailing newline is removed from the result. A file ending in a blank line loses it, exactly as with `$(cat file)`. If the trailing newlines are data, you need the sentinel workaround regardless of which form you used to read the file.
  • Is there an equivalent trick for reading only the first line without an external process?
    Yes — `IFS= read -r line < file` reads the first line using only the shell's builtin `read` and an input redirection. Setting `IFS=` empty keeps leading and trailing whitespace, and `-r` stops backslashes being interpreted. No `head`, no `sed`, no process created.
  • How much does one fork actually cost — is this worth optimising?
    A single fork plus exec on Linux is well under a millisecond, so for a one-off read it is noise and readability wins. It matters when it multiplies: a loop over thousands of files, a hot path in a CI script, or a container entrypoint on a constrained box. Optimise the loop, not the one-liner.

saying these in an interview costs you the question

  • Says $(<file) is POSIX and safe under /bin/sh
  • Thinks it preserves trailing newlines unlike $(cat file)
  • Believes it streams instead of loading the whole file
  • Claims it accepts a pipeline inside the parentheses
  • Assumes the result needs no quoting

context