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.
Remarque:
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).bash
script) qui exécute de manière robuste sur une seule ligne substitutions .bash
comme Shell (des reformulations conformes à POSIX sont possibles):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'
^
Est placé dans sa propre expression de jeu de caractères [...]
Pour le traiter comme un littéral. ^
Est le seul caractère. vous ne peut pas représenter comme [^]
, car il a une signification spéciale à cet endroit (négation).^
Caractères. sont échappés en tant que \^
. \
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:
^
en tant que \^
s///
De 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 à 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
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"
'\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 .
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"
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"
\
- é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 regexquoteSubst()
guillemets à utiliser dans la chaîne de substitution d'un appel s///
.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.$(...)
) 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.
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.