Est-il possible d’échapper à des méta-caractères regex de manière fiable avec sed

Je me demande s’il est possible d’écrire une commande sed 100% fiable pour échapper à tous les méta-caractères de regex dans une chaîne d’entrée afin qu’elle puisse être utilisée dans une commande sed ultérieure. Comme ça:

 #!/bin/bash # Trying to replace one regex by another in an input file with sed search="/abc\n\t[az]\+\([^ ]\)\{2,3\}\3" replace="/xyz\n\t[0-9]\+\([^ ]\)\{2,3\}\3" # Sanitize input search=$(sed 'script to escape' <<< "$search") replace=$(sed 'script to escape' <<< "$replace") # Use it in a sed command sed "s/$search/$replace/" input 

Je sais qu’il existe de meilleurs outils pour travailler avec des chaînes fixes au lieu de modèles, par exemple awk , perl ou python . Je voudrais juste prouver si c’est possible ou non avec sed . Je dirais que nous allons nous concentrer sur les expressions rationnelles de base de POSIX pour avoir encore plus de plaisir! 🙂

J’ai essayé beaucoup de choses mais chaque fois que je pouvais trouver une consortingbution qui a brisé ma tentative. Je pensais que le garder abstrait comme script to escape ne conduirait personne dans la mauvaise direction.

Au fait, la discussion a eu lieu ici . Je pensais que cela pourrait être un bon endroit pour recueillir des solutions et probablement les briser et / ou les élaborer.

Remarque:

  • Si vous recherchez des fonctionnalités préemballées basées sur les techniques présentées dans cette réponse:
    • bash fonctions bash qui permettent une échappée robuste, même dans les substitutions sur plusieurs lignes, se trouvent au bas de ce post (plus une solution perl utilisant le support intégré de perl pour une telle fuite).
    • La réponse de @ EdMorton contient un outil (script bash ) qui exécute de manière robuste des substitutions sur une seule ligne .
  • Tous les snippets supposent que bash est le shell (les reformulations conformes à POSIX sont possibles):

Solutions SINGLE-line


Échapper à un littéral de chaîne pour l’utiliser comme expression régulière dans sed :

Pour donner crédit lorsque le crédit est dû: j’ai trouvé la regex utilisée ci-dessous dans cette réponse .

En supposant que la chaîne de recherche est une chaîne sur une seule ligne:

 search='abc\n\t[az]\+\([^ ]\)\{2,3\}\3' # sample input containing metachars. searchEscaped=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$search") # escape it. sed -n "s/$searchEscaped/foo/p" <<<"$search" # if ok, echoes 'foo' 
  • Chaque caractère sauf ^ est placé dans son propre jeu de caractères pour [...] traiter comme un littéral.
    • Notez que ^ est le seul caractère. vous ne pouvez pas représenter comme [^] , car il a une signification particulière à cet endroit (négation).
  • Ensuite, ^ caractères. sont échappés comme \^ .

L'approche est robuste, mais pas efficace.

La robustesse vient du fait que nous n'essayons pas d'anticiper tous les caractères spéciaux de regex - qui varieront selon les dialectes de regex - mais que nous nous concentrerons sur seulement 2 entités partagées par tous les dialectes de regex :

  • la possibilité de spécifier des caractères littéraux à l'intérieur d'un jeu de caractères.
  • la possibilité d'échapper à un littéral ^ comme \^

Echapper un littéral de chaîne à utiliser comme chaîne de remplacement dans la commande s/// sed :

La chaîne de remplacement dans une commande sed s/// n'est pas une expression régulière, mais elle reconnaît les espaces réservés qui font référence à la chaîne entière correspondant aux regex ( & ) ou aux résultats spécifiques du groupe de capture par index ( \1 , \2 ..), il faut donc les échapper, avec le délimiteur de regex (habituel), / .

En supposant que la chaîne de remplacement est une chaîne simple ligne:

 replace='Laurel & Hardy; PS\2' # sample input containing metachars. replaceEscaped=$(sed 's/[&/\]/\\&/g' <<<"$replace") # escape it sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar" # if ok, outputs $replace as is 


Solutions multilignes


Échapper à un littéral de chaîne MULTI-LINE pour l'utiliser comme expression rationnelle dans sed :

Remarque : Cela n'a de sens que si plusieurs lignes d'entrée (éventuellement TOUTES) ont été lues avant de tenter une correspondance.
Comme les outils tels que sed et awk fonctionnent par défaut sur une seule ligne, des étapes supplémentaires sont nécessaires pour leur permettre de lire plusieurs lignes à la fois.

 # Define sample multi-line literal. search='/abc\n\t[az]\+\([^ ]\)\{2,3\}\3 /def\n\t[AZ]\+\([^ ]\)\{3,4\}\4' # Escape it. searchEscaped=$(sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$search" | tr -d '\n') #' # Use in a Sed command that reads ALL input lines up front. # If ok, echoes 'foo' sed -n -e ':a' -e '$!{N;ba' -e '}' -e "s/$searchEscaped/foo/p" <<<"$search" 
  • Les nouvelles lignes dans les chaînes d'entrée multi-lignes doivent être traduites en '\n' chaînes , ce qui est la manière dont les nouvelles lignes sont encodées dans une regex.
  • $!a\'$'\n''\\n' ajoute une chaîne '\n' à chaque ligne de sortie sauf la dernière (la dernière nouvelle ligne est ignorée, car elle a été ajoutée par <<< )
  • tr -d '\n supprime alors toutes les nouvelles lignes de la chaîne ( sed ajoute une à chaque fois qu’elle imprime l’espace de son modèle), remplaçant efficacement toutes les nouvelles lignes de l’entrée par '\n' chaînes '\n' .
  • -e ':a' -e '$!{N;ba' -e '}' est la forme d'un idiome sed conforme à POSIX qui lit toutes les lignes d'entrée une boucle, laissant ainsi les commandes suivantes opérer sur toutes les lignes d'entrée à une fois que.

    • Si vous utilisez GNU sed (uniquement), vous pouvez utiliser son option -z pour simplifier la lecture de toutes les lignes d'entrée:
      sed -z "s/$searchEscaped/foo/" <<<"$search"

Echapper à un littéral de chaîne MULTI-LINE pour l'utiliser comme chaîne de remplacement dans la commande s/// sed :

 # Define sample multi-line literal. replace='Laurel & Hardy; PS\2 Masters\1 & Johnson\2' # Escape it for use as a Sed replacement ssortingng. IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$replace") replaceEscaped=${REPLY%$'\n'} # If ok, outputs $replace as is. sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar" 
  • Les nouvelles lignes dans la chaîne d'entrée doivent être conservées en tant que nouvelles lignes, mais \ -escaped.
  • -e ':a' -e '$!{N;ba' -e '}' est la forme conforme à POSIX d'un idiom sed qui lit toutes les lignes d'entrée en boucle.
  • 's/[&/\]/\\&/g échappe à toutes les instances & , \ et / , comme dans la solution à une seule ligne.
  • s/\n/\\&/g' puis \ -prefixe toutes les nouvelles lignes.
  • IFS= read -d '' -r est utilisé pour lire la sortie de la commande sed telle quelle (pour éviter la suppression automatique des nouvelles lignes de fin qu'une substitution de commande ( $(...) ) effectuerait).
  • ${REPLY%$'\n'} supprime alors une seule ligne de fin, que le <<< a implicitement ajouté à l'entrée.


fonctions bash basées sur ce qui précède (pour sed ):

  • quoteRe() citations (échappements) à utiliser dans une regex
  • quoteSubst() utilisé dans la chaîne de substitution d'un appel s/// .
  • les deux manipulent correctement les entrées multilignes
    • Notez que, par défaut, sed lit une seule ligne à la fois, l'utilisation de quoteRe() avec des chaînes multi-lignes n'a de sens que dans les commandes sed qui lisent explicitement plusieurs (ou toutes) les lignes à la fois.
    • En outre, l'utilisation de substitutions de commandes ( $(...) ) pour appeler les fonctions ne fonctionnera pas pour les chaînes comportant des nouvelles lignes de fin ; dans ce cas, utilisez quelque chose comme IFS= read -d '' -r escapedValue <(quoteSubst "$value")
 # SYNOPSIS # quoteRe  quoteRe() { sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$1" | tr -d '\n'; } 
 # SYNOPSIS # quoteSubst  quoteSubst() { IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$1") printf %s "${REPLY%$'\n'}" } 

Exemple:

 from=$'Cost\(*):\n$3.' # sample input containing metachars. to='You & I'$'\n''eating A\1 sauce.' # sample replacement ssortingng with metachars. # Should print the unmodified value of $to sed -e ':a' -e '$!{N;ba' -e '}' -e "s/$(quoteRe "$from")/$(quoteSubst "$to")/" <<<"$from" 

Notez l'utilisation de -e ':a' -e '$!{N;ba' -e '}' pour lire toutes les entrées à la fois, de sorte que la substitution multi-lignes fonctionne.



solution perl :

Perl a un support intégré pour échapper à des chaînes arbitraires pour une utilisation littérale dans une regex: la fonction quotemeta() ou son équivalent \Q...\E citant .
L'approche est la même pour les chaînes à une ou plusieurs lignes. par exemple:

 from=$'Cost\(*):\n$3.' # sample input containing metachars. to='You owe me $1/$& for'$'\n''eating A\1 sauce.' # sample replacement ssortingng w/ metachars. # Should print the unmodified value of $to. # Note that the replacement value needs NO escaping. perl -s -0777 -pe 's/\Q$from\E/$to/' -- -from="$from" -to="$to" <<<"$from" 
  • Notez l'utilisation de -0777 pour lire toutes les entrées en même temps, de sorte que la substitution multi-lignes fonctionne.

  • L'option -s permet de placer -= -style définitions de variables Perl après -- après le script, avant tout opérande de nom de fichier.

S’appuyant sur la réponse de @ mklement0 dans ce fil de discussion, l’outil suivant remplacera toute chaîne de ligne simple (par opposition à regexp) par toute autre chaîne simple ligne utilisant sed et bash :

 $ cat sedstr #!/bin/bash old="$1" new="$2" file="${3:--}" escOld=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<< "$old") escNew=$(sed 's/[&/\]/\\&/g' <<< "$new") sed "s/$escOld/$escNew/g" "$file" 

Pour illustrer la nécessité de cet outil, essayez de remplacer a.*/b{2,}\nc par d&e\1f en appelant directement sed :

 $ cat file a.*/b{2,}\nc axx/bb\nc $ sed 's/a.*/b{2,}\nc/d&e\1f/' file sed: -e expression #1, char 16: unknown option to `s' $ sed 's/a.*\/b{2,}\nc/d&e\1f/' file sed: -e expression #1, char 23: invalid reference \1 on `s' command's RHS $ sed 's/a.*\/b{2,}\nc/d&e\\1f/' file a.*/b{2,}\nc axx/bb\nc # .... and so on, peeling the onion ad nauseum until: $ sed 's/a\.\*\/b{2,}\\nc/d\&e\\1f/' file d&e\1f axx/bb\nc 

ou utilisez l'outil ci-dessus:

 $ sedstr 'a.*/b{2,}\nc' 'd&e\1f' file d&e\1f axx/bb\nc 

La raison pour laquelle cela est utile est qu'il peut être facilement augmenté en utilisant des délimiteurs de mots pour remplacer des mots si nécessaire, par exemple dans la syntaxe GNU sed :

 sed "s/\<$escOld\>/$escNew/g" "$file" 

alors que les outils qui fonctionnent réellement sur les chaînes (par exemple, index() ) ne peuvent pas utiliser de délimiteurs de mots.