66

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?

Amelio Vazquez-Reina
  • 40,169
  • 77
  • 197
  • 294

3 Answers3

104

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

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
  • 9
    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](http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags) – flying sheep Aug 17 '15 at 11:11
  • 7
    @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. – Stéphane Chazelas Aug 13 '19 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 '22 at 13:31
  • 1
    @MarkReed That's a zsh thing: in [IFS](https://zsh.sourceforge.io/Doc/Release/Parameters.html#index-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. – Gilles 'SO- stop being evil' Aug 15 '22 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 '22 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 :).

angus
  • 12,131
  • 3
  • 44
  • 40
-3

You can also use tr to replace newline with space:

lines=($(mycommand | tr '\n' ' '))
select line in ("${lines[@]}"); do
  echo "$line"
  break
done
John P
  • 115
  • 8
Jimchao
  • 95
  • 3
    what if the lines contain spaces ? – don_crissti Oct 21 '16 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. – Stéphane Chazelas Aug 13 '19 at 12:29
  • Were my edits reasonable? I couldn't get it to work as it was, – John P Aug 13 '19 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 '19 at 18:25