ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

Bash 中是否有等价物?


P
Peter Mortensen

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

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

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


很好,它有效。另一个问题是为什么要为它添加报价? “${1:-/proc/${$}/fd/0}”
您在命令行上提供的文件名可能有空格。
使用 /proc/$$/fd/0/dev/stdin 有什么区别吗?我注意到后者似乎更常见,看起来更直接。
最好将 -r 添加到您的 read 命令中,这样它就不会意外吃掉 \ 字符;使用 while IFS= read -r line 保留前导和尾随空格。
在绝大多数情况下,您应该避免这种情况。如果您只想将输入回显到输出,cat 已经完成了。很多时候,处理可能发生在 Awk 脚本中,而 shell while read 循环只会使事情复杂化。显然,在某些情况下,您确实需要在 shell 循环中一次处理文件中的一行,但如果您刚刚在 Google 中找到了这个答案,您应该知道这是一种常见的新手反模式。
P
Peter Mortensen

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

#!/bin/bash
less <&0

标准输入是文件描述符零。以上将通过管道传输到您的 bash 脚本的输入发送到 less's 标准输入。

Read more about file descriptor redirection


我希望我有更多的赞成票给你,我多年来一直在寻找这个。
在这种情况下使用 <&0 没有任何好处 - 无论有没有它,您的示例都可以正常工作 - 看起来,您从 bash 脚本中调用的工具默认情况下会看到与脚本本身相同的标准输入(除非脚本使用它第一的)。
@mkelement0 因此,如果一个工具读取了输入缓冲区的一半,我调用的下一个工具会得到其余的吗?
“缺少文件名(“less --help”寻求帮助)”当我这样做时...... Ubuntu 16.04
这个答案中的“或来自文件”部分在哪里?
C
Community

这是最简单的方法:

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

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

#!/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,默认情况下,行首和行尾的 Space 和 Tab 序列将被忽略(修剪)。 - 当行由单个 -e、-n 或 -E 组成时,使用 printf 而不是 echo 以避免打印空行。但是,有一个解决方法是使用 env POSIXLY_CORRECT=1 echo "$line" 执行支持它的外部 GNU echo。请参阅:如何回显“-e”?

请参阅:How to read stdin when no arguments are passed? at stackoverflow SE


您可以将 [ "$1" ] && FILE=$1 || FILE="-" 简化为 FILE=${1:--}。 (狡辩:最好避免使用全大写的 shell 变量,以避免与 environment 变量发生名称冲突。)
我的荣幸;实际上,${1:--} POSIX 兼容的,因此它应该可以在所有类似 POSIX 的 shell 中工作。在所有此类 shell 中不起作用的是进程替换 (<(...));例如,它可以在 bash、ksh、zsh 中工作,但不能在 dash 中工作。此外,最好将 -r 添加到您的 read 命令中,这样它就不会意外吃掉 \ 个字符;前置 IFS= 以保留前导和尾随空格。
事实上,您的代码仍然会因为 echo 而中断:如果一行包含 -e-n-E,则不会显示。要解决此问题,您必须使用 printf: printf '%s\n' "$line"。我没有将它包含在我之前的编辑中……当我修复此错误 :( 时,我的编辑经常被回滚。
不,它不会失败。如果第一个参数是 '%s\n',则 -- 无用
你的回答对我来说很好(我的意思是没有错误或我不再知道的不需要的功能)——尽管它不像 Perl 那样处理多个参数。事实上,如果您想处理多个参数,您最终会写出 Jonathan Leffler 的出色答案 — 实际上您的答案会更好,因为您将 IFS=readprintf 而不是 echo 一起使用。 :)
P
Peter Mortensen

我认为这是直接的方法:

$ 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

这不符合海报从标准输入或文件参数读取的要求,这只是从标准输入读取。
抛开@nash 的有效反对意见:read默认情况下从标准输入读取,因此< /dev/stdin不需要
C
Community

每当 IFS 中断输入流时,echo 解决方案都会添加新行。 @fgm's answer 可以稍作修改:

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

您能否解释一下“每当 IFS 中断输入流时,回声解决方案会添加新行”是什么意思?如果您指的是 read 的行为:而 read 确实 可能会被字符拆分为多个标记。包含在 $IFS 中,如果您只指定一个 single 变量名(但默认修剪和前导和尾随空格),它只会返回一个 single 标记。
@mklement0 我 100% 同意您对 read$IFS 的行为 - echo 本身添加了没有 -n 标志的新行。 “echo 实用程序将任何指定的操作数写入标准输出,由单个空白 (` ') 字符分隔并后跟换行符 (`\n') 字符。”
知道了。但是,要模拟 Perl 循环,您需要echo 添加的尾随 \n:Perl 的 $_ 包括从读取的行中以 \n 结尾的行,而 bash 的 read 没有。 (但是,正如@gniourf_gniourf 在其他地方指出的那样,更可靠的方法是使用 printf '%s\n' 代替 echo)。
J
Jonathan Leffler

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

尽管经常被准确地嘲笑为 UUOCcat 的无用使用),但有时 cat 是完成这项工作的最佳工具,并且可以说这是其中之一:

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

唯一的缺点是它创建了一个在子 shell 中运行的管道,因此 while 循环中的变量赋值之类的东西在管道之外是不可访问的。解决此问题的 bash 方法是 Process Substitution

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

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


多个文件的优点。我不知道对资源和性能的影响是什么,但如果你不在 bash、ksh 或 zsh 上,因此不能使用进程替换,你可以尝试使用命令替换的 here-doc(分布在 3行)>>EOF\n$(cat "$@")\nEOF。最后,一个小问题:while IFS= read -r linewhile (<>) 在 Perl 中所做的更好的近似(保留前导和尾随空格 - 尽管 Perl 也保留尾随 \n)。
g
gniourf_gniourf

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。感谢您分享您的答案,对不起,它没有得到应有的爱!
s
sorpigal

更精确地...

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

我认为这本质上是对 stackoverflow.com/a/6980232/45375 的评论,而不是答案。要明确注释:将 IFS=-r 添加到 read 命令可确保读取每一行 未修改(包括前导和尾随空格)。
g
gniourf_gniourf

请尝试以下代码:

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

请注意,即使经过修改,这也不会从标准输入或多个文件中读取,因此它不是问题的完整答案。 (在答案首次提交 3 年多之后,在几分钟内看到两次编辑也令人惊讶。)
@JonathanLeffler 很抱歉编辑了这样一个旧(而且不是很好)的答案……但我无法忍受看到这个可怜的 read 没有 IFS=-r,而可怜的 $line 没有它的健康引用。
@gniourf_gniourf:我不喜欢 read -r 符号。 IMO,POSIX 弄错了;该选项应该启用尾部反斜杠的特殊含义,而不是禁用它 - 这样现有脚本(从 POSIX 存在之前)不会因为省略 -r 而中断。然而,我观察到它是 IEEE 1003.2 1992 的一部分,这是 POSIX shell 和实用程序标准的最早版本,但即使在那时它也被标记为附加,所以这是对久违的机会的抱怨。我从来没有遇到过麻烦,因为我的代码不使用 -r;我一定很幸运。这点不理我。
@JonathanLeffler 我真的同意 -r 应该是标准的。我同意在不使用它会导致麻烦的情况下不太可能。但是,损坏的代码就是损坏的代码。我的编辑首先是由严重错过其引号的可怜的 $line 变量触发的。我在处理 read 时修复了它。我没有修复 echo,因为那是一种会被回滚的编辑。 :(
它是如何工作的? IFS= 是什么东西?为什么有必要?有一些信息in a comment
P
Peter Mortensen

我结合了上述所有答案,并创建了一个适合我需要的 shell 函数。这是来自我的两台 Windows 10 机器的 Cygwin 终端,我在它们之间有一个共享文件夹。我需要能够处理以下问题:

猫文件.cpp |发送

TX <文件.cpp

TX 文件.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
}

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


P
Peter Mortensen
#!/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

C
Chiarcos

两种原则方式:

将参数文件和标准输入通过管道传输到单个流中,然后像标准输入一样处理(流方法)

或将标准输入(和参数文件)重定向到命名管道并像文件一样处理(文件方法)

流式方法

对早期答案的小修改:

使用猫,而不是更少。它更快,您不需要分页。

使用 $1 读取第一个参数文件(如果存在)或 $* 读取所有文件(如果存在)。如果这些变量为空,则从标准输入读取(就像 cat 一样)#!/bin/bash cat $* | ...

文件方法

写入命名管道有点复杂,但这允许您将标准输入(或文件)视为单个文件:

使用 mkfifo 创建管道。

并行化写作过程。如果命名管道没有被读取,它可能会阻塞。

要将标准输入重定向到子进程(在这种情况下是必要的),请使用 <&0 (与其他人评论的不同,这里不是可选的)。 #!/bin/bash mkfifo /tmp/myStream cat $* <&0 > /tmp/myStream & # 分离子进程 (!) AddYourCommandHere /tmp/myStream # 像文件一样处理输入,rm /tmp/myStream # 清理

文件方法:变化

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

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

P
Peter Mortensen

代码 ${1:-/dev/stdin} 只会理解第一个参数,因此您可以使用它:

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

M
Marinos An

从标准输入读入变量或从文件读入变量。

现有答案中的大多数示例使用从标准输入读取的每一行立即回显的循环。这可能不是您真正想要做的。

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

让我们看一个例子。下面的脚本打印作为文件或通过标准输入传递的证书(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: ....
# ....


c
cmcginty

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

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

P
Peter Mortensen

以下内容适用于标准 sh(在 Debian 上使用 Dash 测试)并且可读性很强,但这只是个人喜好问题:

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

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


恕我直言,这是最好的答案,因为它指向了真正的解决方案:cat "${1:--}" | any_command。读取 shell 变量并回显它们可能适用于小文件,但不能很好地扩展。
[ -n "$1" ] 可以简化为 [ "$1" ]
P
Peter Mortensen

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

P
Peter Mortensen

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

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

P
Peter Mortensen

和...

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

我得到以下输出:

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

然后决定:

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

P
Peter Mortensen

利用:

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

cat 的输出将被放入命令行。命令行具有最大大小。这也不会逐行读取,而是逐字读取。