52

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
84

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

2
  • 8
    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 – flying sheep Aug 17 '15 at 11:11
  • 3
    @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
5

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 ? – 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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.