Maneira mais eficiente de alterar 1 linha em um arquivo

Maneira mais eficiente de alterar 1 linha em um arquivo

Quero alterar a primeira linha de centenas de arquivos recursivamente da maneira mais eficiente possível. Um exemplo do que quero fazer é mudar #!/bin/bashpara #!/bin/sh, então criei este comando:

find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;

Mas, no meu entender, fazer desta forma sed tem que ler o arquivo inteiro e substituir o original. Existe uma maneira mais eficiente de fazer isso?

Responder1

Sim, sed -ilê e reescreve o arquivo por completo e, como o comprimento da linha muda, é necessário, pois move as posições de todas as outras linhas.

... mas neste caso, o comprimento da linha não precisa realmente mudar. Podemos substituir a linha hashbang #!/bin/sh␣␣por dois espaços à direita. O sistema operacional irá removê-los ao analisar a linha hashbang. (Como alternativa, use duas novas linhas ou um sinal de nova linha + hash, ambos criando linhas extras que o shell eventualmente ignorará.)

Tudo o que precisamos fazer é abrir o arquivo para escrita desde o início, sem truncá-lo. Os redirecionamentos usuais >não >>podem fazer isso, mas no Bash, o redirecionamento de leitura e gravação <>parece funcionar:

echo '#!/bin/sh  ' 1<> foo.sh

ou usando dd(devem ser opções padrão POSIX):

echo '#!/bin/sh  ' | dd of=foo.sh conv=notrunc

Observe que, estritamente falando, ambos também reescrevem a nova linha no final da linha, mas isso não importa.

Obviamente, o acima substitui o início do arquivo fornecido incondicionalmente. Adicionar uma verificação de que o arquivo original tem o hashbang correto é deixado como um exercício... Independentemente disso, eu provavelmente não faria isso em produção e, obviamente, isso não funcionará se você precisar alterar a linha para ummais longoum.

Responder2

Uma otimização seria usar {} +em vez de {} \;.

find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +

Em vez de invocar um processo sed para cada arquivo encontrado, você fornece os arquivos como argumentos para um único processo sed.

Especificação POSIX de encontrar em{} +(meu negrito):

Se a expressão primária for pontuada por um <sinal de mais>, a expressão primária deverá sempre ser avaliada como verdadeira e os nomes de caminho para os quais a expressão primária é avaliada deverão ser agregados em conjuntos.O utilitário utility_name deve ser invocado uma vez para cada conjunto de nomes de caminhos agregados.

Responder3

Eu faria:

#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
shebang_to_replace=$'#!/bin/bash\n'
       new_shebang=$'#!/bin/sh -\n'

length=$#shebang_to_replace

ret=0
for file in **/*(N.L+$((length - 1)));do
  if
    read -u0 -k $length shebang < $file &&
      [[ $shebang = $shebang_to_replace ]]
  then
    print -rn -- $new_shebang 1<> $file || ret=$?
  fi
done
exit $ret

ComoA abordagem de @ilkkachu, o arquivo será substituído por uma string exatamente do mesmo tamanho. As diferenças são:

  • ignoramos arquivos ocultos e arquivos em diretórios ocultos (pense .gitem um, por exemplo), pois é improvável que você queira considerá-los (você usou find ./*which teria ignorado os arquivos e diretórios ocultos do diretório atual, mas não os dos subdiretórios). Adicione o Dqualificador glob se desejar.
  • não nos preocupamos em procurar arquivos que não são grandes o suficiente para conter o shebang original para substituir (usamos .como equivalente a -type f, então já estamos recuperando as informações do inode do arquivo, então podemos também verificar o tamanho lá ).
  • na verdade, estamos verificando se o arquivo começa com o shebang correto para substituir, lendo o mínimo de bytes necessário (aqui deve ser, zshpois outros shells não podem lidar com valores de bytes arbitrários).
  • estamos usando #!/bin/sh -como substituto qual é o shebang correto para /bin/shscripts ( a propósito, #!/bin/bash -seria o shebang correto para scripts). /bin/bashVerPor que o "-" no "#! /bin/sh -" shebang?para detalhes.

Erros na substituição de arquivos são relatados no status de saída, mas não erros ao percorrer a árvore de diretórios, nem erros na leitura dos arquivos, embora isso possa ser adicionado.

Em qualquer caso, apenas substitui os shebangs que sãoexatamente #!/bin/bash, não outras coisas que usam bashcomo intérprete como #! /bin/bash, #! /bin/bash -Oextglob, #! /usr/bin/env bash, #! /bin/bash -efu. Para esses, você precisa decidir o que fazer. -efusão shopções, mas -Oextglobnão têm shequivalente, por exemplo.

Você pode estendê-lo para oferecer suporte aos casos mais fáceis, como:

#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
zmodload zsh/system || exit

minlength=11 # length of "#!/bin/bash"
maxlength=1024 # arbitrary here.

ret=0
for file in **/*(N.L+$minlength);do
  if
    sysread -s $maxlength buf < $file &&
      [[ $buf =~ $'(^#![\t ]*((/usr)?/bin/env[ \t]+bash|/bin/bash)([ \t]+-([aCefux]*))?[ \t]*)\n' ]]
  then
    shebang=$match[1] newshebang="#!/bin/sh -$match[5]"
    print -r -- ${(r[$#shebang])newshebang} 1<> $file || ret=$?
  fi
done
exit $ret

Aqui permitindo uma série de shebangs diferentes com uma série de opções suportadas que são reproduzidas no novo /bin/shshebang, preenchido à direita (com o r[length]sinalizador de expansão de parâmetro) para o mesmo tamanho do original.

Responder4

Os arquivos são um longo intervalo contíguo de bytes. Sua substituição bashpor shprecisará essencialmente remover os dois bytes (assumindo UTF-8 ou similar) que compõem o arquivo ba. Os arquivos não podem ter buracos, então tudo a partir de então shterá que ser gravado dois bytes antes no arquivo.

Isto requer uma reescrita de todo o arquivo, ou pelo menos começando pela parte alterada.

Existem maneiras desubstituirbytes em um arquivo, por exemplo com espaços inocentes se o formato permitir isso, sem precisar reescrever o arquivo inteiro, veja a resposta aceita.

informação relacionada