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

下面的Perl脚本(my.pl)既可以从命令行参数中的文件读取,也可以从标准输入 (STDIN)读取:

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

perl my.pl将从标准输入中读取,而perl my.pl a.txt将从a.txt中读取。这很方便。

Bash中也有类似的功能吗?

463863 次浏览

请尝试以下代码:

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

更准确的说……

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

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

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

如果定义了替换${1:-...},则接受$1。否则,使用自己进程的标准输入的文件名。

echo解决方案在IFS中断输入流时添加新行。@fgm的回答可以稍加修改:

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

也许最简单的解决方案是使用合并重定向操作符重定向标准输入:

#!/bin/bash
less <&0

标准输入是文件描述符0。上面的代码将通过管道传输到bash脚本的输入发送到更少的标准输入。

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

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

虽然经常被准确地嘲笑为UUOC (cat的无用使用),但有时cat是最适合这项工作的工具,这是有争议的:

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

唯一的缺点是它创建了一个在子shell中运行的管道,所以像while循环中的变量赋值这样的事情在管道之外是不可访问的。bash的方法是进程替换:

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

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

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

$ 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

使用:

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

这是最简单的方法:

#!/bin/sh
cat -

用法:

$ echo test | sh my_script.sh
test

要将stdin赋值给变量,你可以使用:STDIN=$(cat -)或只是简单地使用STDIN=$(cat)作为操作符是不必要的(根据@mklement0评论)。


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

#!/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 ? at stackoverflow SE

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变量,在这种情况下,你不需要重置IFS。如果你想要另一个名字,你必须像这样重置IFS(当然,除非你不想这样做,并且知道你在做什么):

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

下面的代码使用标准sh(在Debian上用破折号测试),可读性很强,但这是个人品味的问题:

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

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

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

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

我觉得这些答案都不能接受。特别是,接受的答案只处理第一个命令行参数,而忽略其余的。它试图模拟的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

我将上述所有答案结合起来,创建了一个适合我需要的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
}

如果有任何方法,你可以看到进一步优化这一点,我想知道。

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

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

作为一种变通方法,你可以使用/ dev目录中的stdin设备:

....| for item in `cat /dev/stdin` ; do echo $item ;done
#!/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

两种主要方法:

  • 将参数文件和stdin管道到单个流和进程中,如stdin (流的方法)
  • 或重定向stdin(和参数文件)到一个命名管道和进程,就像一个文件(文件的方法)

流的方法

对之前答案的小修改:

  • 使用cat,而不是less。这样更快,而且不需要分页。

  • 使用$1从第一个参数文件读取(如果存在)或$*从所有文件读取(如果存在)。如果这些变量为空,则从stdin中读取(就像cat那样)

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

文件的方法

写入一个命名管道有点复杂,但这允许你把stdin(或文件)当作一个单独的文件:

  • 使用mkfifo创建管道。

  • 并行化写入过程。如果未读取命名管道,则可能阻塞。

  • 要将stdin重定向到子进程(在这种情况下是必要的),使用<&0(与其他人注释的不同,这里是没有可选的)。

      #!/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

此外,它允许你迭代文件和stdin,而不是连接到一个单一的流:

for file in $FILES; do
AddYourCommandHere $file
done

与…

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

从stdin读入变量或从文件读入变量。

现有答案中的大多数示例使用立即回显每一行,因为它是从stdin读取.循环。这也许不是你真正想做的。

在许多情况下,您需要编写一个脚本来调用只接受file参数的命令。但是在你的脚本中,你可能也想要支持stdin。在这种情况下,您需要首先读取完整的stdin,然后将其作为文件提供。

让我们看一个例子。下面的脚本打印一个证书的证书详细信息(以PEM格式),该证书可以作为文件传递,也可以通过stdin传递。

# 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: ....
# ....