web-dev-qa-db-fra.com

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

J'ai remarqué que si j'ajoute \n à un modèle 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 faire en sorte que cela fonctionne?

274
Belmin Fernandez

Dans l'appel le plus simple de sed , il a une ligne de texte dans l'espace de motif, à savoir. 1 ligne de \n texte délimité de l'entrée. La ligne unique dans l'espace de motif n'a pas de \n... Voilà pourquoi votre regex ne trouve rien.

Vous pouvez lire plusieurs lignes dans l'espace de motifs et manipuler les choses étonnamment bien, mais avec un effort plus que normal .. Sed a un ensemble de commandes qui permettent ce type de chose ... Ici est un lien vers un Résumé des commandes pour sed . C'est le meilleur que j'ai trouvé, et ça m'a fait rouler.

Cependant, oubliez l'idée de "one-liner" une fois que vous commencez à utiliser les micro-commandes de sed. Il est utile de le disposer comme un programme structuré jusqu'à ce que vous en ayez la sensation ... Il est étonnamment simple et tout aussi inhabituel. Vous pouvez 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 cela va au-delà du travail avec une seule ligne, la plupart des gens préfèrent autre chose ...
Je laisserai quelqu'un d'autre suggérer autre chose .. Je ne sais vraiment pas quel serait le meilleur choix (j'utiliserais sed, mais c'est parce que je ne connais pas assez bien 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 script, condensé en ce qui est évidemment plus difficile à lire et à travailler, mais certains appelleraient douteusement 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 "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                   
255
Peter.O

Utilisez Perl au 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 -e est votre séquence de ligne de commande standard "replace in place" et -0777 entraîne Perl à Slurp entier. Voir perldoc perlrun pour en savoir plus.

196
codehead

Je pense qu'il vaut mieux remplacer \n symbole avec un autre symbole, puis travaillez comme d'habitude:

par 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 sait pas, \n est la fin de la ligne UNIX, \r\n - les fenêtres, \r - Mac OS classique. Le texte UNIX normal n'utilise pas \r symbole, il est donc sûr de l'utiliser dans ce cas.

Vous pouvez également utiliser un symbole exotique pour remplacer temporairement\n. Par exemple -\f (symbole de flux 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'
108
xara

Tout bien considéré, engloutir tout le fichier 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. Pour de tels cas, d'autres réponses fournies ici offrent des solutions personnalisées qui sont garanties de fonctionner sur une petite empreinte mémoire.

Pour toutes les autres situations de hack et slash, il suffit de faire précéder -e '1h;2,$H;$!d;g' suivi de votre argument regex sed original fait à peu près 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

Qu'est-ce que -e '1h;2,$H;$!d;g' faire?

Le 1, 2,$, $! les parties sont des spécificateurs de ligne qui limitent les lignes sur lesquelles s'exécute directement la commande suivante.

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

Ainsi étendu, c'est ce qui se passe sur chaque ligne d'une entrée de ligne N.

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

La commande g ne reçoit pas de spécificateur de ligne, mais la commande d précédente a une clause spéciale " Démarrer le cycle suivant. ", ce qui empêche g de s'exécuter sur toutes les lignes sauf la dernière.

Quant à la signification de chaque commande:

  • Le premier h suivi de Hs sur chaque ligne copie lesdites lignes d'entrée dans l'espace de rétention de sed. (Pensez à un tampon de texte arbitraire.)
  • Ensuite, d supprime chaque ligne pour empêcher l'écriture de ces lignes dans la sortie. L'espace de maintien est cependant conservé.
  • Enfin, sur la toute dernière ligne, g restaure l'accumulation de chaque ligne à partir de l'espace de maintien de sorte que sed est capable d'exécuter son expression régulière sur l'ensemble de l'entrée (plutôt que d'une ligne à la fois), et est donc en mesure de correspondre sur \ns.
52
antak

sed dispose de trois commandes pour gérer les opérations sur plusieurs lignes: N , D et P (les comparer à normal n, d et p).

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

Quelque chose comme:

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

Vous pouvez mais c'est difficile . Je recommande de passer à un autre outil. S'il existe une expression régulière qui ne correspond à 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 sauts de ligne consécutifs dans votre chaîne de recherche, vous pouvez utiliser le "mode paragraphe" d'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 complètement le fichier en mémoire.

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

Je pense que c'est la solution sed pour l'appariement de 2 lignes.

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

Si vous voulez faire correspondre 3 lignes, alors ...

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

Si vous voulez faire correspondre 4 lignes, alors ...

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

Si la pièce de rechange dans la commande "s" rétrécit les lignes, alors un peu plus compliqué comme ça

# 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 de remplacement augmente 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'

cette deuxième méthode est une simple substitution de copier-coller textuellement pour les fichiers texte habituels de petite taille (besoin d'un fichier de script Shell)

#!/bin/bash

# copy & paste content that you want to substitute

AA=$( cat <<\EOF | sed -z -e 's#\([][^$*\.#]\)#\\\1#g' -e 's#\n#\\n#g'
a test
Please do not
EOF
)

BB=$( cat <<\EOF | sed -z -e 's#\([&\#]\)#\\\1#g' -e 's#\n#\\n#g'
not a test
Be
EOF
)

sed -z -i 's#'"${AA}"'#'"${BB}"'#g' *.txt   # apply to all *.txt files
10
mug896

GNU sed a un -z option 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

Attention: Si vous utilisez ^ et $ ils correspondent maintenant au début et à la fin des lignes délimitées par un caractère NUL (pas \n). Et, pour assurer des correspondances sur tous vos (\n- séparés) les lignes sont substituées, n'oubliez pas d'utiliser l'indicateur g pour les substitutions globales (par exemple s/.../.../g).


Crédits: @ stéphane-chazelas d'abord mentionné -z dans un commentaire ci-dessus.

7
Peterino
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 (multiligne), c est le commande de changement suivi d'un nouveau texte not a test \nBe

Dans le cas où le texte à remplacer est très long, je suggère la syntaxe ex .

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

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

C'est assez simple. Outre la substitution standard; vous n'avez besoin que de $!N, P et D ici.

4
mikeserv

En dehors de Perl, une approche générale et pratique pour l'édition multiligne pour les flux (et les fichiers aussi) est la suivante:

Créez d'abord un nouveau séparateur de ligne UNIQUE comme vous le souhaitez, 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 ASCII séparateur de ligne par le vôtre et vice versa.)

4
guest

Ceci est une petite modification de la réponse intelligente de xara pour le faire fonctionner sur OS X (j'utilise 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').

2
abeboparebop

Je voulais ajouter quelques lignes de HTML à un fichier en utilisant sed, (et je me suis retrouvé ici). Normalement, je n'utilisais que Perl, mais j'étais sur une boîte qui avait sed, bash et pas grand-chose d'autre. J'ai trouvé que si je changeais la chaîne en une seule ligne et que bash/sed interpolait le\t\n tout fonctionnait:

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 pour échapper aux guillemets doubles et aux barres obliques, mais parfois l'abstraction est le voleur de temps.

1
Alexx Roche

Sed interrompt l'entrée sur les nouvelles lignes. Il ne conserve qu'une seule ligne par boucle.
Il n'y a donc aucun moyen de faire correspondre un \n (nouvelle ligne) si l'espace de motif ne le contient pas.

Il existe cependant un moyen de faire en sorte que sed garde deux lignes consécutives dans l'espace de motif en utilisant la boucle:

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

Ajoutez tout traitement nécessaire entre le N et le P (en remplaçant le 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é.

0
Isaac