Як читати з файлу або STDIN в Bash?


244

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

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

perl my.plбуде читати зі STDIN, а perl my.pl a.txtчитатиме з a.txt. Це дуже зручно.

Цікаво, чи є еквівалент у Bash?

Відповіді:


409

Наступне рішення зчитується з файлу, якщо сценарій викликається з ім'ям файлу як першим параметром, $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

Мабуть, найпростішим рішенням є перенаправлення stdin за допомогою оператора перенаправлення, що об'єднується:

#!/bin/bash
less <&0

Stdin - дескриптор файлу нульовий. Вищезгадане надсилає вхід, поданий у ваш bash-скрипт, у stdin менше.

Детальніше про перенаправлення дескриптора файлів .


1
Я б хотів, щоб у вас було більше грошей, які я вам дав, я шукав це протягом багатьох років.
Маркус Даунінг

13
Немає користі для використання <&0в цій ситуації - ваш приклад буде працювати однаково з ним або без нього - мабуть, інструменти, які ви запускаєте в рамках bash-скрипту, за замовчуванням бачать той самий stdin, що і сам сценарій (якщо сценарій спочатку не споживає його).
mklement0

@ mkelement0 Отже, якщо інструмент зчитує половину вхідного буфера, чи наступний інструмент, на який я посилаюсь, отримає решту?
Асад Саєдюддін

"Відсутнє ім'я файлу (" менше - допомога "для довідки)", коли я це роблю ... Ubuntu 16.04
OmarOthman

5
де в цій відповіді частина "або з файлу"?
Себастьян

84

Ось найпростіший спосіб:

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

Щоб прочитати з файлу або 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який його підтримує. Див.: Як я лунаю "-е"?

Див.: Як читати stdin, коли не передаються аргументи? при stackoverflow SE


Ви можете спростити [ "$1" ] && FILE=$1 || FILE="-"це FILE=${1:--}. (Quibble: краще уникати змінних оболонок верхнього регістру, щоб уникнути зіткнень імен зі змінними середовища .)
mklement0

Моє задоволення; на самому справі, ${1:--} є POSIX-сумісним, тому він повинен працювати у всіх POSIX-подібних оболонок. У всіх таких оболонках не буде працювати процес заміщення ( <(...)); Наприклад, він буде працювати в bash, ksh, zsh, але не в тирі. Крім того, краще додати -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.
наш

2
Залишаючи @ поважної заперечення Неша в сторону: 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прапора. "Утиліта ехо записує будь-які задані операнди, розділені одним порожнім (` `) символом і слідом за новим рядком (` \ 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 і тому не можете використовувати підстановку процесу, ви можете спробувати тут-doc з підстановкою команд (поширюється на 3 лінії) >>EOF\n$(cat "$@")\nEOF. Нарешті, прислівник: while IFS= read -r lineє кращим наближенням того, що while (<>)робиться в Perl (зберігає провідні та відсталі пробіли - хоча Perl також зберігає трейлінг \n).
mklement0

4

Поведінка Perl з кодом, наведеним в ОП, може приймати жоден або кілька аргументів, і якщо аргумент є одним дефісом, -це розуміється як stdin. Крім того, завжди можна мати ім'я файлу $ARGV. Жодна з наведених відповідей поки що наслідує поведінку Перла в цьому відношенні. Ось чиста можливість 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

2

Точніше ...

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

2
Я припускаю, що це по суті коментар до stackoverflow.com/a/6980232/45375 , а не відповідь. Для того, щоб зробити коментар явним: додавання IFS=і -r до readкомандним гарантує , що кожен рядок читається незміненій (включаючи початкові і кінцеві пробіли).
mklement0

2

Будь ласка, спробуйте наступний код:

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

1
Зауважте, що навіть із поправками це не читатиметься зі стандартного введення чи з декількох файлів, тому це не є повною відповіддю на питання. (Дивно також побачити дві зміни за лічені хвилини після того, як відповідь була вперше подана.)
Джонатан Леффлер

@JonathanLeffler вибачте за редагування такої старої (і не дуже хорошої) відповіді ... але я не витримав бачити цього бідного readбез IFS=і -r, а бідного $lineбез здорових цитат.
gniourf_gniourf

1
@gniourf_gniourf: Мені не подобається read -rпозначення. IMO, 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

1

Я поєднав усі вищезазначені відповіді і створив функцію оболонки, яка б відповідала моїм потребам. Це з терміналу 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
}

Якщо є якийсь спосіб, який ви бачите для подальшої оптимізації цього, я хотів би знати.


0

Наведені нижче роботи стандартні sh(Тестовано dashна Debian) і цілком читаються, але це питання смаку:

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

Деталі: Якщо перший параметр не порожній, то catцей файл, інше catстандартний ввід. Тоді висновок усього ifоператора обробляється commands_and_transformations.


ИМХО краща відповідь так , тому що це вказує на справжнє рішення: cat "${1:--}" | any_command. Читання змінних оболонок та повторення їх може працювати для невеликих файлів, але вони не так масштабуються.
Андреас Шпіндлер

[ -n "$1" ]Може бути спрощена [ "$1" ].
agc

0

Цей простий у використанні термінал:

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

-1

Як щодо

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

Вихідні дані catбудуть розміщені в командному рядку. Командний рядок має максимальний розмір. Також це буде читати не рядок за рядком, а слово за словом.
Notinlist
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.