Сначала легкая часть: чтение файлов построчно из оболочки происходит медленно, возможно, вместо этого вы захотите использовать tr
. Подробнее см. man tr
.
Во-вторых, для этого вам понадобится временный файл. Вы не можете читать и записывать файл одновременно (во всяком случае, не из оболочки). Таким образом, вам нужно сделать что-то вроде этого:
tr -- "$1" "$2" <"$file" >"$file".tmp
mv -f -- "$file".tmp "$file"
Очевидная проблема заключается в том, что происходит, если tr
терпит неудачу по какой-либо причине (например, потому что $1
пуст). Затем $file.tmp
все равно будет создан (он создается, когда строка анализируется оболочкой), затем $file
заменяется им.
Таким образом, несколько более безопасный способ сделать это будет выглядеть следующим образом:
tr -- "$1" "$2" <"$file" >"$file".tmp && \
mv -f -- "$file".tmp "$file"
Теперь $file
заменяется только тогда, когда tr
завершается успешно.
Но что, если есть другой файл с именем $file.tmp
? Ну, это будет перезаписано. Здесь все становится сложнее: технически правильный способ сделать это примерно так:
tmp="$( mktemp -t "${0##*/}"_"$$"_.XXXXXXXX )" && \
trap 'rm -f "$tmp"' EXIT HUP INT QUIT TERM || exit 1
tr -- "$1" "$2" <"$file" >"$tmp" && \
cat -- "$tmp" >"$file" && \
rm -f -- "$tmp"
Здесь mktemp
создает временный файл; trap
гарантирует, что этот файл будет очищен, если скрипт выйдет (нормально или аварийно); и cat -- "$tmp" >"$file"
используется вместо mv
, чтобы убедиться, что разрешения $file
сохранены. Это все равно может завершиться ошибкой, если, скажем, у вас нет прав на запись в $file
, но временный файл в конечном итоге будет удален.
Кроме того, GNU sed
имеет возможность редактировать файлы на месте, поэтому вы можете сделать что-то вроде этого:
sed -i "s/$1/$2/g" "$file"
Однако это тоже не так просто, как кажется. Вышеприведенное не удастся, если $1
или $2
имеют особое значение для sed
. Поэтому вам нужно сначала сбежать от них:
in=$( printf '%s\n' "$1" | sed 's:[][\/.^$*]:\\&:g' )
out=$( printf '%s\n' "$2" | sed 's:[\/&]:\\&:g;$!s/$/\\/' )
sed -i "s/$in/$out/" "$file"