Shell – Split zsh array from subshell by linebreak

arrayshell-scriptzsh

I would like to instantiate a zsh array from a subshell

myarray=($(somecommand)

and I check if I received what I wanted with

for element in $myarray ; do echo "===" ; echo $element ; echo "---" ; done

By default I see that whitespaces (spaces and line breaks) are used to separate elements.

I also found that I can use ${(s:-:)"$(somecommand)"} to separate by -, in which case spaces and line breaks do not break elements (i.e. array elements can contain line breaks).

So far I fail to split at line breaks only. I.e. if somecommand returns

Alice
Bob
Arthur C Clarke
Neo
Thomas Anderson

I want my above for loop to print:

===
Alice
---
===
Bob
---
===
Arthur C Clarke
---
===
Neo
---
===
Thomas Anderson
---

How do I achieve that? (And possibly pointers where to look that up in the manual.)

Best Answer

The native splitting operator (beside the Bourne-like $IFS word splitting which is done on unquoted command substitutions) is with the s parameter expansion flag:

array=(${(ps:\n:)"$(cmd)"})

would split the output of cmd on newline, discarding the empty elements (the empty lines).

p is to enable those \x expansions. As ps:\n: is a common one, it's got a shorter alias: f (to split on line feeds):

array=(${(f)"$(cmd)"})

To preserve the empty lines, you'd do:

array=("${(f@)$(cmd)"})

Now, beware that like in most other shells, command substitution strips all the trailing newline characters, so all trailing empty lines. To preserve those, you can do:

array=("${(f@)$(cmd; echo .)}")
array[-1]=()  # remove that last line added by echo .

With $IFS word splitting:

IFS=$'\n'
array=($(cmd)) # removes empty lines. Note that contrary to other
               # Bourne-like shells, zsh doesn't do globbing there
               # so you don't need the "set -o noglob"

IFS=$'\n\n' # like in ksh93, doubling an IFS-whitespace character
            # removes its special treatment as a whitespace character
array=($(cmd)) # preserves empty lines except the trailing ones

IFS=$'\n\n'
array=($(cmd; echo .)); array[-1]=() # preserves all empty lines.

To avoid modifying $IFS globally, you can do the above in an anonymous function:

(){
  local IFS=$'\n\n'
  array=($(cmd; echo .)); array[-1]=()
}

Also beware that the $array expansion skips the empty elements. So if you wanted to loop over all the elements including the empty ones, you'd need:

for i ("$array[@]") ...

or

for i ("${(@)array}") ...

not

for i ($array) ...
for i in $array