Combining the excellent mentioned resources from @Giles's answer, @choroba's comment, and the answers from another question. I've put together the following code examples to illustrate the differences:
IFS (aka Internal Field Separator) specifies the inline delimiters (multiple characters are accepted, order is irrelevant). It defaults to IFS=$' \t\n'. It is only relevant if read is given multiple variable targets.
read's -d argument specifies the line delimiter (only the first character is accepted). It defaults to -d $'\n'.
As such,
# IFS=, -d $'\n', with tab separated fields, across two lines
echo $'a\tb\tc\nz\tx\ty' | while IFS= read -rd $'\t' a b c; do echo "[$a] [$b] [$c]"; done
# [a] [] []
# [b] [] []
# [c
# z] [] []
# [x] [] []
# IFS=tab, with tab separated fields, across two lines
echo $'a\tb\tc\nz\tx\ty' | while IFS=$'\t' read -r a b c; do echo "[$a] [$b] [$c]"; done
# [a] [b] [c]
# [z] [x] [y]
# IFS=tab, with tab separated fields, across two lines, with only a single variable target
echo $'a\tb\tc\nz\tx\ty' | while IFS=$'\t' read -r a; do echo "[$a]"; done
# [a b c]
# [z x y]
# IFS=tab, with space and tab separated fields, across two lines
echo $'a b\tc\nz\tx y' | while IFS=$'\t' read -r a b c; do echo "[$a] [$b] [$c]"; done
# [a b] [c] []
# [z] [x y] []
# IFS=tab+space, with space and tab separated fields, across two lines
echo $'a b\tc\nz\tx y' | while IFS=$'\t ' read -r a b c; do echo "[$a] [$b] [$c]"; done
# [a] [b] [c]
# [z] [x] [y]
# IFS=newline, -d '', with space and tab separated fields, across two lines
echo $'a b\tc\nz\tx y' | while IFS=$'\n' read -rd '' a b c; do echo "[$a] [$b] [$c]"; done
# outputs nothing, as no delimiter means no lines for inline splitting
# IFS=newline, -d '', with space and tab separated fields, across two lines, with trailing null character
printf 'a b\tc\nz\tx y\0' | while IFS=$'\n' read -rd '' a b c; do echo "[$a] [$b] [$c]"; done
# outputs a single line, with two newline separated fields:
# [a b c] [z x y] []
# IFS=newline, -d $'\0', with space and tab separated fields, across two lines, with trailing null character
printf 'a b\tc\nz\tx y\0' | while IFS=$'\n' read -rd $'\0' a b c; do echo "[$a] [$b] [$c]"; done
# outputs a single line, with two newline separated fields:
# [a b c] [z x y] []
As such,
IFS splits "fields" across a "line", it is an "inline" splitter
-d splits "lines", it is a "line" splitter
- customise
IFS to customise what separates "fields"
- customise
-d to customise what separates "lines"
One use case where -d is valuable, is reading each field individually, in a specific order:
echo $'a b\tc\nz\tx y' | {
read -rd ' ' a
echo "a=[$a]"
read -rd $'\t' b
echo "b=[$b]"
read -rd $'\n' c
echo "c=[$c]"
read -rd $'\t' z
echo "z=[$z]"
read -rd $' ' x
echo "x=[$x]"
read -rd $'\n' y
echo "y=[$y]"
}
# a=[a]
# b=[b]
# c=[c]
# z=[z]
# x=[x]
# y=[y]
As such,
IFS is only necessary to be defined iff your read call accepts multiple variable targets.
- If your
read call only accepts a single variable argument, IFS is discarded, which means that IFS= in such cases only serves a cosmetic function.
@Giles's answer covers IFS outside the context of read.
Such a use case could be selecting a filename from a directory that contains two files, one with a space inside it, and one without:
cd "$(mktemp -d)" || exit 1
touch 'before-space after-space.txt'
touch 'no-space.txt'
# using arrays
# results in correct fields for selection
mapfile -t list < <(ls -1)
select node in "${list[@]}"; do
echo "via mapfile, [$node]"
break
done
echo
# outputs:
# 1) before-space after-space.txt
# 2) no-space.txt
# #? 1
# via mapfile, [before-space after-space.txt]
# using word splitting with default `IFS`
# results in mangled fields for selection
select node in $(ls -1); do
echo "IFS=default [$node]"
break
done
echo
# outputs:
# 1) before-space
# 2) after-space.txt
# 3) no-space.txt
# #? 1
# IFS=default [before-space]
# using word splitting with `IFS=$'\n'`
# results in the correct fields for selection
IFS=$'\n'
select node in $(ls -1); do
echo "IFS=newline [$node]"
break
done
echo
# outputs:
# 1) before-space after-space.txt
# 2) no-space.txt
# #? 1
# IFS=newline [before-space after-space.txt]
# using word splitting with `IFS=`
# results in a jumbled field for selection
IFS=
select node in $(ls -1); do
echo "IFS= [$node]"
break
done
echo
# outputs:
# 1) before-space after-space.txt
# no-space.txt
# #? 1
# IFS= [before-space after-space.txt
# no-space.txt]