web-dev-qa-db-fra.com

Est-il possible d'échapper de manière fiable aux métacaractères regex avec sed

Je me demande s'il est possible d'écrire une commande sed 100% fiable pour échapper à tous les métacaractères 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[a-z]\+\([^ ]\)\{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 concentrons-nous sur les expressions rationnelles POSIX de base pour avoir encore plus de plaisir! :)

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

Btw, la discussion est venue ici . J'ai pensé que cela pourrait être un bon endroit pour collecter des solutions et probablement les casser et/ou les élaborer.

51
hek2mgl

Remarque:

  • Si vous recherchez une fonctionnalité préemballée basée sur les techniques décrites dans cette réponse:
    • bash fonctions qui permettent un échappement robuste même dans multiligne les substitutions peuvent être trouvées au bas de ce post (plus une solution Perl qui utilise le support intégré de Perl pour un tel échappement).
    • @ la réponse d'EdMorton contient un outil (bash script) qui exécute de manière robuste sur une seule ligne substitutions .
  • Tous les extraits de code supposent bash comme Shell (des reformulations conformes à POSIX sont possibles):

Solutions sur une seule ligne


Échapper un littéral de chaîne pour l'utiliser en tant que regex dans sed:

Pour donner du crédit là où le crédit est dû: j'ai trouvé l'expression régulière utilisée ci-dessous dans cette réponse .

En supposant que la chaîne de recherche est une chaîne de ligne single -:

search='abc\n\t[a-z]\+\([^ ]\)\{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 sa propre expression de jeu de caractères [...] Pour le traiter comme un littéral.
    • Notez que ^ Est le seul caractère. vous ne peut pas représenter comme [^], car il a une signification spéciale à cet endroit (négation).
  • Ensuite, ^ Caractères. sont échappés en tant que \^.
    • Notez que vous ne pouvez pas simplement échapper à chaque caractère en plaçant un \ Devant, car cela peut transformer un caractère littéral en métachar, par exemple \< Et \b Sont des limites de mots dans certains outils, \n Est une nouvelle ligne, \{ Est le début d'un intervalle RE comme \{1,3\} , etc.

L'approche est robuste, mais pas efficace.

La robustesse vient de pas essayant d'anticiper tous les caractères spéciaux regex - qui varient selon les dialectes regex - mais à se concentrer sur seulement 2 fonctionnalités partagé par tous les dialectes regex:

  • la possibilité de spécifier des caractères littéraux dans un jeu de caractères.
  • la possibilité d'échapper à un littéral ^ en tant que \^

Échapper un littéral de chaîne pour l'utiliser comme chaîne de remplacement dans la commande s/// De sed:

La chaîne de remplacement dans une commande seds/// 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 à l'expression régulière (&) Ou des résultats de groupe de capture spécifiques par index (\1, \2, ...), donc ceux-ci doivent être échappés, ainsi que le délimiteur de regex (habituel), /.

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

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 MULTI-lignes


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

Remarque : Cela n'a de sens que si plusieurs lignes d'entrée (éventuellement TOUS) ont été lus avant d'essayer de faire correspondre.
Comme des outils tels que sed et awk fonctionnent sur une ligne single à la fois par défaut, des étapes supplémentaires sont nécessaires pour les faire lire plusieurs lignes à la fois.

# Define sample multi-line literal.
search='/abc\n\t[a-z]\+\([^ ]\)\{2,3\}\3
/def\n\t[A-Z]\+\([^ ]\)\{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 sauts de ligne dans les chaînes d'entrée multilignes doivent être traduits en '\n' chaînes, c'est ainsi que les sauts de ligne sont encodés dans une expression régulière.
  • $!a\'$'\n''\\n' Ajoute chaîne'\n' À chaque ligne de sortie mais à la dernière (la dernière nouvelle ligne est ignorée, car elle a été ajoutée par <<<)
  • tr -d '\n Supprime ensuite tous les réels sauts de ligne de la chaîne (sed en ajoute un chaque fois qu'il imprime son espace de motif), remplaçant efficacement tous les sauts de ligne en entrée par '\n' Chaînes.
  • -e ':a' -e '$!{N;ba' -e '}' Est la forme conforme à POSIX d'un idiome sed qui lit tous les lignes d'entrée une boucle, laissant ainsi les commandes suivantes fonctionner sur toutes les lignes d'entrée à la fois .

    • Si vous utilisez [~ # ~] gnu [~ # ~]sed (uniquement), vous pouvez utiliser son option -z Pour simplifier la lecture de tous lignes d'entrée à la fois:
      sed -z "s/$searchEscaped/foo/" <<<"$search"

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

# Define sample multi-line literal.
replace='Laurel & Hardy; PS\2
Masters\1 & Johnson\2'

# Escape it for use as a Sed replacement string.
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 sauts de ligne dans la chaîne d'entrée doivent être conservés en tant que sauts de ligne réels, mais \ - échappé.
  • -e ':a' -e '$!{N;ba' -e '}' Est la forme conforme à POSIX d'un idiome sed qui lit tous lignes d'entrée une boucle.
  • 's/[&/\]/\\&/g Échappe à toutes les instances de &, \ Et /, Comme dans la solution à ligne unique.
  • s/\n/\\&/g' Puis \ - préfixe toutes les nouvelles lignes.
  • IFS= read -d '' -r Est utilisé pour lire la sortie de la commande sed tel quel (pour éviter la suppression automatique des sauts de ligne de fin qu'une substitution de commande ($(...)) effectuerait).
  • ${REPLY%$'\n'} Supprime ensuite un retour à la ligne single, que le <<< A implicitement ajouté à l'entrée.


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

  • quoteRe() guillemets (échappe) pour une utilisation dans un regex
  • quoteSubst() guillemets à utiliser dans la chaîne de substitution d'un appel s///.
  • les deux gèrent multiligne saisissez correctement
    • Notez que parce que sed lit une ligne single à la fois par défaut, l'utilisation de quoteRe() avec des chaînes multilignes n'a de sens que dans sed commandes qui lisent explicitement plusieurs (ou toutes) lignes à la fois.
    • De plus, l'utilisation de substitutions de commandes ($(...)) pour appeler les fonctions ne fonctionnera pas pour les chaînes qui ont trailing newlines; dans ce cas, utilisez quelque chose comme IFS= read -d '' -r escapedValue <(quoteSubst "$value")
# SYNOPSIS
#   quoteRe <text>
quoteRe() { sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$1" | tr -d '\n'; }
# SYNOPSIS
#  quoteSubst <text>
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 string 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 en même temps, afin que la substitution multiligne fonctionne.



Perl solution:

Perl a un support intégré pour échapper des chaînes arbitraires pour une utilisation littérale dans une expression régulière: le quotemeta() function ou son équivalent \Q...\E entre guillemets .
L'approche est la même pour les chaînes monolignes et multilignes; par exemple:

from=$'Cost\(*):\n$3.' # sample input containing metachars.
to='You owe me $1/$& for'$'\n''eating A\1 sauce.' # sample replacement string 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, afin que la substitution multiligne fonctionne.

  • L'option -s Permet de placer les définitions de variable Perl de style -<var>=<val> Après -- Après le script, avant tout opérande de nom de fichier.

66
mklement0

En s'appuyant sur réponse de @ mklement dans ce fil, l'outil suivant remplacera toute chaîne sur une seule ligne (par opposition à regexp) par toute autre chaîne sur une seule 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, envisagez 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é pour utiliser des délimiteurs de mots pour remplacer les mots si nécessaire, par ex. dans la syntaxe GNU sed:

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

tandis que les outils qui fonctionnent réellement sur les chaînes (par exemple awk's index()) ne peuvent pas utiliser de délimiteurs Word.

15
Ed Morton