Книга: UNIX — универсальная среда программирования

5.5 Команда overwrite: замена файла

5.5 Команда overwrite: замена файла

В команде sort есть флаг для замены файла:

$ sort файл1 -о файл2

Ее эквивалент:

$ sort файл1 > файл2

Если файл1 и файл2 — это один и тот же файл, то после операции переключения > входной файл станет пустым перед сортировкой. Но с флагом команда выполняется правильно, потому что входной файл сортируется и сохраняется во временном файле перед созданием выходного файла.

Могут использовать флаг и другие команды. Например, редактор sed может редактировать файл с заменой:

$ sed 's/UNIX/UNIX (TM)/g' -o ch2 Так не получится!

Непрактично изменять все подобные команды, вводя флаг — это не лучшее решение. Более целесообразным представляется централизованное выполнение функций, как в случае операции > интерпретатора, для чего мы создадим программу overwrite. Первый ее вариант выглядит так:

$ sed 's/UNIX/UNIX (TM)/g' гл2 | overwrite гл2

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

# overwrite: copy standard input to output after EOF
# version 1. BUG here
PATH=/bin:/usr/bin
case $# in
1) ;;
*) echo 'Usage: overwrite file' 1>&2; exit 2
esac
new=/tmp/overwr.$$
trap 'rm -f $new; exit 1' 1 2 15
cat >$new # collect the input
cp $new $1 # overwrite the input file
rm -f $new

Команда cp используется вместо команды mv, чтобы не изменились права доступа и остался прежним владелец выходного файла, если он уже существует. Хотя этот вариант и чрезвычайно прост, здесь возможна "фатальная" ошибка: если пользователь нажмет клавишу DEL (УДЛ) во время выполнения команды cp, первоначальный выходной файл будет уничтожен. Необходимо соблюдать осторожность, поскольку прерывание может остановить замену входного файла:

# overwrite: copy standard input to output after EOF
# version 2. BUG here too
PATH=/bin:/usr/bin
case $# in 1) ;;
*) echo 'Usage: overwrite file' 1>&2; exit 2
esac
new=/tmp/overwr1.$$
old=/tmp/overwr2.$$
trap 'rm -f $new $old; exit 1' 1 2 15
cat >$new # collect the input
cp $1 $old # save original file
trap '' 1 2 15 # we are committed; ignore signals
cp $new $1 # overwrite the input file
rm -f $new $old

Если клавиша DEL будет нажата прежде, чем начнется работа с исходным файлом, то произойдет удаление временных файлов и файл останется один. После сохранения файла сигналы игнорируются, поэтому выполнение последней команды cp не прервется. Если команда cp начала выполняться, команда overwrite обязана заменить исходный файл.

Здесь есть некоторая тонкость. Рассмотрим последовательность:

$ sed 's/UNIX/UNIX(TM)g' special | overwrite special
command garbled: s/UNIX(TM)g
$ ls -l special
-rw-rw-rw- 1 you 0 Oct 1 09:02 special #$%@*!
$

Если в программе, поставляющей входной поток для команды overwrite, произойдет ошибка, то выходной поток будет пустым, и overwrite обязательно (с сознанием выполненного долга) уничтожит файл, заданный в качестве аргумента.

Во избежание такого финала можно предложить несколько решений. Команда overwrite могла бы запрашивать подтверждение перед заменой файла, но, сделав команду диалоговой, мы потеряем большую часть ее достоинств. Она могла бы проверять, что ее входной поток не пуст (с помощью test -2), но это некрасиво и к тому же неверно: выходной поток мог быть создан до обнаружения ошибки.

Наилучшее решение заключается в том, чтобы выполнять программу, поставляющую данные, под контролем команды overwrite, чтобы можно было проверить ее код завершения. Это, правда, противоречит традициям и здравому смыслу: ведь в конвейере команда overwrite обычно должна быть последней, но для правильной работы она должна идти первой. Однако overwrite ничего не выдает в стандартный выходной поток, поэтому можно считать, что не происходит потери общности. Более того, ее синтаксис не является каким-то необычным: time, nice, nohup представляют собой команды, аргументами которых служат другие команды. Ниже приведен безопасный вариант:

# overwrite: copy standard input to output after EOF
# final version
opath=$PATH
PATH=/bin:/usr/bin
case $# in
0|1) echo 'Usage: overwrite file cmd [args]' 1>&2; exit 2
esac
file=$1; shift
new=/tmp/overwr1.$$; old=/tmp/overwr2.$$
trap 'rm -f $new $old; exit 1' 1 2 15 # clean up files
if PATH=$opath "$@" >$new # collect input
then
 cp $file $old # save original file
 trap '' 1 2 15 # we are committed; ignore signals
 cp $new $file
else
 echo "overwrite: $1 failed, $file unchanged" 1>&2 exit 1
fi
rm -f $new $old

Встроенная команда интерпретатора shift сдвигает весь список аргументов на одну позицию влево: $2 становится $1, $3 становится $2 и т.д. Строка обозначает все аргументы (после shift), как и $*, но без интерпретации; мы вернемся к ее рассмотрению в разд. 5.7.

Заметьте, что значение PATH нужно восстановить перед выполнением команды пользователя; если этого не сделать, то команды, не находящиеся в /bin или /usr/bin, будут недоступны для overwrite.

Теперь команда overwrite выполняется верно (хотя и она получилась несколько громоздкой):

$ cat notice
Unix is a Trademark of Bell Laboratories
$ overwrite notice sed 's/UNIXUNIX(TM)/g' notice
command garbled: s/UNIXUNIX(TM)/g
overwrite: sed failed, notice unchanged
$ cat notice
UNIX is a Trademark of Bell Laboratories
He изменился

$ overwrite notice sed 's/UNIX/UNIX(TM)/g' notice
$ cat notice
UNIX(TM) is a Trademark of Bell Laboratories
$

Типичной задачей является использование редактора sed для замены всех вхождений одного слова на другое слово. Имея под рукой команду overwrite, легко написать программу на языке shell для ее решения:

$ cat replace
# replace: replace str1 in files with str2, in place
PATH=/bin:/usr/bin
case $# in
0|1|2) echo 'Usage: replace str1 str2 files' 1>&2; exit 1
esac
left="$1"; right="$2"; shift; shift
for i do
 overwrite $i sed "s@$left@$right@g" $i
done
$ cat footnote
UNIX is not an acronym
$ replace UNIX Unix footnote
$ cat footnote
Unix is not an acronym
$

(Вспомните: если список в цикле for пуст, то по умолчанию он равен $*.) Мы использовали @ вместо / для разбиения в команде подстановки, поскольку менее вероятно, что @ вступит в конфликт с входной строкой. Команда replace устанавливает PATH равным /bin:/usr/bin, исключая $HOME/bin. Это означает, что overwrite должна находиться в /usr/bin, чтобы команда replace сработала. Мы сделали такое предположение для простоты; если вы не можете поместить overwrite в /usr/bin, вам придется добавить $HOME/bin к PATH в команде replace или явно задать полное имя overwrite. В дальнейшем будем полагать, что команды, которые мы создаем, находятся в /usr/bin, где им и следует быть.

Упражнение 5.17

Почему команда overwrite не использует сигнал 0 в команде trap, чтобы файлы удалялись при выходе из нее? Подсказка: попробуйте нажать клавишу DEL во время выполнения следующей программы:

trap "echo exiting; exit 1" 0 2
sleep 10

Упражнение 5.18

Добавьте флаг -v к команде replace для вывода всех измененных строк на /dev/tty.

Подсказка: s/$left/$right/g $vflag.

Упражнение 5.19

Увеличьте надежность команды replace, чтобы ее выполнение не зависело от символов в строке замены.

Упражнение 5.20

Можно ли использовать replace для замены i на index всюду в программе? Какие вы внесли бы изменения, чтобы добиться этого?

Упражнение 5.21

Достаточно ли команда replace эффективна и удобна, чтобы находиться в каталоге /usr/bin? Не лучше ли вводить по мере необходимости подходящие команды редактора sed (да или нет)? Обоснуйте свой ответ.

Упражнение 5.22

(Усложненное.) Команда

$ overwrite файл 'who | sort'

не выполняется. Объясните причину этого и исправьте ее. Подсказка: посмотрите eval в справочном руководстве по sh(1). Как ваше решение повлияет на интерпретацию специальных символов в команде?

Оглавление книги


Генерация: 1.208. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз