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]
man bash
. – choroba Nov 10 '21 at 09:57If some code samples could be provided, that illustrate what the documentation is trying to communicate, that would be appreciated. For foolish ol' me, the documentation is too obtuse without illustration.
– balupton Nov 10 '21 at 10:06