web-dev-qa-db-fra.com

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

Je veux utiliser sed pour remplacer quoi que ce soit dans une chaîne entre le premier AB et la première occurrence de AC (inclus) avec XXX.

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

ssABteAstACABnnACss

et je voudrais une sortie 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
25
بارپابابا

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

De toute évidence, nous voulons faire correspondre

  1. AB,
    suivi par
  2. toute quantité autre que AC,
    suivi par
  3. AC

Malheureusement, sed ne 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 telle que @ (ou même [123]), nous pouvons faire [^@]* ou [^123]*. Nous pouvons donc contourner les limites de sed en modifiant toutes les occurrences de AC en @ puis en recherchant

  1. AB,
    suivi par
  2. un nombre quelconque de tout autre chose que @,
    suivi par
  3. @

comme ça:

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

La dernière partie modifie les instances inégalées de @ retour à AC.

Mais, bien sûr, c'est une approche imprudente, car l'entrée peut déjà contenir @ caractères, donc, en les faisant correspondre, nous pourrions obtenir des faux positifs. Cependant, étant donné qu'aucune variable Shell n'aura jamais de 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 s'assurer que GNU sont activées, l'utilisateur ne doit pas avoir défini la variable Shell POSIXLY_CORRECT.)

Si vous utilisez sed avec GNU's -z flag 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 commande printf. Si votre chaîne d'entrée peut contenir n'importe quel caractère, y compris NUL, alors voyez réponse de Stéphane Chazelas qui ajoute une méthode d'échappement intelligente.

17
John1024

Certaines implémentations sed prennent en charge cela. ssed a un mode PCRE:

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

AT&T ast sed a conjonction et négation lors de l'utilisation regexps augmentée :

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 faire s/AB[^:]*://, et si ce caractère peut 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. Comme sed traite une ligne à la fois, la nouvelle ligne ne se produit jamais dans l'espace modèle , 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 implémentations sed car elles ne prennent pas en charge [^\n]. Avec GNU sed, vous devez vous assurer que la compatibilité POSIX n'est pas activée (comme avec la variable d'environnement POSIXLY_CORRECT).

7
Stéphane Chazelas

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 AC en utilisant "tout ce qui ne contient pas AC" suivi de AC, qui fait la même chose que Perl's .*?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, telles que 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 \n au lieu:

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 AC par un saut de 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.

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, après tout, être 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
5
gresolio

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

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

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

ssAB-+-+-+-+ACABnnACss
0
bu5hman

La solution est assez simple. .* est gourmand, mais pas absolument gourmand. Pensez à faire correspondre ssABteAstACABnnACss avec l'expression rationnelle AB.*AC. Le AC qui suit .* doit avoir une correspondance. Le problème est que parce que .* est gourmand, le AC suivant correspondra au lastAC plutôt qu'au premier. .* mange le premier AC tandis que le littéral AC dans l'expression rationnelle correspond au dernier dans ssABteAstACABnn AC ss. Pour éviter que cela ne se produise, remplacez simplement le premier AC par quelque chose 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 s'arrêter au pied de -foobar- dans ssABteAst-foobar-ABnnACss car il n'y a pas d'autre -foobar- que cela -foobar-, et l'expression rationnelle -foobar- DOIT avoir une correspondance. Le problème précédent était que l'expression rationnelle AC avait deux correspondances, mais parce que .* était gourmand, la dernière correspondance pour AC a été sélectionnée. Cependant, avec -foobar-, une seule correspondance est possible, et cette correspondance prouve que .* n'est pas absolument gourmand. L'arrêt de bus pour .* se produit où il ne reste que n correspondance pour le reste de l'expression rationnelle suivant .*.

Notez que cette solution échouera si un AC apparaît avant le premier AB car le mauvais AC sera remplacé par -foobar-. Par exemple, après la première substitution de sed, ACssABteAstACABnnACss devient -foobar-ssABteAstACABnnACss; par conséquent, aucune correspondance ne peut être trouvée avec AB.*-foobar-. Cependant, si la séquence est toujours ... AB ... AC ... AB ... AC ..., alors cette solution réussira.

0
JD Graham

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

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
0
midori