Como fazer uma substituição de texto em uma hierarquia de pastas grandes?

11

Quero pesquisar e substituir algum texto em um grande conjunto de arquivos, excluindo algumas instâncias. Para cada linha, quero um prompt perguntando se preciso substituí-la ou não. Algo semelhante ao vim :%s/from/to/gc(com o cprompt de confirmação), mas em um conjunto de pastas. Existe alguma ferramenta ou script de linha de comando que possa ser usado?

balki
fonte
Sobre a importância da formatação correta: eu inicialmente leria seu comando como s/from/to/gcom uma falha de formatação depois dele, em vez de s/from/to/gcenfatizar o cque você tentou escrever (você não pode fazer isso com o Markdown, você pode fazê-lo com <code>e <strong>tags HTML).
Gilles 'SO- stop be evil'

Respostas:

19

Por que não usar o vim?

Abra todos os arquivos no vim

vim $(find . -type f)

Ou abra apenas arquivos relevantes (como sugerido por Caleb)

vim $(grep 'from' . -Rl)

E execute a substituição em todos os buffers

:bufdo %s/from/to/gc | update

Você também pode fazê-lo sed, mas meu conhecimento é limitado.

Gert
fonte
Obrigado, sua resposta me fez pensar duas vezes mais tarde: percebi que havia perdido completamente a parte interativa. Eu não acho que isso seja possível com o sed (não há canais de entrada / saída suficientes).
Gilles 'SO- stop be evil'
1
Você pode acelerar isso não abrindo TODOS os arquivos no buffer atual usando, em grepvez de, findpara abrir apenas arquivos que tenham correspondências conhecidas. vim $(grep 'from' . -Rl)
Caleb
Obrigado.O c (astreriks em torno de c) é necessário? ou é um problema de formatação?
Balki # 5/11
@balki é um problema de "formatação". Corrigido
Gert
5

Você pode fazer algo bruto com um pequeno script Perl que é instruído a executar substituições linha por linha ( -l -pe) nos arquivos passados ​​como argumentos ( -i):

perl -i -l -pe '
    if (/from/) {                            # is the source text present on this line?
        printf STDERR ("%s: %s [y/N]? ", $ARGV, $_);  # display a prompt
        $r=<STDIN>;                                   # read user response
        if ($r =~ /^[Yy]/) {                          # if user entered Y:
            s/from/to/g;                              # replace all occurences on this line
    }' /path/to/files

As possíveis melhorias seriam colorir partes do prompt e dar suporte a coisas como "substituir todas as ocorrências no arquivo atual". Solicitar separadamente cada ocorrência em uma linha seria mais difícil.

Segunda parte, combinando os arquivos. se não houver muitos arquivos envolvidos e você estiver executando o zsh, poderá corresponder todos os arquivos no diretório atual e seus subdiretórios recursivamente:

perl -i -l -pe '…' **/*(.)

Se o seu shell for bash ≥4, você poderá executar perl … **/*, mas isso produzirá mensagens de erro falsas porque o sed tentará (e falhará) executar nos diretórios. Se você quiser executar a substituição apenas em um conjunto de arquivos, como arquivos C, poderá restringir as correspondências (que funcionam no bash ≥4 ou zsh):

perl -i -l -pe '…' **/*.[hc]

Se você precisar de um controle mais preciso sobre os arquivos que você está substituindo, ou se seu shell não possui a construção de correspondência de diretório recursiva **, ou se você tiver muitos arquivos e receber um erro de "linha de comando muito longa", use find. Por exemplo, para executar uma substituição em todos os arquivos nomeados *.hou *.cno diretório atual e em seus subdiretórios (em sistemas mais antigos, pode ser necessário usá-lo em \;vez de +no final da linha (o +formulário é mais rápido, mas não está disponível em todos os lugares).

find . -type f -name '*.[hc]' -exec perl -i -l -pe '…' {} +

Dito isto, eu me ateria a um editor interativo se você precisar de interação. Gert mostrou uma maneira de fazer isso no Vim , embora exija a abertura de todos os arquivos pelos quais você deseja pesquisar, o que pode ser um problema se houver muito.

No Emacs, veja como você pode fazer isso:

  1. Reúna os nomes dos arquivos com M-x find-name-dired(especifique um diretório de nível superior) ou M-x find-dired(especifique uma findlinha de comando arbitrária ).
  2. No buffer direcionado resultante , pressione tpara marcar todos os arquivos e, em seguida, Q( dired-do-query-replace-regexp) para executar uma substituição solicitando os arquivos marcados.
Gilles 'SO- parar de ser mau'
fonte
1

sdiff(consulte http://www.gnu.org/software/diffutils/manual/diffutils.html#Invoking-sdiff ) pode ser útil aqui. Com ele, você pode fazer patches interativos. Portanto, fazê-lo com um arquivo temporário que você criou executando operações de substituição usando sedpode ser uma solução possível:

# use file descriptor 3 to still allow use of stdin
while IFS= read -r -d '' file <&3; do

  # write the result of the replacement into a temporary file
  sed -r 's/something/something_else/g' -- "$file" > replacer_tmp

  if cmp -s -- "$file" replacer_tmp; then
    continue; # nothing was replaced
  fi

  echo "There is something to replace in '$file'! Starting interactive diff."
  echo

  sdiff -o "$file" -s -d -- "$file" replacer_tmp

  echo

done 3< <(find . -type f -print0)

(Loop de arquivo usando substituição de processo não POSIX e read -dconforme suportado, por exemplo bash.)

phk
fonte