Input Redirection, Here Documents, and Here Strings
Commands do not need to know whether their input came from a keyboard, file, pipe, or text embedded in a script. Bash can connect any of those sources to stdin. The engineering challenge is choosing the form that preserves quoting, readability, and data integrity.
Learning objectives
By the end of this lesson
- Redirect stdin from files and existing descriptors.
- Use here documents for readable multi-line input.
- Control expansion inside here documents by quoting the delimiter.
- Use here strings for small single-value inputs.
- Select safe input patterns for loops, configuration generation, and automation commands.
1. < connects a file to stdin
Input redirection lets a program read a regular file through its normal stdin interface. The program does not need a filename argument if it already knows how to read stdin.
printf '%s\n' alpha beta gamma > values.txt
wc -l < values.txt
sort < values.txtNotice the difference between wc -l values.txt and wc -l < values.txt: when the filename is passed as an argument, wc may include the filename in its output; when input arrives only through stdin, it has no filename to report.
2. Input redirection changes descriptor 0 before launch
flowchart LR F["values.txt"] -->|"open for reading"| B["Bash redirection"] B -->|"fd 0"| C["sort"] C -->|"fd 1"| T["terminal"]
The shell opens the file and arranges descriptor 0 before the command begins. A failure to open the input file can prevent the command from running at all.
missing="/definitely/not/here"
if sort <"$missing"; then
printf 'sorted\n'
else
printf 'input could not be opened or sort failed\n' >&2
fi3. Feed a while read loop without losing whitespace
A canonical Bash line-reading pattern is:
while IFS= read -r line; do
printf 'line=<%s>\n' "$line"
done < values.txtIFS= prevents leading/trailing IFS whitespace from being trimmed by read. -r prevents backslashes from being treated as escape characters. Together they make the loop a much safer representation of input lines.
Do not write for line in $(cat file) to iterate file lines. Command substitution plus word splitting destroys the original line structure.
4. A here document embeds multi-line stdin in the script
A here document begins with <<DELIMITER and ends at a line containing the delimiter by itself. Bash supplies the intervening text as input to the command.
cat <<EOF
service=api
environment=staging
replicas=3
EOFHere documents are valuable for templates, command input, SQL fragments, configuration snippets, and explanatory multi-line messages. The delimiter word can be any clear token that does not appear as a standalone line in the content.
5. Quoting the delimiter controls expansion
An unquoted here-document delimiter allows parameter expansion, command substitution, and arithmetic expansion in the body. Quoting the delimiter suppresses those expansions.
name="api"
count=3
cat <<EOF
expanded name=$name
expanded arithmetic=$(( count + 1 ))
EOF
cat <<'EOF'
literal name=$name
literal arithmetic=$(( count + 1 ))
EOF<<EOFExpansions occurUseful for intentional templates<<'EOF'Body is treated literally for these expansionsSafer for scripts/config containing dollar signs or command syntaxIf here-document content includes untrusted values, do not assume quoting the delimiter performs safe templating. Validate and encode values for the destination format.
6. <<- can strip leading tab characters
The <<- form removes leading tab characters from here-document lines and the closing delimiter. It exists to make shell source indentation easier while preserving the generated content.
if true; then
cat <<-EOF
indented in source with tabs
but leading tabs are stripped
EOF
fiOnly tab characters are stripped—not arbitrary spaces. Editors that automatically convert tabs to spaces can therefore change the result.
7. A here string supplies one expanded value as stdin
Bash's here-string syntax <<< is useful for short inputs:
value="release-2026.08.09"
grep -o '[0-9][0-9.]*' <<<"$value"
read -r prefix version <<<"release 2026.08.09"
printf 'prefix=%s version=%s\n' "$prefix" "$version" A here string is a Bash extension rather than POSIX shell syntax. It is excellent for small values, but a pipe or parameter operation may be clearer when the data flow becomes complex.
8. Feed non-interactive commands deliberately
Some commands accept structured instructions on stdin. A here document can make that interaction reproducible:
# Demonstration with a simple parser instead of a destructive tool.
while IFS='=' read -r key value; do
printf 'key=%q value=%q\n' "$key" "$value"
done <<'CONFIG'
service=payments
environment=staging
replicas=4
CONFIGFor tools that provide explicit non-interactive flags or structured file formats, prefer those interfaces over scripting an interactive prompt. Sending answers to a human-oriented prompt is often brittle.
9. Be careful when a loop itself consumes stdin
If a loop reads from stdin and a command inside the loop also reads stdin, the inner command can accidentally consume future loop input. Give the inner command a different input source or explicitly disconnect it.
while IFS= read -r path; do
printf 'processing %s\n' "$path"
# Example command that should NOT consume the loop's stdin:
some_command </dev/null
done < paths.txtThis class of bug is common with commands such as ssh, interactive utilities, and tools that opportunistically read stdin.
10. Use a dedicated descriptor for complex readers
When multiple consumers need independent input channels, open a dedicated descriptor:
exec 3< values.txt
while IFS= read -r -u 3 line; do
printf 'from fd3: %s\n' "$line"
done
exec 3<&-Bash's read -u FD reads from the specified descriptor instead of stdin. This can keep a script's primary stdin available for another purpose.
11. Hands-on lab: render a configuration safely
Create a tiny configuration generator. Use a quoted here document for a literal header and an unquoted here document only where you intentionally want controlled Bash expansion.
mkdir -p "$HOME/devops-academy/bash/chapter03/lesson03"
cd "$HOME/devops-academy/bash/chapter03/lesson03"
service=${SERVICE:-api}
environment=${ENVIRONMENT:-development}
replicas=${REPLICAS:-2}
{
cat <<'HEADER'
# Generated by DevOps Academy Bash lab.
# Do not edit this file manually.
HEADER
cat <<CONFIG
service=$service
environment=$environment
replicas=$replicas
CONFIG
} > app.conf
cat app.conf
while IFS='=' read -r key value; do
[[ $key == \#* || -z $key ]] && continue
printf 'parsed %-12s -> %s\n' "$key" "$value"
done < app.confVerification checklist
12. Knowledge check
Question 1. What does cmd < file change?
Question 2. How do you suppress Bash expansion inside a here document?
<<'EOF'.Question 3. Why is while IFS= read -r line safer than iterating over $(cat file)?
13. Summary
Input redirection lets commands consume files and embedded text through the same stdin interface. Here documents are ideal for readable multi-line data, quoted delimiters give literal bodies, and here strings are convenient for short Bash values. When loops and nested commands share stdin, manage descriptors deliberately so one consumer cannot steal another's input.
14. Further reading
- GNU Bash Reference Manual — Redirections and Here Documents.
- GNU Bash Reference Manual —
readbuiltin. - POSIX Shell Command Language — here-document syntax.
- ShellCheck guidance on
read -r, command substitution, and input loops.
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.