如何从Bash中的文件或STDIN中读取?


244

以下Perl脚本(my.pl)可以从命令行args上的文件或STDIN中读取:

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

perl my.pl将从STDIN读取,而perl my.pl a.txt从读取a.txt。这很方便。

想知道Bash中是否有一个等效项吗?

Answers:


409

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

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

如果定义,则替换${1:-...}将采用$1自己过程的标准输入的文件名。


1
很好,可以。另一个问题是为什么要为其添加报价?“ $ {1:-/ proc / $ {$} / fd / 0}”
大港

15
您在命令行上提供的文件名可以为空格。
Fritz G. Mehner,2011年

3
是否有使用什么区别/proc/$$/fd/0/dev/stdin?我注意到后者似乎更常见并且看起来更简单。
Knowah

19
最好添加-r到您的read命令中,以免意外吃到\ 字符。用于while IFS= read -r line保留前导和尾随空格。
mklement0

1
@NeDark:很好奇;我刚刚验证了它即使在使用时也可以在该平台上运行/bin/sh-您是否使用的不是bashor 的外壳sh
mklement0

119

也许最简单的解决方案是使用合并的重定向运算符重定向stdin:

#!/bin/bash
less <&0

Stdin是文件描述符零。上面的代码将输入到bash脚本中的输入发送到less的stdin中。

阅读有关文件描述符重定向的更多信息


1
希望我能有更多的赞成意见给我,多年来我一直在寻找。
Marcus Downing 2014年

13
<&0在这种情况下使用它没有任何好处-您的示例在有或没有它的情况下都可以工作-貌似,默认情况下,您从bash脚本中调用的工具看到的标准输入与脚本本身相同(除非脚本首先使用它)。
mklement0

@ mkelement0因此,如果一个工具读取了一半的输入缓冲区,我调用的下一个工具会得到其余的吗?
Asad Saeeduddin

当我这样做时“缺少文件名(“ --help”较少))... Ubuntu 16.04
OmarOthman

5
这个答案中“或来自文件”部分在哪里?
塞巴斯蒂安

84

这是最简单的方法:

#!/bin/sh
cat -

用法:

$ echo test | sh my_script.sh
test

要将stdin分配给变量,您可以使用:STDIN=$(cat -)或仅STDIN=$(cat)不需要使用运算符(根据@ mklement0 comment)。


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

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done

要从文件或stdin中读取(如果不存在参数),可以将其扩展为:

#!/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,默认情况下的序列SpaceTab在线条的开始和结束都被忽略(修剪)。

-使用printf的,而不是echo到避免打印空行当线由单一的-e-n或者-E。但是,有一种解决方法,可以使用env POSIXLY_CORRECT=1 echo "$line"它来执行支持它的外部 GNU echo。请参阅:如何回显“ -e”?

请参阅:无参数时如何读取stdin?在stackoverflow SE


您可以简化[ "$1" ] && FILE=$1 || FILE="-"FILE=${1:--}。(Quibble:最好避免使用全大写的Shell变量,以避免名称与环境变量发生冲突。)
mklement0 2015年

我的荣幸; 实际上,它${1:--} 是与 POSIX兼容的,因此它应该可以在所有类似POSIX的外壳中使用。在所有这些shell中都无效的是进程替换(<(...));例如,它可以在bash,ksh,zsh中使用,但不能在破折号中使用。另外,最好将其添加-r到您的read命令中,这样它就不会偶然吃掉\ 字符。优先IFS= 保留前导和尾随空格。
mklement0

4
其实你的代码仍然打破,因为echo:如果线路由-e-n或者-E,它不会被显示。要解决此问题,您必须使用printfprintf '%s\n' "$line"。我没有在以前的编辑中包括它…经常在解决此错误时回滚我的编辑:(
gniourf_gniourf 2015年

1
不,它不会失败。而且--是无用的,如果第一个参数是'%s\n'
gniourf_gniourf

1
您的回答对我来说很好(我的意思是,我不再知道任何错误或不需要的功能)—尽管它不像Perl那样对待多个参数。实际上,如果您想处理多个参数,您将最终编写Jonathan Leffler的出色答案—实际上,您最好使用IFS=with readprintf代替echo:)
gniourf_gniourf 2015年

19

我认为这是简单明了的方法:

$ 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

4
这不符合发布者从stdin或文件参数中读取的要求,而只是从stdin中读取。
nash

2
离开@纳什的有效反对旁白:read从标准输入读取默认情况下,所以有没有必要< /dev/stdin
mklement0

13

echo只要IFS中断输入流,该解决方案就会添加新行。@fgm的答案可以修改一下:

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

您能否解释一下“只要IFS中断输入流,echo解决方案就会添加新行”是什么意思?如果你指的read的行为:在read 没有可能分裂成由字符多个令牌。包含在$IFS,它只返回一个单一的令牌,如果你只指定一个变量名(但修剪和领先的,默认情况下尾随空白)。
mklement0

@ mklement0我同意您100%的行为,read并且$IFS- echo本身添加了没有-n标志的新行。“ echo实用程序将任何指定的操作数写入标准输出,这些操作数由单个空格('')字符分隔,后跟换行符(`\ n')字符。
David Souther

得到它了。但是,要模拟Perl循环,您需要\n添加以下结尾echo:Perl $_ 包括\n读取的行结尾的行,而bash read则没有。(但是,正如@gniourf_gniourf在其他地方指出的那样,更健壮的方法是使用printf '%s\n'代替echo)。
mklement0'3

8

问题中的Perl循环从命令行上的所有文件名参数中读取,或者,如果未指定文件,则从标准输入中读取。如果没有指定文件,我看到的所有答案似乎都在处理单个文件或标准输入。

尽管通常被准确地嘲笑为UUOC(对的无用使用cat),但有时cat它是工作的最佳工具,并且可以说这是其中之一:

cat "$@" |
while read -r line
do
    echo "$line"
done

唯一的缺点是它创建了在子外壳中运行的管道,因此while无法在管道外部访问循环中的变量分配之类的内容。在bash周围的办法是进程替换

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

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


1
关于多个文件的优点。我不知道这将对资源和性能产生什么影响,但是如果您不在bash,ksh或zsh上,因此无法使用进程替换,则可以尝试使用此处的命令替换文档(分布于3行)>>EOF\n$(cat "$@")\nEOF。最后,一个怪癖:while IFS= read -r linewhile (<>)Perl中的更好近似(保留前导和尾随空白-尽管Perl也保留尾随\n)。
mklement0

4

Perl的行为以及OP中给出的代码可以不带任何参数,也可以不带多个参数,如果参数是单个连字符,-则可以理解为stdin。而且,始终可以使用来命名文件名$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变量,在这种情况下,您不需要reset IFS。如果您想使用其他名称,则必须IFS像这样重设(当然,除非您不想要那样,并且不知道自己在做什么):

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

2

更精确地...

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

2
我认为这实质上是对stackoverflow.com/a/6980232/45375的评论,而不是答案。为了使注释明确: 在命令上添加 IFS=-rread确保每行都被读取且未修改(包括前导和尾随空格)。
mklement0

2

请尝试以下代码:

while IFS= read -r line; do
    echo "$line"
done < file

1
请注意,即使进行了修订,也不会从标准输入或多个文件中读取该内容,因此它并不是该问题的完整答案。(令人惊讶的是,在首次提交答案后三年多的时间内,几分钟内就看到了两次编辑。)
乔纳森·莱夫勒

编辑这样一个旧的(而不是真正的好)答案@JonathanLeffler对不起......但我无法忍受看到这个可怜的readIFS=-r,而穷人$line没有它的健康引号。
gniourf_gniourf 2015年

1
@gniourf_gniourf:我不喜欢这种read -r表示法。IMO,POSIX弄错了;该选项应启用尾随反斜杠的特殊含义,而不是禁用反斜杠-这样,现有脚本(从POSIX存在之前)不会中断,因为-r省略了。但是,我观察到它是IEEE 1003.2 1992的一部分,它是POSIX Shell和实用程序标准的最早版本,但是即使在那时它也被标记为附加功能,因此这对于长期的机会很不利。我从来没有遇到麻烦,因为我的代码没有使用-r。我一定很幸运。对此我无视。
Jonathan Leffler'3

1
@JonathanLeffler我真的同意这-r应该是标准的。我同意,不使用它可能会导致麻烦。虽然,破损代码是破损代码。我的编辑首先是由那个糟糕的$line变量触发的,该变量严重错过了它的引号。我修好了read它。我没有解决这个问题,echo因为那是可以回滚的编辑类型。:(
gniourf_gniourf 2015年

1

代码${1:-/dev/stdin}只会理解第一个参数,因此,如何处理。

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done

1

我找不到这些答案中的任何一个。特别是,接受的答案仅处理第一个命令行参数,而忽略其余参数。试图模拟的Perl程序将处理所有命令行参数。因此,被接受的答案甚至无法回答问题。其他答案使用bash扩展名,添加不必要的“ cat”命令,仅适用于将输入回显到输出的简单情况,或者只是不必要地复杂。

但是,我必须给他们一些荣誉,因为他们给了我一些想法。这是完整的答案:

#!/bin/sh

if [ $# = 0 ]
then
        DEFAULT_INPUT_FILE=/dev/stdin
else
        DEFAULT_INPUT_FILE=
fi

# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done

1

我结合了以上所有答案,并创建了一个适合我需要的shell函数。这是从我的2台Windows10机器的cygwin终端上获得的,它们之间有一个共享文件夹。我需要能够处理以下问题:

  • cat file.cpp | tx
  • tx < file.cpp
  • tx file.cpp

在指定了特定文件名的地方,我在复制过程中需要使用相同的文件名。在输入数据流已经通过管道传输的地方,那么我需要生成一个具有小时分和秒的临时文件名。共享的主文件夹包含一周中各天的子文件夹。这是出于组织目的。

看哪,满足我需求的最终脚本:

tx ()
{
  if [ $# -eq 0 ]; then
    local TMP=/tmp/tx.$(date +'%H%M%S')
    while IFS= read -r line; do
        echo "$line"
    done < /dev/stdin > $TMP
    cp $TMP //$OTHER/stargate/$(date +'%a')/
    rm -f $TMP
  else
    [ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
  fi
}

如果您有任何办法可以进一步优化此功能,我想知道。


0

以下代码可与标准代码一起使用sh(已dash在Debian上进行测试),并且可读性强,但这只是一个趣味问题:

if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

详细信息:如果第一个参数为非空cat,则为该文件,否则为cat标准输入。然后,整个if语句的输出由处理commands_and_transformations


恕我直言,最佳答案之所以如此,是因为它指向了真正的解决方案:cat "${1:--}" | any_command。读取shell变量并回显它们可能适用于小型文件,但扩展性不佳。
Andreas Spindler

[ -n "$1" ]可以简化为[ "$1" ]
AGC

0

这是一个易于使用的终端:

$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3

-1

怎么样

for line in `cat`; do
    something($line);
done

的输出cat将放入命令行。命令行具有最大大小。同样,这不会逐行读取,而是逐字读取。
Notinlist,
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.