голова ест лишних персонажей

15

Ожидается, что следующая команда оболочки выведет только нечетные строки входного потока:

echo -e "aaa\nbbb\nccc\nddd\n" | (while true; do head -n 1; head -n 1 >/dev/null; done)

Но вместо этого он просто печатает первую строку: aaa.

То же самое не происходит, когда он используется с опцией -c( --bytes):

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 >/dev/null; done)

Эта команда выводит 1234512345как ожидалось. Но это работает только в реализации утилиты coreutilshead . Реализация busybox по- прежнему потребляет лишние символы, поэтому вывод просто 12345.

Я предполагаю, что этот конкретный способ реализации сделан в целях оптимизации. Вы не можете знать, где заканчивается строка, поэтому вы не знаете, сколько символов вам нужно прочитать. Единственный способ не использовать лишние символы из входного потока - это читать поток побайтно. Но чтение из потока по одному байту за раз может быть медленным. Поэтому я предполагаю, что headсчитывает входной поток в достаточно большой буфер и затем считает строки в этом буфере.

Чего нельзя сказать о случае, когда --bytesиспользуется опция. В этом случае вы знаете, сколько байтов вам нужно прочитать. Таким образом, вы можете прочитать именно это количество байтов и не более того. Реализация corelibs использует эту возможность, но не с busybox , она все же считывает больше байта, чем требуется, в буфер. Вероятно, это сделано для упрощения реализации.

Итак, вопрос. Правильно ли для headутилиты потреблять больше символов из входного потока, чем было задано? Есть ли какой-то стандарт для утилит Unix? И если есть, то указывает ли это поведение?

PS

Вы должны нажать, Ctrl+Cчтобы остановить команды выше. Утилиты Unix не перестают читать дальше EOF. Если вы не хотите нажимать, вы можете использовать более сложную команду:

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 | [ `wc -c` -eq 0 ] && break >/dev/null; done)

который я не использовал для простоты.

anton_rh
источник
2
Neardupe unix.stackexchange.com/questions/48777/… и unix.stackexchange.com/questions/84011/… . Кроме того, если бы этот заголовок был в кино. SX, мой ответ был бы Zardoz :)
dave_thompson_085

Ответы:

30

Правильно ли для утилиты head использовать больше символов из входного потока, чем было задано?

Да, это разрешено (см. Ниже).

Есть ли какой-то стандарт для утилит Unix?

Да, POSIX, том 3, Shell & Utilities .

И если есть, то указывает ли это поведение?

В своем введении он делает:

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

headявляется одной из стандартных утилит , поэтому реализация, соответствующая POSIX, должна реализовывать поведение, описанное выше.

GNU head делает пытаться оставить дескриптор файла в правильном положении, но это невозможно искать на трубах, поэтому в тесте он не может восстановить положение. Вы можете увидеть это используя strace:

$ echo -e "aaa\nbbb\nccc\nddd\n" | strace head -n 1
...
read(0, "aaa\nbbb\nccc\nddd\n\n", 8192) = 17
lseek(0, -13, SEEK_CUR)                 = -1 ESPIPE (Illegal seek)
...

read возвращает 17 байт (все доступные входные), headобрабатывает четыре из них , а затем пытается вернуться 13 байт, но она не может. (Вы также можете увидеть здесь, что GNU headиспользует буфер 8 КиБ.)

Когда вы говорите head считать байты (что является нестандартным), он знает, сколько байтов нужно прочитать, поэтому он может (если реализован таким образом) соответствующим образом ограничить свое чтение. Вот почему ваш head -c 5тест работает: GNU headчитает только пять байтов и поэтому не нуждается в поиске для восстановления позиции файлового дескриптора.

Если вы записываете документ в файл и используете его вместо этого, вы получите поведение, которое вам нужно:

$ echo -e "aaa\nbbb\nccc\nddd\n" > file
$ < file (while true; do head -n 1; head -n 1 >/dev/null; done)
aaa
ccc
Стивен Китт
источник
2
Вместо этого можно использовать утилиты line(теперь удаленные из POSIX / XPG, но все еще доступные во многих системах) или read( IFS= read -r line), которые читают по одному байту за раз, чтобы избежать проблемы.
Стефан
3
Обратите внимание, что head -c 5чтение 5 байтов или полный буфер зависит от реализации (также обратите внимание, что head -cэто не стандарт), вы не можете полагаться на это. Вы должны dd bs=1 count=5иметь гарантию, что будет прочитано не более 5 байтов.
Стефан
Спасибо @ Стефан, я обновил -c 5описание.
Стивен Китт
Обратите внимание, что headвстроенная функция ksh93читает по одному байту за раз, head -n 1когда ввод не доступен для поиска.
Стефан
1
@anton_rh, ddкорректно работает только с каналами, bs=1если вы используете countчтение as на каналах as, которое может вернуть меньше запрошенного (но по крайней мере один байт, если eof не достигнут). У GNU ddесть это, iflag=fullblockчто может смягчить это все же.
Стефан
6

из POSIX

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

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

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

Чтения утилита следует читать одну логическую строку из стандартного ввода в один или несколько переменных оболочки.

В случае read, который используется в сценариях оболочки, общий вариант использования будет выглядеть примерно так:

read someline
if something ; then 
    someprogram ...
fi

Здесь стандартный ввод для ввода someprogramтакой же, как и для оболочки, но можно ожидать, что он someprogramбудет читать все, что происходит после первой строки ввода, потребляемой, readа не то, что осталось после буферизованного чтения read. С другой стороны, использование, headкак в вашем примере, гораздо более редко.


Если вы действительно хотите удалить все остальные строки, было бы лучше (и быстрее) использовать какой-либо инструмент, который может обрабатывать весь ввод за один раз, например

$ seq 1 10 | sed -ne '1~2p'   # GNU sed
$ seq 1 10 | sed -e 'n;d'     # works in GNU sed and the BSD sed on macOS

$ seq 1 10 | awk 'NR % 2' 
$ seq 1 10 | perl -ne 'print if $. % 2'
ilkkachu
источник
Но посмотрите раздел «ВХОДНЫЕ ФАЙЛЫ» во введении к POSIX в томе 3 ...
Стивен Китт
1
POSIX говорит: «Когда стандартная утилита читает искомый входной файл и завершает работу без ошибки, прежде чем достигнет конца файла, утилита должна убедиться, что смещение файла в описании открытого файла правильно расположено сразу после последнего байта, обработанного утилита. Для файлов, которые нельзя найти, состояние смещения файла в описании открытого файла для этого файла не указано. "
AlexP
2
Обратите внимание, что если вы не используете -r, readможет прочитать более одной строки (без IFS=него также будут лишены начальные и конечные пробелы и табуляции (со значением по умолчанию $IFS)).
Стефан
@AlexP, да, Стивен только что связал эту часть.
ilkkachu
Обратите внимание, что headвстроенная функция ksh93читает по одному байту за раз, head -n 1когда ввод не доступен для поиска.
Стефан
1
awk '{if (NR%2) == 1) print;}'
ijbalazs
источник
Hellóka :-) и добро пожаловать на сайт! Обратите внимание, мы предпочитаем более сложные ответы. Они должны быть полезны для гуглеров будущего.
Петер - Восстановить Монику