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"