如何从bash中的变量逐行读取?


48

我有一个变量,其中包含命令的多行输出。从变量逐行读取输出的最有效方法是什么?

例如:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi

Answers:


64

您可以使用while循环进行进程替换:

while read -r line
do
    echo "$line"
done < <(jobs)

读取多行变量的最佳方法是设置一个空白IFS变量,并printf在该变量后面加上换行符:

# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
   echo "$line"
done < <(printf '%s\n' "$var")

注意:根据shellcheck sc2031的规定,使用进程替代比使用管道更可取,以避免[巧妙地]创建子shell。

另外,请注意,通过命名变量jobs可能会引起混淆,因为这也是常见的shell命令的名称。


3
如果要保留所有空格,请使用while IFS= read...。如果要防止\解释,请使用read -r
Peter.O 2011年

我已经修复了fred.bear提到的问题,并将其更改echoprintf %s,以便您的脚本即使在非输入输入的情况下也可以使用。
吉尔(Gilles)“所以,别再邪恶了”

要从多行变量中读取,此处的字符串比从printf进行管道传递更可取(请参见l0b0的答案)。
ata

1
@ata尽管我经常听到这个“可取的”信息,但必须注意,herestring始终要求/tmp目录是可写的,因为它依赖于能够创建临时工作文件。如果您发现自己在/tmp只读的受限系统上(并且不能被自己更改),则对使用替代解决方案(例如printf管道)的可能性感到满意。
语法错误

1
在第二个示例中,如果多行变量不包含尾随换行符,则将丢失最后一个元素。更改为:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett 2015年

26

要逐行处理命令行的输出(说明):

jobs |
while IFS= read -r line; do
  process "$line"
done

如果数据已经在变量中:

printf %s "$foo" | 

printf %s "$foo"与几乎相同echo "$foo",但按$foo字面意义打印,而如果以开头,则echo "$foo"可能会解释$foo为echo命令的选项-,并且可能会$foo在某些shell中扩展反斜杠序列。

请注意,在某些shell(ash,bash,pdksh,但不是ksh或zsh)中,管道的右侧在单独的进程中运行,因此您在循环中设置的任何变量都会丢失。例如,以下行计数脚本在这些shell中输出0:

n=0
printf %s "$foo" |
while IFS= read -r line; do
  n=$(($n + 1))
done
echo $n

一种解决方法是将脚本的其余部分(或至少需要$n循环值的部分)放入命令列表中:

n=0
printf %s "$foo" | {
  while IFS= read -r line; do
    n=$(($n + 1))
  done
  echo $n
}

如果对非空行执行的操作足够好并且输入量不是很大,则可以使用分词:

IFS='
'
set -f
for line in $(jobs); do
  # process line
done
set +f
unset IFS

说明:设置IFS为单个换行符会使单词拆分仅在换行符处发生(与默认设置下的任何空白字符相对)。set -f关闭glob切换(即通配符扩展),否则会在命令替换$(jobs)或变量substitutino 的结果下发生$foo。该for环作用于的各个部分$(jobs),它们都是在命令输出非空行。最后,还原全局和IFS设置。


我在设置IFS和取消设置IFS时遇到了麻烦。我认为正确的做法是存储IFS的旧值,然后将IFS设置回该旧值。我不是bash专家,但是根据我的经验,这会让您重回原来的行为。
Bjorn Roche

1
@BjornRoche:在函数内,使用local IFS=something。它不会影响全局范围的值。IIRC,unset IFS不会让您恢复默认值(如果事先不是默认值,则肯定无法正常工作)。
彼得·科德斯

14

问题:如果使用while循环,它将在subshel​​l中运行,并且所有变量都将丢失。解决方案:用于循环

# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'

for line in $variableWithSeveralLines; do
 echo "$line"

 # return IFS back if you need to split new line by spaces:
 IFS=$IFS_BAK
 IFS_BAK=
 lineConvertedToArraySplittedBySpaces=( $line )
 echo "{lineConvertedToArraySplittedBySpaces[0]}"
 # return IFS back to newline for "for" loop
 IFS_BAK=$IFS
 IFS=$'\n'

done 

# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=

非常感谢!!以上所有解决方案对我而言都是失败的。
hax0r_n_code 2013年

while readbash循环到循环中意味着while循环在子shell中,因此变量不是全局的。 while read;do ;done <<< "$var"使循环体不是子壳。(最近的bash可以选择cmd | while不像ksh一样将循环的主体放在子shell中。)
Peter Cordes

另请参阅此相关文章
通配符

10

在最新的bash版本中,使用mapfilereadarray将命令输出有效地读取到数组中

$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305

免责声明:可怕的例子,但是您可以比ls自己想出更好的命令来使用


这是一个不错的方法,但是/ var / tmp在我的系统上带有临时文件。无论如何+1
Eugene Yarmash 2011年

@eugene:这很有趣。它在什么系统(发行版/ OS)上?
sehe 2011年

它是FreeBSD8。如何复制:放入readarray一个函数并多次调用该函数。
尤金·雅玛什

不错,@ sehe。+1
Teresa e Junior

7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

参考文献:


3
-r这也是一个好主意;它可以防止\` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS =`(这对于防止丢失空白至关重要)
Peter.O 2011年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.