将多行输出捕获到Bash变量中

686

我有一个名为'myscript'的脚本,它输出以下内容:

abc
def
ghi

在另一个脚本中,我调用:

declare RESULT=$(./myscript)

$RESULT 取得其值。

abc def ghi

是否有一种方法可以将结果存储为含有换行符的形式,或以'\n'字符的形式存储,以便我可以使用 'echo -e' 输出它?


2
这让我感到惊讶。你没有 $(cat ./myscipt) 吗?否则我本来以为它会尝试执行命令 abc、def 和 ghi。 - Johannes Schaub - litb
@litb:是的,我想是这样的;你也可以使用$(<./myscript),它避免了执行命令。 - Jonathan Leffler
3
(注:上面两个评论是针对问题的修订开始为“我有一个包含以下内容的脚本'myscript'”,导致了这些问题。目前问题的修订(“我有一个输出以下内容的'script'脚本”)使得这些评论变得无关紧要。然而,修订是在2011年11月11日之后进行的,远远晚于两个评论的时间。) - Jonathan Leffler
有关 $IFS 的信息,请参见 What is the exact meaning of IFS=$'\n' - Peyman Mohamadpour
7个回答

1290

实际上,RESULT 包含了你想要的内容 - 为了演示:

echo "$RESULT"

您所展示的内容来自于:

echo $RESULT
正如评论中所指出的那样,两者的区别在于:(1)使用双引号的方式(echo "$RESULT")会完全保留变量中的内部空格,包括换行符、制表符、多个空格等 —— 而(2)不使用引号(echo $RESULT)则会将一个或多个空格、制表符和换行符替换为单个空格。因此,(1)保留了输入变量的形状,而(2)则创建了一个可能非常长的单行输出,其中“单词”由单个空格分隔(“单词”是一个非空白字符序列,其中任何单词都不必包含字母数字字符)。

76
@troelskn说的区别在于,(1)双引号包裹的变量会完全保留变量内部值的间距,包括换行、制表符、多个空格等所有内容,而(2)不用引号包裹的变量会将每个由一个或多个空格、制表符和换行符组成的序列替换为单个空格。因此,(1)保留了输入变量的形状,而(2)则创建了一个可能非常长的单行输出,其中“单词”之间由单个空格分隔(其中“单词”是一系列非空白字符;任何一个单词中都不需要包含字母数字)。 - Jonathan Leffler
28
为了让答案更易理解:答案表明echo "$RESULT" 会保留换行符,而echo $RESULT则不会。 - Yu Shen
在某些情况下,这无法保留换行符和前导空格。 - CommaToast
4
你打算详细说明吗?尾随的换行符会丢失;没有容易解决这个问题的方法。至于前导空格,我不知道有什么情况会丢失它们。 - Jonathan Leffler
作为补充。根据官方BASH手册,当使用echo $RESULT时,会发生单词分割,而对于echo "$RESULT"则会发生引号移除。这正是导致这两者行为不同的确切原因。 - Michael Lee

101
另一个需要注意的问题是,命令替换$() — 会删除末尾的换行符。可能并不总是重要的,但如果你真的想要完全保留输出的内容,你将需要使用另一行和一些引用。
RESULTX="$(./myscript; echo x)"
RESULT="${RESULTX%x}"

如果您想处理所有可能的文件名(以避免发生未定义的行为,如对错误的文件进行操作),这一点尤其重要。

6
我曾经需要使用一个有缺陷的shell工作一段时间,它不能移除命令替换(不是进程替换)中的最后一个换行符,这导致几乎所有东西都崩溃了。例如,如果你执行pwd=`pwd`; ls $pwd/$file,你将得到一个在 / 字符之前的换行符,即使用双引号括起来也没有帮助。这个问题很快被修复了。这发生在1983-5年ICL Perq PNX上; 当时这个shell没有 $PWD 作为内置变量。 - Jonathan Leffler

29

如果你对特定的行感兴趣,可以使用结果数组:

declare RESULT=($(./myscript))  # (..) = array
echo "First line: ${RESULT[0]}"
echo "Second line: ${RESULT[1]}"
echo "N-th line: ${RESULT[N]}"

5
如果在行中有空格,则计数的是字段(即空格之间的内容)而不是行。 - Liam
4
你应该使用readarray和进程替换而不是命令替换:readarray -t RESULT < <(./myscript> - chepner

16

除了@l0b0给出的答案外,我曾遇到这样一种情况,需要保留脚本输出的任何尾随换行符并且检查脚本的返回代码。 而l0b0答案中的'echo x'会将$?重置为零,所以我想出了这个非常狡猾的解决方案:

RESULTX="$(./myscript; echo x$?)"
RETURNCODE=${RESULTX##*x}
RESULT="${RESULTX%x*}"

这太好了,我实际上有一个使用案例,我想要一个接受任意退出代码和可选消息的 die() 函数,并且我想使用另一个 usage() 函数来提供消息,但换行符一直被压缩,希望这可以让我解决这个问题。 - dragon788

7

解析多个输出

介绍

所以,你的 myscript 输出了 3 行,可能看起来像这样:

myscript() { echo $'abc\ndef\nghi'; }

或者
myscript() { local i; for i in abc def ghi ;do echo $i; done ;}

好的,这是一个函数而不是脚本(不需要路径./),但输出结果相同。

myscript
abc
def
ghi

考虑结果代码

为了检查结果代码,测试函数将变为:

myscript() { local i;for i in abc def ghi ;do echo $i;done;return $((RANDOM%128));}

1. 将多个输出存储在一个变量中,并显示换行符

您的操作是正确的:

RESULT=$(myscript)

关于结果代码,您可以添加:

RCODE=$?

即使在同一行:

RESULT=$(myscript) RCODE=$?

然后

echo $RESULT $RCODE

abc def ghi 66

echo "$RESULT"

abc
def
ghi

echo ${RESULT@Q}

$'abc\ndef\nghi'

printf '%q\n' "$RESULT"

$'abc\ndef\nghi'

如果要显示变量定义,请使用declare -p

declare -p RESULT RCODE

declare -- RESULT="abc
def
ghi"
declare -- RCODE="66"

2. 使用mapfile将多个输出解析为数组

将答案存储到myvar变量中:

mapfile -t myvar < <(myscript)
echo ${myvar[2]}

ghi

显示变量$myvar

declare -p myvar

declare -a myvar=([0]="abc" [1]="def" [2]="ghi")

考虑结果代码

如果您需要检查结果代码,可以执行以下操作:

RESULT=$(myscript) RCODE=$?
mapfile -t myvar <<<"$RESULT"

declare -p myvar RCODE

declare -a myvar=([0]="abc" [1]="def" [2]="ghi")
declare -- RCODE="40"

3. 连续使用read命令组解析多个输出结果

{ read firstline; read secondline; read thirdline;} < <(myscript)
echo $secondline
def

显示变量:

declare -p firstline secondline thirdline

declare -- firstline="abc"
declare -- secondline="def"
declare -- thirdline="ghi"

我经常使用:

{ read foo;read foo total use free foo ;} < <(df -k /)

那么

declare -p use free total

declare -- use="843476"
declare -- free="582128"
declare -- total="1515376"

考虑“结果代码”

同一前置步骤:

RESULT=$(myscript) RCODE=$?
{ read firstline; read secondline; read thirdline;} <<<"$RESULT"

declare -p firstline secondline thirdline RCODE

declare -- firstline="abc"
declare -- secondline="def"
declare -- thirdline="ghi"
declare -- RCODE="50"

请查看关于 help readhelp mapfile 的内容,获取有关换行符反斜杠和其他特殊字符的选项! - F. Hauri - Give Up GitHub

1

在尝试了这里大部分的解决方案之后,我发现最简单的方法是显而易见的 - 使用临时文件。我不确定你想要如何处理多行输出,但是你可以使用read逐行处理它。唯一真正无法轻松完成的事情就是将所有内容都放在同一个变量中,但对于大多数实际目的来说,这种方式更容易处理。

./myscript.sh > /tmp/foo
while read line ; do 
    echo 'whatever you want to do with $line'
done < /tmp/foo

快速的hack方法来执行请求的操作:
result=""
./myscript.sh > /tmp/foo
while read line ; do
  result="$result$line\n"
done < /tmp/foo
echo -e $result

请注意,这会添加一行额外的内容。如果您愿意,您可以编写代码来解决这个问题,但我太懒了。

编辑:虽然这个案例完美地运行,但是阅读此文的人应该意识到,在 while 循环中,您可以轻松地压缩 stdin,从而使您的脚本运行一行,清除 stdin 并退出。类似 ssh 会这样做吧?我最近刚看到它,其他代码示例在这里:https://unix.stackexchange.com/questions/24260/reading-lines-from-a-file-with-bash-for-vs-while

再来一次!这次使用不同的文件句柄(stdin、stdout、stderr 是 0-2,因此我们可以在 bash 中使用 &3 或更高)。

result=""
./test>/tmp/foo
while read line  <&3; do
    result="$result$line\n"
done 3</tmp/foo
echo -e $result

你也可以使用mktemp,但这只是一个快速的代码示例。使用mktemp的用法如下:

filenamevar=`mktemp /tmp/tempXXXXXX`
./test > $filenamevar

然后像使用实际文件名一样使用 $filenamevar。可能不需要在这里解释,但有人在评论中抱怨。

我也尝试了其他解决方案,但是在你的第一个建议下,我终于让我的脚本工作了。 - Kar.ma
1
负投票:这太过复杂,且未能避免多个常见的 bash 陷阱 - tripleee
有人告诉我关于奇怪的标准输入文件句柄问题,我当时就惊呆了。让我快速添加一些东西。 - user1279741
1
在Bash中,您可以使用read命令扩展-u 3从文件描述符3中读取。 - Jonathan Leffler
除了@JonathanLeffler的评论,您还可以这样编写:while read -ru $testout line;do echo "对$line执行某些操作";done {testout}< <(./test)而不是创建临时文件! - F. Hauri - Give Up GitHub

0
这样怎么样,它会将每一行读入一个变量中,然后可以随后使用!假设我的脚本输出被重定向到一个名为myscript_output的文件中。
awk '{while ( (getline var < "myscript_output") >0){print var;} close ("myscript_output");}'

5
那不是bash,而是awk。 - vadipp

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