Comment puis-je utiliser sed pour remplacer une chaîne multiligne?

243

J'ai remarqué que, si j'ajoute \nun motif de substitution à l'aide de sed, il ne correspond pas. Exemple:

$ 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

Comment puis-je le faire fonctionner?

Belmin Fernandez
la source
Solution de contournement intelligente ici: unix.stackexchange.com/a/445666/61742 . Bien sûr ce n'est pas performatique! Awk, perl et python sont d’autres options pour effectuer un remplacement en fonction de vos besoins. Il y en a beaucoup d'autres, mais je pense que awk est la plus universelle des différentes distributions Linux (par exemple). Merci!
Eduardo Lucio

Réponses:

235

Dans l'appel le plus simple de sed , il y a une ligne de texte dans l'espace modèle, c'est-à-dire. 1 ligne de \ntexte délimité de l'entrée. La seule ligne dans l'espace des motifs n'a pas \n... C'est pourquoi votre expression rationnelle ne trouve rien.

Vous pouvez lire plusieurs lignes dans l'espace des motifs et manipuler les choses étonnamment bien, mais avec un effort supérieur à la normale. Sed dispose d'un ensemble de commandes permettant ce type de choses ... Voici un lien vers un résumé des commandes pour sed . C'est le meilleur que j'ai trouvé, et m'a fait rouler.

Cependant, oubliez l'idée "one-liner" une fois que vous commencez à utiliser les micro-commandes de sed. Il est utile de le présenter comme un programme structuré jusqu'à ce que vous en ressentiez la sensation ... C'est étonnamment simple et tout aussi inhabituel. Vous pourriez le considérer comme le "langage assembleur" de l'édition de texte.

Résumé: Utilisez sed pour des choses simples, et peut-être un peu plus, mais en général, quand on dépasse le travail avec une seule ligne, la plupart des gens préfèrent autre chose ...
Je laisserai quelqu'un d'autre suggérer quelque chose d'autre .. Je suis Je ne sais vraiment pas quel serait le meilleur choix (j'utiliserais sed, mais c'est parce que je ne connais pas assez bien le langage Perl.)


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  

Ici, c’est le même scénario, condensé dans ce qui est évidemment plus difficile à lire et à utiliser, mais certains appelleraient sans aucun doute un one-liner

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

Voici ma commande "aide-mémoire"

:  # 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                   
Peter.O
la source
167
Tirez-moi maintenant. La pire syntaxe jamais!
Gili
53
C'est une explication fantastique, mais je suis enclin à être d'accord avec @Gili.
Gatoatigrado
11
Votre aide-mémoire a tout.
konsolebox
3
Vous n'avez pas besoin d'une étiquette pour utiliser la tcommande ici. Lorsqu'elle ne reçoit pas d'étiquette, elle se branche par défaut sur la fin du script. Ainsi sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;t;P;D}}' alpha.txtfait exactement la même chose que votre commande en toutes circonstances. Bien sûr, pour ce fichier particulier , sed '/test/{N;s/.*/not a test\nBe/}' alpha.txtfait la même chose, mais mon premier exemple est logiquement équivalent pour tous les fichiers possibles. Notez également que \ndans une chaîne de remplacement ne produit pas de nouvelle ligne; pour ce faire, vous avez besoin d’une barre oblique inverse `\` suivie par une nouvelle ligne.
Wildcard
9
Notez que cette syntaxe est spécifique à GNU ( #commande non séparée de la précédente, \ndans RHS of s). Avec GNU, sedvous pouvez également utiliser des -zenregistrements délimités par NUL (puis lisser toute l'entrée si c'est du texte (qui par définition ne contient pas de NUL)).
Stéphane Chazelas
181

Utilisez perlau lieu de 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 -eest votre séquence de ligne de commande standard "remplacer à la place" et -0777 fait en sorte que perl sculpte les fichiers en entier. Voir perldoc perlrun pour en savoir plus.

tête de code
la source
3
Merci! Pour un travail multiligne, Perl gagne haut la main! J'ai fini par utiliser `$ perl -pi -e '/ bar / baz /' fileA` pour changer le fichier sur place.
Nicholas Tolley Cottrell
3
Il est très courant que l’affiche originale demande sedet que les réponses utilisant awk ou perl apparaissent. Je pense que ce n'est pas sur le sujet, donc, désolé, mais j'ai tiré un moins.
Rho Phi
68
+1 et pas d'accord avec Roberto. Souvent des questions formulées spécifiquement pour l'ignorance de meilleures méthodes. Lorsqu'il n'y a pas de différence contextuelle importante (comme ici), les solutions optimales doivent avoir au moins autant de visibilité que les solutions spécifiques.
geotheory
56
Je pense que la sedréponse ci-dessus prouve qu'une réponse Perl est sur le sujet.
Reinierpost
7
Un peu plus facile: Avec "-p0e", le "-0777" n'est pas nécessaire. unix.stackexchange.com/a/181215/197502
Weidenrinde
96

Je pense qu'il est préférable de remplacer le \nsymbole par un autre symbole, puis de continuer comme d'habitude:

Exemple: code source non travaillé:

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

peut être changé en:

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

Si quelqu'un ne le sait pas, la \nligne UNIX se termine-t \r\n-elle sous Windows, sous \rMac OS classique. Le texte UNIX normal n'utilise pas de \rsymbole, vous pouvez donc l'utiliser en toute sécurité.

Vous pouvez également utiliser un symbole exotique pour remplacer temporairement \ n. À titre d'exemple - \ f (symbole d'alimentation de formulaire). Vous pouvez trouver plus de symboles ici .

cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/'  | tr '\f' '\n'
xara
la source
11
+1 pour ce hack intelligent! Les conseils sur l'utilisation d'un symbole exotique pour remplacer temporairement newline sont particulièrement utiles, sauf si vous êtes absolument certain du contenu du fichier que vous modifiez.
L0j1k
Cela ne fonctionne pas comme cela est écrit sous OS X. Au lieu de cela, il faut remplacer toutes les instances de \rl'argument sedavec $(printf '\r').
Abeboparebop
@abeboparebop: bonne trouvaille! 👍 vous pouvez également installer GNU sed avec homebrew: stackoverflow.com/a/30005262
ssc le
@abeboparebop, Sous OSX, il vous suffit d’ajouter un $avant la chaîne sed pour l’empêcher de le convertir \ren un r. Court exemple: sed $'s/\r/~/'. Exemple complet:cat alpha.txt | tr '\n' '\r' | sed $'s/a test\rPlease do not/not a test\rBe/' | tr '\r' '\n'
Wisbucky
40

Tout bien considéré, engloutir le fichier entier peut être le moyen le plus rapide.

La syntaxe de base est la suivante:

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

Attention, engloutir le fichier entier peut ne pas être une option si le fichier est extrêmement volumineux. Dans de tels cas, les autres réponses fournies ici proposent des solutions personnalisées dont l’efficacité est garantie pour un faible encombrement de la mémoire.

Pour toutes les autres situations de hack et de slash, le fait d’ajouter des préfixes, -e '1h;2,$H;$!d;g'suivi de votre sedargument regex original, permet de faire le travail.

par exemple

$ 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

Que fait -e '1h;2,$H;$!d;g'-il?

Le 1, 2,$, les $!parties sont en ligne spécificateurs qui limite les lignes de la commande suivante directement exécuté.

  • 1: Première ligne seulement
  • 2,$: Toutes les lignes à partir de la seconde
  • $!: Chaque ligne autre que dernière

Tellement développé, c’est ce qui se passe sur chaque ligne d’une entrée de N ligne.

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

La gcommande n'est pas donné une spécificateur de ligne, mais la précédente dcommande a une clause spéciale « Début du prochain cycle. », Et cela empêche gde fonctionner sur toutes les lignes sauf la dernière.

Quant à la signification de chaque commande:

  • Le premier hsuivi de Hs sur chaque ligne copie lesdites lignes d'entrée dans sedl' espace d'attente de . (Pensez à un tampon de texte arbitraire.)
  • Ensuite, délimine chaque ligne pour empêcher l’écriture de ces lignes dans la sortie. L' espace d'attente est toutefois préservé.
  • Enfin, à la toute dernière ligne, grestaure l’accumulation de chaque ligne de l’ espace de réservation de manière à sedpouvoir exécuter sa regex sur l’entrée entière (plutôt que ligne à la fois), et par conséquent correspondre à \ns.
antak
la source
38

seda trois commandes pour gérer les opérations à plusieurs lignes: N, Det P(les comparer à la normale n , det p).

Dans ce cas, vous pouvez faire correspondre la première ligne de votre modèle, utilisez Npour ajouter la deuxième ligne à l' espace de modèle , puis utilisez spour effectuer votre substitution.

Quelque chose comme:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}
Andcoz
la source
2
C'est génial! Plus simple que la réponse acceptée et toujours efficace.
Jeyk
Et tous ceux impliquant l'espace d'attente ( G, H, x...). Vous pouvez également ajouter d'autres lignes dans l'espace de modèle à l'aide de la scommande.
Stéphane Chazelas
cette solution ne fonctionne pas avec le cas suivant "Ceci est \ na test \ na test \ n Merci de ne pas vous
effrayer
@ mug896 vous avez probablement besoin de plusieurs Ncommandes
loa_in_
15

Vous pouvez mais c'est difficile . Je recommande de passer à un autre outil. S'il existe une expression régulière qui ne correspond jamais à aucune partie du texte que vous souhaitez remplacer, vous pouvez l'utiliser comme séparateur d'enregistrement awk dans GNU awk.

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

S'il n'y a jamais deux retours à la ligne consécutifs dans votre chaîne de recherche, vous pouvez utiliser le "mode paragraphe" de awk (une ou plusieurs lignes vides séparent les enregistrements).

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

Une solution simple consiste à utiliser Perl et à charger le fichier intégralement en mémoire.

perl -0777 -pe 's/hello/world/g'
Gilles
la source
1
Comment appliquer la commande perl à un fichier?
sebix
2
@sebix perl -0777 -pe '…' <input-file >output-file. Pour modifier un fichier en place,perl -0777 -i -pe '…' filename
Gilles le
3
Voir aussi GNU sedde l' -zoption (ajoutée en 2012 après cette réponse a été publiée): seq 10 | sed -z 's/4\n5/a\nb/'.
Stéphane Chazelas
7

Je pense que ceci est la solution sed pour 2 lignes correspondantes.

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

Si vous voulez 3 lignes correspondant alors ...

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

Si vous voulez 4 lignes correspondant alors ...

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

Si la pièce de remplacement dans la commande "s" réduit les lignes, un peu plus compliqué comme celui-ci

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

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

Si la partie repacement fait croître les lignes, alors un peu plus compliqué comme ça

# 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'
mug896
la source
Cela devrait faire son chemin vers le haut! Je viens d'utiliser le "-i" au lieu de "-n" pour la substitution de deux lignes, car c'est ce dont j'ai besoin, et accessoirement, c'est aussi dans l'exemple du demandeur.
Nagev
5
sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt

Ici /a test/,/Please do not/est considéré comme un bloc de texte (multi-lignes), cla commande de changement suivie par un nouveau textenot a test \nBe

Dans le cas où le texte à remplacer est très long, je suggérerais une syntaxe ex .

gibies
la source
oops le problème est que sed remplacera tout texte éventuel entre / un test / et / s'il vous plaît, ne le faites pas / aussi bien ... :(
noonex
4
sed -e'$!N;s/^\(a test\n\)Please do not be$/not \1Be/;P;D' <in >out

Élargissez un peu votre fenêtre d'entrée.

C'est assez facile. Outre la substitution standard; vous avez besoin seulement $!N, Pet Dici.

Mikeserv
la source
4

Outre Perl, une approche générale et pratique pour l'édition multiligne des flux (et des fichiers) est la suivante:

Commencez par créer un nouveau séparateur de ligne UNIQUE à votre guise, par exemple

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

Ensuite, dans votre commande sed (ou tout autre outil), vous remplacez \ n par $ {S}, comme

$ 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 remplace le séparateur de lignes ASCII par le vôtre et inversement.)

client
la source
2

Voici une petite modification de la réponse intelligente de xara pour que cela fonctionne sous OS X (j'utilise la version 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'

Au lieu d'utiliser explicitement \r, vous devez utiliser $(printf '\r').

abeboparebop
la source
1
Bien que printf '\r'(ou echo -e '\r') fonctionne correctement, veuillez noter que vous pouvez simplement utiliser la syntaxe du shell $'\r'pour faire référence aux littéraux échappés. Par exemple, echo hi$'\n'therefera écho une nouvelle ligne entre hiet there. De même, vous pouvez envelopper la chaîne entière de sorte que chaque barre oblique inverse \ échappe à son caractère suivant:echo $'hi\nthere'
Dejay Clayton le
1

Je voulais ajouter quelques lignes de HTML à un fichier en utilisant sed, (et je l'ai fini ici). Normalement, je n'utilisais que du perl, mais j'étais sur une boîte qui avait sed, bash et pas grand chose d'autre. J'ai découvert que si je modifiais la chaîne en une seule ligne et laissais bash / sed interpoler le \ t \ n, tout se passait bien:

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

Il serait plus propre d’avoir une fonction permettant d’échapper aux guillemets et aux barres obliques, mais parfois l’abstraction est le voleur de temps.

Alexx Roche
la source
1

GNU seda une -zoption qui permet d’utiliser la syntaxe que l’OP a tenté d’appliquer. ( page de manuel )

Exemple:

$ 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

Sachez que si vous utilisez ^et $qu’ils correspondent maintenant au début et à la fin des lignes délimitées par un caractère NUL (non \n). Et, pour vous assurer que les correspondances de toutes vos \nlignes ( séparées) sont remplacées, n'oubliez pas d'utiliser le gdrapeau pour les substitutions globales (par exemple s/.../.../g).


Crédits: @ stéphane-chazelas a mentionné pour la première fois -z dans un commentaire ci-dessus.

Peterino
la source
0

Sed interrompt les entrées sur les nouvelles lignes. Il ne conserve qu'une ligne par boucle.
Par conséquent, il n'y a aucun moyen de faire correspondre une \n(nouvelle ligne) si l'espace de modèle ne le contient pas.

Il existe un moyen, cependant, d’obliger la boucle à conserver deux lignes consécutives dans l’espace du motif:

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

Ajoutez tout traitement nécessaire entre le N et le P (en remplacement du l).

Dans ce cas (2 lignes):

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

Ou, pour trois lignes:

$ 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

Cela suppose que le même nombre de lignes soit remplacé.

Isaac
la source