Como posso fazer uma busca recursiva e substituir a partir da linha de comando?

62

Usando um shell como o bash ou o zshell, como posso fazer um 'localizar e substituir' recursivo? Em outras palavras, quero substituir cada ocorrência de 'foo' por 'bar' em todos os arquivos neste diretório e em seus subdiretórios.

Nathan Long
fonte
Uma resposta alternativa para as mesmas perguntas pode ser encontrada aqui stackoverflow.com/questions/9704020/…
dunxd

Respostas:

80

Este comando fará isso (testado no Mac OS X Lion e no Kubuntu Linux).

# Recursively find and replace in files
find . -type f -name "*.txt" -print0 | xargs -0 sed -i '' -e 's/foo/bar/g'

Veja como isso funciona:

  1. find . -type f -name '*.txt' encontra, no diretório atual ( . ) e abaixo, todos os arquivos regulares ( -type f ) cujos nomes terminam em .txt
  2. | passa a saída desse comando (uma lista de nomes de arquivos) para o próximo comando
  3. xargs reúne esses nomes de arquivos e os entrega um por um para sed
  4. sed -i '' -e 's/foo/bar/g' significa "editar o arquivo no lugar, sem um backup, e fazer a seguinte substituição ( s/foo/bar ) várias vezes por linha ( /g )" (Vejo man sed )

Note que a parte "sem um backup" na linha 4 está OK para mim, porque os arquivos que estou alterando estão sob controle de versão, assim eu posso facilmente desfazer se houve um erro.

Nathan Long
fonte
9
Nunca pipe nunca encontrar saída para xargs sem o -print0 opção. Seu comando falhará em arquivos com espaços, etc. em seus nomes.
slhck
17
Além disso, apenas find -name '*.txt' -exec sed -i 's/foo/bar/g' {} + fará tudo isso com o GNU find.
Daniel Andersson
6
eu recebo sed: can't read : No such file or directory quando eu corro find . -name '*.md' -print0 | xargs -0 sed -i '' -e 's/ä/ä/g', mas find . -name '*.md' -print0 fornece uma lista de muitos arquivos.
Martin Thoma
8
Isso funciona para mim se eu remover o espaço entre o -i e a ''
Canadian Luke
3
Qual é o significado de '' depois de sed -i o que é '' Função?
Jas
22
find . -type f -name "*.txt" -exec sed -i'' -e 's/foo/bar/g' {} +

Isso remove o xargs dependência.

Ztyx
fonte
4
Isso não funciona com o GNU sed, então falhará na maioria dos sistemas. GNU sed requer que você não coloque nenhum espaço entre -i e ''.
slhck
1
As respostas aceitas fazem um trabalho melhor de explicá-lo. mas +1 para usar a sintaxe correta.
orodbhen
6

Se você está usando o Git, então você pode fazer isso:

git grep -lz foo | xargs -0 sed -i '' -e 's/foo/bar/g'

-l lista apenas nomes de arquivos. -z imprime um byte nulo após cada resultado.

Acabei fazendo isso porque alguns arquivos em um projeto não tinham uma nova linha no final do arquivo, e o sed adicionou uma nova linha mesmo quando não fez outras alterações. (Nenhum comentário sobre se os arquivos devem ou não ter uma nova linha no final.)

David Winiecki
fonte
1
Big +1 para esta solução. O resto do find ... -print0 | xargs -0 sed ... As soluções não só levam muito mais tempo, mas também adicionam novas linhas aos arquivos que já não o possuem, o que é um incômodo ao trabalhar dentro de um repositório do git. git grep é iluminação rápida por comparação.
Josh Kupershmidt
3

Aqui está a minha função zsh / perl que eu uso para isso:

change () {
        from=$1 
        shift
        to=$1 
        shift
        for file in $*
        do
                perl -i.bak -p -e "s{$from}{$to}g;" $file
                echo "Changing $from to $to in $file"
        done
}

E eu iria executá-lo usando

$ change foo bar **/*.java

(por exemplo)

Brian Agnew
fonte
2

Experimentar:

sed -i 's/foo/bar/g' $(find . -type f)

Testado no Ubuntu 12.04.

Este comando NÃO funcionará se nomes de subdiretórios e / ou nomes de arquivos contiverem espaços, mas se você os tiver, não use este comando, pois ele não funcionará.

Geralmente, é uma prática ruim usar espaços em nomes de diretórios e nomes de arquivos.

http://linuxcommand.org/lc3_lts0020.php

Veja "Fatos importantes sobre nomes de arquivos"

nexayq
fonte
Experimente quando tiver arquivo (s) com espaço (s) em seus nomes. (Há uma regra que diz: "Se algo parece bom demais para ser verdade, provavelmente é." Se você "descobriu" uma solução que é mais compacta do que qualquer outra pessoa postou em 3 anos e meio, você deve se perguntar por que isso pode ser.)
Scott
1

Use este script de shell

Agora eu uso esse script de shell, que combina as coisas que aprendi com as outras respostas e com a pesquisa na web. Coloquei em um arquivo chamado change em uma pasta no meu $PATH e fez chmod +x change.

#!/bin/bash
function err_echo {
  >&2 echo "$1"
}

function usage {
  err_echo "usage:"
  err_echo '  change old new foo.txt'
  err_echo '  change old new foo.txt *.html'
  err_echo '  change old new **\*.txt'
  exit 1
}

[ $# -eq 0 ] && err_echo "No args given" && usage

old_val=$1
shift
new_val=$1
shift
files=$* # the rest of the arguments

[ -z "$old_val" ]  && err_echo "No old value given" && usage
[ -z "$new_val" ]  && err_echo "No new value given" && usage
[ -z "$files" ]    && err_echo "No filenames given" && usage

for file in $files; do
  sed -i '' -e "s/$old_val/$new_val/g" $file
done
Nathan Long
fonte
0

Meu caso de uso era eu queria substituir foo:/Drive_Letter com foo:/bar/baz/xyz No meu caso, consegui fazer isso com o seguinte código. Eu estava no mesmo local de diretório onde havia muitos arquivos.

find . -name "*.library" -print0 | xargs -0 sed -i '' -e 's/foo:\/Drive_Letter:/foo:\/bar\/baz\/xyz/g'

Espero que tenha ajudado.

neo7
fonte
0

O seguinte comando funcionou bem no Ubuntu e no CentOS; no entanto, no OS X, continuei recebendo erros:

find . -name Root -exec sed -i 's/1.2.3.4\/home/foo.com\/mnt/' {} \;

sed: 1: "./Root": código de comando inválido.

Quando tentei passar os params via xargs funcionou bem sem erros:

find . -name Root -print0 | xargs -0 sed -i '' -e 's/1.2.3.4\/home/foo.com\/mnt/'
John Morgan
fonte
O fato de você ter mudado -i para -i '' é provavelmente mais relevante do que o fato de você ter mudado -exec para -print0 | xargs -0. BTW, você provavelmente não precisa do -e.
Scott