Прочитать последнюю строку файла bash
I have an . odd issue with a bash shell script that I was hoping to get some insight on.
My team is working on a script that iterates through lines in a file and checks for content in each one. We had a bug where, when run via the automated process that sequences different scripts together, the last line wasn't being seen.
The code used to iterate over the lines in the file (name stored in DATAFILE was
We could run the script from the command line and it would see every line in the file, including the last one, just fine. However, when run by the automated process (which runs the script that generates the DATAFILE just prior to the script in question), the last line is never seen.
We updated the code to use the following to iterate over the lines, and the problem cleared up:
Note: DATAFILE has no newline ever written at the end of the file.
My question is two part. Why would the last line not be seen by the original code, and why this would change make a difference?
I only thought I could come up with as to why the last line would not be seen was:
- The previous process, which writes the file, was relying on the process to end to close the file descriptor.
- The problem script was starting up and opening the file prior fast enough that, while the previous process had "ended", it hadn't "shut down/cleaned up" enough for the system to close the file descriptor automatically for it.
That being said, it seems like, if you have 2 commands in a shell script, the first one should be completely shut down by the time the script runs the second one.
Any insight into the questions, especially the first one, would be very much appreciated.
As workaround: add blank lines after cat, executed in a subshell: (cat "$DATAFILE"; echo "") | while read line
7 Answers 7
The C standard says that text files must end with a newline or the data after the last newline may not be read properly.
ISO/IEC 9899:2011 §7.21.2 Streams
A text stream is an ordered sequence of characters composed into lines, each line consisting of zero or more characters plus a terminating new-line character. Whether the last line requires a terminating new-line character is implementation-defined. Characters may have to be added, altered, or deleted on input and output to conform to differing conventions for representing text in the host environment. Thus, there need not be a one-to- one correspondence between the characters in a stream and those in the external representation. Data read in from a text stream will necessarily compare equal to the data that were earlier written out to that stream only if: the data consist only of printing characters and the control characters horizontal tab and new-line; no new-line character is immediately preceded by space characters; and the last character is a new-line character. Whether space characters that are written out immediately before a new-line character appear when read in is implementation-defined.
I would not have expected a missing newline at the end of file to cause trouble in bash (or any Unix shell), but that does seem to be the problem reproducibly ( $ is the prompt in this output):
It is also not limited to bash — Korn shell ( ksh ) and zsh behave like that too. I live, I learn; thanks for raising the issue.
As demonstrated in the code above, the cat command reads the whole file. The for line in `cat $DATAFILE` technique collects all the output and replaces arbitrary sequences of white space with a single blank (I conclude that each line in the file contains no blanks).
Tested on Mac OS X 10.7.5.
альтернативные версии
если ваш файл окажется огромным, вам лучше exit после прочтения нужной линии. Таким образом, вы экономите время процессора.
если вы хотите дать номер строки с переменной bash, вы можете использовать:
Вау, все возможности!
или один из них в зависимости от вашей версии Awk:
(возможно, вам придется попробовать nawk или gawk команда).
есть ли инструмент, который только печатает эту конкретную строку? Не один из стандартных инструментов. Однако, sed - Это, наверное, самый близкий и простой в использовании.
этот вопрос помечен как Bash, вот способ Bash (≥4): Используйте mapfile С -s (скип) и -n (count) вариант.
Если вам нужно получить 42-ю строку файла file :
в этот момент у вас будет массив ary поля которых содержат строки file (включая конечную новую строку), где мы пропустили первые 41 строку ( -s 41 ), и остановился после прочтения одной строки ( -n 1 ). Так что это действительно 42-я линия. Чтобы распечатать его:
Если вам нужен диапазон строк, скажите диапазон 42-666 (включительно) и скажите, что вы не хотите делать математику самостоятельно, и распечатайте их на stdout:
Если вам нужно обработать и эти строки, хранить конечную новую строку не очень удобно. В этом случае используйте (отделка):
вы можете иметь функцию сделать это для вас:
никаких внешних команды, только Bash builtins!
вы также можете использовать sed print и quit:
по моим тестам, с точки зрения производительности и читаемости моя рекомендация:
tail -n+N | head -1
tail -n+N будет печатать все, начиная с строки N и head -1 остановит его после одной строки.
альтернатива head -N | tail -1 возможно, немного более читаемый. Например, это будет печатать 7th строка:
head -7 input.txt | tail -1
когда дело доходит до производительности, нет большой разницы для меньших размеров, но она будет превзойдена tail | head (сверху), когда файлы становятся огромными.
топ-проголосовали sed 'NUMq;d' интересно знать, но я бы сказал, что это будет понято меньшим количеством людей из коробки, чем решение головы/хвоста, и это также медленнее, чем хвост/голова.
в моих тестах обе версии хвостов/голов превзошли sed 'NUMq;d' последовательно. Это соответствует другим контрольным показателям, которые были опубликованы. Трудно найти случай, когда хвосты/головы были действительно плохими. Это также неудивительно, так как это операции, которые вы ожидаете сильно оптимизировать в современной системе Unix.
чтобы получить представление о различиях в производительности, это число, которое я получаю огромный файл (9,3 г):
- tail -n+N | head -1 : 3.7 сек
- head -N | tail -1 : 4.6 сек
- sed Nq;d : 18.8 сек
результаты могут отличаться, но производительность head | tail и tail | head , в общем, сопоставимо для меньших входов и sed всегда медленнее значительным фактором (около 5x или около того).
чтобы воспроизвести мой бенчмарк, вы можете попробовать следующее, Но имейте в виду, что он создаст файл 9.3 G в текущем рабочем каталоге:
вот результат запуска на моей машине (ThinkPad X1 Углерод с SSD и 16G памяти). Я предполагаю, что в конечном итоге все выйдет из кэша, а не с диска:
Я пишу сценарий для чтения команд из файла и выполнения определенной команды. Я хочу, чтобы мой сценарий работал либо для одиночных входных аргументов, либо когда аргументом является имя файла, содержащее рассматриваемые аргументы.
Мой код ниже работает, за исключением одной проблемы, он игнорирует последнюю строку файла. Итак, если бы файл был таким.
Приведенный ниже сценарий запускает команду только для file.txt.
Конечно, есть очевидная работа, когда после цикла while я повторяю шаги внутри цикла, чтобы завершить эти команды с последним файлом, но это нежелательно, поскольку любые изменения, которые я делаю внутри цикла while, должны повторяться снова. вне цикла while. Я поискал в Интернете и не нашел никого, кто задает этот точный вопрос. Я уверен, что он там есть, но не нашел.
Результат, который я получаю, выглядит следующим образом.
Моя версия bash - 3.2.48
Похоже, в вашем входном файле отсутствует символ новой строки после последней строки. Когда read это происходит, он устанавливает переменную ( $line в данном случае) на последнюю строку, но затем возвращает ошибку конца файла, поэтому цикл не выполняется для этой последней строки. Чтобы обойти это, вы можете заставить цикл выполняться в случае read успеха или читать что-либо в $line :
РЕДАКТИРОВАТЬ: || условие in the while - это так называемое логическое значение короткого замыкания - оно пытается выполнить первую команду ( read line ), и если это удастся, оно пропускает вторую ( [[ -n "$line" ]] ) и проходит цикл (в основном, пока выполняется read успешно, он работает так же, как ваш исходный скрипт). В случае read сбоя, он проверяет вторую команду ( [[ -n "$line" ]] ) - если read что-либо считывается $line до достижения конца файла (то есть, если в файле была незавершенная последняя строка), это будет успешным, поэтому while условие в целом считается для успешного выполнения, и цикл запускается еще раз.
После обработки последней незавершенной строки тест снова запускается. На этот раз read произойдет сбой (он все еще находится в конце файла), и, поскольку read в $line этот раз ничего не было прочитано, [[ -n "$line" ]] тест также завершится сбоем, поэтому while условие в целом не выполняется, и цикл завершается.
EDIT2: [[ ]] это условное выражение bash - это не обычная команда, но ее можно использовать вместо нее. Его основная цель - добиться успеха или потерпеть неудачу в зависимости от состояния внутри него. В этом случае -n проверка означает "$line" успешность, если операнд ( ) НЕ пуст. Там в список других условий испытаний здесь , а также на странице людей для test команды.
Как мне перебрать каждую строку текстового файла с помощью Bash ?
С помощью этого скрипта:
Я получаю на экране такой вывод:
(Позже я хочу сделать что-то более сложное, $p чем просто вывод на экран.)
Переменная окружения SHELL (из env):
/bin/bash --version выход:
cat /proc/version выход:
Файл peptides.txt содержит:
О, я вижу, здесь многое произошло: все комментарии были удалены, а вопрос снова открыт. Для справки: принятый ответ в статье "Прочитать файл" построчно, присваивая значение переменной, решает проблему каноническим способом и должен быть предпочтительнее принятого здесь. - fedorqui 'SO stop harming' 30 авг.
Один из способов сделать это:
Как указано в комментариях, это имеет побочные эффекты, заключающиеся в обрезке ведущих пробелов, интерпретации последовательностей обратной косой черты и пропуске последней строки, если в ней отсутствует завершающий перевод строки. Если это вызывает беспокойство, вы можете:
В исключительных случаях, если тело цикла может читать из стандартного ввода , вы можете открыть файл, используя другой файловый дескриптор:
Здесь 10 - это просто произвольное число (отличное от 0, 1, 2).
Как мне интерпретировать последнюю строку? Файл peptides.txt перенаправлен на стандартный ввод и как-то на весь блок while? - Peter Mortensen 5 окт.
«Вставьте файл peptides.txt в этот цикл while, чтобы команде 'read' было что использовать». Мой "кошачий" метод аналогичен, отправляя вывод команды в блок while для использования "read", только он запускает другую программу для выполнения работы. - Warren Young 5 окт.
Сделайте двойные кавычки !! эхо "$ p" и файл . поверьте, он вас укусит, если вы этого не сделаете . Я ЗНАЮ! лол - Mike Q 19 авг.
Обе версии не могут прочитать последнюю строку, если она не заканчивается символом новой строки. Всегда используйте while read p || [[ -n $p ]]; do . - dawg 7 сен '16 в 14:15
и однострочный вариант:
Эти параметры пропустят последнюю строку файла, если нет перевода строки в конце.
Избежать этого можно следующим образом:
В общем, если вы используете «кот» только с одним аргументом, вы делаете что-то неправильно (или неоптимально). - JesperE 5 окт.
Я использую «cat file |» как начало многих моих команд просто потому, что я часто создаю прототипы с «head file |» - mat kelcey 26 фев '14 в 21:33
Это может быть не так эффективно, но гораздо более читабельно, чем другие ответы. - Savage Reader 22 дек.
Вариант 1a: цикл while: по одной строке за раз: перенаправление ввода
Вариант 1b: цикл while: по одной строке за раз:
открыть файл, прочитать из файлового дескриптора (в данном случае файлового дескриптора №4).
Для варианта 1b: нужно ли снова закрывать файловый дескриптор? Например, цикл может быть внутренним. - Peter Mortensen 5 окт.
Дескриптор файла будет очищен при выходе из процесса. Явное закрытие может быть выполнено для повторного использования номера fd. Чтобы закрыть fd, используйте другой exec с синтаксисом & -, например: exec 4 <& - - Stan Graves 5 окт.&>
Спасибо за вариант 2. У меня возникли огромные проблемы с вариантом 1, потому что мне нужно было читать из стандартного ввода внутри цикла; в таком случае вариант 1 работать не будет. - masgo 4 июн.
Вы должны более четко указать, что Вариант 2 категорически не рекомендуется . @masgo Option 1b должен работать в этом случае, и его можно комбинировать с синтаксисом перенаправления ввода из варианта 1a, заменив done < $filename на done 4
Мне нужно перебрать содержимое файла, например tail -n +2 myfile.txt | grep 'somepattern' | cut -f3 , при выполнении команд ssh внутри цикла (потребляет stdin); вариант 2 здесь кажется единственным выходом? - user5359531 12 ноя '18 в 23:21
Это не лучше, чем другие ответы, но это еще один способ выполнить работу в файле без пробелов (см. Комментарии). Я обнаружил, что мне часто нужны однострочники, чтобы копаться в списках в текстовых файлах без дополнительного шага по использованию отдельных файлов сценариев.
Этот формат позволяет мне поместить все это в одну командную строку. Измените часть «echo $ word» на все, что захотите, и вы сможете выполнить несколько команд, разделенных точкой с запятой. В следующем примере содержимое файла используется в качестве аргументов двух других сценариев, которые вы, возможно, написали.
Или, если вы собираетесь использовать это как редактор потока (изучите sed), вы можете выгрузить вывод в другой файл следующим образом.
Я использовал их, как написано выше, потому что я использовал текстовые файлы, в которых я создал их с одним словом в строке. (См. Комментарии). Если у вас есть пробелы, которые вы не хотите разделять словами / строками, это становится немного уродливее, но та же команда по-прежнему работает следующим образом:
Это просто указывает оболочке разделять только символы новой строки, а не пробелы, а затем возвращает среду к тому, что было раньше. На этом этапе вы можете подумать о том, чтобы поместить все это в сценарий оболочки, а не втиснуть все в одну строку.
What does POSIX say?
The POSIX read command specification says:
The read utility shall read a single line from standard input.
By default, unless the -r option is specified, shall act as an escape character. An unescaped shall preserve the literal value of the following character, with the exception of a . If a follows the , the read utility shall interpret this as line continuation. The and shall be removed before splitting the input into fields. All other unescaped characters shall be removed after splitting the input into fields.
If standard input is a terminal device and the invoking shell is interactive, read shall prompt for a continuation line when it reads an input line ending with a , unless the -r option is specified.
The terminating (if any) shall be removed from the input and the results shall be split into fields as in the shell for the results of parameter expansion (see Field Splitting); [. ]
Note that '(if any)' (emphasis added in quote)! It seems to me that if there is no newline, it should still read the result. On the other hand, it also says:
STDIN
The standard input shall be a text file.
and then you get back to the debate about whether a file that does not end with a newline is a text file or not.
However, the rationale on the same page documents:
Although the standard input is required to be a text file, and therefore will always end with a (unless it is an empty file), the processing of continuation lines when the -r option is not used can result in the input not ending with a . This occurs if the last line of the input file ends with a . It is for this reason that "if any" is used in "The terminating (if any) shall be removed from the input" in the description. It is not a relaxation of the requirement for standard input to be a text file.
That rationale must mean that the text file is supposed to end with a newline.
The POSIX definition of a text file is:
3.395 Text File
A file that contains characters organized into zero or more lines. The lines do not contain NUL characters and none can exceed bytes in length, including the character. Although POSIX.1-2008 does not distinguish between text files and binary files (see the ISO C standard), many utilities only produce predictable or meaningful output when operating on text files. The standard utilities that have such restrictions always specify "text files" in their STDIN or INPUT FILES sections.
This does not stipulate 'ends with a ' directly, but does defer to the C standard and it does say "A file that contains characters organized into zero or more lines" and when we look at the POSIX definition of a "Line" it says:
3.206 Line
A sequence of zero or more non- characters plus a terminating character.
so per the POSIX definition a file must end in a terminating newline because it's made up of lines and each line must end in a terminating newline.
A solution to the 'no terminal newline' problem
Note Gordon Davisson's answer. A simple test shows that his observation is accurate:
Therefore, his technique of:
will work for files without a newline at the end (at least on my machine).
I'm still surprised to find that the shells drop the last segment (it can't be called a line because it doesn't end with a newline) of the input, but there might be sufficient justification in POSIX to do so. And clearly it is best to ensure that your text files really are text files ending with a newline.
Я пишу сценарий для чтения команд из файла и выполнения определенной команды. Я хочу, чтобы мой скрипт работал либо для одного входного аргумента, либо когда аргумент является именем файла, содержащим рассматриваемые аргументы.
мой код ниже работает, за исключением одной проблемы, он игнорирует последнюю строку файла. Итак, если файл был следующим.
сценарий размещена ниже работает только команда файл.txt
есть, конечно, очевидная работа вокруг того, где после цикла while я повторяю шаги для внутри цикла, чтобы завершить эти команды с последним файлом, но это нежелательно, так как любые изменения, которые я делаю внутри цикла while, должны быть повторены снова вне цикла while. Я искал в интернете и не мог найти никого, кто задавал бы этот точный вопрос. Я уверен, что он где-то там, но я его не нашел.
выход я получаю как следует.
моя версия bash-3.2.48
похоже, что в вашем входном файле отсутствует символ новой строки после последней строки. Когда read встречи-это переменная ( $line в этом случае) до последней строки, но затем возвращает ошибку конца файла, поэтому цикл не выполняется для этой последней строки. Чтобы обойти это, вы можете выполнить цикл, если read успешно или он читал что-нибудь в $line :
"правка": || в то время как состояние, что известно как короткое замыкание boolean -- он пытается выполнить первую команду ( read line ), и если это удастся, он пропускает второй ( [[ -n "$line" ]] ) и проходит через цикл (в основном, до тех пор, пока read успешно, он работает так же, как ваш оригинальный скрипт). Если read не удается, он проверяет вторую команду ( [[ -n "$line" ]] ) -- if read читать ничего $line прежде чем попасть в конец файла (т. е. если в файле была unterminated последняя строка), это будет успешным, поэтому while условие в целом считается получилось, и петля проходит еще раз.
после последней незаконченной обработки, он снова выполните тест. На этот раз read произойдет сбой (он все еще находится в конце файла), и так как read ничего не читал в $line в этот раз будет и не так while условие в целом терпит неудачу,и цикл завершается.
EDIT2: о [[ ]] является условным выражением bash - это не обычная команда, но его можно использовать вместо одного. Его основная цель-добиться успеха или потерпеть неудачу, основываясь на состоянии внутри него. В этом случае -n тест означает успех, если операнд ( "$line" ) непуст. Есть список других условий тестирования здесь, а также в man-странице для .
ваша проблема, кажется, отсутствует возврат каретки в вашем файле.
Если вы cat ваш файл, вы должны увидеть последнюю строку успешно появляется перед promopt.
в противном случае попробуйте добавить :
в силу последней строке файла.
используя grep с циклом while:
используя grep . вместо grep "" пропустит пустые строки.
используя IFS= сохраняет любой отступ линии нетронутым.
Я нашел код, который читает файл, включая последнюю строку, и работает без команды [[]]:
есть ли "канонический" способ делать это? Я использовал head -n | tail -1 Что делает трюк, но мне было интересно, есть ли инструмент Bash, который специально извлекает строку (или диапазон строк) из файла.
под "каноническим" я подразумеваю программу, основная функция которой делает это.
head и труба с tail будет медленным для огромного файла. Я бы предложил sed такой:
здесь NUM - номер строки, которую вы хотите напечатать; так, например, sed '10q;d' file для печати 10-й строки file .
NUMq немедленно прекратит работу, когда номер строки NUM .
d удалит строку вместо печати; это запрещено в последней строке, потому что q вызывает пропуск остальной части сценария при выходе.
если у вас NUM в переменной, вы хотите использовать двойные кавычки вместо одинарных:
будет печатать 2-ю строку
строка 10 до строки 33
для добавления строк с помощью sed, вы можете проверить это:
у меня есть уникальная ситуация, когда я могу сравнить решения, предложенные на этой странице, и поэтому я пишу этот ответ как консолидацию предлагаемых решений с включенным временем выполнения для каждого.
Настройка
у меня есть 3.261 гигабайт ASCII текстовый файл данных с одной парой ключ-значение в строке. Файл содержит 3,339,550,320 строк и не открывается в любом редакторе, который я пробовал, включая мой go-to Vim. Мне нужно подмножество этого файла в чтобы исследовать некоторые из значений, которые я обнаружил только начальную строку ~500,000,000.
потому что в файле так много строк:
- мне нужно извлечь только подмножество строк, чтобы сделать что-то полезное с данными.
- чтение каждой строки, ведущей к значениям, о которых я забочусь, займет много времени.
- если решение читает мимо строк, о которых я забочусь, и продолжает читать остальную часть файла, он будет тратить время чтения почти 3 миллиардов нерелевантных строк и занимает 6x больше, чем необходимо.
мой лучший сценарий-это решение, которое извлекает только одну строку из файла, не читая никаких других строк в файле, но я не могу думать о том, как я бы это сделал в Bash.
я буду использовать time встроенный для проверки каждой команды.
базовый
сначала давайте посмотрим, как head tail устранение:
базовый по рядка 50 млн. 00:01:15.321, если бы я пошла прямо по строке 500 миллионов было бы ~12.5 протокол.
вырезать
я сомневаюсь в этом, но стоит попробовать:
это заняло 00: 05: 12.156 для запуска, что намного медленнее, чем базовая линия! Я не уверен, прочитал ли он весь файл или просто до строки 50 миллионов перед остановкой, но независимо от этого это не кажется жизнеспособным решением проблемы.
на awk
я только запустил решение с exit потому что я не собирался ждать запуска полного файла:
этот код был запущен в 00: 01: 16.583, что всего на ~1 секунду медленнее, но все еще не улучшилось по сравнению с базовой линией. При такой скорости, если бы команда exit была исключена, вероятно, потребовалось бы около ~76 минут, чтобы прочитать весь файл!
Perl
я также запустил существующее решение Perl:
этот код был запущен в 00: 01: 13.146, что составляет ~2 секунды быстрее, чем базовый. Если бы я запустил его на полных 500,000,000, это, вероятно, заняло бы ~12 минут.
sed
лучшие ответы на доске, вот мой результат:
этот код работал в 00: 01: 12.705, что на 3 секунды быстрее, чем базовая линия, и ~0.4 секунд быстрее, чем Perl. Если бы я запустил его на полных 500,000,000 строках, это, вероятно, заняло бы ~12 протокол.
mapfile
у меня есть bash 3.1 и поэтому я не могу проверить решение mapfile.
вывод
похоже, по большей части, трудно улучшить head tail решение. В лучшем случае sed решение обеспечивает увеличение эффективности ~3%.
(проценты, рассчитанные по формуле % = (runtime/baseline - 1) * 100 )
строки 50,000,000
- 00:01:12.705 (-00:00:02.616 = -3.47%) sed
- 00:01:(-00 13.146 :00:02.175 = -2.89%) perl
- 00:01:15.321 (+00:00:00.000 = +0.00%) head|tail
- 00:01:16.583 (+00:00:01.262 = +1.68%) awk
- 00:05:12.156 (+00:03:56.835 = +314.43%) cut
строка 500,000,000
- 00:12:07.050 (-00:00:26.160) sed
- 00:12:11.460 (-00:00:21.750) perl
- 00:12:33.210 (+00:00:00.000) head|tail
- 00:12:45.830 (+00:00:12.620) awk
- 00:52:01.560 (+00:40:31.650) cut
ряд 3,338,559,320
- 01:20:54.599 (-00:03:05.327) sed
- 01:21:24.045 (-00:02:25.227) perl
- 01:23:49.273 (+00:00:00.000) head|tail
- 01:25:13.548 (+00:02:35.735) awk
- 05:47:23.026 (+04:24:26.246) cut
С awk Это довольно быстро:
когда это верно, поведение по умолчанию awk выполняется: .
Читайте также: