如何循环遍历 shell 命令的输出?

82
我希望编写一个脚本,可以循环遍历 shell 命令 ps 的输出(可能是数组?)。
以下是该命令的示例和输出:
$ 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

将其转换为bash脚本(片段):

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

但是输出结果变成了:

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

如何在Bash脚本中像在shell输出中那样循环遍历每一行?

4个回答

140

除非你正在将内部字段分隔符$IFS的值更改为\n,否则永远不要循环遍历shell命令的结果,如果您想逐行处理它。这是因为这些行将受到单词拆分的影响,从而导致您看到的实际结果。这意味着,例如,如果您有一个像这样的文件:

foo bar
hello world

下面的for循环

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

为您提供:

foo
bar
hello
world

即使使用 IFS='\n',行仍可能成为文件名扩展的主题。


我建议使用while+read,因为read会逐行读取。

此外,如果你要搜索属于某个二进制文件的pid,我建议使用pgrep。然而,由于python可能出现不同的二进制文件,比如python2.7python3.4,我建议向pgrep传递-f,这样它就会搜索整个命令行,而不仅仅是搜索名为python的二进制文件。但是这也会找到像cat foo.py这样启动的进程。你已经被警告了!最后,你可以根据自己的需求细化传递给pgrep的正则表达式。

例如:

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

或者如果您还想要进程名称:

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

如果你想要将进程名称和pid分别存储在不同的变量中:

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

你看,read提供了一种灵活且稳定的逐行处理命令输出的方式。


顺便说一句,如果你更喜欢使用ps .. | grep命令而不是pgrep,请使用以下循环:

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

注意我如何更改etimecmd的顺序。 因此可以将可能包含空格的cmd读入单个变量中。 这可以工作是因为read将把行分解为变量,正如您指定变量的次数一样。 行的剩余部分 - 可能包括空格 - 将被分配给在命令行中指定的最后一个变量。

我猜你是指“pgrep”而不是“prep”? - adic26
哦,当然可以……顺便说一下,如果你需要 etime,请在循环中使用 ps -p "$pid" -o etime。当然,你也可以使用 ps .. | grep 命令行,但仍需将其管道传递给 while read ... - hek2mgl
即使您设置了 IFS,命令替换的扩展仍然受到路径名扩展的影响,因此 for 循环是错误的。 - chepner
3
cmd | while read 这个方法有几个可能的问题,具体取决于循环内部的内容:while 循环在子 shell 中运行,因此它设置的变量等不会在循环结束后继续存在;如果循环中有任何内容从 stdin 读取,则会消耗命令输出。如果出现问题,可以改用 while read -u3 -r ... done 3< <(cmd)。但是 <( ... ) 结构并不适用于所有 shell,请确保使用 bash-only shebang(#!/bin/bash,而非 #!/bin/sh)启动脚本。 - Gordon Davisson
@GordonDavisson 当然,如果 while 循环内的代码应在当前 shell 的范围内运行,我们需要使用进程替换而不是管道传递到 while read。如果 while 循环内的命令从 stdin 读取,则建议将 < /dev/tty 重定向到该命令中(如果适用)。但您的建议也是一个好点!最终这取决于具体情况。我不想过多扩展答案,而且我通常认为 for 循环存在更多问题。 - hek2mgl
1
即使在POSIX中,使用命名管道也可以克服这个问题,而进程替换仅提供了一种方便的语法来进行管理。 - chepner

13

我发现你可以这样做,只需使用双引号:

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

这将把每一行保存到数组中,而不是每个项目.


这将把命令的整个输出放入数组的单个元素中。 - Etan Reisner
@EtanReisner 谢谢你的发现。 - jkdba
2
@Roland 这里有一个很棒的答案,介绍了<<<<<<之间的区别。链接 - jkdba
这在Ubuntu中给我一个重定向错误。 - Sam
@San,我无法在Ubuntu中使用上述语法复制您的错误。 - jkdba
显示剩余3条评论

9
当在bash中使用for循环时,默认情况下会将给定的列表按空格分割,可以通过使用所谓的内部字段分隔符IFS来进行调整。

IFS是用于扩展后的单词拆分和使用read内置命令将行拆分为单词的内部字段分隔符。默认值为“”。

对于您的示例,我们需要告诉IFS使用换行符作为断点。
IFS=$'\n'

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

这个例子在我的机器上返回以下输出。

  668 /usr/bin/python /usr/bin/ud    03:05:54
27892 python                            00:01

命令替换仍然受到路径名扩展的影响。正确做法是使用while循环。 - chepner

0

这里是受到@Gordon Davisson评论启发的另一个基于Bash的解决方案。

我们需要(至少bash v1.13.5(1992年)或更高版本),因为使用了进程替换2,3,4while read var; do { ... }; done <<(...);等。

#!/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;

注意:您可以在<(...)中使用任何简单或复杂的命令/命令集,这些命令可能具有多个输出行。
而每个代码的功能是如何实现的,请参见此处

以下是一种单行方式:
while IFS= read -a oL ; do { echo "${oL}"; }; done < <(ps -ewo pid,cmd,etime | grep python | grep -v grep | grep -v sh); unset oL;

(由于进程替换尚未成为POSIX的一部分,因此在许多符合POSIX标准的shell或bash-shell的POSIX shell模式中不支持它。自1992年以来,bash中存在进程替换(即从现在/2020年算起已有28年),并且在ksh86(1985年之前)中也存在1。因此,POSIX应该将其包含在内。)
如果您或任何用户想要在符合POSIX标准的shell(即:sh、ash、dash、pdksh/mksh等)中使用类似进程替换的东西,请查看NamedPipes


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接