Как читать из файла или STDIN в Bash?

Следующий скрипт Perl (my.pl) может читать либо из файла в аргументах командной строки, либо из STDIN:

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

perl my.pl будет читать из STDIN, а perl my.pl a.txt будет читать из a.txt. Это очень удобно.

Хотите знать, есть ли эквивалент в Bash?

Ответ 1

Следующее решение читается из файла, если вызывается script с именем файла в качестве первого параметра $1 в противном случае от стандартного ввода.

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

Подстановка ${1:-...} принимает $1, если определено иначе используется имя файла стандартного ввода собственного процесса.

Ответ 2

Возможно, самым простым решением является перенаправление stdin с помощью оператора перенаправления слияния:

#!/bin/bash
less <&0

Stdin - это дескриптор файла. Вышеуказанное отправляет входной канал на ваш bash script на менее стандартный.

Подробнее о перенастройке дескриптора файла.

Ответ 3

Вот простейший способ:

#!/bin/sh
cat -

Использование:

$ echo test | sh my_script.sh
test

Чтобы назначить stdin для переменной, вы можете использовать: STDIN=$(cat -) или просто просто STDIN=$(cat), поскольку оператор не нужен (согласно @mklement0 comment).


Чтобы проанализировать каждую строку со стандартного ввода, попробуйте выполнить script:

#!/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 в начале и конце строк игнорируются (обрезаются).

- используйте printf вместо echo, чтобы избежать печати пустых строк, когда строка состоит из одного -e, -n или -e. Однако есть временное решение, используя env POSIXLY_CORRECT=1 echo "$line", который выполняет внешний GNU echo, который его поддерживает. Смотрите: Как эхо "-e ​​" ?

Смотрите: Как читать stdin, когда аргументы не передаются? в stackoverflow SE

Ответ 4

Я думаю, что это прямолинейный путь:

$ 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

Ответ 5

Решение echo добавляет новые строки всякий раз, когда IFS прерывает входной поток. @fgm answer можно немного изменить:

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

Ответ 6

В цикле Perl в вопросе читаются все аргументы имени файла в командной строке или со стандартного ввода, если файлы не указаны. Ответы, которые я вижу, кажутся обработкой одного файла или стандартного ввода, если файл не указан.

Хотя часто высмеивается как UUOC (бесполезное использование cat), бывают случаи, когда cat - лучший инструмент для работа, и можно утверждать, что это один из них:

cat "[email protected]" |
while read -r line
do
    echo "$line"
done

Единственным недостатком этого является то, что он создает конвейер, работающий в под-оболочке, поэтому такие вещи, как присвоения переменных в цикле while, недоступны вне конвейера. bash способ Замена процесса:

while read -r line
do
    echo "$line"
done < <(cat "[email protected]")

Это оставляет цикл while, запущенный в основной оболочке, поэтому переменные, установленные в цикле, доступны вне цикла.

Ответ 7

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, и в этом случае вам не нужно reset IFS. Если вы хотите другое имя, вы должны reset IFS как это сделать (если, конечно, вы этого не хотите и знаете, что делаете):

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

Ответ 8

Пожалуйста, попробуйте следующий код:

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

Ответ 9

Более точно...

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

Ответ 10

Код ${1:-/dev/stdin} будет просто понимать первый аргумент, поэтому, как насчет этого.

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

Ответ 11

Я не считаю, что любые из этих ответов приемлемы. В частности, принятый ответ обрабатывает только первый параметр командной строки и игнорирует остальные. Программа 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 "[email protected]" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done

Ответ 12

Следующее работает со стандартным sh (протестировано с dash на Debian) и вполне читаемо, но это вопрос вкуса:

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

Подробности: если первый параметр не пуст, то cat этот файл, иначе cat стандартный ввод. Затем вывод целого оператора if обрабатывается commands_and_transformations.

Ответ 13

Я объединил все вышеперечисленные ответы и создал функцию оболочки, которая бы соответствовала моим потребностям. Это от терминала cygwin моих двух машин Windows10, где у меня была общая папка между ними. Мне нужно иметь возможность обрабатывать следующее:

  • 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
}

Если есть какой-то способ, который вы можете увидеть, чтобы еще больше оптимизировать это, я хотел бы знать.

Ответ 14

Как насчет

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