68

I thought the following would group the output of my_command in an array of lines:

IFS='\n' array_of_lines=$(my_command);

so that $array_of_lines[1] would refer to the first line in the output of my_command, $array_of_lines[2] to the second, and so forth.

However, the command above doesn't seem to work well. It seems to also split the output of my_command around the character n, as I have checked with print -l $array_of_lines, which I believe prints elements of an array line by line. I have also checked this with:

echo $array_of_lines[1]
echo $array_of_lines[2]
...

In a second attempt, I thought adding eval could help:

IFS='\n' array_of_lines=$(eval my_command);

but I got the exact same result as without it.

Finally, following the answer on List elements with spaces in zsh, I have also tried using parameter expansion flags instead of IFS to tell zsh how to split the input and collect the elements into an array, i.e.:

array_of_lines=("${(@f)$(my_command)}");

But I still got the same result (splitting happening on n)

With this, I have the following questions:

Q1. What are "the proper" ways of collecting the output of a command in an array of lines?

Q2. How can I specify IFS to split on newlines only?

Q3. If I use parameter expansion flags as in my third attempt above (i.e. using @f) to specify the splitting, does zsh ignore the value of IFS? Why didn't it work above?

0

3 Answers 3

107

TL, DR:

array_of_lines=("${(@f)$(my_command)}")

First mistake (→ Q2): IFS='\n' sets IFS to the two characters \ and n. To set IFS to a newline, use IFS=$'\n'.

Second mistake: to set a variable to an array value, you need parentheses around the elements: array_of_lines=(foo bar).

This would work, except that it strips empty lines, because consecutive whitespace counts as a single separator:

IFS=$'\n' array_of_lines=($(my_command))

You can retain the empty lines except at the very end by doubling the whitespace character in IFS:

IFS=$'\n\n' array_of_lines=($(my_command))

To keep trailing empty lines as well, you'd have to add something to the command's output, because this happens in the command substitution itself, not from parsing it.

IFS=$'\n\n' array_of_lines=($(my_command; echo .)); unset 'array_of_lines[-1]'

(assuming the output of my_command doesn't end in a non-delimited line; also note that you lose the exit status of my_command)

Note that all the snippets above leave IFS with its non-default value, so they may mess up subsequent code. To keep the setting of IFS local, put the whole thing into a function where you declare IFS local (here also taking care of preserving the command's exit status):

collect_lines() {
  local IFS=$'\n\n' ret
  array_of_lines=($("$@"; ret=$?; echo .; exit $ret))
  ret=$?
  unset 'array_of_lines[-1]'
  return $ret
}
collect_lines my_command

But I recommend not to mess with IFS; instead, use the f expansion flag to split on newlines (→ Q1):

array_of_lines=("${(@f)$(my_command)}")

Or to preserve trailing empty lines:

array_of_lines=("${(@f)$(my_command; echo .)}")
unset 'array_of_lines[-1]'

The value of IFS doesn't matter there. I suspect that you used a command that splits on IFS to print $array_of_lines in your tests (→ Q3).

5
  • 10
    This is so complicated! "${(@f)...}" is the same as ${(f)"..."}, but in a different way. (@) inside double quotes means “yield one word per array element” and (f) means “split into an array by newline”. PS: Please link to the docs Aug 17, 2015 at 11:11
  • 8
    @flyingsheep, no ${(f)"..."} would skip empty lines, "${(@f)...}" preserves them. That's the same distinction between $argv and "$argv[@]". That "$@" thing to preserve all elements of an array comes from the Bourne shell in the late 70s. Aug 13, 2019 at 12:28
  • "You can retain the empty lines except at the very end by doubling the whitespace character in IFS:" I wouldn't expect that to work, since IFS is itself a set of characters, not a string. Is that a special case? I couldn't get it to work in my testing; double newlines in IFS still split on every line.
    – Mark Reed
    Aug 15, 2022 at 13:31
  • 1
    @MarkReed That's a zsh thing: in IFS, “If an IFS white space character appears twice consecutively in the IFS, this character is treated as if it were not an IFS white space character”. With IFS=$'\n', separators are sequences of newlines, so information about empty lines (consecutive newlines) disappears and the array only has the non-empty lines. With IFS=$'\n', splitting happens at each newline, so there is a separate array element for each line, empty or not, apart from trailing empty lines which $(…) removes. Aug 15, 2022 at 15:39
  • Ah, I misunderstood the mechanism and thought you were somehow splitting on double-newlines. Got it now, thanks!
    – Mark Reed
    Aug 15, 2022 at 16:05
7

Two issues: first, apparently double quotes also don't interpret backslash escapes (sorry about that :). Use $'...' quotes. And according to man zshparam, to collect words in an array you need to enclose them in parenthesis. So this works:

% touch 'a b' c d 'e f'
% IFS=$'\n' arr=($(ls)); print -l $arr
a b
c
d
e f
% print $arr[1]
a b

I can't answer your Q3. I hope I'll never have to know such esoteric things :).

-3

You can also use tr to replace newline with space:

lines=($(mycommand | tr '\n' ' '))
select line in ("${lines[@]}"); do
  echo "$line"
  break
done
4
  • 3
    what if the lines contain spaces ? Oct 21, 2016 at 16:51
  • 2
    That makes no sense. Both SPC and NL are in the default value of $IFS. Translating one to the other makes no difference. Aug 13, 2019 at 12:29
  • Were my edits reasonable? I couldn't get it to work as it was,
    – John P
    Aug 13, 2019 at 18:11
  • (timed out) I admit I edited it without really understanding what the intent was, but I think the translation is a good start for separation based on string manipulation. I wouldn't translate to spaces, and split on them, unless the expected behavior were more like echo, where inputs are all more or less heaps of words separated by who cares.
    – John P
    Aug 13, 2019 at 18:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .