In bash, what does the `$(<file)` form of command substitution do differently from `$(cat file)`, and when would you not use it?
answer
- there is no command in the parentheses
- same output, one fewer process
- matters per iteration, not per call
- ksh heritage, not in POSIX
- 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 sBoth 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#!/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
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.
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.
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.
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