голова їсть зайвих символів


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як очікувалося. Але це працює тільки в Coreutils реалізації headутиліти. Реалізація зайнятої програми все ще їсть зайві символи, тому вихід просто 12345.

Я думаю, що цей конкретний спосіб реалізації зроблений для оптимізації. Ви не можете знати, де закінчується рядок, тому ви не знаєте, скільки символів потрібно прочитати. Єдиний спосіб не споживати зайві символи з вхідного потоку - це читати потік у байті. Але читання з потоку один байт за часом може бути повільним. Тож я думаю, що headчитає вхідний потік на достатньо великий буфер, а потім підраховує рядки в цьому буфері.

Те саме не можна сказати у випадку, коли --bytesвикористовується опція. У цьому випадку ви знаєте, скільки байтів вам потрібно прочитати. Тож ви можете прочитати саме таку кількість байтів і не більше того. Реалізація corelibs використовує цю можливість, однак зайнятий не робить, він все ще читає більше байтів, ніж потрібно в буфер. Це, мабуть, робиться для спрощення реалізації.

Отже питання. Чи правильно 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)

яку я не використовував для простоти.


2
Neardupe unix.stackexchange.com/questions/48777/… та unix.stackexchange.com/questions/84011/… . Крім того, якби ця назва була в
кіно. SX

Відповіді:


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 байт.
Стефан Шазелас

Спасибі @ Stéphane, я оновив -c 5опис.
Стівен Кітт

Зауважте, що headвбудований ksh93зчитує один байт за той час, head -n 1коли вхід не є доступним для пошуку.
Стефан Шазелас

1
@anton_rh, ddпрацює коректно лише з трубами, bs=1якщо ви використовуєте, countяк читає на трубах, може повертати менше, ніж потрібно (але принаймні один байт, якщо не досягнуто eof). GNU dd, iflag=fullblockщо може полегшити це, хоча.
Стефан Шазелас

6

від POSIX

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

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

Однак це стосується readвбудованої / утиліти: всі оболонки, які я можу знайти, readз труб по одному байту і стандартний текст може бути інтерпретований так, що це потрібно зробити, щоб можна було прочитати саме той один рядок:

The Читання утиліта слід читати одну логічну рядок зі стандартного вводу в один або кілька змінних оболонки.

У випадку 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'

Але дивіться розділ "ВХІДНІ ФАЙЛИ" вступу POSIX до тома 3 ...
Стівен Кітт

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

2
Зауважте, що якщо ви не використовуєте -r, вони readможуть читати більше одного рядка (без IFS=цього також не буде знімати провідні та кінцеві пробіли та вкладки (зі значенням за замовчуванням $IFS)).
Стефан Шазелас

@AlexP, так, Стівен просто зв’язав цю частину.
ilkkachu

Зауважте, що headвбудований ksh93зчитує один байт за той час, head -n 1коли вхід не є доступним для пошуку.
Стефан Шазелас

1
awk '{if (NR%2) == 1) print;}'

Hellóka :-) і ласкаво просимо на сайті! Зауважте, ми віддаємо перевагу більш детальним відповідям. Вони повинні бути корисними для гуглерів майбутнього.
петерх
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.