Command substitution: splitting on newline but not space


set -f              # turn off globbing
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

Or using a subshell to make the IFS and option changes local:

( set -f; IFS='
'; exec cmd $(cat <file) )

The shell performs field splitting and filename generation on the result of a variable or command substitution that is not in double quotes. So you need to turn off filename generation with set -f, and configure field splitting with IFS to make only newlines separate fields.

There's not much to be gained with bash or ksh constructs. You can make IFS local to a function, but not set -f.

In bash or ksh93, you can store the fields in an array, if you need to pass them to multiple commands. You need to control expansion at the time you build the array. Then "${a[@]}" expands to the elements of the array, one per word.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"

You can do this with a temporary array.


$ cat input
$ cat
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Fill the array:

$ IFS=$'\n'; set -f; foo=($(<input))

Use the array:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./ "${foo[@]}"

Can't figure out a way of doing that without that temporary variable - unless the IFS change isn't important for cmd, in which case:

$ IFS=$'\n'; set -f; cmd $(<input) 

should do it.

Looks like the canonical way to do this in bash is something like

unset args
while IFS= read -r line; do 
done < file

cmd "${args[@]}"

or, if your version of bash has mapfile:

mapfile -t args < filename
cmd "${args[@]}"

The only difference I can find between the mapfile and the while-read loop versus the one-liner

(set -f; IFS=$'\n'; cmd $(<file))

is that the former will convert a blank line to an empty argument, while the one-liner will ignore a blank line. In this case the one-liner behavior is what I'd prefer anyway, so double bonus on it being compact.

I would use IFS=$'\n' cmd $(<file) but it doesn't work, because $(<file) is interpreted to form the command line before IFS=$'\n' takes effect.

Though it doesn't work in my case, I've now learned that a lot of tools support terminating lines with null (\000) instead of newline (\n) which does make a lot of this easier when dealing with, say, file names, which are common sources of these situations:

find / -name '*.config' -print0 | xargs -0 md5

feeds a list of fully-qualified file names as arguments to md5 without any globbing or interpolating or whatever. That leads to the non-built-in solution

tr "\n" "\000" <file | xargs -0 cmd

although this, too, ignores empty lines, though it does capture lines that have only whitespace.