I'm trying to figure out how IFS affects word splitting in bash. The behavior is context dependent in a way that doesn't seem to match the intuition of word splitting.
The general ideas seems simple enough. Quoting from the bash man page:
The shell treats each character of IFS as a delimiter, and splits the results of the other expansions into words on these characters. ... Note that if no expansion occurs, no splitting is performed.
This can be easily verified for instance by setting the IFS variable to ',' and invoking a shell function with a comma separated argument list.
echo_n () {
echo Num args: $#, Args: "$@"
}
( IFS=','
args=foo,bar,baz
echo_n $args
)
As expected this results in three distinct arguments to echo_n
Num args: 3, Args: foo bar baz
Invoking echo_n directly with the comma separated list fails because no expansion is triggered.
IFS=, echo_n foo,bar,baz
results in
Num args: 1, Args: foo,bar,baz
Up to here things seem rather contorted, but I can wrap my head around them. When we start adding for loops to the picture, things get hairier.
(IFS=,; for i in foo,bar,baz ; do echo_n $i; done)
results in
Num args: 3, Args: foo bar baz
which defeats the purpose of the for loop.
Now, I can force IFS word splitting where I want it to through any of several bash tricks that force some form of expansion to be triggerd. For instance:
(IFS=,; for i in ${NO_VAR:-foo,bar,baz} ; do echo_n $i; done)
results in
Num args: 1, Args: foo
Num args: 1, Args: bar
Num args: 1, Args: baz
(The trick consists in evaluating an undefined variable NO_VAR with a default value.)
Another similar trick, relying on command substitution:
(IFS=,; for i in $(echo foo,bar,baz) ; do echo_n $i; done)
So here's the question: what is the recommended, idiomatic way to control the context where IFS word splitting is performed?
3.5. 7 Word Splitting The shell treats each character of $IFS as a delimiter, and splits the results of the other expansions into words using these characters as field terminators.
IFS is a variable for the line separator (or actually "Internal Field Separator"). That code will effectively empty out the line separator for your read command and set it to its default.
The IFS variable is used in shells (Bourne, POSIX, ksh, bash) as the input field separator (or internal field separator). Essentially, it is a string of special characters which are to be treated as delimiters between words/fields when splitting a line of input. The default value of IFS is space, tab, newline.
It's important to realize why the following fails:
$ IFS=, echo_n foo,bar,baz
Num args: 1, Args: foo,bar,baz
The pre-command assignment to IFS
only applies inside echo_n
; foo,bar,baz
is not split on ,
because any word-splitting on this command line (or lack thereof) takes place before echo_n
runs.
(IFS=,; for i in foo,bar,baz ; do echo_n $i; done)
results in a single iteration because IFS
is only used to split the results of expansions (and by read
, see below), not literal strings. The word-splitting done by the shell when it is first parsing a command line is effectively hard-coded to only split on whitespace.
It's not entirely clear what you want to accomplish, but a good rule of thumb is that if you are setting the value of IFS
globally, you are doing something wrong (or at least suboptimally). There are only two situations where I can recall usefully modifying IFS
:
IFS=, read -r a b c
to split a line containing commas into multiple (here, 3) pieces. The change to IFS
is local to read
; whatever string it reads is read intact, and only split internally by read
.
foo=$(IFS=.; echo "${foo[*]}")
to join the elements of an array into a single string with a .
as the delimiter. Note this is a global change to IFS
, but only in a global scope that disappears after the command substitution completes.
Related to your for
loop examples, anytime you want to iterate over something other than a hard-coded list (which includes the expansion of an array), you probably want to use a while
loop with read
instead of a for
loop, as per Bash FAQ 001.
Take your for
loop here, for example:
(IFS=,; for i in $(echo foo,bar,baz) ; do echo_n $i; done)
I would instead split it into an array first, then iterate with for
:
data="foo,bar,baz"
IFS=, read -r -a items <<< "$data"
for i in "${data[@]}"; do
echo_n "$i"
done
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With