Come fare una ricerca / sostituzione ricorsiva di una stringa con awk o sed?


678

Come trovo e sostituisco ogni ricorrenza di:

subdomainA.example.com

con

subdomainB.example.com

in ogni file di testo sotto l' /home/www/albero della directory ricorsivamente?


93
Suggerimento: non eseguire le operazioni seguenti in un albero di checkout svn ... sovrascriverà i file di cartelle .svn magici.
J. Polfer,

7
oh mio dio, questo è esattamente quello che ho appena fatto. Ma ha funzionato e non sembra aver fatto alcun danno. Qual è il peggio che potrebbe succedere?
J. Katzwinkel,

5
@ J.Katzwinkel: per lo meno, potrebbe corrompere i checksum, che potrebbero danneggiare il tuo repository.
ninjagecko,

3
Suggerimento rapido per tutte le persone che usano sed: aggiungerà nuove righe finali ai tuoi file. Se non li desideri, esegui prima una ricerca di sostituzione che non corrisponda a nulla e commetti questo per git. Quindi fai quello vero. Quindi rifacimento interattivo ed eliminazione del primo.
funroll

5
Puoi escludere una directory, come git, dai risultati usando -path ./.git -prune -oin find . -path ./.git -prune -o -type f -name '*matchThisText*' -print0prima di eseguire il piping su xargs
devinbost

Risposte:


851
find /home/www \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i 's/subdomainA\.example\.com/subdomainB.example.com/g'

-print0indica finddi stampare ciascuno dei risultati separati da un carattere null, anziché da una nuova riga. Nel caso improbabile che la tua directory contenga file con nuove righe nei nomi, ciò consente comunque di xargslavorare sui nomi di file corretti.

\( -type d -name .git -prune \)è un'espressione che ignora completamente tutte le directory denominate .git. Potresti facilmente espanderlo, se usi SVN o hai altre cartelle che vuoi conservare - basta abbinarle a più nomi. È approssimativamente equivalente a -not -path .git, ma più efficiente, perché invece di controllare ogni file nella directory, lo salta del tutto. Il -odopo è necessario a causa di come -prunefunziona effettivamente.

Per ulteriori informazioni, vedere man find.


132
Su OSX potresti riscontrare sed: 1: "...": invalid command code .problemi. Sembra che l'opzione -i prevede l'estensione e analizza il 's/../...'comando. Soluzione: passare l'estensione '' all'opzione -i come sed -i '' 's/....
Robert Lujo,

6
Nota: se lo usi su una directory e ti chiedi perché svn stnon mostri cambiamenti, è perché hai modificato anche i file nelle directory .svn! Usa find . -maxdepth 1 -type f -print0 | xargs -0 sed -i 's/toreplace/replaced/g'invece.
ACK_stoverflow,

57
Inoltre, fai attenzione se sei in un repository git. Pensavo di essere intelligente testandolo su un ramo chiaro in modo da poter ripristinare se avesse fatto qualcosa di brutto, ma invece ho corrotto il mio indice git.
Ciryon,

13
Usalo grep -r 'hello' -l --null . | xargs -0 sed -i 's#hello#world#g'per evitare di modificare file non correlati (sed potrebbe cambiare la codifica dei file).
caiguanhao,

6
"ma invece ho corrotto il mio indice git." Non preoccuparti troppo di ciò che puoi fare solo find .git ... | ... 'sed -i s/(the opposite from before)/g'per correggere il tuo indice git
Massey101,

259

Nota : non eseguire questo comando su una cartella che include un repository git: le modifiche a .git potrebbero danneggiare l'indice git.

find /home/www/ -type f -exec \
    sed -i 's/subdomainA\.example\.com/subdomainB.example.com/g' {} +

Rispetto ad altre risposte qui, questo è più semplice della maggior parte e utilizza sed invece di perl, che è ciò che la domanda originale ha posto.


50
Nota che se stai usando BSD sed (incluso su Mac OS X) dovrai dare un'arg esplicita stringa vuota -iall'opzione sed . cioè: sed -i '' 's/original/replacement/g'
Nathan Craike il

2
@JohnZwinck Il mio errore, ho perso il +. Stranamente, la soluzione di Nikita è più veloce per me.
Sam

6
@AoeAoe: +riduce notevolmente il numero di sedprocessi generati. È più efficiente.
John Zwinck,

4
Come posso farlo in modo sicuro in una cartella con un repository git?
Hatshepsut,

20
E 'sicuro di eseguire su una cartella contenente un repo git se si esclude il repo dai risultati di trovare: find . -not -path '*/\.git*' -type f ....
Dale Anderson,

213

Il modo più semplice per me è

grep -rl oldtext . | xargs sed -i 's/oldtext/newtext/g'

1
@Anatoly: solo una domanda: come posso escludere i file binari (file eseguibili) ?
user2284570,

3
@ user2284570 Utilizzare i flag -Io --binary-file=without-matchgrep.
Zéychin,

34
Funziona particolarmente bene, quando è necessario escludere directory, come con .svn. Ad esempio:grep -rl oldtext . --exclude-dir=.svn | xargs sed -i 's/oldtext/newtext/g'
phyatt,

11
brew install gnu-sede utilizzare gsedsu OSX per evitare un mondo di dolore.
P

1
ragazzi Prego attenzione, se il progetto è git versione, utilizzare questo invece: git grep -rl oldtext . | xargs sed -i 's/oldtext/newtext/g'. non è per niente bello fotterti la tua .gitreg
Paolo

61

Tutti i trucchi sono quasi uguali, ma mi piace questo:

find <mydir> -type f -exec sed -i 's/<string1>/<string2>/g' {} +
  • find <mydir>: cerca nella directory.

  • -type f:

    Il file è di tipo: file normale

  • -exec command {} +:

    Questa variante dell'azione -exec esegue il comando specificato sui file selezionati, ma la riga di comando viene creata aggiungendo alla fine ciascun nome di file selezionato; il numero totale di invocazioni del comando sarà molto inferiore al numero di file corrispondenti. La riga di comando è costruita nello stesso modo in cui xargs costruisce le sue righe di comando. Nel comando è consentita solo un'istanza di `{} '. Il comando viene eseguito nella directory iniziale.


@ user2284570 con -exec? Prova a impostare il percorso sull'eseguibile anziché sul nome di uno strumento.
I159,

@ I159: No: esclude i file binari eseguibili (ma include gli script di shell) .
user2284570

8
@ I159 Questa risposta non è identica a quella di John Zwinck ?
Ripristina Monica, per favore, l'

1
@ user2284570 Il concetto di "file binario" non è del tutto ben definito. È possibile utilizzare il filecomando per provare a determinare il tipo di ciascun file, ma le variazioni casuali nel suo output potrebbero essere leggermente sconcertanti. L' opzione -I(aka --mime) aiuta in qualche modo, o --mime-typese ce l'hai. Il modo in cui esattamente questo refactoring ordinato per fare ciò è purtroppo fuori portata per questa piccola casella di commento. Forse pubblicare una domanda separata se hai bisogno di aiuto? (Forse aggiungere un commento con un link qui allora.)
Tripleee

1
la risposta più pulita! grazie
amico

39
cd /home/www && find . -type f -print0 |
  xargs -0 perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g'

2
Sono curioso, c'è un motivo per usare -print0e xargsinvece di -execo -execdir?
Philipp,

4
C'è: da "man find": il comando specificato viene eseguito una volta per ogni file corrispondente. Cioè, se ci sono 2000 file in / home / www, allora 'find ... -exec ...' comporterà 2000 invocazioni di perl; mentre 'trova ... | xargs ... 'invocherà perl solo una o due volte (supponendo ARG_MAX di circa 32K e una lunghezza media del nome file di 20).
Impiegato russo il

2
@Spazio russo: ecco perché lo useresti find -exec command {} +- evita invocazioni eccessive del comando come xargs, ma senza un processo separato.
John Zwinck,

2
Su quale piattaforma? La soluzione xargs è portatile, le invocazioni "magiche" di "find ... -exec" che non invocano un sottoprocesso per ogni file trovato non lo sono.
Impiegato russo il

4
@EmployedRussian, find -exec ... {} +è stato specificato POSIX dal 2006.
Charles Duffy,

34

Per me la soluzione più semplice da ricordare è https://stackoverflow.com/a/2113224/565525 , ovvero:

sed -i '' -e 's/subdomainA/subdomainB/g' $(find /home/www/ -type f)

NOTA : -i ''risolve il problema OSXsed: 1: "...": invalid command code .

NOTA : se ci sono troppi file da elaborare, otterrai Argument list too long. Soluzione alternativa: utilizzo find -execo xargssoluzione sopra descritti.


4
Il workarounddovrebbe essere la sintassi preferita in tutti i casi.
Ripristina Monica, per favore, l'

1
Il problema con la sostituzione del comando $(find...)è che la shell non può gestire i nomi dei file con spazi bianchi o altri metacaratteri della shell. Se sai che questo non è un problema, questo approccio va bene; ma abbiamo troppe domande in cui le persone non sono state avvisate di questo problema o non hanno capito l'avvertimento.
Tripleee

30

Per chiunque usi Silver Searcher ( ag)

ag SearchString -l0 | xargs -0 sed -i 's/SearchString/Replacement/g'

Poiché ag ignora git / hg / svn file / cartelle per impostazione predefinita, è sicuro eseguirlo all'interno di un repository.


16

Una bella oneliner come extra. Usando git grep.

git grep -lz 'subdomainA.example.com' | xargs -0 perl -i'' -pE "s/subdomainA.example.com/subdomainB.example.com/g"

3
Buona idea se si lavora all'interno di un repository git in quanto non si rischia di sovrascrivere .git / contents (come riportato nei commenti a un'altra risposta).
mahemoff,

1
Grazie, lo uso come funzione bash refactor() { echo "Replacing $1 by $2 in all files in this git repository." git grep -lz $1| xargs -0 perl -i'' -pE "s/$1/$2/g" }Utilizzo, ad esempio per sostituire "parola" con "spada": refactor word swordquindi verifica cosa ha fatto git diff.
Paul Rougieux,

16

Per ridurre i file in modo ricorsivo sed, è possibile grepper l'istanza della stringa:

grep -rl <oldstring> /path/to/folder | xargs sed -i s^<oldstring>^<newstring>^g

Se corri man grepnoterai che puoi anche definire una --exlude-dir="*.git"bandiera se vuoi omettere di cercare nelle directory .git, evitando problemi di indice git come altri hanno cortesemente sottolineato.

Ti guida a:

grep -rl --exclude-dir="*.git" <oldstring> /path/to/folder | xargs sed -i s^<oldstring>^<newstring>^g

13

Questo è compatibile con i repository git e un po 'più semplice:

Linux:

git grep -l 'original_text' | xargs sed -i 's/original_text/new_text/g'

Mac:

git grep -l 'original_text' | xargs sed -i '' -e 's/original_text/new_text/g'

(Grazie a http://blog.jasonmeridth.com/posts/use-git-grep-to-replace-strings-in-files-in-your-git-repository/ )


Più saggia da usare l git-grep' -zopzione insieme a xargs -0.
gniourf_gniourf,

git grepovviamente ha senso solo in un gitrepository. La sostituzione generale sarebbe grep -r.
Tripleee,

@gniourf_gniourf Puoi spiegarmi?
Petr Peller,

2
@PetrPeller: con -z, git-grepseparerà i campi di output da byte nulli anziché da newline; e con -0, xargsleggerà l'input separato da byte nulli, anziché spazi vuoti (e non fare cose strane tra virgolette). Quindi, se non si desidera che il comando di pausa, se i nomi dei file contengono spazi, virgolette o altri caratteri divertenti, il comando è: git grep -z -l 'original_text' | xargs -0 sed ....
gniourf_gniourf,

10
find /home/www/ -type f -exec perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g' {} +

find /home/www/ -type f elencherà tutti i file in / home / www / (e le sue sottodirectory). Il flag "-exec" indica a find di eseguire il seguente comando su ogni file trovato.

perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g' {} +

è il comando eseguito sui file (molti alla volta). Il {}viene sostituito da nomi di file. Alla +fine del comando dice finddi creare un comando per molti nomi di file.

Per la findpagina man: "La riga di comando è costruita più o meno allo stesso modo in cui xargs costruisce le sue linee di comando."

Quindi è possibile raggiungere il tuo obiettivo (e gestire nomi di file contenenti spazi) senza usare xargs -0, o -print0.


8

Ne avevo solo bisogno e non ero contento della velocità degli esempi disponibili. Quindi ho pensato al mio:

cd /var/www && ack-grep -l --print0 subdomainA.example.com | xargs -0 perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g'

Ack-grep è molto efficiente nella ricerca di file rilevanti. Questo comando ha sostituito ~ 145 000 file con un gioco da ragazzi, mentre altri hanno impiegato così tanto tempo che non potevo aspettare fino al termine.


Bello, ma grep -ril 'subdomainA' *non è neanche lontanamente veloce grep -Hr 'subdomainA' * | cut -d: -f1.
trusktr,

@Henno: solo una domanda: come posso escludere i file binari (file eseguibili) ?
user2284570,

ack-grep lo fa automaticamente per te.
Henno,

@Henno: include script di shell?
user2284570

Sì. Ecco un elenco completo dei tipi di file supportati: beyondgrep.com/documentation
Henno

6

Un metodo semplice se è necessario escludere directory ( --exclude-dir=.svn) e potrebbe anche avere nomi di file con spazi (utilizzando 0Byte con grep -Zexargs -0

grep -rlZ oldtext . --exclude-dir=.svn | xargs -0 sed -i 's/oldtext/newtext/g'

6

Il modo più semplice per sostituire ( tutti i file, directory, ricorsivo )

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

Nota: a volte potrebbe essere necessario ignorare alcuni file nascosti, ad esempio .gitè possibile utilizzare il comando sopra.

Se si desidera includere l'uso di file nascosti,

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

In entrambi i casi la stringa fooverrà sostituita con una nuova stringabar


5

grep -lr 'subdomainA.example.com' | while read file; do sed -i "s/subdomainA.example.com/subdomainB.example.com/g" "$file"; done

Immagino che la maggior parte delle persone non sappia di poter reindirizzare qualcosa in un "file di lettura" e questo evita quegli argomenti cattivi -print0, salvando gli spazi nei nomi dei file.

L'aggiunta ulteriore di echoprima di sed ti consente di vedere quali file cambieranno prima di farlo.


Il motivo -print0è utile perché gestisce casi che while readsemplicemente non sono in grado di gestire: una nuova riga è un carattere valido in un nome file Unix, quindi affinché il tuo codice sia completamente robusto, deve anche far fronte a tali nomi file. (Inoltre, si desidera read -revitare alcuni fastidiosi comportamenti ereditati da POSIX read.)
Tripleee

Inoltre, sedè una no-op se non ci sono corrispondenze, quindi grepnon è davvero necessario; sebbene sia un'utile ottimizzazione per evitare di riscrivere file che non contengono alcuna corrispondenza, se ne possiedi molti o vuoi evitare di aggiornare inutilmente i timbri di data sui file.
tripla il

5

Puoi usare awk per risolverlo come di seguito,

for file in `find /home/www -type f`
do
   awk '{gsub(/subdomainA.example.com/,"subdomainB.example.com"); print $0;}' $file > ./tempFile && mv ./tempFile $file;
done

spero che questo ti possa aiutare !!!


Funziona su MacO senza problemi! Tutti i sedcomandi basati non sono riusciti quando sono stati inclusi i binari anche con le impostazioni specifiche di osx.
Jankapunkt,

Attenzione ... questo esploderà se uno dei file findrestituiti ha uno spazio nei loro nomi! E 'molto più sicuro da utilizzare while read: stackoverflow.com/a/9612560/1938956
Soren Bjornstad

4

Prova questo:

sed -i 's/subdomainA/subdomainB/g' `grep -ril 'subdomainA' *`

1
Ciao @RikHic, bel consiglio - stava pensando a qualcosa del genere; sfortunatamente quella formattazione sopra non è andata proprio bene :) Quindi proverò con un pre tag (non funziona) - quindi con i backtick di escape allora: sed -i 's/subdomainA/subdomainB/g'` grep -ril 'subdomainA' /home/www/*` - questo non sembra ancora troppo buono, ma dovrebbe sopravvivere al copypaste :) Saluti!
sdaau,

4
#!/usr/local/bin/bash -x

find * /home/www -type f | while read files
do

sedtest=$(sed -n '/^/,/$/p' "${files}" | sed -n '/subdomainA/p')

    if [ "${sedtest}" ]
    then
    sed s'/subdomainA/subdomainB/'g "${files}" > "${files}".tmp
    mv "${files}".tmp "${files}"
    fi

done

4

Secondo questo post del blog:

find . -type f | xargs perl -pi -e 's/oldtext/newtext/g;'

Come sfuggire alle barre /? Ad esempio, voglio sostituire gli indirizzi IP: xxx.xxx.xxx.xxxperxxx.xxx.xxx.xxx/folder
Pathros

Puoi sfuggire a /con \. Ad esempio:find . -type f | xargs perl -pi -e 's/xxx.xxx.xxx.xxx\/folder/newtext/g;'
J.Hpour

3

Se non ti dispiace usare viminsieme a grepo findstrumenti, potresti seguire la risposta data dall'utente Gert in questo link -> Come fare una sostituzione del testo in una gerarchia di cartelle grandi? .

Ecco l'accordo:

  • ricorsivamente grep per la stringa che si desidera sostituire in un determinato percorso e prendere solo il percorso completo del file corrispondente. (quello sarebbe il $(grep 'string' 'pathname' -Rl).

  • (facoltativo) se si desidera effettuare un pre-backup di tali file su una directory centralizzata, è possibile utilizzare anche questo: cp -iv $(grep 'string' 'pathname' -Rl) 'centralized-directory-pathname'

  • successivamente puoi modificare / sostituire a piacimento vimseguendo uno schema simile a quello fornito sul link indicato:

    • :bufdo %s#string#replacement#gc | update

2

Un po 'vecchia scuola ma questo ha funzionato su OS X.

Ci sono alcuni trucchi:

• Modifica solo i file con estensione .slsnella directory corrente

.deve essere evitato per assicurarsi sedche non li valuti come "qualsiasi carattere"

,è usato come seddelimitatore invece del solito/

Nota anche che serve per modificare un modello Jinja per passare a variablenel percorso di un import(ma questo è fuori tema).

Innanzitutto, verifica che il tuo comando sed faccia quello che vuoi (questo stamperà solo le modifiche a stdout, non cambierà i file):

for file in $(find . -name *.sls -type f); do echo -e "\n$file: "; sed 's,foo\.bar,foo/bar/\"+baz+\"/,g' $file; done

Modifica il comando sed quando necessario, quando sei pronto per apportare modifiche:

for file in $(find . -name *.sls -type f); do echo -e "\n$file: "; sed -i '' 's,foo\.bar,foo/bar/\"+baz+\"/,g' $file; done

Nota -i ''nel comando sed , non volevo creare un backup dei file originali (come spiegato in Modifiche sul posto con sed su OS X o nel commento di Robert Lujo in questa pagina).

Felice gente seding!


2

solo per evitare di cambiare anche

  • NearlysubdomainA.example.com
  • subdomainA.example.comp.other

ma comunque

  • subdomainA.example.com.IsIt.good

(forse non buono nell'idea alla base della radice del dominio)

find /home/www/ -type f -exec sed -i 's/\bsubdomainA\.example\.com\b/\1subdomainB.example.com\2/g' {} \;

2

Uso solo top:

find . -name '*.[c|cc|cp|cpp|m|mm|h]' -print0 |  xargs -0 tops -verbose  replace "verify_noerr(<b args>)" with "__Verify_noErr(<args>)" \
replace "check(<b args>)" with "__Check(<args>)" 

più uno per `` *. [c | cc | cp | cpp | m | mm | h] ''
Spazio frattale

2

Ecco una versione che dovrebbe essere più generale della maggior parte; non richiede find(usando duinvece), per esempio. Richiede xargs, che si trovano solo in alcune versioni di Plan 9 (come 9front).

 du -a | awk -F' '  '{ print $2 }' | xargs sed -i -e 's/subdomainA\.example\.com/subdomainB.example.com/g'

Se vuoi aggiungere filtri come le estensioni dei file usa grep:

 du -a | grep "\.scala$" | awk -F' '  '{ print $2 }' | xargs sed -i -e 's/subdomainA\.example\.com/subdomainB.example.com/g'

1

Per Qshell (qsh) su IBMi, non bash come taggato da OP.

Limitazioni dei comandi qsh:

  • find non ha l'opzione -print0
  • xargs non ha l'opzione -0
  • sed non ha l'opzione -i

Quindi la soluzione in qsh:

    PATH='your/path/here'
    SEARCH=\'subdomainA.example.com\'
    REPLACE=\'subdomainB.example.com\'

    for file in $( find ${PATH} -P -type f ); do

            TEMP_FILE=${file}.${RANDOM}.temp_file

            if [ ! -e ${TEMP_FILE} ]; then
                    touch -C 819 ${TEMP_FILE}

                    sed -e 's/'$SEARCH'/'$REPLACE'/g' \
                    < ${file} > ${TEMP_FILE}

                    mv ${TEMP_FILE} ${file}
            fi
    done

Avvertenze:

  • La soluzione esclude la gestione degli errori
  • Non Bash come taggato da OP

Questo ha alcuni fastidiosi problemi con la citazione e la lettura di righe for.
tripla il

1

Se vuoi usarlo senza distruggere completamente il tuo repository SVN, puoi dire 'trova' per ignorare tutti i file nascosti facendo:

find . \( ! -regex '.*/\..*' \) -type f -print0 | xargs -0 sed -i 's/subdomainA.example.com/subdomainB.example.com/g'

Le parentesi sembrano essere superflue. In precedenza questo aveva un errore di formattazione che lo rendeva inutilizzabile (il rendering Markdown avrebbe mangiato alcuni caratteri della regex).
Tripleee,

1

Usando la combinazione di grepesed

for pp in $(grep -Rl looking_for_string)
do
    sed -i 's/looking_for_string/something_other/g' "${pp}"
done

@tripleee L'ho modificato un po '. In questo caso l'output per il comando ha grep -Rl patterngenerato un elenco di file in cui si trova il modello. I file non vengono letti in forloop.
Pawel,

Eh? Hai ancora un forciclo; se un nome di file restituito contiene spazi bianchi, non funzionerà correttamente, poiché la shell tokenizza l' forelenco degli argomenti. Ma poi usi la variabile del nome del file senza virgolette all'interno del ciclo, quindi si romperà lì se lo risolvessi. Correggere questi bug rimanenti renderebbe il tuo identico alla risposta di @ MadMan2064.
Tripleee,

@tripleee sì, è vero, mi mancava questo.
Pawel,

1

Per sostituire tutte le occorrenze in un repository git è possibile utilizzare:

git ls-files -z | xargs -0 sed -i 's/subdomainA\.example\.com/subdomainB.example.com/g'

Vedi Elenca i file nel repository git locale? per altre opzioni per elencare tutti i file in un repository. Le -zopzioni dicono a git di separare i nomi dei file con zero byte, il che assicura che xargs(con l'opzione -0) è possibile separare i nomi dei file, anche se contengono spazi o quant'altro.


1
perl -p -i -e 's/oldthing/new_thingy/g' `grep -ril oldthing *`

1
Non usare awk/ sed, ma perl è comune (ad eccezione di sistemi embedded / solo con busybox).
pevik,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.