Primeiro a parte fácil: ler arquivos linha por linha a partir do shell é lento, você provavelmente vai querer usar tr . Veja man tr para detalhes.
Em segundo lugar, você precisa de um arquivo temporário para isso. Você não pode ler e escrever um arquivo ao mesmo tempo (não a partir do shell). Então você precisa fazer algo assim:
tr -- "$1" "$2" <"$file" >"$file".tmp
mv -f -- "$file".tmp "$file"
Um problema óbvio com isso é o que acontece se tr falhar, por qualquer razão (digamos, porque $1 está vazio). Então $file.tmp ainda será criado (é criado quando a linha é analisada pelo shell), então $file é substituído por ele.
Então, uma maneira um pouco mais segura de fazer isso ficaria assim:
tr -- "$1" "$2" <"$file" >"$file".tmp && \
mv -f -- "$file".tmp "$file"
Agora, $file é substituído apenas quando tr for bem-sucedido.
Mas e se houver outro arquivo chamado $file.tmp ao redor? Bem, isso será substituído. É aí que fica mais complicado: a maneira tecnicamente correta de fazer isso é algo assim:
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"
Aqui mktemp cria um arquivo temporário; trap garante que este arquivo seja limpo se o script sair (normalmente ou de forma anormal); e cat -- "$tmp" >"$file" é usado em vez de mv para garantir que as permissões de $file sejam preservadas. Isso ainda pode falhar se, por exemplo, você não tiver permissões de gravação em $file , mas o arquivo temporário será removido eventualmente.
Alternativamente, o GNU sed tem uma opção para editar arquivos no lugar, então você pode fazer algo assim:
sed -i "s/$1/$2/g" "$file"
No entanto, isso não é tão simples quanto parece. Os itens acima falharão se $1 ou $2 tiverem um significado especial para sed . Então você precisa escapar primeiro:
in=$( printf '%s\n' "$1" | sed 's:[][\/.^$*]:\&:g' )
out=$( printf '%s\n' "$2" | sed 's:[\/&]:\&:g;$!s/$/\/' )
sed -i "s/$in/$out/" "$file"