159

I need to read the output of a command in my script into an array. The command is, for example:

ps aux | grep | grep | x 

and it gives the output line by line like this:

10
20
30

I need to read the values from the command output into an array, and then I will do some work if the size of the array is less than three.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
barp
  • 6,489
  • 9
  • 30
  • 37
  • 5
    Hey @barp, ANSWER YOUR QUESTIONS, lest your type be a drain on the entire community. – James Mar 24 '14 at 18:59
  • 11
    @James the issue's not with the fact that he's not answering his question... this is a Q/A site. He just didn't *mark* them as answered. He should mark them. Hint. @ barp – DDPWNAGE Oct 04 '15 at 06:31
  • 5
    Please @barp, mark the question as answered. – smonff Mar 23 '16 at 13:38
  • Related: [Looping through the content of a file in Bash](https://stackoverflow.com/a/41646525/6862601) since reading the output of a command through process substitution is similar to reading from a file. – codeforester Jul 25 '18 at 20:06

4 Answers4

231

The other answers will break if output of command contains spaces (which is rather frequent) or glob characters like *, ?, [...].

To get the output of a command in an array, with one line per element, there are essentially 3 ways:

  1. With Bash≥4 use mapfile—it's the most efficient:

    mapfile -t my_array < <( my_command )
    
  2. Otherwise, a loop reading the output (slower, but safe):

    my_array=()
    while IFS= read -r line; do
        my_array+=( "$line" )
    done < <( my_command )
    
  3. As suggested by Charles Duffy in the comments (thanks!), the following might perform better than the loop method in number 2:

    IFS=$'\n' read -r -d '' -a my_array < <( my_command && printf '\0' )
    

    Please make sure you use exactly this form, i.e., make sure you have the following:

    • IFS=$'\n' on the same line as the read statement: this will only set the environment variable IFS for the read statement only. So it won't affect the rest of your script at all. The purpose of this variable is to tell read to break the stream at the EOL character \n.
    • -r: this is important. It tells read to not interpret the backslashes as escape sequences.
    • -d '': please note the space between the -d option and its argument ''. If you don't leave a space here, the '' will never be seen, as it will disappear in the quote removal step when Bash parses the statement. This tells read to stop reading at the nil byte. Some people write it as -d $'\0', but it is not really necessary. -d '' is better.
    • -a my_array tells read to populate the array my_array while reading the stream.
    • You must use the printf '\0' statement after my_command, so that read returns 0; it's actually not a big deal if you don't (you'll just get an return code 1, which is okay if you don't use set -e – which you shouldn't anyway), but just bear that in mind. It's cleaner and more semantically correct. Note that this is different from printf '', which doesn't output anything. printf '\0' prints a null byte, needed by read to happily stop reading there (remember the -d '' option?).

If you can, i.e., if you're sure your code will run on Bash≥4, use the first method. And you can see it's shorter too.

If you want to use read, the loop (method 2) might have an advantage over method 3 if you want to do some processing as the lines are read: you have direct access to it (via the $line variable in the example I gave), and you also have access to the lines already read (via the array ${my_array[@]} in the example I gave).

Note that mapfile provides a way to have a callback eval'd on each line read, and in fact you can even tell it to only call this callback every N lines read; have a look at help mapfile and the options -C and -c therein. (My opinion about this is that it's a little bit clunky, but can be used sometimes if you only have simple things to do — I don't really understand why this was even implemented in the first place!).


Now I'm going to tell you why the following method:

my_array=( $( my_command) )

is broken when there are spaces:

$ # I'm using this command to test:
$ echo "one two"; echo "three four"
one two
three four
$ # Now I'm going to use the broken method:
$ my_array=( $( echo "one two"; echo "three four" ) )
$ declare -p my_array
declare -a my_array='([0]="one" [1]="two" [2]="three" [3]="four")'
$ # As you can see, the fields are not the lines
$
$ # Now look at the correct method:
$ mapfile -t my_array < <(echo "one two"; echo "three four")
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # Good!

Then some people will then recommend using IFS=$'\n' to fix it:

$ IFS=$'\n'
$ my_array=( $(echo "one two"; echo "three four") )
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # It works!

But now let's use another command, with globs:

$ echo "* one two"; echo "[three four]"
* one two
[three four]
$ IFS=$'\n'
$ my_array=( $(echo "* one two"; echo "[three four]") )
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="t")'
$ # What?

That's because I have a file called t in the current directory… and this filename is matched by the glob [three four]… at this point some people would recommend using set -f to disable globbing: but look at it: you have to change IFS and use set -f to be able to fix a broken technique (and you're not even fixing it really)! when doing that we're really fighting against the shell, not working with the shell.

$ mapfile -t my_array < <( echo "* one two"; echo "[three four]")
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="[three four]")'

here we're working with the shell!

that other guy
  • 116,971
  • 11
  • 170
  • 194
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • 5
    This is great, I never heard about `mapfile` before, it's exactly what I've been missing for years. I guess recent versions of `bash` has so many nice new features, I should just spend a few days reading the docs and writing down a nice cheatsheet. – Gene Pavlovsky Apr 19 '16 at 20:36
  • 9
    Btw, to use this syntax `< <(command)` in shell scripts, the shebang line should be `#!/bin/bash` - if run as `#!/bin/sh`, bash will exit with a syntax error. – Gene Pavlovsky Apr 19 '16 at 20:37
  • 1
    Another side note, if you expect your `<( ... )` enclosed command might fail with an error, the error code is not propagated outside of the line. E.g. `mapfile -t my_array < <(false) || echo 'failed' >&2` would not echo `failed`, the place to add error checking then should be inside: `mapfile -t my_array < <(false || echo 'failed' >&2)` outputs `failed`. I wish there was an option to propagate the error code outside. `set -e -o pipefail` doesn't help. – Gene Pavlovsky Apr 19 '16 at 20:44
  • 1
    Expanding on @GenePavlovsky's helpful note, the script must also be run with the bash command `bash my_script.sh` and not the sh command `sh my_script.sh` – Vito Jan 16 '17 at 15:46
  • 2
    @Vito: indeed, this answer is only for Bash, but this shouldn't be a problem, as strictly-compliant POSIX shells don't even implement arrays (`sh` and `dash` don't know about arrays at all, except, of course, for the positional parameters `$@` array). – gniourf_gniourf Jan 16 '17 at 15:53
  • `bash -v` tells you the version. OS X, sorry, MacOS is still on 3.2 (feb 2019) – CousinCocaine Feb 11 '19 at 13:29
  • 4
    As another alternative that doesn't require bash 4.0, consider `IFS=$'\n' read -r -d '' -a my_array < <(my_command && printf '\0')` -- it both works correctly in bash 3.x, and also passes through a failed exit status from `my_command` to the `read`. – Charles Duffy Mar 18 '19 at 22:53
  • 1
    @GenePavlovsky - You've never heard of `mapfile` since it's a synonym for the more descriptive `readarray`. – DocSalvager Apr 27 '19 at 20:29
  • 2
    For whatever reason, running this from the terminal works with `source`, but from a cron job, invoking only with `bash` works. When I `source` from a cron job, it says: syntax error near unexpected token `<'. Any idea why? – CodingInCircles Nov 15 '19 at 15:42
  • @CodingInCircles: not easy to answer with so little information. You can ask a new question where you can add more information and context. – gniourf_gniourf Nov 15 '19 at 20:44
  • As I see `mapfile -t my_array < <( my_command )` has exit status 0 if `my_command` fails. – pmor Nov 11 '22 at 15:04
  • For whatever reason I could not get the one liner with `read` to work with Bash 3.2 on MacOS, however the while loop works great. – Paul Wheeler Mar 21 '23 at 08:37
108

You can use

my_array=( $(<command>) )

to store the output of command <command> into the array my_array.

You can access the length of that array using

my_array_length=${#my_array[@]}

Now the length is stored in my_array_length.

  • 25
    What if the output of $(command) has spaces and multiple lines with spaces? I added "$(command)" and it places all output from all lines into the first [0] element of the array. – ikwyl6 Jun 30 '16 at 00:44
  • 3
    @ikwyl6 a workaround is assigning the command output to a variable and then making an array with it or adding it to an array. `VAR="$()"` and then `my_array=("$VAR")` or `my_array+=("$VAR")` – Vito Jan 16 '17 at 16:59
  • @Vito `VAR="$(COMMAND)"` doesn't need double quotes cuz variables made from a command substitution output already double-quotes the output implicitly, making it easier to double-quote things inside `COMMAND`. – rautamiekka Jan 22 '21 at 16:08
  • 2
    @ikwyl6 If the input contains newlines, then do what the other answer suggested and change `IFS` to reflect that. `IFS=$'\n' my_array=( $() )`. This worked for me – smac89 Mar 25 '21 at 04:10
  • 1
    What if you have a line containing only a `*`? You'll get a list of filenames in the current directory in your array. – Charles Duffy Apr 02 '21 at 16:10
22

Here is a simple example. Imagine that you are going to put the files and directory names (under the current folder) to an array and count them. The script would be like;

my_array=( `ls` )
my_array_length=${#my_array[@]}
echo $my_array_length

Or, you can iterate over this array by adding the following script:

for element in "${my_array[@]}"
do
   echo "${element}"
done

Please note that this is the core concept and the input must be sanitized before the processing, i.e. removing extra characters, handling empty Strings, and etc. (which is out of the topic of this thread).

Youness
  • 1,920
  • 22
  • 28
-1

It helps me all the time suppose you want to copy whole list of directories into current directory into an array

bucketlist=($(ls))
#then print them one by one
for bucket in "${bucketlist[@]}"; do
echo " here is bucket: ${bucket}"
done
Usman Ali Maan
  • 368
  • 2
  • 12