Skip to main content
1 of 2
Satō Katsura
  • 13.7k
  • 2
  • 34
  • 52

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.

Satō Katsura
  • 13.7k
  • 2
  • 34
  • 52