Прокручування вмісту файлу в Bash


1387

Як я повторюю кожен рядок текстового файлу з Bash ?

За допомогою цього сценарію:

echo "Start!"
for p in (peptides.txt)
do
    echo "${p}"
done

Я отримую цей вихід на екрані:

Start!
./runPep.sh: line 3: syntax error near unexpected token `('
./runPep.sh: line 3: `for p in (peptides.txt)'

(Пізніше я хочу зробити щось складніше, $pніж просто вивести на екран.)


Змінна середовища SHELL становить (від env):

SHELL=/bin/bash

/bin/bash --version вихід:

GNU bash, version 3.1.17(1)-release (x86_64-suse-linux-gnu)
Copyright (C) 2005 Free Software Foundation, Inc.

cat /proc/version вихід:

Linux version 2.6.18.2-34-default (geeko@buildhost) (gcc version 4.1.2 20061115 (prerelease) (SUSE Linux)) #1 SMP Mon Nov 27 11:46:27 UTC 2006

Файл peptides.txt містить:

RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL

19
О, я бачу, що тут сталося багато речей: усі коментарі були видалені, а питання було відкрито. Щойно для довідки, прийнята відповідь у « Прочитати рядок файлу за рядком, присвоюючи значення змінній, вирішує проблему канонічним способом, і слід віддавати перевагу над прийнятою тут.
fedorqui 'ТАК перестаньте шкодити'

Відповіді:


2090

Один із способів зробити це:

while read p; do
  echo "$p"
done <peptides.txt

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

while IFS="" read -r p || [ -n "$p" ]
do
  printf '%s\n' "$p"
done < peptides.txt

За винятком випадків, якщо тіло циклу може читати зі стандартного вводу , ви можете відкрити файл за допомогою іншого дескриптора файлу:

while read -u 10 p; do
  ...
done 10<peptides.txt

Тут 10 - це лише довільне число (відмінне від 0, 1, 2).


7
Як слід інтерпретувати останній рядок? Файл peptides.txt переспрямовується на стандартний вхід і якось на весь блок часу?
Пітер Мортенсен

11
"Перетягніть peptides.txt в цей цикл, а в команді" читати "є що споживати." Мій метод "cat" схожий, і надсилає висновок команди в блок "time" для споживання "read" теж, тільки він запускає іншу програму, щоб завершити роботу.
Воррен Янг

8
Цей метод, здається, пропускає останній рядок файлу.
xastor

5
Подвійне цитування рядків !! відлуння "$ p" і файл .. повірте, він вас вкусить, якщо цього не зробите !!! Я ЗНАЮ! lol
Майк Q

5
Обидві версії не читають остаточний рядок, якщо він не закінчується новим рядком. Завжди використовуйтеwhile read p || [[ -n $p ]]; do ...
dawg

447
cat peptides.txt | while read line 
do
   # do something with $line here
done

і однолінійний варіант:

cat peptides.txt | while read line; do something_with_$line_here; done

Ці параметри будуть пропускати останній рядок файлу, якщо немає каналу останнього рядка.

Уникнути цього можна за допомогою наступного:

cat peptides.txt | while read line || [[ -n $line ]];
do
   # do something with $line here
done

68
Загалом, якщо ви використовуєте "cat" лише з одним аргументом, ви робите щось не так (або неоптимально).
JesperE

27
Так, це просто не так ефективно, як у Бруно, адже він запускає іншу програму, без потреби. Якщо ефективність має значення, зробіть це Бруно. Я пам'ятаю свій спосіб, тому що ви можете використовувати його з іншими командами, де синтаксис "переадресація з" не працює.
Воррен Янг

74
З цим є ще одна, більш серйозна проблема: оскільки цикл while є частиною конвеєра, він працює в нижній частині корпусу , і, отже, будь-які змінні, встановлені всередині циклу, втрачаються при його виході (див. Bash-hackers.org/wiki/doku). php / дзеркальне відображення / bashfaq / 024 ). Це може дуже дратувати (залежно від того, що ви намагаєтеся зробити в циклі).
Гордон Девіссон

25
Я використовую "cat file |" як початок багатьох моїх команд виключно тому, що я часто прототипую з "головним файлом |"
мат kelcey

62
Це може бути не так ефективно, але це набагато читабельніше, ніж інші відповіді.
Дивак-читач

144

Варіант 1a: Під час циклу: Один рядок за один раз: переадресація вводу

#!/bin/bash
filename='peptides.txt'
echo Start
while read p; do 
    echo $p
done < $filename

Варіант 1b: Цикл циклу: Поодинокий рядок:
Відкрийте файл, прочитайте з дескриптора файлу (у цьому випадку дескриптор файлу №4).

#!/bin/bash
filename='peptides.txt'
exec 4<$filename
echo Start
while read -u4 p ; do
    echo $p
done

Для варіанта 1b: чи потрібно дескриптор файлу знову закривати? Наприклад, петля може бути внутрішньою петлею.
Пітер Мортенсен

3
Дескриптор файлу буде очищений з виходами процесу. Явне закриття може бути зроблене для повторного використання номера fd. Щоб закрити fd, використовуйте інший exec із синтаксисом & -, наприклад: exec 4 <& -
Stan Graves

1
Дякую за Варіант 2. Я зіткнувся з величезними проблемами з Варіантом 1, тому що мені потрібно було читати зі stdin у циклі; у такому випадку Варіант 1 не працюватиме.
масго

4
Вам слід чіткіше зазначити, що Варіант 2 сильно не рекомендується . @masgo Варіант 1b повинен працювати в цьому випадку і може поєднуватися з синтаксисом переадресації входу з Варіанту 1a шляхом заміни done < $filenameна done 4<$filename(що корисно, якщо ви хочете прочитати ім'я файлу з параметра команди, і в цьому випадку його можна просто замінити $filenameна $1).
Егор Ганс

Мені потрібно перебирати вміст файлу, наприклад tail -n +2 myfile.txt | grep 'somepattern' | cut -f3, під час виконання команд ssh всередині циклу (споживає stdin); варіант 2 тут видається єдиним способом?
користувач5359531

85

Це не краще, ніж інші відповіді, але це ще один спосіб зробити роботу у файлі без пробілів (див. Коментарі). Я вважаю, що мені часто потрібні одне вкладиші, щоб викопати списки в текстових файлах без зайвого кроку використання окремих файлів скриптів.

for word in $(cat peptides.txt); do echo $word; done

Цей формат дозволяє мені розмістити все це в одному командному рядку. Змініть частину "echo $ word" на все, що завгодно, і ви можете видавати кілька команд, розділених крапками з комою. У наступному прикладі вміст файлу використовується як аргументи у двох інших сценаріях, які ви, можливо, написали.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done

Або якщо ви маєте намір використовувати це на зразок редактора потоків (вивчити sed), ви можете перенести вихід у інший файл наступним чином.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done > outfile.txt

Я використовував їх, як написано вище, тому що я використовував текстові файли, де я створив їх одним словом на рядок. (Див. Коментарі) Якщо у вас є пробіли, за якими ви не хочете ділити слова / рядки, це стає трохи негарніше, але ця ж команда все ще працює так:

OLDIFS=$IFS; IFS=$'\n'; for line in $(cat peptides.txt); do cmd_a.sh $line; cmd_b.py $line; done > outfile.txt; IFS=$OLDIFS

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

Удачі!


6
Баш $ (<peptides.txt), можливо, є більш елегантним, але все одно неправильно, те, що Джоао сказав правильно, ви виконуєте логіку підстановки команд, де пробіл або новий рядок - те саме. Якщо у рядку є пробіл, цикл виконує TWICE або більше для цього одного рядка. Отже, ваш код повинен правильно читати: для слова в $ (<peptides.txt); do .... Якщо ви фактично знаєте, що пробілів немає, то рядок дорівнює слову, і ви все в порядку.
maxpolk

2
@ JoaoCosta, maxpolk: Гарні моменти, які я не розглядав. Я відредагував оригінальну публікацію, щоб відобразити їх. Дякую!
Могутній

2
Використання forробить вхідні жетони / рядки предметом розширення оболонок, що зазвичай небажано; спробуйте це: for l in $(echo '* b c'); do echo "[$l]"; done- як ви побачите, *- навіть незважаючи на те, що спочатку цитується літерал - розширюється до файлів у поточному каталозі.
mklement0

2
@dblanchard: Останній приклад, використовуючи $ IFS, повинен ігнорувати пробіли. Ви спробували цю версію?
mightypile

4
Те, як ця команда стає набагато складнішою, як вирішуються вирішальні питання, дуже добре пояснює, чому використання forітерацій рядків файлів - погана ідея. Крім того, аспект розширення, згаданий у @ mklement0 (навіть незважаючи на те, що, ймовірно, його можна обійти, додавши пропущені цитати, що знову робить речі складнішими та менш читабельними).
Єгор Ганс

69

Ще кілька речей, не охоплених іншими відповідями:

Читання з розмежованого файлу

# ':' is the delimiter here, and there are three fields on each line in the file
# IFS set below is restricted to the context of `read`, it doesn't affect any other code
while IFS=: read -r field1 field2 field3; do
  # process the fields
  # if the line has less than three fields, the missing fields will be set to an empty string
  # if the line has more than three fields, `field3` will get all the values, including the third field plus the delimiter(s)
done < input.txt

Читання з виводу іншої команди, використовуючи підстановку процесу

while read -r line; do
  # process the line
done < <(command ...)

Цей підхід кращий, ніж command ... | while read -r line; do ...тому, що цикл while працює в поточній оболонці, а не в нижній частині, як у випадку з останньою. Дивіться пов’язаний пост Змінна, змінена в циклі часу, не запам'ятовується .

Наприклад, читання з нульовим обмеженим введенням, наприклад find ... -print0

while read -r -d '' line; do
  # logic
  # use a second 'read ... <<< "$line"' if we need to tokenize the line
done < <(find /path/to/dir -print0)

Пов’язане прочитання: BashFAQ / 020 - Як я можу знайти та безпечно обробити імена файлів, що містять нові рядки, пробіли чи обидва?

Читання з декількох файлів одночасно

while read -u 3 -r line1 && read -u 4 -r line2; do
  # process the lines
  # note that the loop will end when we reach EOF on either of the files, because of the `&&`
done 3< input1.txt 4< input2.txt

На основі @ chepner в відповідь тут :

-uє розширенням bash. Для сумісності з POSIX кожен виклик виглядатиме приблизно так read -r X <&3.

Читання цілого файлу в масив (версії Bash раніше до 4)

while read -r line; do
    my_array+=("$line")
done < my_file

Якщо файл закінчується неповним рядком (новий рядок відсутній в кінці), то:

while read -r line || [[ $line ]]; do
    my_array+=("$line")
done < my_file

Читання цілого файлу в масив (версії Bash 4x і пізніші)

readarray -t my_array < my_file

або

mapfile -t my_array < my_file

І потім

for line in "${my_array[@]}"; do
  # process the lines
done

Схожі повідомлення:


зауважте, що замість command < input_filename.txtвас завжди можна зробити input_generating_command | commandабоcommand < <(input_generating_command)
masterxilo

1
Дякуємо, що прочитали файл в масив. Саме те, що мені потрібно, тому що мені потрібно кожен рядок проаналізувати двічі, додати нові змінні, зробити деякі перевірки тощо
frank_108

45

Скористайтеся циклом часу, наприклад:

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

Примітки:

  1. Якщо встановити IFSнеправильно, ви втратите відступ.

  2. Ви майже завжди повинні використовувати параметр -r з читанням.

  3. Не читайте рядки за допомогою for


2
Чому -rваріант?
Девід К. Ранкін

2
@ DavidC.Rankin Опція -r перешкоджає інтерпретації зворотної косої риски. Note #2є посиланням, де це докладно описано ...
Джахід

Поєднайте це з опцією "прочитати -у" в іншій відповіді, і тоді це ідеально.
Флорін Андрій

@FlorinAndrei: Наведений вище приклад не потребує цього -uваріанту, ти говориш про інший приклад -u?
Джахід

Переглянув ваші посилання та був здивований, що у примітці 2 немає відповіді, яка просто посилається на ваше посилання. На цій сторінці ви знайдете все, що вам потрібно знати про цю тему. Або відповіді, що стосуються лише посилань, відволікають чи щось таке?
Єгор Ганс

14

Припустимо, у вас є цей файл:

$ cat /tmp/test.txt
Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR

Є чотири елементи, які змінять значення вихідного файлу, прочитаного багатьма рішеннями Bash:

  1. Порожній рядок 4;
  2. Провідні або кінцеві пробіли на двох лініях;
  3. Підтримка значення окремих рядків (тобто кожен рядок є записом);
  4. Рядок 6 не закінчується CR.

Якщо ви хочете, щоб текст текстовий файл був рядок, включаючи порожні рядки та кінцеві рядки без CR, ви повинні використовувати цикл час, і ви повинні мати альтернативний тест для остаточного рядка.

Ось методи, які можуть змінити файл (порівняно з тим, що catповертається):

1) Втратити останній рядок та провідні та кінцеві пробіли:

$ while read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'

(Якщо ви робите while IFS= read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txtзамість цього, ви зберігаєте провідні та кінцеві пробіли, але все одно втрачаєте останній рядок, якщо він не закінчується CR)

2) Використовуючи підстановку процесу на catзаповіт, читається весь файл одним глотком і втрачає значення окремих рядків:

$ for p in "$(cat /tmp/test.txt)"; do printf "%s\n" "'$p'"; done
'Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR'

(Якщо ви вилучите "з нього, $(cat /tmp/test.txt)ви прочитаєте файл слово за словом, а не одним глотком. Також, мабуть, не те, що призначено ...)


Найбільш надійний і найпростіший спосіб читати файл по черзі та зберігати всі інтервали:

$ while IFS= read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'    Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space    '
'Line 6 has no ending CR'

Якщо ви хочете зняти провідні та торгові простори, видаліть IFS=деталь:

$ while read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'
'Line 6 has no ending CR'

(Текстовий файл без закінчення \n, хоча досить поширений, вважається зламаним під POSIX. Якщо ви можете розраховувати на те, що \nвам не потрібно || [[ -n $line ]]в whileциклі, слід.)

Детальніше у FAQ FAQ


13

Якщо ви не хочете, щоб ваше читання було порушено символом нового рядка, використовуйте -

#!/bin/bash
while IFS='' read -r line || [[ -n "$line" ]]; do
    echo "$line"
done < "$1"

Потім запустіть скрипт з ім'ям файлу як параметром.


4
#!/bin/bash
#
# Change the file name from "test" to desired input file 
# (The comments in bash are prefixed with #'s)
for x in $(cat test.txt)
do
    echo $x
done

7
Ця відповідь потребує застережень, згаданих у відповіді могутнього , і вона може погано вийти з ладу, якщо будь-який рядок містить метахарактеристики оболонки (через нецитируваний "$ x").
Toby Speight

7
Я насправді здивований, що люди ще не придумали звичайного Не читайте рядки за ...
Егор Ганс

3

Ось мій приклад із реального життя, як циклічно виводити рядки другої програми, перевіряти наявність підрядків, скидати подвійні лапки зі змінної, використовувати цю змінну поза циклом. Думаю, досить багато хто ставить ці питання рано чи пізно.

##Parse FPS from first video stream, drop quotes from fps variable
## streams.stream.0.codec_type="video"
## streams.stream.0.r_frame_rate="24000/1001"
## streams.stream.0.avg_frame_rate="24000/1001"
FPS=unknown
while read -r line; do
  if [[ $FPS == "unknown" ]] && [[ $line == *".codec_type=\"video\""* ]]; then
    echo ParseFPS $line
    FPS=parse
  fi
  if [[ $FPS == "parse" ]] && [[ $line == *".r_frame_rate="* ]]; then
    echo ParseFPS $line
    FPS=${line##*=}
    FPS="${FPS%\"}"
    FPS="${FPS#\"}"
  fi
done <<< "$(ffprobe -v quiet -print_format flat -show_format -show_streams -i "$input")"
if [ "$FPS" == "unknown" ] || [ "$FPS" == "parse" ]; then 
  echo ParseFPS Unknown frame rate
fi
echo Found $FPS

Оголосити змінну поза циклом, встановити значення та використовувати його поза циклом потрібно виконати <<< "$ (...)" синтаксис . Додаток потрібно запускати в контексті поточної консолі. Котирування навколо команди зберігають нові рядки вихідного потоку.

Потім відповідність циклу для підрядів читає ім’я = пара значень , розбиває праву частину останнього = символ, опускає першу цитату, опускає останню цитату, у нас є чисте значення для використання в іншому місці.


3
Хоча відповідь правильна, я розумію, як це закінчилося тут. Основний метод такий же, як запропонований багатьма іншими відповідями. Крім того, він повністю заглушається у вашому прикладі FPS.
Єгор Ганс

0

Це приходить досить пізно, але з думкою, що це може комусь допомогти, я додаю відповідь. Також це може бути не найкращим способом. headкоманда може бути використана з -nаргументом для читання n рядків із початку файлу, а також tailкоманда може бути використана для читання знизу. Тепер, щоб отримати n-й рядок з файлу, ми заголовляємо n рядків , передаємо дані в хвіст лише 1 рядок з трубопровідних даних.

   TOTAL_LINES=`wc -l $USER_FILE | cut -d " " -f1 `
   echo $TOTAL_LINES       # To validate total lines in the file

   for (( i=1 ; i <= $TOTAL_LINES; i++ ))
   do
      LINE=`head -n$i $USER_FILE | tail -n1`
      echo $LINE
   done

1
Не робіть цього. Перебирання номерів рядків та отримання кожного окремого рядка за допомогою sedабо head+ tailє неймовірно неефективним, і, звичайно, виникає питання, чому ви просто не використовуєте одне з інших рішень тут. Якщо вам потрібно знати номер рядка, додайте до його while read -rциклу лічильник або використовуйте nl -baдля додавання префікса номер рядка до кожного рядка перед циклом.
трійка

-1

@Peter: Це може допомогти вам

echo "Start!";for p in $(cat ./pep); do
echo $p
done

Це поверне результат

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL


3
Ця відповідь перемагає всі принципи, встановлені гарними відповідями вище!
кодове літо

3
Видаліть цю відповідь.
dawg

3
Тепер хлопці, не перебільшуйте. Відповідь погана, але, здається, працює, принаймні для простих випадків використання. Поки це не передбачено, якщо помилкова відповідь не позбавляє права відповіді на існування.
Єгор Ганс

3
@EgorHans, я не погоджуюся: сенс відповідей полягає в тому, щоб навчити людей писати програмне забезпечення. Навчити людей робити речі таким чином, який, як ви знаєте , шкідливий для них, а люди, які використовують їх програмне забезпечення (вводячи помилки / несподівану поведінку тощо), свідомо шкодять іншим. Відповідь, яка, як відомо, шкідлива, не має "права на існування" на добре підготовленому навчальному ресурсі (а курація - це саме те, що ми, люди, які голосують і правлять тут, повинні робити).
Чарльз Даффі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.