如何读取Bash中的文件或stdin?

时间:2011-08-08 09:27:56

标签: bash stdin

在Perl中,以下代码将从命令行args或stdin:

中指定的文件中读取
while (<>) {
   print($_);
}

这很方便。我只是想知道什么是从bash中读取文件或stdin的最简单方法。

19 个答案:

答案 0 :(得分:341)

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

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

如果另外定义,则替换${1:-...}需要$1 使用自己进程的标准输入的文件名。

答案 1 :(得分:93)

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

#!/bin/bash
less <&0

Stdin是文件描述符零。上面将输入管道传输给你的bash脚本到less的stdin。

Read more about file descriptor redirection

答案 2 :(得分:63)

这是最简单的方法:

#!/bin/sh
cat -

用法:

$ echo test | sh my_script.sh
test

要将 stdin 分配给变量,您可以使用:STDIN=$(cat -)或只是STDIN=$(cat),因为不需要运算符(根据@mklement0 comment)。< / p>


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

#!/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,默认情况下,行的开头和结尾的 Space Tab 的序列将被忽略(修剪)

     

- 当行包含单个printfecho-e时,使用-n代替-E以避免打印空行。但是,通过使用env POSIXLY_CORRECT=1 echo "$line"执行支持它的外部 GNU echo,有一种解决方法。请参阅:How do I echo "-e"?

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

答案 3 :(得分:14)

我认为这是直截了当的方式:

$ 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 :(得分:13)

只要echo打破输入流,IFS解决方案就会添加新行。 @fgm's answer可以稍微修改一下:

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

答案 5 :(得分:7)

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

虽然经常被嘲笑为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中运行,因此循环中设置的变量可以在循环外部访问。

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

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

答案 7 :(得分:2)

更准确......

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

答案 8 :(得分:2)

请尝试以下代码:

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

答案 9 :(得分:1)

代码${1:-/dev/stdin}只会理解第一个参数,那么,这个怎么样。

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

答案 10 :(得分:1)

我发现这些答案都不可接受。特别是,接受的答案仅处理第一个命令行参数并忽略其余部分。它试图模拟的Perl程序处理所有命令行参数。所以接受的答案甚至都没有回答这个问题。其他答案使用bash扩展名,添加不必要的“cat&#39;命令,仅适用于将输入回显到输出的简单情况,或者只是不必要的复杂。

然而,我必须给他们一些信任,因为他们给了我一些想法。这是完整的答案:

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

答案 11 :(得分:1)

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

答案 12 :(得分:1)

两种原则方式:

  • 要么将参数文件和标准输入管道传输到一个单一的流中,然后像标准输入那样处理(流方法
  • 或者将标准输入(和参数文件)重定向到一个命名管道并像文件一样处理(文件方法

流方法

对早期答案的小修改:

  • 使用cat,而不是less。它更快,您不需要分页。

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

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

文件方式

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

  • 使用 mkfifo 创建管道。

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

  • 要将标准输入重定向到子进程(在这种情况下是必要的),请使用 <&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

此外,它允许您遍历文件和标准输入,而不是将所有内容连接到一个流中:

for file in $FILES; do
    AddYourCommandHere $file
done

答案 13 :(得分:0)

以下适用于标准sh(在Debian上使用dash测试过)并且非常易读,但这是一个品味问题:

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

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

答案 14 :(得分:0)

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

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

答案 15 :(得分:0)

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

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

答案 16 :(得分:0)

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

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

答案 17 :(得分:0)

白...

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

我得到以下输出:
Ignored 1265 characters from standard input. Use "-stdin" or "-" to tell how to handle piped input.

然后决定为:

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

答案 18 :(得分:-1)

怎么样

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