Come posso usare sed per sostituire una stringa multilinea?


243

Ho notato che, se aggiungo \nuno schema per sostituirlo con sed, non corrisponde. Esempio:

$ cat > alpha.txt
This is
a test
Please do not
be alarmed

$ sed -i'.original' 's/a test\nPlease do not/not a test\nBe/' alpha.txt

$ diff alpha.txt{,.original}

$ # No differences printed out

Come posso farlo funzionare?


Soluzione alternativa qui: unix.stackexchange.com/a/445666/61742 . Ovviamente non è performativo! Altre buone opzioni per eseguire una sostituzione in base alle proprie esigenze possono essere awk, perl e python. Ce ne sono molti altri, ma credo che awk sia il più universale nelle varie distribuzioni Linux (ad esempio). Grazie!
Eduardo Lucio,

Risposte:


235

Nella chiamata più semplice di sed , ha una riga di testo nello spazio del modello, vale a dire. 1 riga di \ntesto delimitato dall'input. La singola linea nello spazio modello non ha \n... Ecco perché la tua regex non sta trovando nulla.

Puoi leggere più righe nello spazio modello e manipolare le cose sorprendentemente bene, ma con uno sforzo più che normale. Sed ha una serie di comandi che consentono questo tipo di cose ... Ecco un link a un Riepilogo comandi per sed . È il migliore che ho trovato e mi ha fatto girare.

Tuttavia, dimentica l'idea "one-liner" una volta che inizi a utilizzare i micro comandi di sed. È utile stenderlo come un programma strutturato fino a quando non ne hai la sensazione ... È sorprendentemente semplice e altrettanto insolito. Potresti pensarlo come il "linguaggio assemblatore" della modifica del testo.

Riepilogo: Usa sed per cose semplici, e forse un po 'di più, ma in generale, quando va oltre il lavoro con una singola linea, la maggior parte delle persone preferisce qualcos'altro ...
Lascerò qualcun altro suggerire qualcos'altro ... Sono davvero non sono sicuro di quale sarebbe la scelta migliore (userei sed, ma è perché non conosco il perl abbastanza bene.)


sed '/^a test$/{
       $!{ N        # append the next line when not on the last line
         s/^a test\nPlease do not$/not a test\nBe/
                    # now test for a successful substitution, otherwise
                    #+  unpaired "a test" lines would be mis-handled
         t sub-yes  # branch_on_substitute (goto label :sub-yes)
         :sub-not   # a label (not essential; here to self document)
                    # if no substituion, print only the first line
         P          # pattern_first_line_print
         D          # pattern_ltrunc(line+nl)_top/cycle
         :sub-yes   # a label (the goto target of the 't' branch)
                    # fall through to final auto-pattern_print (2 lines)
       }    
     }' alpha.txt  

Qui è la stessa sceneggiatura, condensata in ciò che è ovviamente più difficile da leggere e con cui lavorare, ma alcuni chiamerebbero senza dubbio un liner

sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;ty;P;D;:y}}' alpha.txt

Ecco il mio comando "cheat-sheet"

:  # label
=  # line_number
a  # append_text_to_stdout_after_flush
b  # branch_unconditional             
c  # range_change                     
d  # pattern_delete_top/cycle          
D  # pattern_ltrunc(line+nl)_top/cycle 
g  # pattern=hold                      
G  # pattern+=nl+hold                  
h  # hold=pattern                      
H  # hold+=nl+pattern                  
i  # insert_text_to_stdout_now         
l  # pattern_list                       
n  # pattern_flush=nextline_continue   
N  # pattern+=nl+nextline              
p  # pattern_print                     
P  # pattern_first_line_print          
q  # flush_quit                        
r  # append_file_to_stdout_after_flush 
s  # substitute                                          
t  # branch_on_substitute              
w  # append_pattern_to_file_now         
x  # swap_pattern_and_hold             
y  # transform_chars                   

167
Sparami adesso. La peggiore sintassi di sempre!
Gili,

53
Questa è una spiegazione fantastica, ma sono propenso a concordare con @Gili.
gatoatigrado,

11
Il tuo cheat-sheet ha tutto.
konsolebox,

3
Non è necessaria un'etichetta per utilizzare il tcomando qui: quando non viene assegnata un'etichetta, per impostazione predefinita si ramifica alla fine dello script. Quindi sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;t;P;D}}' alpha.txtfa esattamente lo stesso del tuo comando in tutte le circostanze. Naturalmente per questo particolare file, sed '/test/{N;s/.*/not a test\nBe/}' alpha.txtfa anche la stessa cosa, ma il mio primo esempio è logicamente equivalente per tutti i file possibili. Si noti inoltre che \nin una stringa di sostituzione non viene prodotta una nuova riga; hai bisogno di una barra rovesciata `\` seguita da una nuova riga per farlo.
Carattere jolly

9
Si noti che quella sintassi è specifica per GNU ( #comando non separato dal precedente, \nin RHS di s). Con GNU sedpuoi anche usare -zper usare i record delimitati da NUL (e poi biasimare l'intero input se è testo (che per definizione non contiene NUL)).
Stéphane Chazelas,

181

Usa perlinvece di sed:

$ perl -0777 -i.original -pe 's/a test\nPlease do not/not a test\nBe/igs' alpha.txt
$ diff alpha.txt{,.original}
2,3c2,3
< not a test
< Be
---
> a test
> Please do not

-pi -eè la sequenza standard della riga di comando "sostituisci sul posto", e -0777 fa sì che il perl assorba file interi. Vedi perldoc perlrun per saperne di più.


3
Grazie! Per il lavoro multilinea, il perl vince a mani basse! Ho finito per usare `$ perl -pi -e 's / bar / baz /' fileA` per cambiare il file sul posto.
Nicholas Tolley Cottrell,

3
È molto comune che il poster originale richieda sede vengano visualizzate le risposte usando awk o perl. Penso che non sia in tema, quindi, scusa, ma ne ho sparato uno meno.
Rho Phi,

68
+1 e non sono d'accordo con Roberto. Spesso domande formulate specificamente per ignoranza di metodi migliori. Quando non c'è una sostanziale differenza contestuale (come qui), le soluzioni ottimali dovrebbero ottenere almeno lo stesso profilo di quelle specifiche della domanda.
geotheory

56
Penso che la sedrisposta sopra dimostri che una risposta Perl è in tema.
reinierpost,

7
Un po 'più semplice: con "-p0e" non è necessario "-0777". unix.stackexchange.com/a/181215/197502
Weidenrinde

96

Penso che sia meglio sostituire il \nsimbolo con qualche altro simbolo e quindi lavorare come al solito:

ad es. codice sorgente non funzionante:

cat alpha.txt | sed -e 's/a test\nPlease do not/not a test\nBe/'

può essere modificato in:

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test\rPlease do not/not a test\rBe/'  | tr '\r' '\n'

Se qualcuno non lo sa, \nsta terminando la linea UNIX, \r\n- windows, \r- Mac OS classico. Il normale testo UNIX non usa il \rsimbolo, quindi è sicuro usarlo per questo caso.

Puoi anche usare alcuni simboli esotici per sostituire temporaneamente \ n. Ad esempio - \ f (simbolo del feed del modulo). Puoi trovare più simboli qui .

cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/'  | tr '\f' '\n'

11
+1 per questo trucco intelligente! Particolarmente utile è il consiglio sull'uso di un simbolo esotico per sostituire temporaneamente la nuova riga a meno che non si sia assolutamente certi del contenuto del file che si sta modificando.
L0j1k,

Questo non funziona come scritto su OS X. Invece, è necessario sostituire tutte le istanze di \rnell'argomento sedcon $(printf '\r').
abeboparebop,

@abeboparebop: grande scoperta! 👍 in alternativa, installa GNU sed usando homebrew: stackoverflow.com/a/30005262
ssc

@abeboparebop, Su OSX, devi solo aggiungere una $prima della stringa sed per impedirne la conversione \rin una r. Breve esempio: sed $'s/\r/~/'. Esempio completo:cat alpha.txt | tr '\n' '\r' | sed $'s/a test\rPlease do not/not a test\rBe/' | tr '\r' '\n'
wisbucky

40

Tutto sommato , divorare l'intero file potrebbe essere il modo più veloce per andare.

La sintassi di base è la seguente:

sed -e '1h;2,$H;$!d;g' -e 's/__YOUR_REGEX_GOES_HERE__...'

Intendiamoci, divorare l'intero file potrebbe non essere un'opzione se il file è tremendamente grande. In questi casi, altre risposte fornite qui offrono soluzioni personalizzate che garantiscono il funzionamento con un ingombro di memoria ridotto.

Per tutte le altre situazioni di hacking e slash, basta semplicemente anteporre -e '1h;2,$H;$!d;g'seguito sedall'argomento regex originale praticamente per fare il lavoro.

per esempio

$ echo -e "Dog\nFox\nCat\nSnake\n" | sed -e '1h;2,$H;$!d;g' -re 's/([^\n]*)\n([^\n]*)\n/Quick \2\nLazy \1\n/g'
Quick Fox
Lazy Dog
Quick Snake
Lazy Cat

Cosa fa -e '1h;2,$H;$!d;g'?

La 1, 2,$, $!le parti sono linea specifiche dell'avvio quel limite che riveste la seguente comando direttamente girare.

  • 1: Solo prima riga
  • 2,$: Tutte le righe a partire dalla seconda
  • $!: Ogni riga diversa dall'ultima

Così espanso, questo è ciò che accade su ciascuna riga di un ingresso di linea N.

  1: h, d
  2: H, d
  3: H, d
  .
  .
N-2: H, d
N-1: H, d
  N: H, g

Al gcomando non viene assegnato un identificatore di linea, ma il dcomando precedente ha una clausola speciale " Avvia ciclo successivo " e ciò impedisce l' gesecuzione su tutte le linee tranne l'ultima.

Per quanto riguarda il significato di ciascun comando:

  • La prima hseguita da Hs su ogni riga copie dette linee di ingresso in sed's hold space . (Pensa al buffer di testo arbitrario.)
  • Successivamente, dscarta ogni riga per impedire che queste vengano scritte nell'output. Lo spazio di attesa viene tuttavia conservato.
  • Infine, sull'ultima riga, gripristina l'accumulo di ogni linea dallo spazio di attesa in modo che sedsia in grado di eseguire la sua regex sull'intero input (piuttosto che in una linea alla volta), e quindi è in grado di partita su \ns.

38

sedha tre comandi per gestire operazioni multilinea: N, De P(li confronta al normale n , de p).

In questo caso, puoi abbinare la prima linea del tuo motivo, usare Nper aggiungere la seconda linea allo spazio del motivo e quindi usare sper fare la tua sostituzione.

Qualcosa di simile a:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}

2
Questo e spettacolare! Più semplice della risposta accettata e comunque efficace.
jeyk,

E tutti quelli che coinvolgono lo spazio stiva ( G, H, x...). Altre righe possono essere aggiunte allo spazio del modello anche con il scomando.
Stéphane Chazelas,


questa soluzione non funziona con il seguente caso "This is \ na test \ na test \ n Per favore, non essere allarmato"
mug896

@ mug896 molto probabilmente avrai bisogno di più Ncomandi
loa_in_

15

Puoi ma è difficile . Consiglio di passare a uno strumento diverso. Se esiste un'espressione regolare che non corrisponde mai a nessuna parte del testo che desideri sostituire, puoi usarla come separatore di record awk in GNU awk.

awk -v RS='a' '{gsub(/hello/, "world"); print}'

Se non ci sono mai due newline consecutive nella tua stringa di ricerca, puoi usare la "modalità paragrafo" di awk (una o più righe vuote record separati).

awk -v RS='' '{gsub(/hello/, "world"); print}'

Una soluzione semplice è utilizzare Perl e caricare il file completamente in memoria.

perl -0777 -pe 's/hello/world/g'

1
Come applicare il comando perl a un file?
sebix,

2
@sebix perl -0777 -pe '…' <input-file >output-file. Per modificare un file in atto,perl -0777 -i -pe '…' filename
Gilles

3
Vedere anche GNU seds' -zopzione (aggiunta nel 2012 dopo che la risposta è stata pubblicata): seq 10 | sed -z 's/4\n5/a\nb/'.
Stéphane Chazelas,

7

Penso che questa sia la soluzione sed per la corrispondenza di 2 linee.

sed -n '$!N;s@a test\nPlease do not@not a test\nBe@;P;D' alpha.txt

Se vuoi che 3 linee corrispondano, allora ...

sed -n '1{$!N};$!N;s@aaa\nbbb\nccc@xxx\nyyy\nzzz@;P;D'

Se vuoi che 4 linee corrispondano, allora ...

sed -n '1{$!N;$!N};$!N;s@ ... @ ... @;P;D'

Se la parte di ricambio nel comando "s" riduce le linee, allora un po 'più complicato come questo

# aaa\nbbb\nccc shrink to one line "xxx"

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@xxx@;$!N;$!N};P;D'

Se la parte di rimpiazzo cresce, allora un po 'più complicata come questa

# aaa\nbbb\nccc grow to five lines vvv\nwww\nxxx\nyyy\nzzz

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@vvv\nwww\nxxx\nyyy\nzzz@;P;s/.*\n//M;P;s/.*\n//M};P;D'

Questo dovrebbe arrivare fino in cima! Ho appena usato "-i" invece di "-n" per la sostituzione a due righe, perché è quello di cui ho bisogno, e per inciso, è anche nell'esempio del richiedente.
Nagev,

5
sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt

Qui /a test/,/Please do not/è considerato un blocco di testo (a più righe), cè il comando di modifica seguito da un nuovo testonot a test \nBe

Nel caso in cui il testo da sostituire sia molto lungo, suggerirei ex sintassi.


oops il problema è che sed sostituirà tutto il testo eventualmente tra / a test / e / Please not /
also

4
sed -e'$!N;s/^\(a test\n\)Please do not be$/not \1Be/;P;D' <in >out

Basta allargare un po 'la finestra sull'input.

È abbastanza facile Oltre alla sostituzione standard; è necessario solo $!N, Pe Dqui.


4

Oltre a Perl, un approccio generale e utile per l'editing multilinea per stream (e anche file) è:

Per prima cosa crea un nuovo separatore di linea UNIQUE come preferisci, per esempio

$ S=__ABC__                     # simple
$ S=__$RANDOM$RANDOM$RANDOM__   # better
$ S=$(openssl rand -hex 16)     # ultimate

Quindi nel tuo comando sed (o in qualsiasi altro strumento) sostituisci \ n con $ {S}, come

$ cat file.txt | awk 1 ORS=$S |  sed -e "s/a test${S}Please do not/not a test\nBe/" | awk 1 RS=$S > file_new.txt

(awk sostituisce il separatore di riga ASCII con il tuo e viceversa.)


2

Questa è una piccola modifica della risposta intelligente di xara per farlo funzionare su OS X (sto usando 10.10):

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test$(printf '\r')Please do not/not a test$(printf '\r')Be/'  | tr '\r' '\n'

Invece di usare esplicitamente \r, devi usare $(printf '\r').


1
Mentre printf '\r'(o echo -e '\r') funzionano correttamente, tieni presente che puoi semplicemente utilizzare la sintassi della shell $'\r'per fare riferimento ai letterali con escape. Ad esempio, echo hi$'\n'therefarà eco una nuova riga tra hie there. Allo stesso modo, puoi avvolgere l'intera stringa in modo che ogni barra inversa \ sfugga al carattere successivo:echo $'hi\nthere'
Dejay Clayton,

1

Volevo aggiungere alcune righe di HTML a un file usando sed (e sono finito qui). Normalmente userei solo perl, ma ero su una scatola con sed, bash e non molto altro. Ho scoperto che se ho cambiato la stringa in una sola riga e ho lasciato bash / sed interpolare il \ t \ n tutto ha funzionato:

HTML_FILE='a.html' #contains an anchor in the form <a name="nchor" />
BASH_STRING_A='apples'
BASH_STRING_B='bananas'
INSERT="\t<li>$BASH_STRING_A<\/li>\n\t<li>$BASH_STRING_B<\/li>\n<a name=\"nchor\"\/>"
sed -i "s/<a name=\"nchor"\/>/$INSERT/" $HTML_FILE

Sarebbe più pulito avere una funzione per sfuggire alle doppie virgolette e alle barre, ma a volte l'astrazione è il ladro del tempo.


1

GNU sedha -zun'opzione che consente di utilizzare la sintassi che OP ha tentato di applicare. ( pagina man )

Esempio:

$ cat alpha.txt
This is
a test
Please do not
be alarmed
$ sed -z 's/a test\nPlease do not\nbe/not a test\nBe/' -i alpha.txt
$ cat alpha.txt
This is
not a test
Be alarmed

Attenzione: se si utilizza ^e $ora corrispondono all'inizio e alla fine delle righe delimitate da un carattere NUL (non \n). E, per garantire \nche vengano sostituite le corrispondenze su tutte le linee ( separate), non dimenticare di usare la gbandiera per le sostituzioni globali (ad es s/.../.../g.).


Crediti: @ stéphane-chazelas menzionato per la prima volta -z in un commento sopra.


0

Sed interrompe l'input su newline. Mantiene solo una riga per loop.
Pertanto non è possibile abbinare un \n(newline) se lo spazio del pattern non lo contiene.

C'è un modo, tuttavia, è possibile fare in modo che sed mantenga due linee consecutive nello spazio del pattern usando il loop:

sed 'N;l;P;D' alpha.txt

Aggiungi l'eventuale elaborazione necessaria tra N e P (sostituendo il l).

In questo caso (2 righe):

$ sed 'N;s/a test\nPlease do not/not a test\nBe/;P;D' alpha.txt
This is
not a test
Be
be alarmed

Oppure, per tre righe:

$ sed -n '1{$!N};$!N;s@a test\nPlease do not\nbe@not a test\nDo\nBe@;P;D' alpha.txt 
This is
not a test
Do
Be alarmed

Ciò presuppone che la stessa quantità di righe venga sostituita.

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.