Correspondance non gourmande avec l'expression rationnelle SED (émuler les perl. *?)

22

Je veux utiliser sedpour remplacer quoi que ce soit dans une chaîne entre la première ABet la première occurrence de AC(inclus) par XXX.

Par exemple , j'ai cette chaîne (cette chaîne est pour un test uniquement):

ssABteAstACABnnACss

et je voudrais similaire à ceci: ssXXXABnnACss.


Je l'ai fait avec perl:

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

mais je veux l'implémenter avec sed. Les éléments suivants (à l'aide de l'expression rationnelle compatible Perl) ne fonctionnent pas:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
بارپابابا
la source
2
Cela n'a aucun sens. Vous avez une solution qui fonctionne en Perl, mais vous souhaitez utiliser Sed, pourquoi?
Kusalananda

Réponses:

16

Les expressions régulières sed correspondent à la plus longue correspondance. Sed n'a pas d'équivalent de non gourmand.

Évidemment, ce que nous voulons faire, c'est

  1. AB,
    suivi de
  2. tout montant autre que AC,
    suivi de
  3. AC

Malheureusement, sedne peut pas faire # 2 - du moins pas pour une expression régulière à plusieurs caractères. Bien sûr, pour une expression régulière à un seul caractère comme @(ou même [123]), nous pouvons faire [^@]*ou [^123]*. Et donc nous pouvons contourner les limites de sed en changeant toutes les occurrences de ACen @puis en recherchant

  1. AB,
    suivi de
  2. tout autre chose que @,
    suivi de
  3. @

comme ça:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'

La dernière partie @remplace les instances inégalées de back to AC.

Mais, bien sûr, c'est une approche imprudente, car l'entrée peut déjà contenir des @caractères, donc, en les faisant correspondre, nous pourrions obtenir des faux positifs. Cependant, étant donné qu'aucune variable shell n'aura jamais de caractère NUL ( \x00), NUL est probablement un bon caractère à utiliser dans la solution de contournement ci-dessus au lieu de @:

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

L'utilisation de NUL nécessite GNU sed. (Pour vous assurer que les fonctionnalités GNU sont activées, l'utilisateur ne doit pas avoir défini la variable shell POSIXLY_CORRECT.)

Si vous utilisez sed avec l' -zindicateur GNU pour gérer les entrées séparées par NUL, telles que la sortie de find ... -print0, alors NUL ne sera pas dans l'espace de motif et NUL est un bon choix pour la substitution ici.

Bien que NUL ne puisse pas être dans une variable bash, il est possible de l'inclure dans une printfcommande. Si votre chaîne d'entrée peut contenir n'importe quel caractère, y compris NUL, alors voyez la réponse de Stéphane Chazelas qui ajoute une méthode d'échappement intelligente.

John1024
la source
Je viens de modifier votre réponse pour ajouter une longue explication; n'hésitez pas à le couper ou à le faire reculer.
G-Man dit `` Réintègre Monica '' le
@ G-Man C'est une excellente explication! Très bien fait. Je vous remercie.
John1024
Vous pouvez echoou printfun \ \ 000 très bien dans bash (ou l'entrée pourrait provenir d'un fichier). Mais en général, une chaîne de texte n'a bien sûr pas de NUL.
ilkkachu
@ilkkachu Vous avez raison à ce sujet. Ce que j'aurais dû écrire, c'est qu'aucune variable ou paramètre shell ne peut contenir de NUL. Réponse mise à jour.
John1024
Cela ne serait-il pas beaucoup plus sûr si vous changiez ACde AC@dos?
Michael Vehrs du
7

Certaines sedimplémentations prennent en charge cela. sseda un mode PCRE:

ssed -R 's/AB.*?AC/XXX/g'

AT&T ast sed a une conjonction et une négation lors de l'utilisation des expressions rationnelles augmentées :

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/g'

De manière portable, vous pouvez utiliser cette technique: remplacez la chaîne de fin (ici AC) par un seul caractère qui n'apparaît ni dans la chaîne de début ni dans la chaîne de fin (comme :ici) pour que vous puissiez le faire s/AB[^:]*://, et au cas où ce caractère pourrait apparaître dans l'entrée , utilisez un mécanisme d'échappement qui ne se heurte pas aux chaînes de début et de fin.

Un exemple:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/g; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'

Avec GNU sed, une approche consiste à utiliser la nouvelle ligne comme caractère de remplacement. Parce que sedtraite une ligne à la fois, la nouvelle ligne ne se produit jamais dans l'espace de motif, donc on peut faire:

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/g;s/\n/AC/g'

Cela ne fonctionne généralement pas avec d'autres sedimplémentations car elles ne prennent pas en charge [^\n]. Avec GNU, sedvous devez vous assurer que la compatibilité POSIX n'est pas activée (comme avec la variable d'environnement POSIXLY_CORRECT).

Stéphane Chazelas
la source
6

Non, les expressions rationnelles sed n'ont pas de correspondance non gourmande.

Vous pouvez faire correspondre tout le texte jusqu'à la première occurrence de ACen utilisant «tout ce qui ne contient pas AC» suivi de AC, ce qui fait la même chose que Perl .*?AC. Le fait est que «tout ce qui ne contient pas AC» ne peut pas être exprimé facilement comme une expression régulière: il y a toujours une expression régulière qui reconnaît la négation d'une expression régulière, mais l'expression rationnelle de négation se complique rapidement. Et dans sed portable, ce n'est pas possible du tout, car l'expression rationnelle de négation nécessite de grouper une alternance qui est présente dans les expressions régulières étendues (par exemple dans awk) mais pas dans les expressions régulières de base portables. Certaines versions de sed, comme GNU sed, ont des extensions à BRE qui permettent d'exprimer toutes les expressions régulières possibles.

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

En raison de la difficulté de nier une expression régulière, cela ne se généralise pas bien. Ce que vous pouvez faire à la place est de transformer temporairement la ligne. Dans certaines implémentations sed, vous pouvez utiliser des sauts de ligne comme marqueur, car ils ne peuvent pas apparaître dans une ligne d'entrée (et si vous avez besoin de plusieurs marqueurs, utilisez le saut de ligne suivi d'un caractère variable).

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

Cependant, sachez que backslash-newline ne fonctionne pas dans un jeu de caractères avec certaines versions sed. En particulier, cela ne fonctionne pas dans GNU sed, qui est l'implémentation sed sur Linux non embarqué; dans GNU sed, vous pouvez utiliser à la \nplace:

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

Dans ce cas précis, il suffit de remplacer le premier ACpar une nouvelle ligne. L'approche que j'ai présentée ci-dessus est plus générale.

Une approche plus puissante dans sed consiste à enregistrer la ligne dans l'espace d'attente, à supprimer tout sauf la première partie «intéressante» de la ligne, à échanger l'espace d'attente et l'espace modèle ou à ajouter l'espace modèle à l'espace d'attente et à répéter. Cependant, si vous commencez à faire des choses aussi compliquées, vous devriez vraiment penser à passer à awk. Awk n'a pas non plus de correspondance non gourmande, mais vous pouvez fractionner une chaîne et enregistrer les parties en variables.

Gilles 'SO- arrête d'être méchant'
la source
@ilkkachu Non, ce n'est pas le cas. s/\n//gsupprime toutes les nouvelles lignes.
Gilles 'SO- arrête d'être méchant'
asdf. Bon, ma mauvaise.
ilkkachu
3

sed - correspondance non gourmande par Christoph Sieghart

L'astuce pour obtenir une correspondance non gourmande dans sed est de faire correspondre tous les caractères à l'exception de celui qui met fin à la correspondance. Je sais, une évidence, mais j'ai perdu de précieuses minutes et les scripts shell devraient être, après tout, rapides et faciles. Donc, au cas où quelqu'un d'autre en aurait besoin:

Correspondance gourmande

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Correspondance non gourmande

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

gresolio
la source
3
Le terme «sans cervelle» est ambigu. Dans ce cas, il n'est pas clair que vous (ou Christoph Sieghart) y ayez réfléchi. En particulier, cela aurait été bien si vous aviez montré comment résoudre le problème spécifique dans la question (où le zéro de plus d'expression est suivi de plusieurs caractères ) . Vous trouverez peut-être que cette réponse ne fonctionne pas bien dans ce cas.
Scott
Le trou du lapin est beaucoup plus profond qu'il ne me semblait à première vue. Vous avez raison, cette solution de contournement ne fonctionne pas bien pour l'expression régulière à plusieurs caractères.
gresolio
0

Dans votre cas, vous pouvez simplement annuler le caractère de fermeture de cette façon:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
midori
la source
2
La question dit: «Je veux remplacer n'importe quoi entre la première ABet la première occurrence de ACavec XXX…», et donne ssABteAstACABnnACsscomme exemple une entrée. Cette réponse fonctionne pour cet exemple , mais ne répond pas à la question en général. Par exemple, ssABteCstACABnnACssdevrait également produire la sortie aaXXXABnnACss, mais votre commande passe inchangée sur cette ligne.
G-Man dit `` Réintègre Monica '' le
0

La solution est assez simple. .*est gourmand, mais il n'est pas absolument gourmand. Envisagez une correspondance ssABteAstACABnnACssavec l'expression rationnelle AB.*AC. Ce ACqui suit .*doit en fait avoir une correspondance. Le problème est que parce qu'il .*est gourmand, le suivant ACcorrespondra au dernier AC plutôt qu'au premier. .*mange le premier ACtandis que le littéral ACdans l'expression rationnelle correspond au dernier dans ssABteAstACABnn AC ss. Pour éviter que cela ne se produise, remplacez simplement le premier ACpar quelque chose de ridicule pour le différencier du second et de toute autre chose.

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

Le gourmand .*va maintenant arrêter au pied de -foobar-dans , ssABteAst-foobar-ABnnACsscar il n'y a pas d' autre -foobar-que cela -foobar-, et le regexp -foobar- DOIT avoir un match. Le problème précédent était que l'expression régulière ACavait deux correspondances, mais parce qu'elle .*était gourmande, la dernière correspondance pour a ACété sélectionnée. Cependant, avec -foobar-, une seule correspondance est possible, et cette correspondance prouve que ce .*n'est pas absolument gourmand. L'arrêt de bus pour .*se produit où il ne reste qu'une correspondance pour le reste de l'expression rationnelle suivante .*.

Notez que cette solution échouera si un ACapparaît avant le premier ABcar le mauvais ACsera remplacé par -foobar-. Par exemple, après la première sedsubstitution, ACssABteAstACABnnACssdevient -foobar-ssABteAstACABnnACss; par conséquent, aucune correspondance ne peut être trouvée AB.*-foobar-. Cependant, si la séquence est toujours ... AB ... AC ... AB ... AC ..., alors cette solution réussira.

JD Graham
la source
0

Une alternative est de changer la chaîne pour que vous vouliez la correspondance gourmande

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

Utilisez revpour inverser la chaîne, inversez vos critères de correspondance, utilisez sedde la manière habituelle, puis inversez le résultat ....

ssAB-+-+-+-+ACABnnACss
bu5hman
la source