Чтение и запись файла: команда tee
Хорошо известно, что такая команда:
cat filename | some_sed_command >filename
стирает имя файла, так как перенаправление вывода, выполняемое перед командой, вызывает усечение имени файла.
Можно решить проблему следующим образом:
cat file | some_sed_command | tee file >/dev/null
но я не уверен, что это сработает в любом случае: что произойдет, если файл (и результат команды sed) очень большой? Как операционная система может избежать перезаписи содержимого, которое до сих пор не прочитано? Я вижу, что есть также команда "губка", которая должна работать в любом случае: она "безопаснее", чем тройник?
4 ответа
Можно решить проблему следующим образом:
cat file | some_sed_command | tee file >/dev/null
Нет
Шансы file
будет усечено падение, но нет гарантии cat file | some_sed_command | tee file >/dev/null
не будет усекать file
,
Все зависит от того, какая команда обрабатывается первой, в отличие от того, что можно ожидать, команды в конвейере не обрабатываются слева направо. Нет никакой гарантии, какая команда будет выбрана первой, поэтому можно просто думать о ней как о случайно выбранной и никогда не полагаться на то, что оболочка не выберет вызывающую.
Поскольку вероятность того, что нарушающая команда будет выбрана первой между тремя командами, ниже, чем вероятность того, что нарушающая команда будет выбрана первой между двумя командами, менее вероятно, что file
будет усечено, но это все еще произойдет.
script.sh
:
#!/bin/bash
for ((i=0; i<100; i++)); do
cat >file <<-EOF
foo
bar
EOF
cat file |
sed 's/bar/baz/' |
tee file >/dev/null
[ -s file ] &&
echo 'Not truncated' ||
echo 'Truncated'
done |
sort |
uniq -c
rm file
% bash script.sh
93 Not truncated
7 Truncated
% bash script.sh
98 Not truncated
2 Truncated
% bash script.sh
100 Not truncated
Поэтому никогда не используйте что-то вроде cat file | some_sed_command | tee file >/dev/null
, использование sponge
как предположил Оли.
В качестве альтернативы для более ограниченных сред и / или относительно небольших файлов можно использовать строку здесь и подстановку команд, чтобы прочитать файл перед выполнением любой команды:
$ cat file
foo
bar
$ for ((i=0; i<100; i++)); do <<<"$(<file)" sed 's/bar/baz/' >file; done
$ cat file
foo
baz
За sed
в частности, вы можете использовать его -i
аргумент на месте. Он просто сохраняет обратно в файл, который он открыл, например:
sed -i 's/ /-/g' filename
Если вы хотите сделать что-то более громкое, если вы делаете больше, чем sed
да, вы можете буферизовать все это с sponge
(от moreutils
пакет), который "впитает" весь стандартный поток перед записью в файл. Это как tee
но с меньшей функциональностью. Для базового использования, это в значительной степени замена:
cat file | some_sed_command | sponge file >/dev/null
Это безопаснее? Определенно. Вероятно, он имеет ограничения, поэтому, если вы делаете что-то колоссальное (и не можете редактировать на месте с помощью sed), вы можете захотеть внести изменения во второй файл, а затем mv
этот файл обратно к исходному имени файла. Это должно быть атомарно (поэтому все, что зависит от этих файлов, не сломается, если им нужен постоянный доступ).
Ах, но sponge
не единственный вариант; вам не нужно получать moreutils
для того, чтобы заставить это работать должным образом. Любой механизм будет работать, если он удовлетворяет следующим двум требованиям:
- Он принимает имя выходного файла в качестве параметра.
- Он создает выходной файл только после обработки всего ввода.
Видите ли, хорошо известная проблема, на которую ссылается OP, заключается в том, что оболочка создаст все файлы, необходимые для работы каналов, прежде чем даже начнет выполнять команды в конвейере, поэтому именно оболочка фактически усекает выходной файл (который, к сожалению, также является входным файлом) еще до того, как какая-либо из команд сможет начать выполнение.
tee
Команда не работает, даже если она удовлетворяет первому требованию, потому что она не удовлетворяет второму требованию: она всегда будет создавать выходной файл сразу после запуска, так что это по сути так же плохо, как создание канала прямо в выходной файл. (На самом деле это еще хуже, потому что его использование вводит недетерминированную случайную задержку перед усечением выходного файла, поэтому вы можете подумать, что он работает, хотя на самом деле это не так.)
Итак, все, что нам нужно для решения этой проблемы, - это какая-то команда, которая буферизует все свои входные данные перед созданием какого-либо вывода, и которая способна принимать имя выходного файла в качестве параметра, так что нам не нужно передавать его вывод в выходной файл. Одна такая команда shuf
, Таким образом, следующее будет выполнять то же самое, что sponge
делает:
shuf --output=file --random-source=/dev/zero
--random-source=/dev/zero
часть трюки shuf
делать свое дело без всяких перетасовок, поэтому он буферизует ваш ввод, не изменяя его.
Вы можете использовать Vim в режиме Ex:
ex -sc '%!some_sed_command' -cx filename
%
выбрать все строки!
Команда запускаx
Сохранить и выйти