从管道中读取数值到shell变量中

260

我正在尝试让bash处理从stdin管道传输的数据,但是没有成功。我的意思是以下任何一种方式都不起作用:

echo "hello world" | test=($(< /dev/stdin)); echo test=$test
test=

echo "hello world" | read test; echo test=$test
test=

echo "hello world" | test=`cat`; echo test=$test
test=

我希望输出结果为test=hello world。我尝试过在"$test"周围加上双引号,但也不起作用。


1
你的例子.. echo "hello world" | read test; echo test=$test 对我来说运行良好.. 结果: test=hello world ; 你在什么环境下运行这个程序?我使用的是bash 4.2.. - alex.pilon
3
@alex.pilon,我正在运行Bash 4.2.25版本,他的例子对我也无效。也许这是Bash运行时选项或环境变量的问题?我发现这个例子也不能用Sh执行,所以也许Bash可以尝试与Sh兼容? - Hibou57
2
@Hibou57 - 我在bash 4.3.25中再次尝试了一下,但它不再起作用了。我的记忆有点模糊,我不确定我可能做了什么才能让它工作。 - alex.pilon
2
@Hibou57 @alex.pilon 在bash4>=4.2中,通过shopt -s lastpipe命令,管道中的最后一个命令应该会影响到变量 -- http://tldp.org/LDP/abs/html/bashver4.html#LASTPIPEOPT - imz -- Ivan Zakharyaschev
@alex.pilon,$test变量可能已经绑定了先前的尝试。使用新会话重复此操作将会失败,这是预期的结果。 - Tasos Papastylianou
显示剩余2条评论
17个回答

205

使用

IFS= read var << EOF
$(foo)
EOF

可以通过以下方式欺骗read,使其接受管道输入:

echo "hello world" | { read test; echo test=$test; }

或者编写这样一个函数:

read_from_pipe() { read "$@" <&0; }

但这没有意义--你的变量赋值可能不会持续!管道可能会生成子shell,在该子shell中,环境是按值而不是引用继承的。这就是为什么read不会使用来自管道的输入--它是未定义的。

顺带一提,http://www.etalabs.net/sh_tricks.html是一个很好的收集了在bourne shell(sh)中打架奇怪和不兼容性必需品的集合。


3
让我们再试一次(显然在这个标记中转义反引号很有趣): test=\echo“hello world”| {read test; echo $test;} `` - Compholio
1
我可以问一下你为什么在分组这两个命令时使用了 {} 而不是 () 吗? - Jürgen Paul
4
关键不在于让read从管道中读取输入,而在于在执行read的同一 shell 中使用该变量。 - chepner
那个可以完美地工作。https://dev59.com/zWw05IYBdhLWcg3w3ViX - Buzut
1
尝试使用此解决方案时出现“bash权限被拒绝”的错误。我的情况有些不同,但我无法在任何地方找到答案,对我有用的是(一个不同的示例,但类似的用法):pip install -U echo $(ls -t *.py | head -1)。如果有人遇到类似的问题并像我一样偶然发现了这个答案,请参考。 - Ivan Bilan
显示剩余4条评论

130

如果您想读取大量的数据并逐行单独处理,则可以使用类似以下方式的方法:

cat myFile | while read x ; do echo $x ; done

如果你想将行拆分成多个单词,可以使用多个变量代替x,就像这样:

cat myFile | while read x y ; do echo $y $x ; done

或者说:

while read x y ; do echo $y $x ; done < myFile

但是,一旦您开始想要在这种情况下进行任何真正聪明的事情,最好使用一些脚本语言,如Perl,您可以尝试这样做:

perl -ane 'print "$F[0]\n"' < myFile

学习Perl可能需要一定的时间(我猜其他语言也是如此), 但如果你想做更复杂的脚本,长远来看,你会发现它更容易。我建议阅读《Perl Cookbook》和当然还有Larry Wall等人编写的《Perl程序设计语言》。


11
"alternatively" 是正确的方式。不要使用 UUoC 和子shell。请参考 BashFAQ/024 - Dennis Williamson

65

这是另一个选项

$ read test < <(echo hello world)

$ echo $test
hello world

31
<(..)$(..) 更具有显著优势,因为 <(..) 会在命令产生输出后立即将每行返回给调用者。而 $(..) 会等待命令完成并生成所有输出后才将任何输出提供给调用者。 - Derek Mahar
这被称为进程替代pass v1.7.4使用它从/dev/urandom生成密码。 - crimson_king

56

read 无法从管道中读取输入(或者结果会因为管道创建子 shell 而丢失)。不过,在 Bash 中你可以使用 Here String:

$ read a b c <<< $(echo 1 2 3)
$ echo $a $b $c
1 2 3

但是请参考@chepner的回答,了解关于lastpipe的信息。


3
简单易懂的一句话,容易理解。这个答案需要更多的赞。 - David Parks
2
<<< 添加换行符可能不是所需的。 - LoganMzz
1
@LoganMzz:没错,但是read会消耗它,因为它是默认的分隔符。因此,变量不包含它。请注意,即使echo输出一个换行符,这也不会导致变量包含一个换行符:d=$(echo "foo"),因为命令替换会删除尾随的换行符。 - Dennis Williamson

49

我不是Bash方面的专家,但我想知道为什么没有提出这个解决方案:

stdin=$(cat)

echo "$stdin"

这是一个简短的证明,它对我有效:

$ fortune | eval 'stdin=$(cat); echo "$stdin"'

4
这可能是因为"read"是一个bash命令,而"cat"是一个单独的二进制文件,将在子进程中启动,因此效率较低。 - dj_segfault
14
有时候,简单明了比效率更加重要 :) - Rondo
7
肯定是最直接的答案。 - drwatsoncode
1
@djanowski 但这并不一定是给定脚本的预期行为。如果有一种优雅地处理stdin缺失并在其不存在时回退到“常规”行为的方法就好了。这篇文章几乎做到了 - 它接受参数或stdin。唯一缺少的是能够在两者都不存在时提供使用帮助。 - Dale C. Anderson
3
在寻找替代接受答案时,我决定选择类似于这个答案的东西来满足我的使用需求 :) ${@:-$(cat)} - tinnick
显示剩余5条评论

33

bash 4.2 引入了 lastpipe 选项,它允许您的代码按照原本的写法运行,在当前 shell 中执行管道中的最后一个命令,而不是在子 shell 中执行。

shopt -s lastpipe
echo "hello world" | read test; echo test=$test

3
啊!太好了,这个。如果在交互式 shell 中进行测试,还要输入:"set +m"(在 .sh 脚本中不需要)。 - XXL
这对我来说是一个长期的烦恼...您可以将shopt -s lastpipeset +m(或set -o monitor)添加到/.bashrc或/.bash_aliases中,但是有一个bug,即set +m不会生效。我发现通过将其添加到export PROMPT_COMMAND='set +m'可以解决此问题。(https://askubuntu.com/questions/1395963/bash-set-m-option-does-not-work-when-placed-in-the-bashrc-file) - alchemy

23
一个聪明的脚本,既可以从管道读取数据,也可以从命令行参数中读取数据:
#!/bin/bash
if [[ -p /dev/stdin ]]
    then
    PIPE=$(cat -)
    echo "PIPE=$PIPE"
fi
echo "ARGS=$@"

输出:

$ bash test arg1 arg2
ARGS=arg1 arg2

$ echo pipe_data1 | bash test arg1 arg2
PIPE=pipe_data1
ARGS=arg1 arg2

解释: 当一个脚本通过管道接收数据时,/dev/stdin(或/proc/self/fd/0)将成为指向管道的符号链接。

/proc/self/fd/0 -> pipe:[155938]

如果没有,则它将指向当前终端:

/proc/self/fd/0 -> /dev/pts/5

bash的[[ -p选项可以检查它是否为管道。

cat -stdin读取。

如果没有stdin,使用cat -将会一直等待,这就是为什么我们把它放在if条件语句中的原因。


2
你也可以使用/dev/stdin,它是指向/proc/self/fd/0的链接。 - Elie G.

16

将shell命令的隐式管道传输到bash变量的语法是

var=$(command)
或者
var=`command`

在你的示例中,你正在将数据导向一个不需要任何输入的赋值语句。


4
因为$()可以很容易地嵌套。请思考JAVA_DIR=$(dirname $(readlink -f $(which java))),并尝试使用``进行转义。您需要进行三次转义! - albfan

12

在我的看法中,Bash 中从标准输入读取的最佳方式是以下方式,它还允许你在输入结束之前对行进行处理:

while read LINE; do
    echo $LINE
done < /dev/stdin

1
在找到这个之前,我差点疯了。非常感谢您的分享! - Samy Dindane

10

因为我被它迷住了,所以我想留个言。 我发现了这个帖子,是因为我必须重写一个旧的sh脚本 以使其符合POSIX标准。 这基本上意味着通过改写像这样的代码来规避由POSIX引入的管道/子shell问题:

some_command | read a b c

into:

read a b c << EOF
$(some_command)
EOF

并且像这样的代码:

some_command |
while read a b c; do
    # something
done

into:

while read a b c; do
    # something
done << EOF
$(some_command)
EOF

然而,对于空输入,后者的行为并不相同。 使用旧符号表示法时,在空输入上while循环不会进入, 但在POSIX表示法中会进入! 我认为这是由于EOF之前的换行符造成的, 它不能被省略。 行为更像旧符号表示法的POSIX代码如下:

while read a b c; do
    case $a in ("") break; esac
    # something
done << EOF
$(some_command)
EOF

在大部分情况下这应该足够好了。但遗憾的是,如果一些命令输出空行,则此方法仍然不像旧有的表示法那样准确。在旧有的表示法中,while循环体会被执行,而在POSIX表示法中,我们会在循环体前跳出。

解决这个问题的方法可能如下:

while read a b c; do
    case $a in ("something_guaranteed_not_to_be_printed_by_some_command") break; esac
    # something
done << EOF
$(some_command)
echo "something_guaranteed_not_to_be_printed_by_some_command"
EOF

[ -n "$a" ] || break 应该也可以工作 - 但是缺少实际的空行的问题仍然存在。 - joki

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