Como posso obter portabilidade com o sed -i (edição no local)?

41

Estou escrevendo scripts shell para o meu servidor, que é uma hospedagem compartilhada executando o FreeBSD. Também quero testá-los localmente, no meu PC executando o Linux. Por isso, estou tentando escrevê-los de maneira portátil, mas sednão vejo como fazer isso.

Parte do meu site usa arquivos HTML estáticos gerados e essa linha sed insere DOCTYPE correto após cada regeneração:

sed -i '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

Funciona com o GNU sedno Linux, mas o FreeBSD sedespera que o primeiro argumento após a -iopção seja extensão para cópia de backup. É assim que ficaria:

sed -i '' '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

No entanto, o GNU, sedpor sua vez, espera que a expressão siga imediatamente depois -i. (Também requer correções no tratamento da nova linha, mas isso já é respondido aqui )

É claro que posso incluir essa alteração na minha cópia do servidor do script, mas isso prejudicaria, ou seja, meu uso do VCS para controle de versão. Existe uma maneira de conseguir isso com o sed de uma maneira totalmente portátil?

Vermelho
fonte
Os dois snippets sed que você forneceu são idênticos, tem certeza de que não há um erro de digitação? Além disso, eu sou capaz de executar GNU sed fornecendo a extensão de backup logo após-i
Iruvar
Duh, obrigado por descobrir isso. Eu corrigi minha pergunta. A segunda linha resulta em erro no meu sed, espera que '1s / ^ / <! DOCTYPE html> \ n /' seja um arquivo e reclama que não consegue encontrá-lo.
Vermelho
11
Referência cruzada: sinalizador sed in-place que funciona tanto no Mac (BSD) quanto no Linux no Stack Overflow.
MvG

Respostas:

40

O GNU sed aceita uma extensão opcional depois -i. A extensão deve estar no mesmo argumento, sem espaço intermediário. Essa sintaxe também funciona no BSD sed.

sed -i.bak -e '…' SOMEFILE

Observe que no BSD, -itambém muda o comportamento quando existem vários arquivos de entrada: eles são processados ​​independentemente (portanto, $corresponde à última linha de cada arquivo). Além disso, isso não funcionará no BusyBox.

Se você não quiser usar arquivos de backup, poderá verificar qual versão do sed está disponível.

case $(sed --help 2>&1) in
  *GNU*) set sed -i;;
  *) set sed -i '';;
esac
"$@" -e '…' "$file"

Ou, alternativamente, para evitar interferir nos parâmetros posicionais, defina uma função.

case $(sed --help 2>&1) in
  *GNU*) sed_i () { sed -i "$@"; };;
  *) sed_i () { sed -i '' "$@"; };;
esac
sed_i -e '…' "$file"

Se você não quer se preocupar, use Perl.

perl -i -pe '…' "$file"

Se você quiser escrever um script portátil, não use -i- ele não está no POSIX. Faça manualmente o que o sed faz sob o capô - é apenas mais uma linha de código.

sed -e '…' "$file" >"$file.new"
mv -- "$file.new" "$file"
Gilles 'SO- parar de ser mau'
fonte
2
GNU sed -itambém implica -s. E a maneira mais fácil de verificar um GNU sedé com o sed vcomando que é um noop válido para o GNU, mas falha em qualquer outro lugar.
mikeserv
Inspirado pelas dicas acima, aqui está uma linha única (se feio) versão portátil para aqueles que realmente quer um, embora ele não criar um subshell: sed -i$(sed v < /dev/null 2> /dev/null || echo -n " ''") -e '...' "$file"Se não é GNU sed, insere um espaço seguido por duas citações singe depois -ipara que ele trabalha em BSD. GNU sed recebe apenas -i.
Ivan X
2
@IvanX Desconfio de usar a presença do vcomando para testar o GNU sed. E se o FreeBSD decidisse implementá-lo?
Gilles 'SO- stop be evil'
@Gilles Fair, mas a página de manual do GNU sed descreve v como exatamente para esse propósito (detectando que é GNU sed e não outra coisa), então seria de esperar que o * BSD honrasse isso. Não consigo pensar em outro teste, de improviso, que não executa nenhuma ação no GNU sed, enquanto causa um erro no BSD sed (ou vice-versa), além de usar -i em si, mas isso exigiria a criação de um arquivo fictício primeiro. Seu teste para sed acima é bom, mas difícil para inline. Evitar -i inteiramente, como você sugere, certamente parece ser a aposta mais segura, mas estou bem em usar, sed vjá que esse é o seu objetivo de existir.
Ivan X
11
@lapo Uma alternativa é definir uma função, veja minha edição.
Gilles 'SO- stop being evil'
10

Se você não encontrar um truque para tornar o sedjogo agradável, tente:

  1. Não use -i:

    sed '1s/^/<!DOCTYPE html> \n/' "${file_name.html}" > "${file_name.html}.tmp" &&
      mv "${file_name.html}.tmp" "${file_name.html}"
  2. Use Perl

    perl -i -pe 'print "<!DOCTYPE html> \n" if $.==1;' "${file_name.html}"
terdon
fonte
8

ed

Você sempre pode usar edpara acrescentar uma linha a um arquivo existente.

$ printf '0a\n<!DOCTYPE html>\n.\nw\n' | ed my.html

Detalhes

Os bits ao redor do <!DOCTYPE html>são comandos para edinstruí-lo a adicionar essa linha ao arquivo my.html.

sed

Eu acredito que este comando sedtambém pode ser usado:

$ sed -i '1i<!DOCTYPE html>\n` testfile.csv
slm
fonte
Acabei recorrendo ao Perl, mas usar o ed é uma boa alternativa que não é popular entre os usuários do Unix, como deveria ser.
Vermelho
@ Red - feliz em saber que você resolveu o problema. Sim, eu nunca tinha visto isso antes, pesquisando no Google e realmente parecia a maneira mais portátil e adequada de fazer isso.
slm
7

Você também pode fazer manualmente o que perl -ifaz sob o capô:

{ rm -f file && { echo '<!DOCTYPE html>'; cat; } > file;} < file

Assim perl -i, não há backup e, como a maioria das soluções fornecidas aqui, cuidado com isso pode afetar as permissões, a propriedade do arquivo e transformar um link simbólico em um arquivo comum.

Com:

sed '1i\
<!DOCTYPE html>' file 1<> file

sedsobrescreveria o arquivo, não afetaria a propriedade e as permissões ou links simbólicos. Funciona com o GNU sedporque sednormalmente terá lido um buffer cheio de dados de file(4k no meu caso) antes de sobrescrevê-lo com o icomando Isso não funcionaria se o arquivo tivesse mais de 4k, exceto pelo fato de sedtambém armazenar em buffer sua saída.

sedFunciona basicamente em blocos de 4k para leitura e escrita. Se a linha a inserir for menor que 4k, sednunca substituirá um bloco que ainda não leu.

Eu não contaria com isso.

Stéphane Chazelas
fonte
Deve ser echo '<!DOCTYPE html>'ou escapou sem "" aspas.
AD
@AD Bom ponto. Costumo esquecer esse bug ... Característica do bash / zsh interativo, pois geralmente o desativo.
Stéphane Chazelas
Esta é uma das muito poucas respostas aqui que não tem uma falha de segurança aberta de redirecionar a um "arquivo temporário" com uma estática, nome previsível sem verificar se ele já existe. Eu acredito que essa deve ser a resposta aceita. Demonstração muito boa do uso de comandos de grupo também.
Curinga
3

O FreeBSD sed , que também é usado no Mac OS X, precisa da -eopção após a -ialternância para definir e reconhecer o seguinte comando (regex) corretamente e sem ambiguidade.

Em outras palavras, sed -i -e ...deve funcionar com o FreeBSD e GNU sed.

De maneira geral, a omissão da extensão de backup após o FreeBSD sed -irequer alguma sedopção ou opção explícita, a seguir a, -ipara evitar confusão por parte do FreeBSD sedenquanto analisa seus argumentos de linha de comando.

(Observe, no entanto, que sededições in-loco de arquivos levam a alterações no inode de arquivo, consulte Edição "in-loco" de arquivos ).

(Como uma dica geral, as versões recentes do FreeBSD sedtêm a -ropção de aumentar a compatibilidade com o GNU sed).

echo a > testfile.txt
ls -li testfile.txt
#gsed -i -e 's/a/A/' testfile.txt
#bsdsed -i 's/a/A/' testfile.txt  # does not work
bsdsed -i -e 's/a/A/' testfile.txt
ls -li testfile.txt
cat testfile.txt
carlo
fonte
5
Não, nãobsdsed -i -e 's/a/A/' é uma edição no local, é editada ao salvar o original com o sufixo "-e" ( testfile.txt-e).
Stéphane Chazelas
note que o GNU sed também suporta -E (além de -r) para compatibilidade com o FreeBSD. É provável que -E seja especificado na próxima versão do POSIX, portanto todos devemos esquecer esse sentido ou fingir que nunca existiu.
Stéphane Chazelas
2

Para emular sed -ipara um único arquivo de maneira portável, evitando ao máximo as condições de corrida:

sed 'script' <<FILE >file
$(cat file)
FILE

A propósito, isso também lida com o possível problema que sed -i, dependendo das permissões de diretório e arquivo, sed -ipode permitir que um usuário substitua um arquivo que esse usuário não tem permissão para editar.

Você também pode fazer backups como:

sed 'script' <<FILE >file
$(tee file.old <file)
FILE
mikeserv
fonte
2

Você pode usar o Vim no modo Ex:

ex -sc '1i|<!DOCTYPE html>' -cx file
  1. 1 selecione a primeira linha

  2. i inserir texto e nova linha

  3. x salvar e fechar

Ou ainda, como nos comentários, padrão ex antigo simples :

printf '%s\n' 1i '<!DOCTYPE html>' . x | ex file 
Steven Penny
fonte
Não há nada específico do Vim aqui. Isso é totalmente compatível com as especificações POSIX paraex , exceto que as implementações não são necessárias para suportar vários -csinalizadores. Para portabilidade definitiva eu usariaprintf '%s\n' 1i '<!DOCTYPE html>' . x | ex file
Wildcard