87

I want to write a script that loops through the output (array possibly?) of a shell command, ps.

Here is the command and the output:

$ ps -ewo pid,cmd,etime | grep python | grep -v grep | grep -v sh
 3089 python /var/www/atm_securit       37:02
17116 python /var/www/atm_securit       00:01
17119 python /var/www/atm_securit       00:01
17122 python /var/www/atm_securit       00:01
17125 python /var/www/atm_securit       00:00

Convert it into bash script (snippet):

for tbl in $(ps -ewo pid,cmd,etime | grep python | grep -v grep | grep -v sh)
do
   echo $tbl
done

But the output becomes:

3089
python
/var/www/atm_securit
38:06
17438
python
/var/www/atm_securit
00:02
17448
python
/var/www/atm_securit
00:01

How do I loop through every row like in the shell output, but in a bash script?

4 Answers 4

146

Never for loop over the results of a shell command if you want to process it line by line unless you are changing the value of the internal field separator $IFS to \n. This is because the lines will get subject of word splitting which leads to the actual results you are seeing. Meaning if you for example have a file like this:

foo bar
hello world

The following for loop

for i in $(cat file); do
    echo "$i"
done

gives you:

foo
bar
hello
world

Even if you use IFS='\n' the lines might still get subject of Filename expansion


I recommend to use while + read instead because read reads line by line.

Furthermore I would use pgrep if you are searching for pids belonging to a certain binary. However, since python might appear as different binaries, like python2.7 or python3.4 I suggest to pass -f to pgrep which makes it search the whole command line rather than just searching for binaries called python. But this will also find processes which have been started like cat foo.py. You have been warned! At the end you can refine the regex passed to pgrep like you wish.

Example:

pgrep -f python | while read -r pid ; do
    echo "$pid"
done

or if you also want the process name:

pgrep -af python | while read -r line ; do
    echo "$line"
done

If you want the process name and the pid in separate variables:

pgrep -af python | while read -r pid cmd ; do
    echo "pid: $pid, cmd: $cmd"
done

You see, read offers a flexible and stable way to process the output of a command line-by-line.


Btw, if you prefer your ps .. | grep command line over pgrep use the following loop:

ps -ewo pid,etime,cmd | grep python | grep -v grep | grep -v sh \
  | while read -r pid etime cmd ; do
    echo "$pid $cmd $etime"
done

Note how I changed the order of etime and cmd. Thus to be able to read cmd, which can contain whitespace, into a single variable. This works because read will break down the line into variables, as many times as you specified variables. The remaining part of the line - possibly including whitespace - will get assigned to the last variable which has been specified in the command line.

6
  • I am assuming you meant 'pgrep' not 'prep' ?
    – adic26
    Commented Mar 10, 2016 at 21:56
  • Oh, sure....... Btw, if you need the etime, use ps -p "$pid" -o etime in the loop. Of course you can also use your ps .. | grep command line, but still pipe that to while read ... ...
    – hek2mgl
    Commented Mar 10, 2016 at 22:00
  • Even if you set IFS, the expansion of the command substitution is still subject to pathname expansion, so the for loop is just plain wrong.
    – chepner
    Commented Mar 10, 2016 at 23:06
  • 4
    There are a couple of possible problems with the cmd | while read approach, depending on what's inside the loop: the while loop runs in a subshell, so variables etc it sets don't live past the end of the loop, and if anything in the loop reads from stdin, it'll consume the command output. If these cause trouble, you can use while read -u3 -r ... done 3< <(cmd) instead. But the <( ... ) construct is not portable to all shells, so be sure to start the script with a bash-only shebang (#!/bin/bash, not #!/bin/sh) Commented Mar 10, 2016 at 23:58
  • 1
    @GordonDavisson Even in POSIX, the problem can be overcome with named pipes, for which process substitution merely provides a convenient syntax for managing.
    – chepner
    Commented Mar 11, 2016 at 2:21
15

I found you can do this just use double quotes:

while read -r proc; do
     #do work
done <<< "$(ps -ewo pid,cmd,etime | grep python | grep -v grep | grep -v sh)"

This will save each line to the array rather than each item.

8
  • This will put the entire output from the command in a single element of the array. Commented Mar 10, 2016 at 22:00
  • @EtanReisner Thank you for the catch.
    – jkdba
    Commented Mar 10, 2016 at 22:13
  • Why the <<< instead of just < ?
    – Roland
    Commented Jun 18, 2020 at 14:44
  • 2
    @Roland Here is an awesome answer on <<< vs << vs <
    – jkdba
    Commented Jun 19, 2020 at 16:07
  • This is giving me a redirection error in Ubuntu
    – Sanya
    Commented Feb 10, 2022 at 2:01
10

When using for loops in bash it splits the given list by default by whitespaces, this can be adapted by using the so called Internal Field Seperator, or IFS in short .

IFS The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is "".

For your example we would need to tell IFS to use new-lines as break point.

IFS=$'\n'

for tbl in $(ps -ewo pid,cmd,etime | grep python | grep -v grep | grep -v sh)
do
   echo $tbl
done

This example returns the following output on my machine.

  668 /usr/bin/python /usr/bin/ud    03:05:54
27892 python                            00:01
1
  • The command substitution is still subject to pathname expansion. Do it right, and use a while loop.
    – chepner
    Commented Mar 10, 2016 at 23:07
1

Here is another bash-based solution, inspired by comment of @Gordon Davisson.

For this we need (atleast bash v1.13.5 (1992) or later verison), because Process-Substitution2,3,4 while read var; do { ... }; done < <(...);, etc are used.

#!/bin/bash
while IFS= read -a oL ; do {  # reads single/one line
    echo "${oL}";  # prints that single/one line
};
done < <(ps -ewo pid,cmd,etime | grep python | grep -v grep | grep -v sh);
unset oL;

Note: You can use any simple or complex command/command-set inside the <(...) which may have multiple output lines.
And what code does what function is shown here.

And here is a single/one-liner way:
while IFS= read -a oL ; do { echo "${oL}"; }; done < <(ps -ewo pid,cmd,etime | grep python | grep -v grep | grep -v sh); unset oL;

( As Process-Substitution is not part of POSIX yet So its not supported in many POSIX compliant shell or in POSIX shell mode of bash-shell. Process-Substitution existed in bash since 1992 (so that is 28yrs ago from now/2020), & existed in ksh86 (before 1985)1. So POSIX should've included it. )
If you or any user wants to use something similar as Process-Substitution in POSIX compliant shell (i.e: sh, ash, dash, pdksh/mksh, etc), then look into NamedPipes.

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