Подтвердить что ты не робот

Shell script читать пропущенную последнюю строку

У меня есть... нечетная проблема с оболочкой bash script, на которую я надеялся получить некоторое представление.

Моя команда работает над script, которая выполняет итерацию по строкам в файле и проверяет содержимое в каждом из них. У нас была ошибка, когда при запуске через автоматизированный процесс, в котором последовательно выполнялись разные скрипты, последняя строка не просматривалась.

Код, используемый для итерации по строкам в файле (имя, сохраненное в DATAFILE, было

cat "$DATAFILE" | while read line 

Мы могли бы запустить script из командной строки, и он увидит каждую строку в файле, включая последнюю, просто отлично. Однако при запуске автоматизированного процесса (который запускает script, который генерирует DATAFILE непосредственно перед script), последняя строка никогда не будет видна.

Мы обновили код, чтобы использовать следующее, чтобы перебирать строки, и проблема устранена:

for line in `cat "$DATAFILE"` 

Примечание. DATAFILE не имеет новой строки, когда-либо написанной в конце файла.

Мой вопрос состоит из двух частей... Почему бы последняя строка не была видна исходным кодом, и почему это изменило бы значение?

Я только думал, что могу придумать, почему последняя строка не будет видна:

  • Предыдущий процесс, который записывает файл, основывался на завершении процесса, чтобы закрыть дескриптор файла.
  • Проблема script запускалась и открывала файл раньше, чем достаточно, чтобы, в то время как предыдущий процесс "закончился", он недостаточно "выключил/очистил", чтобы система автоматически закрыла дескриптор файла для этого.

Как бы то ни было, кажется, что если у вас есть 2 команды в оболочке script, первая должна быть полностью отключена к тому моменту, когда script запускает второй.

Любое понимание вопросов, особенно первого, будет очень оценено.

4b9b3361

Ответ 1

В стандарте C говорится, что текстовые файлы должны заканчиваться символом новой строки или данными после того, как последняя строка новой строки может быть неправильно прочитана.

ISO/IEC 9899: 2011 §7.21.2 Потоки

Текстовый поток представляет собой упорядоченную последовательность символов, состоящую из строк, каждая строка состоит из нуля или более символов плюс завершающий символ новой строки. Независимо от того, соответствует ли последняя строка завершающему символу новой строки, определяется ли реализация. Символы могут быть добавлены, изменены или удалены на входе и выходе, чтобы соответствовать различным соглашениям для представления текста в среде хоста. Таким образом, между символами в потоке и во внешнем представлении не должно быть взаимно однозначного соответствия. Данные, считываемые из текстового потока, обязательно будут сравниваться с данными, которые ранее были записаны в этот поток, только если: данные состоят только из печатных символов, а контрольные символы - горизонтальной вкладкой и новой строкой; никакому символу новой строки не предшествуют символы пробела; а последний символ - символ новой строки. Будут ли появляться символы пробела, которые выписываются непосредственно перед символом новой строки при чтении, определяется реализацией.

У меня не было бы неожиданной недостающей строки в конце файла, чтобы вызвать проблемы в bash (или любой оболочке Unix), но это похоже на проблему воспроизводимо ($ - подсказка в этом выпуске):

$ echo xxx\\c
xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y
$ cat y
abc
def
ghi
xxx$
$ while read line; do echo $line; done < y
abc
def
ghi
$ bash -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ ksh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ zsh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ for line in $(<y); do echo $line; done      # Preferred notation in bash
abc
def
ghi
xxx
$ for line in $(cat y); do echo $line; done   # UUOC Award pending
abc
def
ghi
xxx
$

Это также не ограничивается bash - оболочка Korn (ksh) и zsh ведут себя так же. Я живу, я учусь; спасибо за поднятие вопроса.

Как показано в приведенном выше коде, команда cat считывает весь файл. for line in 'cat $DATAFILE' собирает все выходные данные и заменяет произвольные последовательности пробелов одним пробелом (я делаю вывод, что каждая строка в файле не содержит пробелов).

Протестировано на Mac OS X 10.7.5.


Что говорит POSIX?

В спецификации команды read POSIX написано:

Утилита чтения должна считывать одну строку из стандартного ввода.

По умолчанию, если опция -r не указана, <обратная косая черта> будет действовать как escape-символ. Неизолированная <обратная косая черта> сохраняет литеральное значение следующего символа, за исключением <новой строки>. Если <backline> следует за <обратным слэшем>, программа чтения должна интерпретировать это как продолжение строки. Символы <обратная косая черта> и <newline> должны быть удалены перед разбиением ввода на поля. Все остальные символы без обратного следа должны быть удалены после разделения ввода на поля.

Если стандартный ввод является терминальным устройством, а вызывающая оболочка является интерактивной, чтение должно запрашивать продолжение строки, когда она считывает строку ввода, заканчивающуюся <backslash> <newline>, если не -r параметр -r.

Отключающая <newline> (если таковая имеется) должна быть удалена из ввода, и результаты должны быть разделены на поля, как в оболочке, для результатов расширения параметров (см. Раздел "Разделение поля"); [...]

Обратите внимание, что "(если есть)" (выделено в цитате)! Мне кажется, что, если нет новой строки, она все равно должна прочитать результат. С другой стороны, в нем также говорится:

STDIN

Стандартный ввод должен представлять собой текстовый файл.

и затем вы возвращаетесь к дискуссиям о том, является ли файл, который не заканчивается символом новой строки, текстовым файлом или нет.

Однако обоснование на тех же страницах документов:

Хотя стандартный ввод необходим как текстовый файл и поэтому всегда заканчивается символом <newline> (если он не является пустым файлом), обработка строк продолжения, когда опция -r не используется, может привести к вводу не заканчивается на <newline>. Это происходит, если последняя строка входного файла заканчивается символом <backslash> <newline>. Именно по этой причине "если есть" используется в "Прекращение <новой строки> (если оно есть) должно быть удалено из ввода" в описании. Это не релаксация требования для стандартного ввода как текстового файла.

Это обоснование должно означать, что текстовый файл должен заканчиваться новой строкой.

Определение текстового файла в POSIX:

3.395 Текстовый файл

Файл, содержащий символы, помещенные в ноль или более строк. Строки не содержат символов NUL, и ни один из них не может превышать длину {LINE_MAX} байтов, включая символ <newline>. Хотя POSIX.1-2008 не различает текстовые файлы и двоичные файлы (см. Стандарт ISO C), многие утилиты производят только предсказуемый или значимый вывод при работе с текстовыми файлами. Стандартные утилиты, которые имеют такие ограничения, всегда указывают "текстовые файлы" в своих разделах STDIN или INPUT FILES.

Это не оговаривает "концы с помощью <newline>" напрямую, но откладывается до стандарта C.


Решение проблемы "no terminal newline"

Обратите внимание на ответ Гордона Дэвисона. Простой тест показывает, что его наблюдение является точным:

$ while read line; do echo $line; done < y; echo $line
abc
def
ghi
xxx
$

Поэтому его техника:

while read line || [ -n "$line" ]; do echo $line; done < y

или же:

cat y | while read line || [ -n "$line" ]; do echo $line; done

будет работать для файлов без новой строки в конце (по крайней мере, на моей машине).


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

Ответ 2

Согласно спецификации POSIX для команды чтения, она должна вернуть ненулевой статус, если "обнаружен конец файла или произошла ошибка". Поскольку EOF обнаружен, когда он читает последнюю "строку", он устанавливает $line а затем возвращает статус ошибки, а статус ошибки предотвращает выполнение цикла на этой последней строке. Решение легко: сделайте цикл выполненным, если команда чтения успешно завершила ИЛИ, если что-то было прочитано в $line.

while read line || [ -n "$line" ]; do

Ответ 3

Добавление дополнительной информации:

При использовании цикла while для чтения строк:

удовлетворяя вышеуказанным требованиям, правильный цикл while будет выглядеть следующим образом:

while IFS= read -r line; do
  ...
done <file

И чтобы он работал с файлами без новой строки в конце (перенос моего решения из здесь):

while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done <file

Или используя grep с циклом while:

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

Ответ 4

Я подозреваю, что новая строка в последней строке вашего файла может вызвать эту проблему. Для тестирования вы можете внести небольшие изменения в свой script и прочитать DATAFILE следующим образом:

while read line
do
    echo $line # do processing here
done < "$DATAFILE"

И посмотрите, не имеет значения.

Ответ 5

Используйте sed для соответствия последней строке файла, который затем добавит новую строку, если она не существует, и чтобы она выполняла встроенную замену файла:

sed -i '' -e '$a\' file

Код из этой ссылки

Примечание. Я добавил пустые одинарные кавычки в -i '', потому что, по крайней мере, в OS X, -i использовал -e в качестве расширения файла для файла резервной копии. Я бы с радостью прокомментировал исходный пост, но мне не хватило 50 баллов. Возможно, это принесет мне немного в этой теме, спасибо.

Ответ 6

Я тестировал это в командной строке

# create dummy file. last line doesn't end with newline
printf "%i\n%i\nNo-newline-here" >testing

Протестируйте свою первую форму (трубопровод до цикла)

cat testing | while read line; do echo $line; done

Это пропустит последнюю строку, что имеет смысл, поскольку read получает только ввод, заканчивающийся символом новой строки.


Проверьте свою вторую форму (подстановка команды)

for line in `cat testbed1` ; do echo $line; done

Это также возвращает последнюю строку


read вводит только вход, если он завершен символом новой строки, поэтому вы пропустите последнюю строку.

С другой стороны, во второй форме

`cat testing` 

расширяется до формы

line1\nline2\n...lineM 

который разделяется оболочкой на несколько полей с использованием IFS, поэтому вы получаете

line1 line2 line3 ... lineM 

Вот почему вы все еще получаете последнюю строку.

p/s: я не понимаю, как вы получаете первую форму, работающую...

Ответ 7

Как обходной путь, перед чтением из текстового файла в файл может быть добавлена ​​новая строка.

echo "\n" >> $file_path

Это гарантирует, что все строки, которые были ранее в файле, будут прочитаны.

Ответ 8

У меня была аналогичная проблема. Я делал кошку файла, соединяя его в сортировку и затем передавая результат в "while read var1 var2 var3". то есть: cat $FILE | sort -k3 | при чтении Count IP Name сделать Работа под "do" была оператором if, который определял изменение данных в поле $Name и на основании изменения или без изменений делал суммы в $Count или печатал суммированную строку в отчете. Я также столкнулся с проблемой, когда я не смог получить последнюю строку для печати в отчете. Я пошел с простой возможностью перенаправить cat/sort в новый файл, повторив новую строку для этого нового файла, и THEN запустил мое "пока прочитанное количество IP-адресов" в новом файле с успешными результатами. то есть: cat $FILE | sort -k3 > NEWFILE echo "\n" → NEWFILE cat NEWFILE | при чтении Count IP Name сделать Иногда простой, неэлегантный - лучший способ пойти.