Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to control IFS word splitting in bash

Tags:

bash

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?

like image 686
AlexBaretta Avatar asked Jan 23 '17 18:01

AlexBaretta


People also ask

What is word splitting bash?

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.

What is IFS readline?

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.

What is default value of IFS in bash?

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.


1 Answers

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:

  1. 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.

  2. 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
like image 147
chepner Avatar answered Sep 27 '22 17:09

chepner