Alterar vários arquivos


194

O comando a seguir está alterando corretamente o conteúdo de 2 arquivos.

sed -i 's/abc/xyz/g' xaa1 xab1 

Mas o que preciso fazer é alterar vários desses arquivos dinamicamente e não sei os nomes dos arquivos. Quero escrever um comando que leia todos os arquivos do diretório atual, começando com xa*e seddeve alterar o conteúdo do arquivo.


61
Você quer dizer sed -i 's/abc/xyz/g' xa*?
Paul R

3
As respostas aqui não são suficientes. Veja unix.stackexchange.com/questions/112023/…
Isaac

Respostas:


135

Melhor ainda:

for i in xa*; do
    sed -i 's/asd/dfg/g' $i
done

porque ninguém sabe quantos arquivos existem e é fácil quebrar os limites da linha de comando.

Aqui está o que acontece quando há muitos arquivos:

# grep -c aaa *
-bash: /bin/grep: Argument list too long
# for i in *; do grep -c aaa $i; done
0
... (output skipped)
#

18
Se houver tantos arquivos, você quebrará o limite da linha de comando no forcomando. Para se proteger contra isso, você teria que usarfind ... | xargs ...
Glenn Jackman

1
Não conheço a implementação, mas o padrão "xa *" precisa ser expandido em algum momento. O shell faz a expansão de maneira diferente fordo que para echoou grep?
Glenn Jackman

4
veja a resposta atualizada. se precisar de mais informações, faça uma pergunta oficial, para que as pessoas possam ajudá-lo.
181

5
No comando sed, você precisa usar em "$i"vez de $ievitar a divisão de palavras nos nomes de arquivos com espaços. Caso contrário, isso é muito bom.
Curinga

4
Em relação à lista, acredito que a diferença é que forfaz parte da sintaxe da linguagem, nem mesmo um componente interno. Pois sed -i 's/old/new' *, a expansão de *ALL deve ser passada como arglist para sed, e tenho certeza de que isso precisa acontecer antes que o sedprocesso possa ser iniciado. Usando o forloop, o arglist completo (a expansão de *) nunca é passado como um comando, apenas armazenado na memória do shell e iterado. No entanto, não tenho nenhuma referência a isso, apenas parece provável que essa seja a diferença. (Eu adoraria ouvir de alguém mais entendido ...)
Wildcard

165

Estou surpreso que ninguém tenha mencionado o argumento -exec a ser encontrado, destinado a esse tipo de caso de uso, embora ele inicie um processo para cada nome de arquivo correspondente:

find . -type f -name 'xa*' -exec sed -i 's/asd/dsg/g' {} \;

Como alternativa, pode-se usar xargs, que invocará menos processos:

find . -type f -name 'xa*' | xargs sed -i 's/asd/dsg/g'

Ou, mais simplesmente, use a + variante exec em vez de ;em find para permitir que find forneça mais de um arquivo por chamada de subprocesso:

find . -type f -name 'xa*' -exec sed -i 's/asd/dsg/g' {} +

7
Eu tive que modificar o comando nesta resposta da seguinte forma: find ./ -type f -name 'xa*' -exec sed -i '' 's/asd/dsg/g' {} \;esse é o local para o comando find ./e um par de aspas simples -ipara o OSX.
Shelbydz 17/03/19

O comando find funciona como é fornecido por ealfonso, ./é igual a .e depois -ipossui apenas o parâmetro backupsuffix.
Uhausbrand #

A -execopção de encontrar junto {} +é suficiente para resolver o problema, conforme indicado, e deve ser adequada para a maioria dos requisitos. Mas xargsé uma escolha melhor em geral, porque também permite o processamento paralelo com a -popção. Quando sua expansão glob é grande o suficiente para exceder o comprimento da linha de comando, é provável que você também se beneficie de uma aceleração em uma execução sequencial.
Amit Naidu

78

Você pode usar grep e sed juntos. Isso permite pesquisar subdiretórios recursivamente.

Linux: grep -r -l <old> * | xargs sed -i 's/<old>/<new>/g'
OS X: grep -r -l <old> * | xargs sed -i '' 's/<old>/<new>/g'

For grep:
    -r recursively searches subdirectories 
    -l prints file names that contain matches
For sed:
    -i extension (Note: An argument needs to be provided on OS X)

3
Bonus deste método para mim foi que eu poderia deslizar em um grep -vpara evitar pastas gitgrep -rl <old> . | grep -v \.git | xargs sed -i 's/<old>/<new>/g'
Martin Lyne

melhor solução para um mac!
Marquis Blount

30

Esses comandos não funcionarão no padrão sedfornecido com o Mac OS X.

De man 1 sed:

-i extension
             Edit files in-place, saving backups with the specified
             extension.  If a zero-length extension is given, no backup 
             will be saved.  It is not recommended to give a zero-length
             extension when in-place editing files, as you risk corruption
             or partial content in situations where disk space is exhausted, etc.

Tentou

sed -i '.bak' 's/old/new/g' logfile*

e

for i in logfile*; do sed -i '.bak' 's/old/new/g' $i; done

Ambos funcionam bem.


2
@sumek Aqui está um exemplo de sessão de terminal no OS X que mostra sed substituir todas as ocorrências: GitHub Gist
funroll

Eu usei isso para substituir duas linhas diferentes em todos os arquivos de configuração do meu site por uma linha abaixo. sed -i.bak "s / supercache_proxy_config / proxy_includes \ / supercache_config / g; s / basic_proxy_config / proxy_include \ / basic_proxy_config / g" sites disponíveis / * Não se esqueça de excluir os arquivos * .bak quando terminar o arquivo bem higiene do sistema.
Josias

19

O @PaulR postou isso como um comentário, mas as pessoas devem vê-lo como uma resposta (e essa resposta funciona melhor para minhas necessidades):

sed -i 's/abc/xyz/g' xa*

Isso funcionará para uma quantidade moderada de arquivos, provavelmente da ordem de dezenas, mas provavelmente não da ordem de milhões .


Suponha que você tenha barras invertidas em suas substituições. Outro exemplo com caminhos de arquivo sed -i 's|auth-user-pass nordvpn.txt|auth-user-pass /etc/openvpn/nordvpn.txt|g' *.ovpn.
Léo Léopold Hertz ·

10

Outra maneira mais versátil é usar find:

sed -i 's/asd/dsg/g' $(find . -type f -name 'xa*')

1
a saída desse comando find é expandida; portanto, isso não soluciona o problema. Em vez disso você deve usar -exec
ealfonso

@erjoalgo isso funciona porque o comando sed pode lidar com vários arquivos de entrada. A expansão do comando find é exatamente o que era necessário para fazê-lo funcionar.
dkinzer

funciona desde que o número de arquivos não ultrapasse os limites da linha de comando.
Ealfonso

Esse limite depende apenas dos recursos de memória disponíveis para a máquina e é exatamente o mesmo que o limite para o exec.
dkinzer

4
Isso simplesmente não é verdade. No seu comando acima, o $ (find. ...) é expandido em um único comando, que pode ser muito longo se houver muitos arquivos correspondentes. Se for muito longo (por exemplo, no meu sistema, o limite é de 2097152 caracteres), você poderá receber um erro: "A lista de argumentos é muito longa" e o comando falhará. Pesquise no Google esse erro para obter informações sobre isso.
Ealfonso

2

Estou usando findpara tarefa semelhante. É bem simples: você deve passá-lo como argumento para o sedseguinte:

sed -i 's/EXPRESSION/REPLACEMENT/g' `find -name "FILE.REGEX"`

Dessa forma, você não precisa escrever loops complexos, e é simples ver quais arquivos você irá alterar, apenas execute findantes de executar sed.


1
É exatamente o mesmo que a resposta de @ dkinzer .
Tao

0

você pode fazer

o texto ' xxxx ' u pesquisa e o substituirá por ' aaaa '

grep -Rn '**xxxx**' /path | awk -F: '{print $1}' | xargs sed -i 's/**xxxx**/**yyyy**/'

0

Se você é capaz de executar um script, aqui está o que eu fiz para uma situação semelhante:

Usando um dicionário / hashMap (matriz associativa) e variáveis ​​para o sedcomando, podemos percorrer a matriz para substituir várias seqüências. A inclusão de um curinga no name_patternarquivo permitirá substituir no local os arquivos por um padrão (isso pode ser algo como name_pattern='File*.txt') em um diretório específico (source_dir ). Todas as alterações são gravadas no logfilenodestin_dir

#!/bin/bash
source_dir=source_path
destin_dir=destin_path
logfile='sedOutput.txt'
name_pattern='File.txt'

echo "--Begin $(date)--" | tee -a $destin_dir/$logfile
echo "Source_DIR=$source_dir destin_DIR=$destin_dir "

declare -A pairs=( 
    ['WHAT1']='FOR1'
    ['OTHER_string_to replace']='string replaced'
)

for i in "${!pairs[@]}"; do
    j=${pairs[$i]}
    echo "[$i]=$j"
    replace_what=$i
    replace_for=$j
    echo " "
    echo "Replace: $replace_what for: $replace_for"
    find $source_dir -name $name_pattern | xargs sed -i "s/$replace_what/$replace_for/g" 
    find $source_dir -name $name_pattern | xargs -I{} grep -n "$replace_for" {} /dev/null | tee -a $destin_dir/$logfile
done

echo " "
echo "----End $(date)---" | tee -a $destin_dir/$logfile

Primeiro, a matriz de pares é declarada, cada par é uma sequência de substituição e, em seguida, WHAT1será substituída por FOR1e OTHER_string_to replaceserá substituída por string replacedno arquivo File.txt. No loop, a matriz é lida, o primeiro membro do par é recuperado como replace_what=$ie o segundo comoreplace_for=$j . O findcomando pesquisa no diretório o nome do arquivo (que pode conter um curinga) e o sed -icomando substitui no (s) mesmo (s) arquivo (s) que foi definido anteriormente. Por fim, adicionei um grepredirecionado ao arquivo de log para registrar as alterações feitas nos arquivos.

Isso funcionou para mim GNU Bash 4.3 sed 4.2.2e com base na resposta de VasyaNovikov para Loop sobre tuplas no bash .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.