简体   繁体   English

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

[英]How to read from a file or standard input in Bash

The following Perl script ( my.pl ) can read from either the file in the command line arguments or from standard input (STDIN):以下 Perl 脚本 ( my.pl ) 可以从命令行参数中的文件或标准输入(STDIN) 中读取:

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

perl my.pl will read from standard input, while perl my.pl a.txt will read from a.txt . perl my.pl将从标准输入读取,而perl my.pl a.txt将从a.txt读取。 This is very handy.这非常方便。

Is there an equivalent in Bash? Bash 中是否有等价物?

The following solution reads from a file if the script is called with a file name as the first parameter $1 and otherwise from standard input.如果使用文件名作为第一个参数$1调用脚本,则以下解决方案从文件中读取,否则从标准输入中读取。

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

The substitution ${1:-...} takes $1 if defined.如果已定义,替换${1:-...}需要$1 Otherwise, the file name of the standard input of the own process is used.否则,使用自己进程的标准输入的文件名。

Perhaps the simplest solution is to redirect standard input with a merging redirect operator:也许最简单的解决方案是使用合并重定向运算符重定向标准输入:

#!/bin/bash
less <&0

Standard input is file descriptor zero.标准输入是文件描述符零。 The above sends the input piped to your bash script into less's standard input.以上将通过管道传输到 bash 脚本的输入发送到less 的标准输入中。

Read more about file descriptor redirection . 阅读有关文件描述符重定向的更多信息

Here is the simplest way:这是最简单的方法:

#!/bin/sh
cat -

Usage:用法:

$ echo test | sh my_script.sh
test

To assign stdin to the variable, you may use: STDIN=$(cat -) or just simply STDIN=$(cat) as operator is not necessary (as per @mklement0 comment ).要将stdin分配给变量,您可以使用: STDIN=$(cat -)或只是简单的STDIN=$(cat)因为不需要运算符(根据@mklement0 注释)。


To parse each line from the standard input , try the following script:要解析标准输入中的每一行,请尝试以下脚本:

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

To read from the file or stdin (if argument is not present), you can extend it to:要从文件或标准输入中读取(如果参数不存在),您可以将其扩展为:

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

Notes:笔记:

- read -r - Do not treat a backslash character in any special way. - read -r - 不要以任何特殊方式处理反斜杠字符。 Consider each backslash to be part of the input line.将每个反斜杠视为输入行的一部分。

- Without setting IFS , by default the sequences of Space and Tab at the beginning and end of the lines are ignored (trimmed). - 如果没有设置IFS ,默认情况下,行首和行尾的SpaceTab序列将被忽略(修剪)。

- Use printf instead of echo to avoid printing empty lines when the line consists of a single -e , -n or -E . - 当行由单个-e-n-E组成时,使用printf而不是echo以避免打印空行。 However there is a workaround by using env POSIXLY_CORRECT=1 echo "$line" which executes your external GNU echo which supports it.但是,有一种解决方法是使用env POSIXLY_CORRECT=1 echo "$line"执行支持它的外部GNU echo See: How do I echo "-e"?请参阅:如何回显“-e”?

See: How to read stdin when no arguments are passed?请参阅:如何在不传递参数时读取标准输入? at stackoverflow SE在 stackoverflow SE

I think this is the straightforward way:我认为这是直接的方法:

$ 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

The echo solution adds new lines whenever IFS breaks the input stream.每当IFS中断输入流时, echo解决方案都会添加新行。 @fgm's answer can be modified a bit: @fgm 的答案可以稍微修改一下:

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

The Perl loop in the question reads from all the file name arguments on the command line, or from standard input if no files are specified.问题中的 Perl 循环从命令行上的所有文件名参数中读取,如果未指定文件,则从标准输入中读取。 The answers I see all seem to process a single file or standard input if there is no file specified.如果没有指定文件,我看到的所有答案似乎都在处理单个文件或标准输入。

Although often derided accurately as UUOC (Useless Use of cat ), there are times when cat is the best tool for the job, and it is arguable that this is one of them:虽然经常被准确地嘲笑为UUOCcat无用使用),但有时cat是完成这项工作的最佳工具,并且可以说这是其中之一:

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

The only downside to this is that it creates a pipeline running in a sub-shell, so things like variable assignments in the while loop are not accessible outside the pipeline.唯一的缺点是它创建了一个在子 shell 中运行的管道,因此在管道外部无法访问while循环中的变量赋值等内容。 The bash way around that is Process Substitution : bash方法是Process Substitution

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

This leaves the while loop running in the main shell, so variables set in the loop are accessible outside the loop.这使得while循环在主 shell 中运行,因此循环中设置的变量可以在循环外访问。

Perl's behavior, with the code given in the OP can take none or several arguments, and if an argument is a single hyphen - this is understood as stdin. Perl 的行为,OP 中给出的代码可以不带参数,也可以带多个参数,如果参数是单个连字符-这被理解为标准输入。 Moreover, it's always possible to have the filename with $ARGV .此外,始终可以将文件名与$ARGV None of the answers given so far really mimic Perl's behavior in these respects.到目前为止给出的答案都没有真正模仿 Perl 在这些方面的行为。 Here's a pure Bash possibility.这是一个纯粹的 Bash 可能性。 The trick is to use exec appropriately.诀窍是适当地使用exec

#!/bin/bash

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

Filename's available in $1 .文件名在$1可用。

If no arguments are given, we artificially set - as the first positional parameter.如果没有给出参数,我们人为设置-作为第一个位置参数。 We then loop on the parameters.然后我们循环参数。 If a parameter is not - , we redirect standard input from filename with exec .如果参数不是- ,我们使用exec从文件名重定向标准输入。 If this redirection succeeds we loop with a while loop.如果此重定向成功,我们将使用while循环进行循环。 I'm using the standard REPLY variable, and in this case you don't need to reset IFS .我使用的是标准的REPLY变量,在这种情况下,您不需要重置IFS If you want another name, you must reset IFS like so (unless, of course, you don't want that and know what you're doing):如果你想要另一个名字,你必须像这样重置IFS (当然,除非你不想要那个并且知道你在做什么):

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

Please try the following code:请尝试以下代码:

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

More accurately...更精确地...

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

I combined all of the above answers and created a shell function that would suit my needs.我结合了上述所有答案并创建了一个适合我需要的 shell 函数。 This is from a Cygwin terminal of my two Windows 10 machines where I had a shared folder between them.这是来自我的两台 Windows 10 机器的 Cygwin 终端,我在它们之间有一个共享文件夹。 I need to be able to handle the following:我需要能够处理以下问题:

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

Where a specific filename is specified, I need to used the same filename during copy.在指定特定文件名的地方,我需要在复制过程中使用相同的文件名。 Where input data stream has been piped through, then I need to generate a temporary filename having the hour minute and seconds.在输入数据流通过管道传输的地方,我需要生成一个包含小时分和秒的临时文件名。 The shared mainfolder has subfolders of the days of the week.共享的主文件夹具有一周中各天的子文件夹。 This is for organizational purposes.这是出于组织目的。

Behold, the ultimate script for my needs:看哪,满足我需求的最终脚本:

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
}

If there is any way that you can see to further optimize this, I would like to know.如果有任何方法可以看到进一步优化这一点,我想知道。

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

Running:跑步:

echo var var2 | bash std.sh

Result:结果:

var var2

Running:跑步:

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

Result:结果:

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

The code ${1:-/dev/stdin} will just understand the first argument, so you can use this:代码${1:-/dev/stdin}只会理解第一个参数,因此您可以使用它:

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

I don't find any of these answers acceptable.我认为这些答案中没有任何一个是可以接受的。 In particular, the accepted answer only handles the first command line parameter and ignores the rest.特别是,接受的答案仅处理第一个命令行参数并忽略其余部分。 The Perl program that it is trying to emulate handles all the command line parameters.它试图模拟的 Perl 程序处理所有命令行参数。 So the accepted answer doesn't even answer the question.所以接受的答案甚至没有回答这个问题。

Other answers use Bash extensions, add unnecessary 'cat' commands, only work for the simple case of echoing input to output, or are just unnecessarily complicated.其他答案使用 Bash 扩展,添加不必要的“cat”命令,仅适用于将输入回显到输出的简单情况,或者只是不必要地复杂。

However, I have to give them some credit, because they gave me some ideas.但是,我必须给他们一些荣誉,因为他们给了我一些想法。 Here is the complete answer:这是完整的答案:

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

Two principle ways:两种原则方式:

  • Either pipe the argument files and stdin into a single stream and process that like stdin ( stream approach )要么将参数文件和标准输入管道传输到一个单一的流中,然后像标准输入一样处理(流方法
  • Or redirect stdin (and argument files) into a named pipe and process that like a file ( file approach )或者将标准输入(和参数文件)重定向到一个命名管道和进程,就像一个文件(文件方法

Stream approach流方法

Minor revisions to earlier answers:对早期答案的小修改:

  • Use cat , not less .cat ,不能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).使用$1从第一个参数文件(如果存在)或$*读取所有文件(如果存在)。 If these variables are empty, read from stdin (like cat does)如果这些变量为空,则从 stdin 读取(就像cat一样)

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

File approach文件方式

Writing into a named pipe is a bit more complicated, but this allows you to treat stdin (or files) like a single file:写入命名管道有点复杂,但这允许您将标准输入(或文件)视为单个文件:

  • Create pipe with mkfifo .使用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).要将 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

File approach: Variation文件方法:变化

Create named pipe only if no arguments are given.仅当没有给出参数时才创建命名管道。 This may be more stable for reading from files as named pipes can occasionally block.这对于从文件中读取可能更稳定,因为命名管道偶尔会阻塞。

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

Also, it allows you to iterate over files and stdin rather than concatenate all into a single stream:此外,它还允许您遍历文件和标准输入,而不是将所有内容连接到一个流中:

for file in $FILES; do
    AddYourCommandHere $file
done

The following works with standard sh (tested with Dash on Debian) and is quite readable, but that's a matter of taste:以下内容适用于标准sh (在 Debian 上用Dash测试)并且非常易读,但这是一个品味问题:

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

Details: If the first parameter is non-empty then cat that file, else cat standard input.详细信息:如果第一个参数非空,则cat该文件,否则cat标准输入。 Then the output of the whole if statement is processed by the commands_and_transformations .然后整个if语句的输出由commands_and_transformations处理。

This one is easy to use on the terminal:这个很容易在终端上使用:

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

As a workaround, you can use the stdin device in the /dev directory:作为解决方法,您可以使用/dev目录中的stdin设备:

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

With...和...

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

I got the following output:我得到以下输出:

Ignored 1265 characters from standard input.忽略标准输入中的 1265 个字符。 Use "-stdin" or "-" to tell how to handle piped input.使用“-stdin”或“-”来告诉如何处理管道输入。

Then decided with for:然后决定使用 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

Use:采用:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM