
Quero alterar a primeira linha de centenas de arquivos recursivamente da maneira mais eficiente possível. Um exemplo do que quero fazer é mudar #!/bin/bash
para #!/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 -i
lê 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
.git
em um, por exemplo), pois é improvável que você queira considerá-los (você usoufind ./*
which teria ignorado os arquivos e diretórios ocultos do diretório atual, mas não os dos subdiretórios). Adicione oD
qualificador 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,
zsh
pois outros shells não podem lidar com valores de bytes arbitrários). - estamos usando
#!/bin/sh -
como substituto qual é o shebang correto para/bin/sh
scripts ( a propósito,#!/bin/bash -
seria o shebang correto para scripts)./bin/bash
VerPor 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 bash
como intérprete como #! /bin/bash
, #! /bin/bash -Oextglob
, #! /usr/bin/env bash
, #! /bin/bash -efu
. Para esses, você precisa decidir o que fazer. -efu
são sh
opções, mas -Oextglob
não têm sh
equivalente, 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/sh
shebang, 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 bash
por sh
precisará 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 sh
terá 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.