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

246

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

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

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

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

Даган
источник

Ответы:

411

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

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

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

Фриц Г. Мехнер
источник
1
Хорошо, это работает. Другой вопрос, почему вы добавляете цитату за это? «$ {1: - / proc / $ {$} / fd / 0}»
Даганг,
15
Имя файла, которое вы указываете в командной строке, может содержать пробелы.
Фриц Г. Мехнер
3
Есть ли разница между использованием /proc/$$/fd/0и /dev/stdin? Я заметил, что последнее кажется более распространенным и выглядит более простым.
ноу
19
Лучше добавить -rк своей readкоманде, чтобы она случайно не съела \ символы; использовать while IFS= read -r lineдля сохранения начальных и конечных пробелов.
mklement0
1
@NeDark: это любопытно; Я только что проверил, что он работает на этой платформе, даже при использовании /bin/sh- вы используете оболочку, отличную от bashили sh?
mklement0
119

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

#!/bin/bash
less <&0

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

Узнайте больше о перенаправлении файловых дескрипторов .

Райан Баллантайн
источник
1
Хотел бы я иметь больше голосов, чтобы дать вам, я искал это годами.
Маркус Даунинг
13
Использование <&0в этой ситуации не имеет смысла - ваш пример будет работать одинаково с ним или без него - по-видимому, инструменты, которые вы вызываете из скрипта bash по умолчанию, видят тот же stdin, что и сам скрипт (если скрипт не использует его первым).
mklement0
@ mkelement0 Итак, если инструмент читает половину входного буфера, получит ли следующий инструмент, который я вызову, остаток?
Асад Саидуддин
"Отсутствует имя файла (" less --help "для справки)", когда я делаю это ... Ubuntu 16.04
OmarOthman
5
где часть "или из файла" в этом ответе?
Себастьян
85

Вот самый простой способ:

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

Для чтения из файла или стандартного ввода (если аргумент отсутствует), вы можете расширить его до:

#!/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который его поддерживает. Смотри: как мне эхо "-е"?

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

kenorb
источник
Вы могли бы упростить [ "$1" ] && FILE=$1 || FILE="-"до FILE=${1:--}. (Каламбур: лучше , чтобы избежать всех заглавных оболочки переменных в конфликт имен избежать с окружающей средой переменных.)
mklement0
С удовольствием; на самом деле, ${1:--} это POSIX-совместимый, поэтому он должен работать во всех POSIX-подобных оболочках. То, что не будет работать во всех таких оболочках, это подстановка процесса ( <(...)); это будет работать в bash, ksh, zsh, но не в dash, например. Кроме того, лучше добавить -rк вашей readкоманде, чтобы она не случайно съела \ символы; готовьтесь IFS= сохранить ведущие и конечные пробелы.
mklement0
4
На самом деле ваш код еще ломается из - за echo: если строка состоит из -e, -nили -E, оно не будет показано. Чтобы исправить это, вы должны использовать printf: printf '%s\n' "$line". Я не включал его в свое предыдущее редактирование… слишком часто мои исправления отменяются, когда я исправляю эту ошибку :(.
gniourf_gniourf
1
Нет, это не подведет. И --бесполезно, если первый аргумент'%s\n'
gniourf_gniourf
1
Ваш ответ меня устраивает (я имею в виду, что я не знаю больше никаких ошибок или нежелательных функций), хотя он не обрабатывает множественные аргументы, как это делает Perl. На самом деле, если вы хотите обработать несколько аргументов, вы в конечном итоге напишете превосходный ответ Джонатана Леффлера - на самом деле ваш будет лучше, если вы будете использовать IFS=с readи printfвместо echo. :),
gniourf_gniourf
19

Я думаю, что это прямой путь:

$ 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
Это не соответствует требованию автора для чтения из stdin или аргумента файла, это только чтение из stdin.
Наш
3
Оставляя @ уважительной возражение Нэша в сторону: readчитает из стандартного ввода по умолчанию , так что нет никакой необходимости в < /dev/stdin.
mklement0
13

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

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"
Дэвид Соутер
источник
Не могли бы вы объяснить, что вы имеете в виду, когда «эхо-решение добавляет новые строки всякий раз, когда IFS прерывает входной поток»? В случае , если вы имели в виду read«s поведение: в то время как read это потенциально разделить на несколько лексем со стороны символов. содержащийся в нем $IFS, он возвращает только один токен, если вы указываете только одно имя переменной (но по умолчанию обрезает начальные и конечные пробелы).
mklement0
@ mklement0 Я согласен на 100% с тобой в поведении readи $IFS- echoсам добавляет новые строки без -nфлага. «Утилита echo записывает все указанные операнды, разделенные одиночными пустыми (` ') символами и сопровождаемые символом новой строки (`\ n'), в стандартный вывод."
Дэвид Саутер
Понял. Однако, чтобы эмулировать цикл Perl, вам нужно\n добавить трейлинг echo: Perl $_ включает в себя строку, заканчивающуюся \nпрочитанной строкой, а bash - readнет. (Однако, как указывает @gniourf_gniourf в другом месте, более надежный подход - использовать printf '%s\n'вместо echo).
mklement0
8

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

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

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

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

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

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

Джонатан Леффлер
источник
1
Отличное замечание о нескольких файлах. Я не знаю, как это повлияет на ресурсы и производительность, но если вы не используете bash, ksh или zsh и, следовательно, не можете использовать подстановку процессов, вы можете попробовать здесь документ с подстановкой команд (разбросано по 3 линии) >>EOF\n$(cat "$@")\nEOF. Наконец, придира: while IFS= read -r lineлучшее приближение к тому, что while (<>)делает в Perl (сохраняет начальные и конечные пробелы - хотя Perl также сохраняет конечные \n).
mklement0
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
gniourf_gniourf
источник
2

Точнее...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file
sorpigal
источник
2
Я предполагаю, что это, по сути, комментарий на stackoverflow.com/a/6980232/45375 , а не ответ. Чтобы сделать комментарий явным: добавление IFS=и -r к readкоманде гарантирует, что каждая строка читается без изменений (включая начальные и конечные пробелы).
mklement0
2

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

while IFS= read -r line; do
    echo "$line"
done < file
Webthusiast
источник
1
Обратите внимание, что даже с внесенными в него поправками он не будет считываться из стандартного ввода или из нескольких файлов, поэтому он не является полным ответом на вопрос. (Также удивительно видеть два изменения за считанные минуты более чем через 3 года после того, как ответ был впервые представлен.)
Джонатан Леффлер
@JonathanLeffler извините за редактирование такого старого (и не очень хорошего) ответа… но я не мог видеть этого бедного readбез IFS=и -r, и бедного $lineбез здоровых цитат.
gniourf_gniourf
1
@gniourf_gniourf: мне не нравятся read -rобозначения. ИМО, POSIX понял это неправильно; эта опция должна включать специальное значение для конечных обратных слешей, а не отключать его, чтобы существующие скрипты (до появления POSIX) не ломались, потому что -rбыл опущен. Однако я замечаю, что это было частью IEEE 1003.2 1992 года, который был самой ранней версией стандарта оболочки и утилит POSIX, но даже тогда он был отмечен как дополнение, так что это вызывает недовольство давно ушедшими возможностями. Я никогда не сталкивался с неприятностями, потому что мой код не использует -r; Мне должно быть повезло. Не обращай на это внимания.
Джонатан Леффлер
1
@JonathanLeffler Я действительно согласен, что -rдолжно быть стандартным. Я согласен, что это вряд ли произойдет в тех случаях, когда неиспользование этого приводит к проблемам. Тем не менее, неработающий код - это неработающий код. Мое редактирование было сначала вызвано этой плохой $lineпеременной, которая сильно пропустила свои кавычки. Я исправил, readпока я был на этом. Я не исправил, echoпотому что это вид редактирования, который откатывается. :(,
gniourf_gniourf
1

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

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done
Такахиро Онодера
источник
1

Я не считаю ни один из этих ответов приемлемым. В частности, принятый ответ обрабатывает только первый параметр командной строки и игнорирует остальные. Программа 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
Gungwald
источник
1

Я объединил все вышеперечисленные ответы и создал функцию оболочки, которая бы соответствовала моим потребностям. Это с терминала cygwin моих 2-х компьютеров с 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
}

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

truthadjustr
источник
0

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

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

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

Notinlist
источник
ИМХО лучший ответ так потому что он указывает на верное решение cat "${1:--}" | any_command. Чтение переменных оболочки и их отображение могут работать для небольших файлов, но не так хорошо масштабируются.
Андреас Шпиндлер
[ -n "$1" ]Может быть упрощена [ "$1" ].
АРУ
0

Этот простой в использовании на терминале:

$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3
cmcginty
источник
-1

Как насчет

for line in `cat`; do
    something($line);
done
Чарльз Купер
источник
Вывод catбудет помещен в командную строку. Командная строка имеет максимальный размер. Также это будет читать не построчно, а слово за словом.
Notinlist