Shell bugs are often painfully polite. The script exits successfully, the log looks ordinary, and only later do you discover that a customer name became two values or a path stopped at its first space. IFS—the Internal Field Separator—is frequently standing near the scene, but it is rarely acting alone.
The useful mental model is narrower than “IFS splits strings.” Bash consults IFS in specific contexts: notably word splitting after certain unquoted expansions, the read builtin, and the way $* joins positional parameters. Quoting and the command doing the reading matter just as much as the characters stored in the variable.
Meet the default separators, then leave them alone
In a normal Bash session, IFS defaults to space, tab, and newline. During word splitting, runs of those IFS whitespace characters are treated specially: leading and trailing whitespace is ignored and adjacent whitespace does not create a parade of empty fields.
text='alpha beta gamma'
printf '<%s>\n' $text
printf '<%s>\n' "$text"<alpha>
<beta>
<gamma>
<alpha beta gamma>The quote marks make the real decision
The first
$textis unquoted, so its expansion is eligible for word splitting and produces three arguments toprintf.The quoted
"$text"stays one argument, including both spaces betweenbetaandgamma.printfrepeats its format for extra arguments; the four output lines do not mean the variable itself changed.This exact command and output were executed with GNU Bash 5.2.21.
Treat quoted expansion as the default. Reach for intentional parsing only where the input format has a real delimiter contract.
Unset and empty are two different states
This distinction is easy to miss in reviews. For Bash word splitting, an unset IFS behaves as though the default space, tab, and newline value were present. Setting IFS to an empty string disables word splitting; it does not restore the default.
payload='left right'
IFS=
printf '<%s>\n' $payload
unset IFS
printf '<%s>\n' $payload<left right>
<left>
<right>Two commands that look similar tell Bash opposite things
IFS=assigns a present but empty value; the unquoted expansion remains a single field.unset IFSremoves the variable, and Bash uses default splitting behavior for the next unquoted expansion.Neither version makes unquoted expansion a good habit: pathname expansion can still run after word splitting.
This output was reproduced with GNU Bash 5.2.21.
A simple
saved=$IFScannot remember whether IFS was originally unset or merely held the default characters. Prefer a narrow scope instead of manual save-and-restore code.
Let read own the delimiter for one record
Delimited records are where IFS feels genuinely helpful. Put the assignment directly in front of read so the choice belongs to that one operation rather than every expansion that follows it.
record='api,,active,'
IFS=, read -r first second third fourth <<< "$record"
printf 'first=<%s> second=<%s> third=<%s> fourth=<%s>\n' \
"$first" "$second" "$third" "$fourth"first=<api> second=<> third=<active> fourth=<>There is more policy here than the short command suggests
IFS=,applies comma splitting to thisreadinvocation; it does not leave the surrounding shell with a comma separator.-rpreventsreadfrom treating backslash as an escape character, which is normally what data ingestion wants.The quoted here-string supplies one Bash value and appends a newline for
readto consume.Adjacent non-whitespace delimiters preserve the empty second field.
When there are more fields than variables,
readassigns the remaining words and intervening delimiters to the last variable according to its documented rules.The shown four-field result was executed with GNU Bash 5.2.21. A trailing empty field is a fragile interchange requirement, so do not treat this convenience pattern as a general CSV reader.
Preserve a line exactly enough to reason about it
input=' alpha beta '
IFS= read -r line <<< "$input"
printf '<%s>\n' "$line"< alpha beta >Both halves of the idiom earn their place
Empty
IFStellsreadnot to strip leading and trailing IFS whitespace.read -rdisables its usual backslash escape processing.Quoting
"$line"preserves the captured value when passing it toprintf.A here-string adds its own terminating newline; use redirection from the real file or process when newline fidelity itself matters.
The spaces shown in this output were verified on GNU Bash 5.2.21.
Arrays expose the trailing-field surprise
Reading into an indexed array is convenient, but convenience should not be confused with lossless serialization. Bash does not preserve the final empty field in this example.
record='api,,active,'
IFS=, read -ra fields <<< "$record"
printf 'count=%d\n' "${#fields[@]}"
printf '<%s>\n' "${fields[@]}"count=3
<api>
<>
<active>The missing fourth element is the lesson
-a fieldsassigns words sequentially to the indexed array and clears existing elements before assignment.The empty field between adjacent commas becomes a real array element.
The delimiter at the end does not yield a fourth array element here; the observed count is three.
"${fields[@]}"expands each element as a separate quoted argument, preserving the empty middle element.This exact result was executed on GNU Bash 5.2.21. If trailing empty columns carry meaning, choose a parser and representation that guarantee them.
For lines, mapfile says what the code means
printf 'red\nblue green\n\n' | {
mapfile -t rows
printf 'count=%d\n' "${#rows[@]}"
printf '<%s>\n' "${rows[@]}"
}count=3
<red>
<blue green>
<>No field splitting is needed for a line collection
mapfile—also available asreadarray—reads lines directly into an indexed array.-tremoves each terminating newline; it does not splitblue greenat the space.The blank third input line is retained as an empty array element.
The brace group keeps the reads and prints in the same pipeline process; pipeline subshell details otherwise matter if
rowsis needed afterward.The output was reproduced with GNU Bash 5.2.21.
mapfileis a Bash feature, not a portable POSIXshcommand.
Filenames need a byte that filenames cannot contain
Line-based loops fail on filenames containing newlines. Unix path components cannot contain a NUL byte, so find -print0 and read -d "" can agree on an unambiguous boundary.
#!/usr/bin/env bash
while IFS= read -r -d '' path; do
printf 'Found: %q\n' "$path"
done < <(find . -type f -print0)Consume NUL-delimited paths without splitting on spaces, tabs, wildcards, or newlines.
The safety comes from the complete protocol
find -print0emits a NUL after each pathname.read -d ""uses Bash’s empty delimiter argument to mean NUL; this is Bash-specific.IFS=prevents whitespace trimming and-rpreserves backslashes.Every use of
"$path"is quoted, preventing later word splitting and pathname expansion.Process substitution avoids piping the loop itself into a subshell, so state changed inside the loop remains available afterward.
printf %qdisplays a shell-reusable representation for diagnosis; pass the quoted pathname directly to real tools instead of parsing that display back.
Keep a changed IFS inside a function
parse_status() {
local IFS=,
local service state owner
read -r service state owner <<< "$1"
printf 'service=%s state=%s owner=%s\n' \
"$service" "$state" "$owner"
}
parse_status 'payments,degraded,on-call'Use a function-local separator when several operations genuinely share one delimiter.
Local scope is easier to trust than restoration
local IFS=,creates a function-local value that Bash restores when the function returns.The remaining variables are local too, preventing parsed data from leaking into caller state.
For only one
read,IFS=, read -r ...is narrower still and usually clearer.Manual restoration has to preserve both the old value and whether the variable was set at all; error paths and early returns make that surprisingly easy to get wrong.
This function expects a simple delimiter format, not RFC 4180 CSV. A field such as
"payments,api"would be parsed incorrectly.
A compact diagnosis for the data that vanished
A value splits at spaces: find an unquoted parameter or command substitution; quote it before changing global IFS.
Leading whitespace disappears during read: use
IFS= read -r variable.Backslashes disappear or join lines: add
-rtoreadunless escape processing was intentional.An empty middle field disappears: check whether the delimiter is IFS whitespace, because whitespace and non-whitespace IFS characters follow different rules.
A trailing empty array field is missing: Bash
read -ais not a lossless record decoder; use a parser that promises the required field model.A filename loop breaks: use a NUL-delimited producer and consumer, then quote every pathname expansion.
IFS changes later code unexpectedly: scope the assignment to a command or declare it local inside a function.
Quoted CSV parses incorrectly: stop extending the IFS trick and use a CSV implementation in Python, Ruby, another language, or a trusted purpose-built tool.
The gentle rule that prevents most IFS bugs
Quote ordinary data. When parsing is truly necessary, name the delimiter beside the one read that needs it. That makes the exceptional code visible to the next person—and, six months from now, that person may be you staring at a filename with a newline and wondering why the script was ever so confident.
Comments and corrections