First the easy part: reading files line by line from the shell is slow, you probably want to use tr
instead. See man tr
for details.
Second, you need a temporary file for that. You can't read and write a file at the same time (not from the shell anyway). So you need to do something like this:
tr -- "$1" "$2" <"$file" >"$file".tmp
mv -f -- "$file".tmp "$file"
An obvious problem with this is what happens if tr
fails, for whatever reason (say because $1
is empty). Then $file.tmp
will still get created (it's created when the line is parsed by the shell), then $file
gets replaced by it.
So a somewhat safer way to do it would look like this:
tr -- "$1" "$2" <"$file" >"$file".tmp && \
mv -f -- "$file".tmp "$file"
Now $file
is replaced only when tr
succeeds.
But what if there is another file named $file.tmp
around? Well, it will get overwritten. This is where it gets more complicated: the technically correct way to do it is something like this:
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"
Here mktemp
creates a temporary file; trap
makes sure this file is cleaned up if the script exits (normally or abnormally); and cat -- "$tmp" >"$file"
is used instead of mv
to make sure permissions of $file
are preserved. This can still fail if, say, you don't have write permissions on $file
, but the temporary file is removed eventually.
Alternatively, GNU sed
has an option to edit files in place, so you could do something like this:
sed -i "s/$1/$2/g" "$file"
However, this is not as simple as it looks either. The above will fail if $1
or $2
have a special meaning for sed
. So you need to escape them first:
in=$( printf '%s\n' "$1" | sed 's:[][\/.^$*]:\\&:g' )
out=$( printf '%s\n' "$2" | sed 's:[\/&]:\\&:g;$!s/$/\\/' )
sed -i "s/$in/$out/" "$file"