Перебирая содержимое файла в Bash

1392

Как мне перебрать каждую строку текстового файла с помощью 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
О, я вижу, что здесь произошло много вещей: все комментарии были удалены, и вопрос был вновь открыт. Просто для справки: принятый ответ в строке «Чтение файла», присваивающий значение переменной, решает проблему каноническим способом и должен быть предпочтительнее, чем принятый здесь.
Федорки "ТАК прекрати вредить"

Ответы:

2100

Один из способов сделать это:

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 перенаправляется на стандартный ввод и как-то на весь блок while?
Питер Мортенсен
11
Msgstr "Вставьте peptides.txt в этот цикл while, чтобы команде read было что потреблять". Мой метод "кошка" похож, посылая вывод команды в блок while для потребления "read", только он запускает другую программу для выполнения работы.
Уоррен Янг
8
Этот метод, кажется, пропускает последнюю строку файла.
xastor
5
Двойные кавычки строк! эхо "$ p" и файл .. поверьте мне, это укусит вас, если вы этого не сделаете !!! Я ЗНАЮ! LOL
Майк Q
5
Обе версии не могут прочитать последнюю строку, если она не заканчивается новой строкой. Всегда используйтеwhile read p || [[ -n $p ]]; do ...
Dawg
449
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 / mirroring / bashfaq / 024 ). Это может быть очень раздражающим (в зависимости от того, что вы пытаетесь сделать в цикле).
Гордон Дэвиссон
25
Я использую "cat file |" как начало многих моих команд исключительно потому, что я часто создаю прототип с помощью "head file |"
Мат Келси
62
Это может быть не так эффективно, но гораздо более читабельно, чем другие ответы.
Savage Reader
145

Вариант 1а: цикл «цикл»: по одной строке: перенаправление ввода

#!/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 здесь представляется единственным выходом?
user5359531
87

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

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

Или, если вы намереваетесь использовать это как потоковый редактор (learn 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

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

Удачи!

mightypile
источник
6
Bash $ (<peptides.txt), возможно, более элегантен, но все же неправильно, как правильно сказал Жоао, вы выполняете логику подстановки команд, где пробел или символ новой строки - это то же самое. Если в строке есть пробел, цикл выполняется ДВАЖДЫ или более для этой одной строки. Поэтому ваш код должен правильно читать: для слова в $ (<peptides.txt); делай .... Если ты точно знаешь, что пробелов нет, то строка равна слову, и ты в порядке.
maxpolk
2
@ JoaoCosta, maxpolk: Хорошие моменты, которые я не учел. Я отредактировал оригинальный пост, чтобы отразить их. Спасибо!
mightypile
2
Использование forделает входные токены / строки подчиненными расширениям оболочки, что обычно нежелательно; попробуйте это: for l in $(echo '* b c'); do echo "[$l]"; done- как вы увидите, *- хотя изначально он был заключен в кавычки - он расширяется до файлов в текущем каталоге.
mklement0
2
@dblanchard: последний пример, использующий $ IFS, должен игнорировать пробелы. Вы пробовали эту версию?
могучий
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 выполняется в текущей оболочке, а не в подоболочке, как в случае последней. См. Соответствующий пост . Переменная, измененная внутри цикла 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

Похожие сообщения:

codeforester
источник
Обратите внимание, что вместо command < input_filename.txtвас всегда можно сделать input_generating_command | commandилиcommand < <(input_generating_command)
masterxilo
1
Спасибо за чтение файла в массив. Именно то, что мне нужно, потому что мне нужно, чтобы каждая строка анализировалась дважды, добавлялась к новым переменным, выполняла некоторые проверки и т. Д.
frank_108
45

Используйте цикл while, например так:

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

Ноты:

  1. Если вы не установите IFSправильно, вы потеряете отступ.

  2. Вы почти всегда должны использовать опцию -r с read.

  3. Не читать строки с for

Jahid
источник
2
Почему -rвариант?
Дэвид С. Рэнкин
2
@ DavidC.Rankin Опция -r предотвращает обратную косую черту. Note #2это ссылка, где это подробно описано ...
Джахид
Объедините это с опцией «read -u» в другом ответе, и тогда все будет идеально.
Флорин Андрей
@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, вы должны использовать цикл while и иметь альтернативный тест для последней строки.

Вот методы, которые могут изменить файл (по сравнению с тем, что 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цикле.)

Больше на BASH FAQ

Dawg
источник
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
Этот ответ нуждается в предостережениях, упомянутых в ответе mightypile , и он может потерпеть неудачу, если какая-либо строка содержит метасимволы оболочки (из-за не заключенных в кавычки "$ x").
Тоби Спейт
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

Объявление переменной вне цикла, установка значения и использование его вне цикла требует выполнения синтаксиса <<< «$ (...)» . Приложение должно быть запущено в контексте текущей консоли. Кавычки вокруг команды сохраняют новые строки выходного потока.

Сопоставление цикла для подстрок затем считывает пару имя = значение , разделяет правую часть символа last = , удаляет первую кавычку, удаляет последнюю кавычку, у нас есть чистое значение, которое будет использоваться в другом месте.

Whome
источник
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
madD7
источник
1
Не делай этого. Циклический над номерами строк и выборка каждой строки путем sedили head+ tailявляется невероятно неэффективным, и, конечно , возникает вопрос , почему вы не просто использовать один из других решений здесь. Если вам нужно знать номер строки, добавьте счетчик в while read -rцикл или используйте, nl -baчтобы добавить префикс номера строки в каждую строку перед циклом.
Трипли
0

Мне нравится использовать xargsвместо while. xargsмощный и дружественный к командной строке

cat peptides.txt | xargs -I % sh -c "echo %"

С помощью xargsвы также можете добавить детализацию с помощью -tи проверку с помощью-p

hamou92
источник
-1

@Peter: Это может сработать для вас

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

Это вернет результат

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Алан Джебакумар
источник
11
Это очень плохо! Почему вы не читаете строки с «для» .
Федорки "ТАК прекратить вредить"
3
Этот ответ побеждает все принципы, изложенные в хороших ответах выше!
codeforester
3
Пожалуйста, удалите этот ответ.
Dawg
3
Ребята, не преувеличивайте. Ответ плохой, но, кажется, работает, по крайней мере, для простых случаев использования. Пока это предусмотрено, плохой ответ не лишает права ответа на существование.
Егор Ганс
3
@EgorHans, я категорически не согласен: смысл ответов - научить людей писать программы. Научить людей делать что-то, что, как вы знаете , вредно для них, а люди, которые используют их программное обеспечение (вводить ошибки / неожиданное поведение и т. Д.), Сознательно вредят другим. Ответ, о котором известно, что он вреден, не имеет «права на существование» в хорошо подготовленном учебном ресурсе (и его отстаивание - это именно то, что мы, те люди, которые голосуют и отмечают, должны здесь делать).
Чарльз Даффи