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

в 14:52, , рубрики: bash, i/o, linux, конвейеры, перенаправление, метки: , , ,

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

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

В частности, иногда бывает так, что вам нужно прочитать какой-то файл, как-то его обработать (например, выбрать оттуда только те строки, которые подходят под некое регулярное выражение), и затем записать результат в тот же самый файл. Допустим, ваш файл называется «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 для файла ввода с соответствующими значениями для файла, используемого для записи стандартного вывода. Посмотреть реализацию этого подхода можно в src/cat.c.

В 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

Источник

Поделиться

* - обязательные к заполнению поля