Страницы

Поиск по вопросам

пятница, 13 декабря 2019 г.

передача содержимого файла двум командам одновременно не всегда работает

#bash


почему конструкции 1 и 2 работают как и ожидается, а конструкция 3 выводит только
количество строк?


вывод первой и последней строк из файла:

$ (head -n 1; tail -n 1) < файл

вывод первой строки и количества строк файла:

$ (head -n 1; wc -l) < файл


(обновление1: на самом деле оказалось, что работает не совсем так: wc выводит число,
на единицу меньшее длины файла — эту строку «съедает» head).
вывод количества строк файла и затем первой строки (не работает — выводится только
количество строк):

$ (wc -l; head -n 1) < файл



обновление2: по поводу «особой реализации» программы head из состава gnu/coreutils
можно возразить, что «busybox»-овая, например, реализация, ведёт себя идентично. такая
команда выводит то же самое, что и в пункте 1:

$ (busybox head -n 1; tail -n 1) < файл


и вообще, «списать всё» на реализацию head вряд ли можно. ведь поведение конструкций:

$ cat файл | (head -n 1; tail -n 1)
$ (head -n 1; tail -n 1) < <(cat файл)


не совпадает с приведённым в первом примере: выводится только первая строка файла.



версии программ (хотя это, скорее всего, вряд ли существенно):

$ head --version
head (GNU coreutils) 8.26
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later .
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by David MacKenzie and Jim Meyering.
$ wc --version
wc (GNU coreutils) 8.26
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later .
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Paul Rubin and David MacKenzie.
$ bash --version
GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.


p.s. и интересно бы узнать, где вообще подобная конструкция ((команда1; команда2)
< файл) задокументирована.
    


Ответы

Ответ 1



правильный вопрос, на который следует отвечать, это: почему вообще работает конструкция (head -n 1; head -n 1) < файл (ниже упоминается как «исходная команда»), выдавая при этом две первые строки файла? ответив на него правильно, можно будет ответить и на остальные прозвучавшие вопросы. ответ получен после просмотра трассировки работы программы head. тестовый файл: $ cat file first line second line last line запускаем под gnu/linux (здесь исходная команда выдаёт две строки): $ (strace head -n 1; head -n 1) < file read(0, "first line\nsecond line\nlast line"..., 8192) = 33 lseek(0, -22, SEEK_CUR) = 11 write(1, "first line\n", 11) = 11 запускаем под solaris (здесь исходная команда выдаёт тоже две строки): $ (truss head -n 1; head -n 1) < file read(0, " f i r s t l i n e\n s".., 4096) = 33 write(1, " f i r s t l i n e\n", 11) = 11 llseek(0, 0xFFFFFFFFFFFFFFEA, SEEK_CUR) = 11 запускаем под freebsd (здесь исходная команда выдаёт только первую строку): $ (truss head -n 1 ; head -n 1) < file read(0,"first line\nsecond line\nlast li"...,32768) = 33 (0x21) write(1,"first line\n",11) = 11 (0xb) оставлены только существенные строки вывода. из них видно, что программа head во всех трёх реализациях считывает все 33 байта из stdin (связанного оболочкой с реальным файлом), а затем записывает первые 11 в stdout. но в случае gnu/linux и solaris после чтения производится «перемотка» указателя (lseek/llseek) на позицию 11 во входном файле (к началу второй строки), а во freebsd этого не происходит. отсюда становится понятным, почему идентичная вроде бы конструкция: $ cat файл | (head -n 1; head -n 1) работает одинаково во всех трёх тестируемых системах (выдавая только первую строку файла): ведь сделать «перемотку» можно только в реальном файле, а когда это не файл, а поток, передаваемый через «трубу» (pipe, |), то переставить указатель невозможно.

Ответ 2



В самой конструкции (команда1; команда2) < файл нет ничего примечательного: запускается subshell, которому на вход задаётся файл. В subshell'е выполняется сначала команда1 потом команда2. Обе они читают из одного stdin'а по очереди. В упрощённом варианте об этом можно думать следующим образом: head читает первую строчку, после этого останавливается и выводит её, остальное читает tail. Но AFAIK данное поведение ни как не стандартизовано. Аналогично, head читает первую строчку, а все остальные достаются wc. wc выводит выводит количество строк на единицу меньше. wc читает всё, а head'у ничего не достаются. Всё это довольно грязные вещи и то что первая команда работает завязано исключительно на реализации GNU head. полагаться на это нельзя. Корректно перенаправить вывод двум разным процессам на bash можно например так: tee >(head -n1) >(tail -n1) >/dev/null Но порядок вывода head и tail на терминал не определён. В Базовой POSIX-совместимой оболочке AFAIK это можно сделать только с помощью именованных каналов. Update Результаты тестирования различных оболочках: Подопытный файл: $ cat /tmp/file head_line body tail_line Проверенные оболочки: * GNU/Linux: bash, dash, bussybox sh * FreeBSD 8.0: csh, bash Тестовые команды: -c 'cat /tmp/file | (head -n 1; tail -n 1)' -c 'cat /tmp/file | (head -n 1; head -n 1)' На всех оболочках даёт одинаковый результат: head_line $ -c '(head -n 1; tail -n 1) -c '(head -n 1; head -n 1)

Ответ 3



почему конструкции 1 и 2 работают как и ожидается, а конструкция 3 выводит только количество строк? <файл в shell (POSIX, bash) перенаправляет файл в стандартный ввод (команда, читающая из stdin (fd 0), получит содержимое файла). wc -l в п. 3 читает весь ввод, поэтому на долю head ничего не остаётся. То что head -n 1 не потребляет весь ввод (что позволяет работать командам в п. 1 и п. 2) не гарантируется документацией для утилиты head(как POSIX так и Gnu). Разумно ожидать, что лишняя работа не будет выполняться, особенно если это не усложняет реализацию. К примеру, FreeBSD head.c: while (cnt && (cp = fgetln(fp, &readlen)) != NULL) { fgetln() использует stdio буфер, поэтому может прочесть из стандартного ввода больше необходимого. Это объясняет почему на FreeBSD (head -n 1; head -n 1) <файл не работает. tail может игнорировать текущую позицию в файле как @Fat-Zer выяснил. Поэтому (head -n 1; tail -n 1)

Ответ 4



почему конструкции 1 и 2 работают как и ожидается, а конструкция 3 выводит только количество строк? Потому, что shell делает один open(), а потом 2 раза dup2(). В результате stdin в командах делит общий указатель чтения-записи, связанный с файловым дескриптором, который вернул open. Несколько неожиданно действительно аккуратное поведение head, (а tail меня реально в этом плане поразил (см. фактуру в ответах и комментариях @alexanderbarakin, @Fat-Zer и @jfs)), которые сдвигают указатель на логически верную позицию, соответствующую байтам, которые они реально бы обработали при последовательном чтении файла.

Комментариев нет:

Отправить комментарий