Como posso substituir uma string em um arquivo?

751

Substituir seqüências de caracteres em arquivos com base em determinados critérios de pesquisa é uma tarefa muito comum. Como posso

  • substituir string foopor bartodos os arquivos no diretório atual?
  • fazer o mesmo recursivamente para subdiretórios?
  • substituir apenas se o nome do arquivo corresponder a outra sequência?
  • substituir apenas se a string for encontrada em um determinado contexto?
  • substituir se a string estiver em um determinado número de linha?
  • substituir várias strings pela mesma substituição
  • substituir várias strings por diferentes substituições
terdon
fonte
2
Esta pretende ser uma sessão de perguntas e respostas canônicas sobre este assunto (consulte esta meta-discussão ). Sinta-se à vontade para editar minha resposta abaixo ou adicionar a sua própria.
terdon

Respostas:

1009

1. Substituindo todas as ocorrências de uma sequência por outra em todos os arquivos no diretório atual:

Esses são casos nos quais você sabe que o diretório contém apenas arquivos regulares e que deseja processar todos os arquivos não ocultos. Se não for esse o caso, use as abordagens em 2.

Todas as sedsoluções nesta resposta assumem o GNU sed. Se estiver usando o FreeBSD ou OS / X, substitua -ipor -i ''. Observe também que o uso do -icomutador com qualquer versão do sedtem certas implicações na segurança do sistema de arquivos e é desaconselhável em qualquer script que você planeja distribuir de qualquer forma.

  • Arquivos não recursivos, apenas neste diretório:

    sed -i -- 's/foo/bar/g' *
    perl -i -pe 's/foo/bar/g' ./* 

    (o perlque falhará para nomes de arquivos terminados em |ou espaço) ).

  • Arquivos regulares e recursivos ( incluindo os ocultos ) neste e em todos os subdiretórios

    find . -type f -exec sed -i 's/foo/bar/g' {} +

    Se você estiver usando o zsh:

    sed -i -- 's/foo/bar/g' **/*(D.)

    (pode falhar se a lista for muito grande, consulte zargsa solução alternativa).

    O Bash não pode verificar diretamente se há arquivos regulares, é necessário um loop (chaves evitam definir as opções globalmente):

    ( shopt -s globstar dotglob;
        for file in **; do
            if [[ -f $file ]] && [[ -w $file ]]; then
                sed -i -- 's/foo/bar/g' "$file"
            fi
        done
    )

    Os arquivos são selecionados quando são arquivos reais (-f) e são graváveis ​​(-w).

2. Substitua apenas se o nome do arquivo corresponder a outra sequência / tiver uma extensão específica / for de um determinado tipo, etc:

  • Arquivos não recursivos, apenas neste diretório:

    sed -i -- 's/foo/bar/g' *baz*    ## all files whose name contains baz
    sed -i -- 's/foo/bar/g' *.baz    ## files ending in .baz
  • Arquivos regulares e recursivos neste e em todos os subdiretórios

    find . -type f -name "*baz*" -exec sed -i 's/foo/bar/g' {} +

    Se você estiver usando o bash (chaves, evite definir as opções globalmente):

    ( shopt -s globstar dotglob
        sed -i -- 's/foo/bar/g' **baz*
        sed -i -- 's/foo/bar/g' **.baz
    )

    Se você estiver usando o zsh:

    sed -i -- 's/foo/bar/g' **/*baz*(D.)
    sed -i -- 's/foo/bar/g' **/*.baz(D.)

    O --serve para dizer sedque não haverá mais sinalizadores na linha de comando. Isso é útil para proteger contra nomes de arquivos começando com -.

  • Se um arquivo é de um determinado tipo, por exemplo, executável (veja man findpara mais opções):

    find . -type f -executable -exec sed -i 's/foo/bar/g' {} +

    zsh:

    sed -i -- 's/foo/bar/g' **/*(D*)

3. Substitua apenas se a sequência for encontrada em um determinado contexto

  • Substitua foopor barapenas se houver bazmais tarde na mesma linha:

    sed -i 's/foo\(.*baz\)/bar\1/' file

    Em sed, o uso \( \)salva salva o que estiver entre parênteses e você pode acessá-lo com \1. Existem muitas variações deste tema, para aprender mais sobre essas expressões regulares, veja aqui .

  • Substitua foopor barapenas se foofor encontrado na coluna 3d (campo) do arquivo de entrada (assumindo campos separados por espaços em branco):

    gawk -i inplace '{gsub(/foo/,"baz",$3); print}' file

    (precisa gawk4.1.0 ou mais recente).

  • Para um campo diferente, use $Nonde Nestá o número do campo de interesse. Para um separador de campo diferente ( :neste exemplo), use:

    gawk -i inplace -F':' '{gsub(/foo/,"baz",$3);print}' file

    Outra solução usando perl:

    perl -i -ane '$F[2]=~s/foo/baz/g; $" = " "; print "@F\n"' foo 

    NOTA: as soluções awke perlafetarão o espaçamento no arquivo (remova os espaços em branco à esquerda e à direita e converta seqüências de espaços em branco em um caractere de espaço nas linhas correspondentes). Para um campo diferente, use $F[N-1]onde Né o número do campo desejado e para um separador de campos diferente (ele $"=":"define o separador de campos de saída :):

    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
  • Substitua foopor barapenas na quarta linha:

    sed -i '4s/foo/bar/g' file
    gawk -i inplace 'NR==4{gsub(/foo/,"baz")};1' file
    perl -i -pe 's/foo/bar/g if $.==4' file

4. Operações de substituição múltipla: substitua por cordas diferentes

  • Você pode combinar sedcomandos:

    sed -i 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file

    Esteja ciente de que a ordem é importante ( sed 's/foo/bar/g; s/bar/baz/g'será substituída foopor baz).

  • ou comandos Perl

    perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
  • Se você tiver um grande número de padrões, é mais fácil salvar seus padrões e suas substituições em um sedarquivo de script:

    #! /usr/bin/sed -f
    s/foo/bar/g
    s/baz/zab/g
  • Ou, se você tiver muitos pares de padrões para que o acima seja possível, você poderá ler pares de padrões de um arquivo (dois padrões separados por espaço, $ pattern e $ Replacement, por linha):

    while read -r pattern replacement; do   
        sed -i "s/$pattern/$replacement/" file
    done < patterns.txt
  • Isso será bastante lento para longas listas de padrões e grandes arquivos de dados, portanto, você pode querer ler os padrões e criar um sedscript a partir deles. O seguinte pressupõe que um delimitador <space> separa uma lista de pares REPLACE MATCH <space> ocorrendo um por linha no arquivo patterns.txt:

    sed 's| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|' <patterns.txt |
    sed -f- ./editfile >outfile

    O formato acima é amplamente arbitrário e, por exemplo, não permite um <espaço> em MATCH ou REPLACE . O método é muito geral: basicamente, se você pode criar um fluxo de saída que se parece com um sedscript, pode originar esse fluxo como um sedscript, especificando sedo arquivo de script como -stdin.

  • Você pode combinar e concatenar vários scripts da mesma maneira:

    SOME_PIPELINE |
    sed -e'#some expression script'  \
        -f./script_file -f-          \
        -e'#more inline expressions' \
    ./actual_edit_file >./outfile

    Um POSIX sedconcatenará todos os scripts em um na ordem em que aparecem na linha de comando. Nenhuma delas precisa terminar em uma linha \neletrônica.

  • grep pode funcionar da mesma maneira:

    sed -e'#generate a pattern list' <in |
    grep -f- ./grepped_file
  • Ao trabalhar com seqüências fixas como padrões, é uma boa prática escapar dos metacaracteres de expressão regular . Você pode fazer isso facilmente:

    sed 's/[]$&^*\./[]/\\&/g
         s| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|
    ' <patterns.txt |
    sed -f- ./editfile >outfile

5. Operações de substituição múltipla: substitua vários padrões pela mesma sequência

  • Substituir qualquer um foo, barou bazcomfoobar

    sed -Ei 's/foo|bar|baz/foobar/g' file
  • ou

    perl -i -pe 's/foo|bar|baz/foobar/g' file
terdon
fonte
2
@ StéphaneChazelas obrigado pela edição, ele realmente corrigiu várias coisas. No entanto, não remova as informações relevantes para o bash. Nem todo mundo usa zsh. Adicione todas as zshinformações, mas não há motivo para remover o material do bash. Além disso, eu sei que o uso do shell para processamento de texto não é ideal, mas há casos em que é necessário. Editei uma versão melhor do meu script original que criará um sedscript em vez de realmente usar o loop de shell para analisar. Isso pode ser útil se você tiver várias centenas de pares de padrões, por exemplo.
terdon
2
@terdon, o seu bash está incorreto. o bash anterior ao 4.3 seguirá links simbólicos ao descer. Também o bash não tem equivalente para o (.)qualificador de globbing, portanto não pode ser usado aqui. (você está perdendo alguns - também). O loop for está incorreto (ausente -r) e significa fazer várias passagens nos arquivos e não traz nenhum benefício sobre um script sed.
Stéphane Chazelas
7
@terdon O que indica --depois sed -ie antes do comando substituto?
Geek
5
@ Geek que é uma coisa POSIX. Significa o fim das opções e permite passar argumentos começando com -. Utilizá-lo garante que os comandos funcionem em arquivos com nomes como -foo. Sem ele, o -fseria analisado como uma opção.
terdon
1
Tenha muito cuidado ao executar alguns dos comandos recursivos nos repositórios git. Por exemplo, as soluções fornecidas na seção 1 desta resposta realmente modificarão os arquivos git internos em um .gitdiretório e atrapalharão seu checkout. Melhor operar dentro / em diretórios específicos por nome.
Pistos
75

Um bom r e pl acement ferramenta Linux é rpl , que foi originalmente escrito para o projeto Debian, por isso está disponível com apt-get install rplem qualquer distro derivada do Debian, e pode ser para os outros, mas caso contrário, você pode baixar o tar.gzarquivo em SourgeForge .

Exemplo mais simples de uso:

 $ rpl old_string new_string test.txt

Observe que se a sequência contiver espaços, ela deverá ser colocada entre aspas. Por padrão, rplcuide de letras maiúsculas, mas não de palavras completas , mas você pode alterar esses padrões com as opções -i(ignorar maiúsculas e minúsculas) e -w(palavras inteiras). Você também pode especificar vários arquivos :

 $ rpl -i -w "old string" "new string" test.txt test2.txt

Ou até especifique as extensões ( -x) a serem pesquisadas ou até mesmo recursivamente ( -R) no diretório:

 $ rpl -x .html -x .txt -R old_string new_string test*

Você também pode procurar / substituir no modo interativo com a -popção (prompt):

A saída mostra os números de arquivos / sequência substituídos e o tipo de pesquisa (maiúsculas / minúsculas, palavras inteiras / parciais), mas pode ficar em silêncio com a opção -q( modo silencioso ), ou ainda mais detalhada, listando números de linhas que contêm corresponde a cada arquivo e diretório com a opção -v( modo detalhado ).

Outras opções que merecem ser lembradas são -e(honor e scapes) que permitem regular expressions, assim você também pode pesquisar abas ( \t), novas linhas ( \n), etc. Mesmo você pode usar -fpara forçar permissões (é claro, somente quando o usuário tiver permissões de gravação) e -dpreservar os tempos de modificação`).

Por fim, se você não tiver certeza sobre o que fará exatamente, use o -s( modo de simulação ).

Fran
fonte
2
Muito melhor no feedback e na simplicidade do que o sed. Eu só gostaria que isso permitisse atuar em nomes de arquivos, e então seria perfeito como está.
Kzqai
1
eu gosto de os -s (modo de simular) :-)
erm3nda
25

Como fazer uma pesquisa e substituir vários arquivos sugere:

Você também pode usar o find e o sed, mas acho que essa pequena linha de perl funciona bem.

perl -pi -w -e 's/search/replace/g;' *.php
  • -e significa executar a seguinte linha de código.
  • -i significa editar no local
  • -w escrever avisos
  • -p loop sobre o arquivo de entrada, imprimindo cada linha após a aplicação do script.

Meus melhores resultados vêm do uso de perl e grep (para garantir que o arquivo tenha a expressão de pesquisa)

perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )
Alejandro Salamanca Mazuelo
fonte
13

Você pode usar o Vim no modo Ex:

substituir string ALF por BRA em todos os arquivos no diretório atual?

for CHA in *
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

fazer o mesmo recursivamente para subdiretórios?

find -type f -exec ex -sc '%s/ALF/BRA/g' -cx {} ';'

substituir apenas se o nome do arquivo corresponder a outra sequência?

for CHA in *.txt
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

substituir apenas se a string for encontrada em um determinado contexto?

ex -sc 'g/DEL/s/ALF/BRA/g' -cx file

substituir se a string estiver em um determinado número de linha?

ex -sc '2s/ALF/BRA/g' -cx file

substituir várias strings pela mesma substituição

ex -sc '%s/\vALF|ECH/BRA/g' -cx file

substituir várias strings por diferentes substituições

ex -sc '%s/ALF/BRA/g|%s/FOX/GOL/g' -cx file
Steven Penny
fonte
13

Eu usei isso:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
  1. Listar todos os arquivos que contêm old_string.

  2. Substitua nova linha no resultado por espaços (para que a lista de arquivos possa ser alimentada sed.

  3. Execute sedesses arquivos para substituir a string antiga por nova.

Atualização: O resultado acima falhará nos nomes de arquivos que contêm espaços em branco. Em vez disso, use:

grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'

o_o_o--
fonte
Observe que isso falhará se qualquer um dos seus nomes de arquivos contiver espaços, guias ou novas linhas. O uso grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'permitirá lidar com nomes de arquivos arbitrários.
terdon
obrigado rapazes. atualização adicionada e deixou o código antigo, pois é uma advertência interessante que pode ser útil para alguém que não conhece esse comportamento.
o_o_o--
6

Da perspectiva do usuário, é uma ferramenta Unix agradável e simples que faz o trabalho perfeitamente qsubst. Por exemplo,

% qsubst foo bar *.c *.h

será substituído foopor bartodos os meus arquivos C. Um recurso interessante é que qsubstvai fazer uma consulta de substituir , ou seja, ele vai me mostrar cada ocorrência fooe perguntar se eu quero substituí-lo ou não. [Você pode substituir incondicionalmente (sem perguntar) pela -goopção, e existem outras opções, por exemplo, -wse você deseja substituir apenas fooquando se trata de uma palavra inteira.]

Como obtê-lo: qsubstfoi inventado por der Mouse (de McGill) e publicado no comp.unix.sources 11 (7) em agosto de 1987. Existem versões atualizadas. Por exemplo, a versão do NetBSD qsubst.c,v 1.8 2004/11/01compila e roda perfeitamente no meu mac.

phs
fonte
2

Eu precisava de algo que iria fornecer uma opção dry-run e iria trabalhar de forma recursiva com um glob, e depois de tentar fazê-lo com awke sedeu desisti e em vez fez isso em python.

O script pesquisa recursivamente todos os arquivos que correspondem a um padrão glob (por exemplo --glob="*.html") para uma regex e substitui pela regex de substituição:

find_replace.py [--dir=my_folder] \
    --search-regex=<search_regex> \
    --replace-regex=<replace_regex> \
    --glob=[glob_pattern] \
    --dry-run

Toda opção longa, como --search-regextem uma opção curta correspondente, ie -s. Corra com -hpara ver todas as opções.

Por exemplo, isso mudará todas as datas de 2017-12-31para 31-12-2017:

python replace.py --glob=myfile.txt \
    --search-regex="(\d{4})-(\d{2})-(\d{2})" \
    --replace-regex="\3-\2-\1" \
    --dry-run --verbose
import os
import fnmatch
import sys
import shutil
import re

import argparse

def find_replace(cfg):
    search_pattern = re.compile(cfg.search_regex)

    if cfg.dry_run:
        print('THIS IS A DRY RUN -- NO FILES WILL BE CHANGED!')

    for path, dirs, files in os.walk(os.path.abspath(cfg.dir)):
        for filename in fnmatch.filter(files, cfg.glob):

            if cfg.print_parent_folder:
                pardir = os.path.normpath(os.path.join(path, '..'))
                pardir = os.path.split(pardir)[-1]
                print('[%s]' % pardir)
            filepath = os.path.join(path, filename)

            # backup original file
            if cfg.create_backup:
                backup_path = filepath + '.bak'

                while os.path.exists(backup_path):
                    backup_path += '.bak'
                print('DBG: creating backup', backup_path)
                shutil.copyfile(filepath, backup_path)

            with open(filepath) as f:
                old_text = f.read()

            all_matches = search_pattern.findall(old_text)

            if all_matches:

                print('Found {} matches in file {}'.format(len(all_matches), filename))

                new_text = search_pattern.sub(cfg.replace_regex, old_text)

                if not cfg.dry_run:
                    with open(filepath, "w") as f:
                        print('DBG: replacing in file', filepath)
                        f.write(new_text)
                else:
                    for idx, matches in enumerate(all_matches):
                        print("Match #{}: {}".format(idx, matches))

                    print("NEW TEXT:\n{}".format(new_text))

            elif cfg.verbose:
                print('File {} does not contain search regex "{}"'.format(filename, cfg.search_regex))


if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='''DESCRIPTION:
    Find and replace recursively from the given folder using regular expressions''',
                                     formatter_class=argparse.RawDescriptionHelpFormatter,
                                     epilog='''USAGE:
    {0} -d [my_folder] -s <search_regex> -r <replace_regex> -g [glob_pattern]

    '''.format(os.path.basename(sys.argv[0])))

    parser.add_argument('--dir', '-d',
                        help='folder to search in; by default current folder',
                        default='.')

    parser.add_argument('--search-regex', '-s',
                        help='search regex',
                        required=True)

    parser.add_argument('--replace-regex', '-r',
                        help='replacement regex',
                        required=True)

    parser.add_argument('--glob', '-g',
                        help='glob pattern, i.e. *.html',
                        default="*.*")

    parser.add_argument('--dry-run', '-dr',
                        action='store_true',
                        help="don't replace anything just show what is going to be done",
                        default=False)

    parser.add_argument('--create-backup', '-b',
                        action='store_true',
                        help='Create backup files',
                        default=False)

    parser.add_argument('--verbose', '-v',
                        action='store_true',
                        help="Show files which don't match the search regex",
                        default=False)

    parser.add_argument('--print-parent-folder', '-p',
                        action='store_true',
                        help="Show the parent info for debug",
                        default=False)

    config = parser.parse_args(sys.argv[1:])

    find_replace(config)

Here é uma versão atualizada do script que destaca os termos e substituições da pesquisa com cores diferentes.

ccpizza
fonte
1
Não entendo por que você faria algo tão complexo. Para recursão, use a opção bash's (ou o equivalente do seu shell) globstare **globs ou find. Para uma corrida a seco, basta usar sed. A menos que você use a -iopção, ela não fará nenhuma alteração. Para um uso de backup sed -i.bak(ou perl -i .bak); para arquivos que não correspondem, use grep PATTERN file || echo file. E por que você faria o python expandir a glob em vez de deixar o shell fazer isso? Por que ao script.py --glob=foo*invés de apenas script.py foo*?
terdon
1
Meu porquê é muito simples: (1) acima de tudo, facilidade de depuração; (2) usando apenas uma única ferramenta bem documentada com uma comunidade de suporte (3) que não conhece sede está awkbem e não está disposto a investir tempo extra para dominá-los, (4) legibilidade, (5) essa solução também funcionará em sistemas não-posix (não que eu precise disso, mas que outra pessoa possa).
Ccpizza
1

O ripgrep (nome do comando rg) é uma grepferramenta, mas também suporta pesquisa e substituição.

$ cat ip.txt
dark blue and light blue
light orange
blue sky
$ # by default, line number is displayed if output destination is stdout
$ # by default, only lines that matched the given pattern is displayed
$ # 'blue' is search pattern and -r 'red' is replacement string
$ rg 'blue' -r 'red' ip.txt
1:dark red and light red
3:red sky

$ # --passthru option is useful to print all lines, whether or not it matched
$ # -N will disable line number prefix
$ # this command is similar to: sed 's/blue/red/g' ip.txt
$ rg --passthru -N 'blue' -r 'red' ip.txt
dark red and light red
light orange
red sky


rg não suporta a opção no local, então você terá que fazer isso sozinho

$ # -N isn't needed here as output destination is a file
$ rg --passthru 'blue' -r 'red' ip.txt > tmp.txt && mv tmp.txt ip.txt
$ cat ip.txt
dark red and light red
light orange
red sky


Consulte a documentação do Rex regex para obter sintaxe e recursos para expressões regulares. O -Pswitch ativará o sabor PCRE2 . rgsuporta Unicode por padrão.

$ # non-greedy quantifier is supported
$ echo 'food land bark sand band cue combat' | rg 'foo.*?ba' -r 'X'
Xrk sand band cue combat

$ # unicode support
$ echo 'fox:αλεπού,eagle:αετός' | rg '\p{L}+' -r '($0)'
(fox):(αλεπού),(eagle):(αετός)

$ # set operator example, remove all punctuation characters except . ! and ?
$ para='"Hi", there! How *are* you? All fine here.'
$ echo "$para" | rg '[[:punct:]--[.!?]]+' -r ''
Hi there! How are you? All fine here.

$ # use -P if you need even more advanced features
$ echo 'car bat cod map' | rg -P '(bat|map)(*SKIP)(*F)|\w+' -r '[$0]'
[car] bat [cod] map


Assim grep, a -Fopção permitirá que as seqüências fixas sejam correspondidas, uma opção útil que eu acho que seddeveria implementar também.

$ printf '2.3/[4]*6\nfoo\n5.3-[4]*9\n' | rg --passthru -F '[4]*' -r '2'
2.3/26
foo
5.3-29


Outra opção útil é a -Uque permite a correspondência multilinha

$ # (?s) flag will allow . to match newline characters as well
$ printf '42\nHi there\nHave a Nice Day' | rg --passthru -U '(?s)the.*ice' -r ''
42
Hi  Day


rg também pode lidar com arquivos no estilo dos

$ # same as: sed -E 's/\w+(\r?)$/123\1/'
$ printf 'hi there\r\ngood day\r\n' | rg --passthru --crlf '\w+$' -r '123'
hi 123
good 123


Outra vantagem rgé que é provável que seja mais rápido do quesed

$ # for small files, initial processing time of rg is a large component
$ time echo 'aba' | sed 's/a/b/g' > f1
real    0m0.002s
$ time echo 'aba' | rg --passthru 'a' -r 'b' > f2
real    0m0.007s

$ # for larger files, rg is likely to be faster
$ # 6.2M sample ASCII file
$ wget https://norvig.com/big.txt    
$ time LC_ALL=C sed 's/\bcat\b/dog/g' big.txt > f1
real    0m0.060s
$ time rg --passthru '\bcat\b' -r 'dog' big.txt > f2
real    0m0.048s
$ diff -s f1 f2
Files f1 and f2 are identical

$ time LC_ALL=C sed -E 's/\b(\w+)(\s+\1)+\b/\1/g' big.txt > f1
real    0m0.725s
$ time rg --no-pcre2-unicode --passthru -wP '(\w+)(\s+\1)+' -r '$1' big.txt > f2
real    0m0.093s
$ diff -s f1 f2
Files f1 and f2 are identical
Sundeep
fonte