如何在Bash中从文件或标准输入读取数据

326

下面的 Perl 脚本(my.pl)可以从命令行参数指定的文件或者标准输入(STDIN)读取:

while (<>) {
   print($_);
}

perl my.pl 会从标准输入读取,而 perl my.pl a.txt 会从文件 a.txt 中读取。这非常方便。

Bash 中有类似的功能吗?

22个回答

533

以下解决方案将从文件中读取数据,如果脚本被调用时带有文件名作为第一个参数$1,否则将从标准输入读取。

while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

如果定义了参数$1,则替换表达式${1:-...}将使用该参数。否则,将使用当前进程标准输入的文件名。


19
您在命令行中提供的文件名可能会包含空格。 - Fritz G. Mehner
7
在使用/proc/$$/fd/0/dev/stdin时有什么区别吗?我注意到后者似乎更常见且更直接。 - knowah
27
建议在您的 read 命令中添加 -r 参数,以避免意外消耗 \ 字符;使用 while IFS= read -r line 保留行首和行尾空格。请注意不要改变原文意思。 - mklement0
1
@NeDark:很奇怪;我刚刚验证了它在那个平台上可以工作,即使使用 /bin/sh - 你使用的是除 bashsh 之外的其他 shell 吗? - mklement0
3
在绝大多数情况下,你应该避免这样做。如果你只是想将输入回显到输出中,cat 已经可以实现这一点。很多时候,处理可以在 Awk 脚本中进行,而 shell 的 while read 循环只会让事情变得更加复杂。显然,有些情况下你确实需要从文件中逐行读取并处理,但如果你是在谷歌上找到了这个答案,那么你应该意识到这是一个常见的新手反模式。 - tripleee
显示剩余7条评论

155

20
在这种情况下使用<&0没有任何好处-您的示例将无论是否使用它都可以正常工作-似乎,从bash脚本中调用的工具默认看到与脚本本身相同的标准输入(stdin)(除非脚本先使用它)。 - mklement0
如果一个工具读取了一半的输入缓冲区,那么我调用的下一个工具会得到剩下的部分吗? - Asad Saeeduddin
1
执行此操作时出现“缺少文件名(请使用“less --help”获取帮助)”... Ubuntu 16.04。 - OmarOthman
6
这个回答中的“or from file”部分在哪里? - Sebastian
正是我所需要的!我还发送了任何CLI参数:less "$@" <&0 - KingBob
显示剩余3条评论

140

这是最简单的方法:

#!/bin/sh
cat -

使用方法:

$ echo test | sh my_script.sh
test

要将标准输入分配给变量,您可以使用:STDIN=$(cat -) 或者简单地使用 STDIN=$(cat),因为该操作符不是必需的(根据@mklement0评论)。


要从标准输入中解析每一行,请尝试以下脚本:

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done
要从文件或标准输入流(如果没有传入参数)中读取内容,你可以扩展它为:

要从文件或标准输入流(如果没有传入参数)中读取,您可以将其扩展为:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")

注意:

- read -r - 不要将反斜杠字符视为特殊字符。将每个反斜杠视为输入行的一部分。

- 如果不设置IFS,默认情况下会忽略行首和行尾的空格制表符序列(被修剪)。

- 使用printf而不是echo可避免在行仅由单个-e-n-E组成时打印空行。但是,通过使用env POSIXLY_CORRECT=1 echo "$line"可以绕过此问题,它会执行您的外部GNUecho,该命令支持这些选项。参见:How do I echo "-e?"

参见:在stackoverflow SE上如何在没有传递参数时读取标准输入?


1
你可以将 [ "$1" ] && FILE=$1 || FILE="-" 简化为 FILE=${1:--}。(小问题:最好避免使用全大写的_shell_变量,以避免与_environment_变量发生名称冲突。) - mklement0
非常愉快;实际上,${1:--} 是符合 POSIX 标准的,因此它应该在所有类 POSIX 的 shell 中都可以工作。在所有这样的 shell 中无法工作的是进程替换 (<(...)); 它将在 bash、ksh、zsh 中工作,但在 dash 中不会。另外,最好在你的 read 命令中添加 -r,这样它就不会意外地吃掉 \ 字符;在前面加上 IFS= 以保留前导和尾随空格。 - mklement0
5
实际上,您的代码仍然会出现问题,因为echo命令:如果一行包含-e-n-E选项,则不会显示该行。为了解决此问题,您必须使用printf命令:printf '%s\n' "$line"。我在之前的编辑中没有包括它...太常见了,当我修复这个错误时,我的编辑经常被撤销 :( - gniourf_gniourf
1
不,它不会失败。如果第一个参数是'%s\n',那么--是无用的。 - gniourf_gniourf
2
你的答案对我来说很好(我的意思是没有我知道的错误或不需要的功能)-尽管它不像Perl那样处理多个参数。实际上,如果你想处理多个参数,最好写Jonathan Leffler的优秀答案-事实上,你的答案会更好,因为你会使用IFS=readprintf而不是echo:). - gniourf_gniourf
显示剩余3条评论

24

我认为这是最直接的方法:

$ cat reader.sh
#!/bin/bash
while read line; do
  echo "reading: ${line}"
done < /dev/stdin

--

$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
  echo "line ${i}"
done

--

$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5

5
这不符合发帖者要求从标准输入或文件参数读取的要求,它只是从标准输入读取。 - nash
3
撇开 @nash 的有效反对意见不谈:read 默认从标准输入读取,因此无需使用 < /dev/stdin - mklement0

18

echo的解决方案会在IFS打断输入流时添加新行。可以对@fgm's answer进行一些修改:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"

你能解释一下你所说的“echo solution adds new lines whenever IFS breaks the input stream”是什么意思吗?如果你指的是read的行为:虽然read有可能根据$IFS中包含的字符分割成多个标记,但如果你只指定一个变量名(默认情况下会修剪前导和尾随空格),它只会返回一个标记。 - mklement0
@mklement0,我完全同意您关于read$IFS的行为 - echo本身会在没有-n标志的情况下添加新行。 "echo实用程序将任何指定的操作数,由单个空格(')字符分隔,并后跟换行符(\ n')字符,写入标准输出。" - David Souther
明白了。然而,为了模拟Perl循环,你需要添加echo的尾随\n:Perl的$_包括从读取的行中的行结束符\n,而bash的read则不包括。 (但是,正如@gniourf_gniourf在其他地方指出的那样,更健壮的方法是使用printf '%s\n'代替echo)。 - mklement0
1
这是一个绝妙的解决方案,它赋予了您的脚本能够被用作 script.sh example 或者 cat example | script.sh 的能力。 - shmup

12
这个问题中的Perl循环读取命令行上所有文件名参数,如果没有指定文件,则从标准输入读取。我看到的所有答案似乎都处理单个文件或标准输入(如果没有指定文件)。
虽然经常被准确地嘲笑为UUOC(不必要的cat使用),但有时cat是最好的工具之一,可以说这是其中之一:
cat "$@" |
while read -r line
do
    echo "$line"
done

这种方法的唯一缺点是它会在一个子shell中创建一个管道,所以while循环中的变量赋值等内容在管道外部不可访问。解决这个问题的方法是使用进程替换
while read -r line
do
    echo "$line"
done < <(cat "$@")

这样,while 循环在主 shell 中运行,因此在循环中设置的变量可以在循环外部访问。

1
关于多个文件的优秀观点。我不知道资源和性能方面的影响,但如果你不使用bash、ksh或zsh,因此无法使用进程替换,你可以尝试使用带有命令替换的here-doc(跨越3行)>>EOF\n$(cat "$@")\nEOF。最后,一个小问题:while IFS= read -r line更接近Perl中while (<>)的功能(保留前导和尾随空格-虽然Perl也保留尾随\n)。 - mklement0

8

Perl的行为,对于OP中给出的代码可以不带参数或带多个参数,如果参数是单破折号-,则被理解为标准输入。此外,始终可以使用$ARGV来获取文件名。 到目前为止,迄今为止给出的答案都没有真正模拟Perl在这些方面的行为。这里有一个纯Bash的可能性。关键是适当地使用exec

#!/bin/bash

(($#)) || set -- -
while (($#)); do
   { [[ $1 = - ]] || exec < "$1"; } &&
   while read -r; do
      printf '%s\n' "$REPLY"
   done
   shift
done

文件名可以在$1中找到。

如果没有给出参数,我们会将-人为地设置为第一个位置参数。然后我们循环处理参数。如果一个参数不是-,我们就用exec从文件名重定向标准输入。如果这个重定向成功,我们就用while循环继续处理。我使用了标准的REPLY变量,在这种情况下你不需要重置IFS。如果你想要另一个名称,你必须像这样重置IFS(当然,如果你不想这样做并且知道自己在做什么,你也可以不这样做):

while IFS= read -r line; do
    printf '%s\n' "$line"
done

这是正确的答案 - 我最近学会了如何使用exec将stdout重定向到指定文件,我应该知道它也可以用于将文件路由到stdin。感谢分享您的答案,很抱歉它没有得到应有的关注! - David Farrell

3
请尝试下面的代码:
while IFS= read -r line; do
    echo "$line"
done < file

2
请注意,即使经过修改,这段代码仍无法从标准输入或多个文件中读取数据,因此它并不能完全回答这个问题。(令人惊讶的是,在回答首次提交后的三年多时间里,竟然出现了两次编辑。) - Jonathan Leffler
@JonathanLeffler 对于编辑这样一个古老(而且不是很好)的答案我感到抱歉...但是我无法忍受看到这个可怜的read没有IFS=-r,以及可怜的$line没有健康的引号。 - gniourf_gniourf
1
@gniourf_gniourf:我不喜欢read -r的表示法。在我看来,POSIX搞错了;该选项应启用尾随反斜杠的特殊含义,而不是禁用它——这样,早于POSIX存在的现有脚本就不会因省略-r而出错。然而,我注意到它是IEEE 1003.2 1992的一部分,这是POSIX shell和实用程序标准的最早版本之一,但即使那时它也被标记为附加内容,所以这只是对已经消失的机会的抱怨。我从未因我的代码不使用-r而遇到麻烦;我一定很幸运。请忽略我关于此事的看法。 - Jonathan Leffler
1
@JonathanLeffler 我非常同意 -r 应该成为标准。我同意在不使用它会导致问题的情况下,这种情况不太可能发生。尽管如此,破损的代码就是破损的代码。我的编辑最初是由那个可怜的 $line 变量触发的,它严重缺少引号。我在处理它时修复了 read。我没有修复 echo,因为这种编辑会被撤销。 :(. - gniourf_gniourf
它是如何工作的?那个IFS=是什么东西?为什么它是必要的?在评论中有一些信息。 - Peter Mortensen
修改后与sorpigal的(后来的)答案完全相同。 - Peter Mortensen

3
更准确地说...
while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file

5
我认为这基本上是对https://dev59.com/zWw05IYBdhLWcg3w3ViX#6980232的评论,而不是答案。为了明确评论:在`read`命令中添加`IFS =-r`可以确保每行都被_未修改_读取(包括前导和尾随空格)。 - mklement0

3
#!/usr/bin/bash

if [ -p /dev/stdin ]; then
       #for FILE in "$@" /dev/stdin
    for FILE in /dev/stdin
    do
        while IFS= read -r LINE
        do
            echo "$@" "$LINE"   #print line argument and stdin
        done < "$FILE"
    done
else
    printf "[ -p /dev/stdin ] is false\n"
     #dosomething
fi

运行中:

echo var var2 | bash std.sh

结果:

var var2

运行中:

bash std.sh < <(cat /etc/passwd)

结果:

root:x:0:0::/root:/usr/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
mail:x:8:12::/var/spool/mail:/usr/bin/nologin

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