Échapper une chaîne pour un motif de remplacement sed

317

Dans mon script bash, j'ai une chaîne externe (reçue de l'utilisateur) que je devrais utiliser dans le modèle sed.

REPLACE="<funny characters here>"
sed "s/KEYWORD/$REPLACE/g"

Comment puis-je échapper à la $REPLACEchaîne afin qu'elle soit acceptée en toute sécurité par sedcomme remplacement littéral?

NOTE: Le KEYWORDest une sous - chaîne muet sans allumettes , etc. Il n'est pas fournie par l' utilisateur.

Alexander Gladysh
la source
13
Essayez-vous d'éviter le problème "Little Bobby Tables" s'ils disent "/ g -e's / PASSWORD =. * / PASSWORD = abc / g '"?
Paul Tomblin
2
Si vous utilisez bash, vous n'avez pas besoin de sed. Il suffit d'utiliseroutputvar="${inputvar//"$txt2replace"/"$txt2replacewith"}".
destenson
@destenson Je pense que vous ne devriez pas mettre les deux variables en dehors des guillemets. Bash peut lire des variables entre guillemets doubles (dans votre exemple, les espaces blancs pourraient gâcher les choses).
Camilo Martin
2
Voir aussi: stackoverflow.com/q/29613304/45375
mklement0
1
@CamiloMartin, voir mon commentaire sur ma propre réponse. Les guillemets à l'intérieur de $ {} ne correspondent pas aux guillemets à l'intérieur. Les deux variables ne sont pas en dehors des guillemets.
destenson

Réponses:

268

Avertissement : cela ne prend pas en compte les sauts de ligne. Pour une réponse plus approfondie, consultez plutôt cette question SO . (Merci, Ed Morton et Niklas Peter)

Notez que tout échapper est une mauvaise idée. Sed a besoin de beaucoup de personnages pour s'échapper pour obtenir leur signification particulière. Par exemple, si vous échappez un chiffre dans la chaîne de remplacement, il se transformera en une référence arrière.

Comme l'a dit Ben Blank, il n'y a que trois caractères à échapper dans la chaîne de remplacement (s'échappe eux-mêmes, barre oblique pour la fin de l'instruction et & pour tout remplacer):

ESCAPED_REPLACE=$(printf '%s\n' "$REPLACE" | sed -e 's/[\/&]/\\&/g')
# Now you can use ESCAPED_REPLACE in the original sed statement
sed "s/KEYWORD/$ESCAPED_REPLACE/g"

Si jamais vous avez besoin de fuir KEYWORD chaîne, voici ce dont vous avez besoin:

sed -e 's/[]\/$*.^[]/\\&/g'

Et peut être utilisé par:

KEYWORD="The Keyword You Need";
ESCAPED_KEYWORD=$(printf '%s\n' "$KEYWORD" | sed -e 's/[]\/$*.^[]/\\&/g');

# Now you can use it inside the original sed statement to replace text
sed "s/$ESCAPED_KEYWORD/$ESCAPED_REPLACE/g"

N'oubliez pas que si vous utilisez un caractère autre que /comme délimiteur, vous devez remplacer la barre oblique dans les expressions ci-dessus par le caractère que vous utilisez. Voir le commentaire de PeterJCLaw pour une explication.

Modifié: en raison de certains cas d'angle précédemment non pris en compte, les commandes ci-dessus ont changé plusieurs fois. Consultez l'historique des modifications pour plus de détails.

Pianosaurus
la source
17
Il convient de noter que vous pouvez éviter d'avoir à échapper aux barres obliques en ne les utilisant pas comme délimiteurs. La plupart (toutes?) Des versions de sed vous permettent d'utiliser n'importe quel caractère, tant qu'il correspond au modèle: $ echo 'foo / bar' | sed s _ / _: _ # foo: bar
PeterJCLaw
2
sed -e / (\ / \ | \\\ | &) / \\ & / g 'ne fonctionnait pas pour moi sur OSX mais cela fonctionne: sed' s / ([\\\ / &]) / \\ & / g 'et il est légèrement plus court.
jcoffland
1
Pour le modèle de recherche KEYWORD, dans GNU sed , voici 2 autres caractères ^, $non mentionnés ci-dessus:s/[]\/$*.^|[]/\\&/g
Peter.O
1
@Jesse: fixe. En fait, c'est l'erreur contre laquelle je mets en garde au tout premier paragraphe. Je suppose que je ne pratique pas ce que je prêche.
Pianosaurus
1
@NeronLeVelu: Je ne suis pas sûr de savoir ce que vous voulez dire, mais "n'a pas de signification particulière dans les canaux ou les variables. Il est analysé par le shell avant d'exécuter le résultat, donc les guillemets doubles à l'intérieur des variables sont sûrs. Par exemple, essayez d'exécuter A='foo"bar' echo $A | sed s/$A/baz/dans bash. Les guillemets doubles sont traités comme les "foo" et "bar" qui l'entourent
Pianosaurus
92

La commande sed vous permet d'utiliser d'autres caractères plutôt /que comme séparateur:

sed 's#"http://www\.fubar\.com"#URL_FUBAR#g'

Les guillemets doubles ne sont pas un problème.

scre_www
la source
5
Vous devez toujours vous échapper, .ce qui autrement aurait une signification particulière. J'ai modifié votre réponse.
ypid
Je viens d'essayer de faire: sed '/CLIENTSCRIPT="foo"/a CLIENTSCRIPT2="hello"' fileavec sed '|CLIENTSCRIPT="foo"|a CLIENTSCRIPT2="hello"' fileet ça ne fait pas pareil.
Dimitri Kopriwa
1
Parce que cela ne s'applique qu'au substitut, cela devrait dire: La scommande (comme en remplacement) de sed vous permet d'utiliser d'autres caractères au lieu de / comme séparateur. En outre, ce serait une réponse à la façon d'utiliser sed sur URL avec des barres obliques. Il ne répond pas à la question OP comment échapper une chaîne entrée par un utilisateur, qui pourrait contenir /, \, mais aussi # si vous décidez de l'utiliser. Et en plus, l'URI peut aussi contenir #
papo
2
ça a changé ma vie! Je vous remercie!
Franciscon Santos
48

Les trois seuls caractères littéraux traités spécialement dans la clause replace sont / (pour fermer la clause), \(pour échapper les caractères, la référence arrière, etc.) et &(pour inclure la correspondance dans le remplacement). Par conséquent, tout ce que vous devez faire est d'échapper à ces trois personnages:

sed "s/KEYWORD/$(echo $REPLACE | sed -e 's/\\/\\\\/g; s/\//\\\//g; s/&/\\\&/g')/g"

Exemple:

$ export REPLACE="'\"|\\/><&!"
$ echo fooKEYWORDbar | sed "s/KEYWORD/$(echo $REPLACE | sed -e 's/\\/\\\\/g; s/\//\\\//g; s/&/\\\&/g')/g"
foo'"|\/><&!bar
Ben Blank
la source
Aussi une nouvelle ligne, je pense. Comment échapper à une nouvelle ligne?
Alexander Gladysh
2
Faites attention au comportement par défaut de l'écho en ce qui concerne les barres obliques inverses. En bash, écho par défaut à aucune interprétation des échappements antislash, ce qui sert le but ici. Dans dash (sh), en revanche, echo interprète les antislashs et n'a aucun moyen, à ma connaissance, de supprimer cela. Par conséquent, dans dash (sh), au lieu de echo $ x, faites printf '% s \ n' $ x.
Youssef Eldakar
En outre, utilisez toujours l'option -r lors d'une lecture pour traiter les barres obliques inverses dans les entrées utilisateur comme des littéraux.
Youssef Eldakar
Pour une compatibilité multiplateforme avec d'autres shells, vous devriez consulter ce document concernant le remplacement des caractères spéciaux sed: grymoire.com/Unix/Sed.html#toc-uh-62
Dejay Clayton
2
@Drux Les trois caractères sont les seuls spéciaux de la clause replace . Beaucoup plus est spécial dans la clause pattern.
lenz
33

Sur la base des expressions régulières de Pianosaurus, j'ai créé une fonction bash qui échappe à la fois au mot-clé et au remplacement.

function sedeasy {
  sed -i "s/$(echo $1 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')/$(echo $2 | sed -e 's/[\/&]/\\&/g')/g" $3
}

Voici comment vous l'utilisez:

sedeasy "include /etc/nginx/conf.d/*" "include /apps/*/conf/nginx.conf" /etc/nginx/nginx.conf
Gurpartap Singh
la source
3
Merci! si quelqu'un d'autre obtient une erreur de syntaxe en essayant de l'utiliser, tout comme moi, n'oubliez pas de l'exécuter en utilisant bash, pas sh
Konstantin Pereiaslov
1
Existe-t-il une fonction juste pour échapper une chaîne pour sed au lieu de s'enrouler autour de sed?
CMCDragonkai
Hé, juste un avertissement général concernant le démarrage de canaux avec un écho comme celui-ci: Certaines (la plupart?) Implémentations d'écho prennent des options (voir man echo), provoquant un comportement inattendu du tube lorsque votre argument $1commence par un tiret. Au lieu de cela, vous pouvez commencer votre pipe avec printf '%s\n' "$1".
Pianosaurus
17

Il est un peu tard pour répondre ... mais il existe un moyen beaucoup plus simple de le faire. Modifiez simplement le délimiteur (c'est-à-dire le caractère qui sépare les champs). Donc, au lieu d' s/foo/bar/écrire s|bar|foo.

Et voici la manière la plus simple de procéder:

sed 's|/\*!50017 DEFINER=`snafu`@`localhost`\*/||g'

La sortie résultante est dépourvue de cette clause DEFINER désagréable.

user2460464
la source
10
Non, &et `` doit toujours être échappé, de même que le délimiteur, celui qui est choisi.
mirabilos
3
Cela a résolu mon problème, car j'avais des caractères "/" dans une chaîne de remplacement. Merci mec!
Evgeny Goldin
travaille pour moi. Ce que je fais est d'essayer de s'échapper $dans la chaîne sur le point d'être modifiée et de conserver la signification de $dans la chaîne de remplacement. dis que je veux changer $XXXla valeur de variable $YYY, sed -i "s|\$XXX|$YYY|g" filefonctionne très bien.
hakunami
11

Il s'avère que vous posez la mauvaise question. J'ai également posé la mauvaise question. La raison pour laquelle c'est faux est le début de la première phrase: "Dans mon script bash ...".

J'avais la même question et j'ai fait la même erreur. Si vous utilisez bash, vous n'avez pas besoin d'utiliser sed pour effectuer des remplacements de chaîne (et c'est beaucoup plus propre d'utiliser la fonction de remplacement intégrée à bash).

Au lieu de quelque chose comme, par exemple:

function escape-all-funny-characters() { UNKNOWN_CODE_THAT_ANSWERS_THE_QUESTION_YOU_ASKED; }
INPUT='some long string with KEYWORD that need replacing KEYWORD.'
A="$(escape-all-funny-characters 'KEYWORD')"
B="$(escape-all-funny-characters '<funny characters here>')"
OUTPUT="$(sed "s/$A/$B/g" <<<"$INPUT")"

vous pouvez utiliser exclusivement les fonctionnalités bash:

INPUT='some long string with KEYWORD that need replacing KEYWORD.'
A='KEYWORD'
B='<funny characters here>'
OUTPUT="${INPUT//"$A"/"$B"}"
destenson
la source
BTW, la coloration syntaxique ici est fausse. Les citations extérieures correspondent et les citations intérieures correspondent. En d'autres termes, il ressemble $Aet $Bn'est pas cité, mais ce n'est pas le cas. Les guillemets à l'intérieur du ${}ne correspondent pas aux guillemets à l'extérieur de celui-ci.
destenson
Vous n'êtes pas obligé de citer le côté droit d'une affectation (sauf si vous voulez faire quelque chose comme ça var='has space') - OUTPUT=${INPUT//"$A"/"$B"}c'est sûr.
Benjamin
Vous n'avez pas réellement à citer le côté droit d'une affectation (à moins que vous ne vouliez qu'elle fonctionne dans le monde réel et pas seulement comme un script de jouet pour montrer votre skilz fou). J'essaie toujours de citer chaque expansion de variable que je ne veux pas que le shell interprète, sauf si j'ai une raison spécifique de ne pas le faire. De cette façon, les choses ont tendance à se briser moins souvent, surtout lorsqu'elles sont fournies avec une entrée nouvelle ou inattendue.
destenson
1
Voir le manuel : "Toutes les valeurs subissent une expansion de tilde, une expansion de paramètres et de variables, une substitution de commande, une expansion arithmétique et une suppression de devis (détaillées ci-dessous)." C'est-à-dire les mêmes que dans les guillemets doubles.
Benjamin
1
Que faire si vous devez utiliser sed sur un fichier?
Efren
1

Utilisez awk - c'est plus propre:

$ awk -v R='//addr:\\file' '{ sub("THIS", R, $0); print $0 }' <<< "http://file:\_THIS_/path/to/a/file\\is\\\a\\ nightmare"
http://file:\_//addr:\file_/path/to/a/file\\is\\\a\\ nightmare
Greggster
la source
2
Le problème, awkc'est qu'il n'a rien de similaire sed -i, ce qui est extrêmement pratique 99% du temps.
Tino
C'est un pas dans la bonne direction, mais awk interprète toujours certains métacaractères dans votre substitution, donc ce n'est toujours pas sûr pour l'entrée utilisateur.
Jeremy Huiskamp
0

Voici un exemple d'un AWK que j'ai utilisé il y a quelque temps. Il s'agit d'un AWK qui imprime de nouveaux AWKS. AWK et SED étant similaires, cela peut être un bon modèle.

ls | awk '{ print "awk " "'"'"'"  " {print $1,$2,$3} " "'"'"'"  " " $1 ".old_ext > " $1 ".new_ext"  }' > for_the_birds

Cela semble excessif, mais d'une manière ou d'une autre cette combinaison de guillemets fonctionne pour garder le 'imprimé en tant que littéraux. Ensuite, si je me souviens bien, les variables sont simplement entourées de guillemets comme celui-ci: "$ 1". Essayez-le, faites-moi savoir comment cela fonctionne avec SED.

Alex
la source
0

J'ai une amélioration par rapport à la fonction sedeasy, qui rompra avec les caractères spéciaux comme tab.

function sedeasy_improved {
    sed -i "s/$(
        echo "$1" | sed -e 's/\([[\/.*]\|\]\)/\\&/g' 
            | sed -e 's:\t:\\t:g'
    )/$(
        echo "$2" | sed -e 's/[\/&]/\\&/g' 
            | sed -e 's:\t:\\t:g'
    )/g" "$3"
}

Alors, qu'est-ce qui est différent? $1et $2entouré de guillemets pour éviter les expansions du shell et préserver les tabulations ou les espaces doubles.

Tuyauterie supplémentaire | sed -e 's:\t:\\t:g'(j'aime :comme jeton) qui transforme un onglet en \t.

Francisco De Zuviria
la source
Mais voir mon commentaire sur la réponse séduisante concernant l'utilisation de l'écho dans les tuyaux.
Pianosaurus
0

Voici les codes d'échappement que j'ai trouvés:

* = \x2a
( = \x28
) = \x29

" = \x22
/ = \x2f
\ = \x5c

' = \x27
? = \x3f
% = \x25
^ = \x5e
Ark25
la source
-1

n'oubliez pas tout le plaisir qui se produit avec la limitation de la coque autour de "et"

donc (en ksh)

Var=">New version of \"content' here <"
printf "%s" "${Var}" | sed "s/[&\/\\\\*\\"']/\\&/g' | read -r EscVar

echo "Here is your \"text\" to change" | sed "s/text/${EscVar}/g"
NeronLeVelu
la source
exactement la direction dont j'avais besoin, pour échapper aux résultats de la recherche, trouvée via google, donc peut être utile pour quelqu'un - terminé par - sed "s / [& \\\ * \\" \ '\ "') (] / \\ & / g '
MolbOrg
-1

Si vous cherchez simplement à remplacer la valeur de variable dans la commande sed, supprimez simplement l'exemple:

sed -i 's/dev-/dev-$ENV/g' test to sed -i s/dev-/dev-$ENV/g test
Shailender Singh
la source
-2

Si le cas se trouve être que vous générez un mot de passe aléatoire à passer pour sedremplacer le modèle, alors vous choisissez de faire attention à quel ensemble de caractères dans la chaîne aléatoire. Si vous choisissez un mot de passe créé en encodant une valeur en base64, il n'y a qu'un seul caractère qui est à la fois possible en base64 et également un caractère spécial dans le sedmodèle de remplacement. Ce caractère est "/" et est facilement supprimé du mot de passe que vous générez:

# password 32 characters log, minus any copies of the "/" character.
pass=`openssl rand -base64 32 | sed -e 's/\///g'`;
Mark Stosberg
la source
-4

Un moyen plus simple de le faire consiste simplement à créer la chaîne avant la main et à l'utiliser comme paramètre pour sed

rpstring="s/KEYWORD/$REPLACE/g"
sed -i $rpstring  test.txt
Javonne Martin
la source
Échoue et extrêmement dangereux, car REMPLACER est fourni par l'utilisateur: REPLACE=/donnesed: -e expression #1, char 12: unknown option to `s'
Tino