- PVSM.RU - https://www.pvsm.ru -

Команда sponge: «губка» для стандартного ввода

Все мы знаем, что при выполнении команд в шелле мы можем перенаправлять [1] стандартный вывод [2] на стандартный ввод других команд, а также записывать его в файл.

Это достаточно детально описано в главе I/O Redirection [3] в «Продвинутом руководстве по программированию на Bash» (Advanced Bash-Scripting Guide [4]).

В частности, иногда бывает так, что вам нужно прочитать какой-то файл, как-то его обработать (например, выбрать оттуда только те строки, которые подходят под некое регулярное выражение), и затем записать результат в тот же самый файл. Допустим, ваш файл называется «messages.log», и вы хотите оставить в нём только те строки, которые начинаются со слова «Success», двоеточия и пробела (а все остальные строки убрать).

Можно предположить, что для этого подойдёт такая команда:

grep "^Success:s" messages.log > messages.log

Но это предположение окажется неправильным — при выполнении этой строчки файл messages.log будет открыт на запись и очищен ещё до того, как grep начнёт его просматривать.

Впрочем, интересно то, что когда grep всё-таки будет запущен, он обнаружит, что вывод перенаправляется в тот же файл, который он пытается прочитать, и сразу же завершится со следующим сообщением:

grep: input file ‘messages.log’ is also the output

То же самое делает и GNU cat (попробуйте выполнить cat messages.log > messages.log):

cat: messages.log: input file is output file

Это делается путём сравнения устройства и inode [5] для файла ввода с соответствующими значениями для файла, используемого для записи стандартного вывода. Посмотреть реализацию этого подхода можно в src/cat.c [6].

В BSD cat таких проверок, кстати, не предусмотрено, но в данном случае это не столь важно: файл так или иначе уже очищен, поэтому читать и записывать нечего, так что cat просто завершится.

Однако возьмём другой пример:

cat messages.log >> messages.log

В данном случае мы не очищаем messages.log, а дописываем вывод команды cat в конец файла. И если cat проверит, что эти два файла совпадают, и завершится, то файл останется в том же состоянии, а пользователь увидит ошибку. А вот если такой проверки нет, то cat войдёт в цикл и будет дополнять файл до тех пор, пока не закончится место или пока пользователь не завершит процесс.

А теперь давайте подумаем, как можно всё-таки записать вывод в тот же файл, который мы читаем. Очевидное решение — это использовать временный файл. То есть:

mv messages.log tmpmessages.log
grep "^Success:s" tmpmessages.log > messages.log
rm tmpmessages.log

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

Ещё один вариант — мы можем использовать sed.

sed -i -n -e '/^Success:s/{p}' messages.log

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

Кстати, на самом деле sed тоже использует временный файл, в этом можно убедиться, посмотрев на вывод strace:

open("messages.log", O_RDONLY)                   = 3
…
open("./sedWiaEAG", O_RDWR|O_CREAT|O_EXCL, 0600) = 4
…
read(3, "Success: 123nError: 123n", 4096)      = 24
write(4, "Success: 123n", 13)                   = 13
read(3, "", 4096)                                = 0
…
close(3)                                         = 0
…
close(4)                                         = 0
…
rename("./sedWiaEAG", "messages.log")            = 0
close(1)                                         = 0
close(2)                                         = 0
exit_group(0)                                    = ?

Очевидно, что нужно иметь возможность как-то обойтись вообще без промежуточных файлов. И такая возможность есть — это программа sponge из moreutils.

sponge reads standard input and writes it out to the specified file. Unlike a shell redirect, sponge soaks up all its input before opening the output file. This allows constructing pipelines that read from and write to the same file.

sponge читает стандартный ввод и записывает его в указанный файл. В отличие от перенаправлений командной оболочки, sponge «впитыает» весь переданный ввод перед тем, как открыть файл, в который его требуется записать. Это позволяет использовать такие конвейеры, где чтение происходит из того же файла, в который осуществляется запись.

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

grep "^Success:s" messages.log | sponge messages.log

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

Желаю всем отличной пятницы!

Автор: MaGIc2laNTern

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/33161

Ссылки в тексте:

[1] перенаправлять: http://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D0%BD%D0%B0%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%BD%D0%BE%D0%B5_%D0%BE%D0%B1%D0%B5%D1%81%D0%BF%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%29

[2] стандартный вывод: http://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B0%D0%BD%D0%B4%D0%B0%D1%80%D1%82%D0%BD%D1%8B%D0%B5_%D0%BF%D0%BE%D1%82%D0%BE%D0%BA%D0%B8

[3] I/O Redirection: http://www.tldp.org/LDP/abs/html/io-redirection.html

[4] Advanced Bash-Scripting Guide: http://www.tldp.org/LDP/abs/html/

[5] inode: http://ru.wikipedia.org/wiki/Inode

[6] src/cat.c: http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/cat.c#n706

[7] Источник: http://habrahabr.ru/post/178141/