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

326

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

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

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

Bash 中有类似的功能吗?

22个回答

3

两种主要的方式:

  • 将参数文件和标准输入管道连接成一个流,像处理标准输入一样处理它(流方法
  • 或将标准输入(和参数文件)重定向到一个命名管道中,并像处理文件一样处理它(文件方法

流方法

稍微修改之前的答案:

  • Use cat, not less. It's faster and you don't need pagination.

  • Use $1 to read from first argument file (if present) or $* to read from all files (if present). If these variables are empty, read from stdin (like cat does)

    #!/bin/bash
    cat $* | ...
    

文件方法

向命名管道中写入数据略微复杂,但这样可以将标准输入(或文件)视为单个文件:

  • Create pipe with mkfifo.

  • Parallelize the writing process. If the named pipe is not read from, it may block otherwise.

  • For redirecting stdin into a subprocess (as necessary in this case), use <&0 (unlike what others have been commenting, this is not optional here).

      #!/bin/bash
      mkfifo /tmp/myStream
      cat $* <&0 > /tmp/myStream &           # separate subprocess (!)
      AddYourCommandHere /tmp/myStream       # process input like a file, 
      rm /tmp/myStream                       # cleaning up
    

文件处理方法:变化

仅在没有给定参数时创建命名管道。这可能对从文件中读取更稳定,因为命名管道偶尔会阻塞。

#!/bin/bash
FILES=$*
if echo $FILES | egrep -v . >&/dev/null; then # if $FILES is empty
   mkfifo /tmp/myStream
   cat <&0 > /tmp/myStream &
   FILES=/tmp/myStream
fi
AddYourCommandHere $FILES     # do something ;)
if [ -e /tmp/myStream ]; then
   rm /tmp/myStream
fi

此外,它允许您迭代文件和标准输入而不是将它们全部连接成单个流:
for file in $FILES; do
    AddYourCommandHere $file
done

很棒的答案!非常灵活。我想要一个能够按文件日期对find命令的输出进行排序的东西,这个方法完美地解决了我的问题。 - undefined

2
以下内容适用于标准的sh(在Debian上测试过Dash),并且非常易读,但这是一种口味的问题:
if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

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

1
在我看来,最好的答案是因为它指向了真正的解决方案:cat "${1:--}" | any_command。读取 shell 变量并将其回显可能适用于小文件,但不太适用于大规模操作。 - Andreas Spindler
1
[ -n "$1" ] 可以简化为 [ "$1" ] - agc

2
我结合以上所有答案,创建了一个适合我的需求的shell函数。这是在我拥有两台Windows 10机器之间共享文件夹的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
}

如果您发现有任何方法可以进一步优化此内容,我希望能得到通知。

1

代码${1:-/dev/stdin}只会理解第一个参数,所以你可以这样使用:

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

1

从标准输入读取到变量或从文件读取到变量

现有答案中的大多数示例使用循环,立即将每行内容从标准输入中读取并输出。这可能不是你真正想做的事情。

在许多情况下,您需要编写一个调用仅接受文件参数的命令的脚本。但在您的脚本中,您可能还想支持标准输入。在这种情况下,您需要首先完全读取标准输入,然后将其作为文件提供。

让我们看一个例子。下面的脚本打印以PEM格式传递的证书(作为文件或通过标准输入)的详细信息。

# print-cert script

content=""
while read line
do
  content="$content$line\n"
done < "${1:-/dev/stdin}"
# Remove the last newline appended in the above loop
content=${content%\\n}

# Keytool accepts certificate only via a file, but in our script we fix this.
keytool -printcert -v -file <(echo -e $content)


# Read from file

cert-print mycert.crt

# Owner: CN=....
# Issuer: ....
# ....


# Or read from stdin (by pasting)

cert-print
#..paste the cert here and press enter
# Ctl-D

# Owner: CN=....
# Issuer: ....
# ....


# Or read from stdin by piping to another command (which just prints the cert(s) ). In this case we use openssl to fetch directly from a site and then print its info.


echo "" | openssl s_client -connect www.google.com:443 -prexit 2>/dev/null \
| sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' \
| cert-print

# Owner: CN=....
# Issuer: ....
# ....


0

我不认为这些答案中有任何一个是可接受的。特别是,被接受的答案只处理第一个命令行参数并忽略其余部分。它试图模拟的 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

0

作为解决方法,您可以在/dev目录中使用stdin设备:

....| for item in `cat /dev/stdin` ; do echo $item ;done

0

随着...

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

我得到了以下输出:

忽略了来自标准输入的1265个字符。使用“-stdin”或“-”来告诉如何处理管道输入。

然后用for决定:
Lnl=$(cat file.txt | wc -l)
echo "Last line: $Lnl"
nl=1

for num in `seq $nl +1 $Lnl`;
do
    echo "Number line: $nl"
    line=$(cat file.txt | head -n $nl | tail -n 1)
    echo "Read line: $line"
    nl=$[$nl+1]
done

0

gniourf_gniourf的答案是正确的,但使用了很多bashisms。由于这个问题是谷歌的顶级结果,这里提供一个符合POSIX标准的版本:

#!/bin/sh

if [ $# -eq 0 ]; then
    set -- -
fi

for f in "$@"; do
    if [ "$f" = - ] || exec < "$f"; then
        while IFS= read -r line; do
            printf '%s\n' "$line"
    done
done

或者如果你想简洁一点:
#!/bin/sh

[ $# -eq 0 ] || set -- -
for f; do
    { [ "$f" = - ] || exec < "$f"; } &&
    while IFS= read -r line; do
        printf '%s\n' "$line"
    done
done

0

这个在终端上使用起来很容易:

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

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