Это достаточно детально описано в главе 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: 123\nError: 123\n", 4096) = 24 write(4, "Success: 123\n", 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
В принципе, всю блогозапись можно было бы сократить до этого примера, но, я думаю, так получилось интереснее, и, возможно, удалось даже рассказать о каких-то нюансах, о которых кто-то из читателей раньше не знал.
Желаю всем отличной пятницы!
ссылка на оригинал статьи http://habrahabr.ru/post/178141/
Добавить комментарий