99

If inner.sh is

#...
echo first
echo second
echo third

And outer.sh is

var=`./inner.sh`
# only wants to use "first"...  

How can var be split by whitespace?

djechlin
  • 59,258
  • 35
  • 162
  • 290

3 Answers3

148

Try this:

var=($(./inner.sh))

# And then test the array with:

echo ${var[0]}
echo ${var[1]}
echo ${var[2]}

Output:

first
second
third

Explanation:

  • You can make an array in bash by doing var=(first second third), for example.
  • $(./inner.sh) runs the inner.sh script, which prints out first, second, and third on separate lines. Since we don't didn't put double quotes around $(...), they get lumped onto the same line, but separated by spaces, so you end up with what it looks like in the previous bullet point.
sampson-chen
  • 45,805
  • 12
  • 84
  • 81
  • 4
    check out the last answer, this method was good for the question, but if you need something more (like keeping the separators in tact, calling a function automatically after each line is read, etc) then mapfile by far is more powerful + it's super efficient, ive tested this with many programs and i've never had to wait long even for files that get to be in 100,000's of lines. Usually it's instant assuming you are using local storage :) – osirisgothra Jun 20 '14 at 15:29
  • 24
    This answer actually split **words** not **lines** into array elements. Technically it answer the question asked, but not the more general case. Test case : `var=( $( echo "1 2 3" ; echo "4 5 6" ) ) ; echo ${var[0]}` I expected "1 2 3", it yields "1". Depends on your use case. `mapfile` does what I need. – Stéphane Gourichon Feb 10 '16 at 18:03
  • @sampson-chen how do I reference a range subset of elements? var[0:2] doesn't work – Tom Oct 27 '16 at 00:53
  • 4
    as @StéphaneGourichon pointed out, this splits words not lines, so this does not work with empty lines, they are simply ignored which effectively screws up the order of lines in the array – ssc May 25 '17 at 11:07
  • Didn't work for me. I had to prefix the `var=( ...` command with `IFS=$'\n' ` and in the next line unset the internal field separator of bash with `unset IFS`. – miu Mar 08 '22 at 14:19
58

Don't forget the builtin mapfile. It's definitely the most efficient in your case: If you want to slurp the whole file in an array, the fields of which will be the lines output by ./inner.sh, do

mapfile -t array < <(./inner.sh)

Then you'll have the first row in ${array[0]}, etc...

For more info on this builtin and all the possible options:

help mapfile

If you just need the first line in a variable called firstline, do

read -r firstline < <(./inner.sh)

These are definitely the most efficient ways!

This small benchmark will prove it:

$ time mapfile -t array < <(for i in {0..100000}; do echo "fdjsakfjds$i"; done)

real    0m1.514s
user    0m1.492s
sys 0m0.560s
$ time array=( $(for i in {0..100000}; do echo "fdjsakfjds$i"; done) )

real    0m2.045s
user    0m1.844s
sys 0m0.308s

If you only want the first word (with space delimiter) of the first line output by ./inner.sh, the most efficient way is

read firstword _ < <(./inner.sh)
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • 4
    imo: the best answer here, mapfile by far is faster than the other methods, i was working with output lines > 14,000 entries, mapfile worked almost instantaneously rather than using an external program, i use the rule of thumb to never involve other programs to do the work that the language is supposed to do for itself, and bash is no exception, that rule goes double when working with variables. – osirisgothra Jun 20 '14 at 15:25
  • 4
    ***WTF*** is mapfile?_ `brew info mapfile || echo "found $(brew desc -s mapfile | wc -l) references to mapfile"` -> **`Error: No available formula for mapfile` `found 0 references to mapfile`** let me go on... `sh-3.2$ mapfile` -> **`sh: mapfile: command not found`** – Alex Gray Jun 23 '15 at 20:28
  • 5
    @alexgray `mapfile` is a Bash≥4 builtin. Not available in POSIX shells or Bash<4. Seems you're running 3.2; you're screwed. Unless you install a more recent version of Bash. – gniourf_gniourf Jun 23 '15 at 22:46
  • 4
    This answer indeed splits **lines** into an array. Test case: `mapfile -t var < <( echo "1 2 3" ; echo "4 5 6" ) ; echo ${var[1]}` correctly yields `4 5 6`. – Stéphane Gourichon Feb 10 '16 at 18:05
  • `GNU bash, version 4.4.12(1)-release (x86_64-apple-darwin15.6.0)`: `-bash: mapfile: command not found`; apparently, the built-in is [also called `readarray`](http://wiki.bash-hackers.org/commands/builtin/mapfile), however: `-bash: readarray: command not found`. Both command variants seem to be a Linux-only thing, they do exist on `GNU bash, version 4.4.7(1)-release (x86_64-pc-linux-gnu)`. So much for cross platform. – ssc May 25 '17 at 11:04
  • @ssc: I doubt they removed `mapfile` from 4.4.12 on OS X. How are you calling `mapfile`? are you really calling it with 4.4.12? When calling it, try `echo "$BASH_VERSION"; mapfile ...` to output the version prior to calling `mapfile`. – gniourf_gniourf May 25 '17 at 12:23
  • I'm calling it on both OSs in the same way; results as shown. I don't see how `echo`ing the bash version would change anything. To relieve your doubt, I suggest you log into OS X and try for yourself. – ssc May 26 '17 at 06:44
  • 1
    @ssc That's to be sure that you're using the correct version of Bash when calling `mapfile` — just because it's a common source of error on OS X. And I have no idea why your error is prefixed with `-bash`. Sorry, I don't have OS X available. Moreover, there are lots of people using `mapfile` on OS X around here, so I suspect that you're the one doing something wrong. I was just trying to help you, starting with the basic, suggesting that you double check the version number. Now I'm sorry I can't be of any further help. – gniourf_gniourf May 26 '17 at 07:17
  • For the edification of anyone curious, you can also use a here-string instead of process substitution if you feed it a newline-delimited (or whatever your IFS is) string. In this case it would look like this: `mapfile -t my_array <<< "$my_string"`. – boweeb Sep 04 '19 at 13:25
  • (Correction for my previous comment: `mapfile` _does not use_ `IFS`. It uses newlines by default. To customize the delimiter use the `-d` argument.) – boweeb Sep 04 '19 at 13:39
  • 1
    For people wondering why commands like read, readarray/mapfile... bring along a process substitution <(...) instead of a straightforward pipe, it's well explained here [Can't pipe in bash's “mapfile” … but why?](https://superuser.com/questions/1348948/cant-pipe-in-bashs-mapfile-but-why/1348950) – luciole75w Nov 19 '19 at 03:30
  • @boweeb The result array is not the same when the feeding string is empty : no items with a process substitution vs. 1 empty item with a here-string. – luciole75w Nov 19 '19 at 04:00
  • @luciole75w gtk -- good catch! – boweeb Nov 19 '19 at 20:38
  • It's worth mentioning that the `read` builtin will return a non-zero exit status upon encountering an EOF, so if you have set `-e` somewhere in your shell script as the [_Bash Strict Mode_](http://redsymbol.net/articles/unofficial-bash-strict-mode/) document suggests, you better mask the exit code of `read`, e. g.: `read -a myarray -d '' -r <<< "${raw_content}" || true`. – Bass Aug 24 '22 at 15:46
10

You're not splitting on whitespaces but rather on newlines. Give this a whirl:

IFS=$'
'

var=$(./echo.sh)
echo $var | awk '{ print $1 }'
unset IFS
Arnestig
  • 2,285
  • 18
  • 30
  • how do I then access the elements of `var`? – djechlin Dec 11 '12 at 16:23
  • `newvar = $(echo $var | awk '{ print $1 }')` and access $newvar – Arnestig Dec 11 '12 at 21:56
  • 1
    thanks, I looked for this and found your nice snippet. You can also write IFS=$'\n' for better readability, and put the two commands on one line without ; between them like: "IFS=$'\n' var=$(./echo.sh)" so you do not need to unset IFS afterwards – splaisan Sep 15 '20 at 13:16