Existe-t-il un moyen d'empêcher sed d'interpréter la chaîne de remplacement? [fermé]

16

Si vous souhaitez remplacer un mot clé par une chaîne à l'aide de sed, sed s'efforce d'interpréter votre chaîne de remplacement. Si la chaîne de remplacement contient des caractères que sed considère spéciaux, comme un caractère '/', cela échouera, à moins bien sûr que vous vouliez que votre chaîne de remplacement contienne des caractères qui indiquent à sed comment agir.

Ex:

VAR="hi/"

sed "s/KEYWORD/$VAR/g" somefile

Existe-t-il un moyen de dire à sed de ne pas essayer d'interpréter la chaîne de remplacement pour les caractères spéciaux? Tout ce que je veux, c'est pouvoir remplacer un mot-clé dans un fichier par le contenu d'une variable, quel que soit ce contenu.

Tal
la source
Si vous voulez mettre des caractères spéciaux sedet les faire ne pas être spéciaux, il suffit de les échapper. VAR='hi\/'ne donne pas un tel problème.
Wildcard
6
Pourquoi tous les downvotes? Cela me semble une question parfaitement raisonnable
roaima
sed(1)interprète simplement ce qu'il obtient. Dans votre cas, il l'obtient via une interpolation de shell. Je pense que vous ne pouvez pas faire ce que vous voulez, mais consultez le manuel. Je sais qu'en Perl (qui fait un sedremplacement passable , avec des expressions régulières beaucoup plus riches), vous pouvez spécifier qu'une chaîne doit être prise littéralement, encore une fois, consultez le manuel.
vonbrand
connexes stackoverflow.com/questions/407523/...
Ciro Santilli冠状病毒审查六四事件法轮功

Réponses:

4

Il n'y a que 4 caractères spéciaux dans la pièce de rechange: \, &, et le retour à la ligne delimiter ( ref )

$ VAR='abc/def&ghi\foo
next line'

$ repl=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$VAR")

$ echo "$repl"
abc\/def\&ghi\\foo\
next line

$ echo ZYX | sed "s/Y/$repl/g"
Zabc/def&ghi\foo
next lineX
glenn jackman
la source
Cela a le même problème que la solution d'Antti - si la chaîne de remplacement dépasse une certaine longueur, vous obtenez une erreur "Liste d'arguments trop longue". Et si la chaîne de remplacement contient '[', ']', '*', '.' Et d'autres caractères similaires? Sed ne les interpréterait-il vraiment pas?
Tal
Le côté de remplacement de s///n'est pas une expression régulière, c'est vraiment juste une chaîne (sauf pour les échappements antislash et &). Si la chaîne de remplacement est si longue, un shell one-liner n'est pas votre solution.
glenn jackman
Une liste très utile si, par exemple, votre chaîne de remplacement est du texte encodé en base64 (par exemple, en remplaçant un espace réservé par une clé SHA256). Ensuite, c'est juste le délimiteur dont vous devez vous soucier.
Heath Raftery
4

Vous pouvez utiliser Perl au lieu de sed avec -p(supposer la boucle sur l'entrée) et -e(donner le programme sur la ligne de commande). Avec Perl, vous pouvez accéder aux variables d'environnement sans les interpoler dans le shell. Notez que la variable doit être exportée :

export VAR='hi/'
perl -p -e 's/KEYWORD/$ENV{VAR}/g' somefile

Si vous ne souhaitez pas exporter la variable partout, fournissez-la uniquement pour ce processus uniquement:

PATTERN="$VAR" perl -p -e 's/KEYWORD/$ENV{PATTERN}/g' somefile

Notez que la syntaxe d'expression régulière de Perl est par défaut légèrement différente de celle de sed.

Antti Haapala
la source
Cela semblait très prometteur, mais lors du test, j'obtiens une erreur "Argument list too long" car ma chaîne de remplacement est trop longue, ce qui est logique - en utilisant cette méthode, nous utilisons la chaîne de remplacement entière dans le cadre des arguments que nous donnons à perl, il y a donc une limite sur la durée.
Tal
1
Non, il ira dans la PATTERN variable d'environnement , pas dans les arguments. Dans tous les cas, cette erreur serait E2BIG, que vous obtiendriez également si vous l'utilisiez sed.
Antti Haapala
2

La solution la plus simple qui gèrerait toujours la grande majorité des valeurs de variables correctement serait d'utiliser un caractère non imprimable comme délimiteur de sedla commande de substitution.

Dans vivous pouvez échapper à n'importe quel caractère de contrôle en tapant Ctrl-V (plus communément écrit comme ^V). Donc, si vous utilisez un caractère de contrôle (j'utilise souvent ^Acomme délimiteur dans ces cas), alors votre sedcommande ne se cassera que si ce caractère non imprimable est présent dans la variable dans laquelle vous déposez.

Vous devez donc taper "s^V^AKEYWORD^V^A$VAR^V^Ag"et ce que vous obtiendrez (en vi) ressemblerait à:

sed "s^AKEYWORD^A$VAR^Ag" somefile

Cela fonctionnera tant $VARqu'il ne contient pas le caractère non imprimable, ^Ace qui est extrêmement improbable.


Bien sûr, si vous transmettez une entrée utilisateur à la valeur de $VAR, tous les paris sont désactivés et vous feriez mieux de désinfecter soigneusement votre entrée plutôt que de compter sur des caractères de contrôle difficiles à taper pour l'utilisateur moyen.


Cependant, il y a plus à se méfier que la chaîne de délimitation. Par exemple, &lorsqu'il est présent dans une chaîne de remplacement, signifie «tout le texte qui a été mis en correspondance». Par exemple, s/stu../my&/remplacerait "stuff" par "mystuff", "piqué" par "mystung", etc. Donc, si vous pourriez avoir un caractère dans la variable que vous déposez en tant que chaîne de remplacement, mais vous voulez utiliser le littéral valeur de la variable uniquement, vous devez effectuer un nettoyage des données avant de pouvoir utiliser la variable comme chaîne de remplacement dans sed. (Le nettoyage des données peut également être effectué sed.)

Caractère générique
la source
C'est un peu mon point - remplacer une chaîne par une autre chaîne est une opération très simple. Doit-il vraiment être aussi compliqué que de déterminer quels personnages sed n'aimera pas et d'utiliser sed pour assainir sa propre entrée? Cela semble ridiculement et inutilement alambiqué. Je ne suis pas un programmeur professionnel, mais je suis presque sûr de pouvoir coder une petite fonction qui remplace un mot-clé par une chaîne dans pratiquement toutes les langues que j'ai rencontrées, y compris bash - j'espérais juste un simple Linux solution en utilisant les outils existants - je ne peux pas croire qu'il n'y en a pas.
Tal
1
@Tal, si votre chaîne de remplacement fait "des centaines de pages" comme vous le mentionnez dans un autre commentaire ... vous pouvez difficilement appeler cela un cas d'utilisation "simple". La réponse ici est Perl, soit dit en passant - je n'ai tout simplement pas appris Perl. La complexité ici vient du fait que vous voulez autoriser N'IMPORTE QUELLE entrée arbitraire comme chaîne de remplacement dans une expression régulière .
Wildcard
Il existe de nombreuses autres solutions que vous pourriez utiliser, dont beaucoup sont très simples. Par exemple, si votre chaîne de remplacement est en fait basée sur une ligne et n'a pas besoin d'être insérée au milieu d'une ligne, utilisez sedla icommande nsert de. Mais ce sedn'est pas un bon outil pour traiter de grandes quantités de texte de manière complexe. Je posterai une autre réponse montrant comment faire avec awk.
Wildcard
1

Vous pouvez utiliser un ,ou un à la |place et il le prendra comme séparateur et techniquement, vous pourriez utiliser n'importe quoi

depuis la page de manuel

\cregexpc
           Match lines matching the regular expression regexp.  The  c  may
      be any character.

Comme vous pouvez le voir, vous devez commencer par un \ avant votre séparateur au début, puis vous pouvez l'utiliser comme séparateur.

à partir de la documentation http://www.gnu.org/software/sed/manual/sed.html#The-_0022s_0022-Command :

The / characters may be uniformly replaced by any other single character 
within any given s command.

The / character (or whatever other character is used in its stead) can appear in 
the regexp or replacement only if it is preceded by a \ character.

Exemple:

sed -e 'somevar|s|foo|bar|'
echo "Hello all" | sed "s_all_user_"
echo "Hello all" | sed "s,all,user,"

echo "Hello/ World" | sed "s,Hello/,Neo,"

user3566929
la source
Vous parlez d'autoriser l'utilisation d'un seul caractère spécifique dans la chaîne de remplacement - dans ce cas, "/". Je parle de l'empêcher d'essayer d'interpréter complètement la chaîne de remplacement. Peu importe le caractère que vous utilisez ("/", ",", "|", etc.), vous risquez toujours de voir ce caractère apparaître dans la chaîne de remplacement. De plus, le caractère initial n'est pas le seul caractère spécial auquel sed s'intéresse, n'est-ce pas?
Tal
@Tal non, il peut prendre n'importe quoi au lieu de /et il ignorera le /bonheur comme je viens de le souligner .. en fait, vous pouvez même le chercher et le remplacer dans une chaîne >>> j'ai édité avec un exemple >>> ces les choses ne sont pas si sûres et vous trouverez toujours un mec plus intelligent
user3566929
@Tal pourquoi voulez-vous l'empêcher d'interpréter? je veux dire que c'est l'utilisation de seden premier lieu, quel est votre projet?
user3566929
Tout ce dont j'ai besoin est de remplacer un mot-clé par une chaîne. sed semble être de loin le moyen le plus courant de le faire sous Linux. La chaîne peut contenir 100 pages. Je ne veux pas essayer de désinfecter la chaîne afin que sed ne panique pas en la lisant - je veux qu'elle puisse gérer tous les caractères de la chaîne, et par "gérer", je veux dire ne pas essayer de trouver magique sens à l'intérieur.
Tal
1
@Tal, bashn'est PAS destiné à la manipulation de chaînes. Du tout, du tout, du tout. C'est pour la manipulation de fichiers et la coordination des commandes . Il se trouve que certaines fonctionnalités pratiques sont intégrées pour les chaînes, mais vraiment limitées et pas très rapides du tout si c'est la principale chose que vous faites. Voir "Pourquoi l'utilisation d'une boucle shell pour traiter du texte est-elle considérée comme une mauvaise pratique?" Certains outils sont conçus pour le traitement de texte sont, dans l' ordre de la plus basique à la plus puissante: sed, awket Perl.
Wildcard
1

S'il est basé sur une ligne et qu'une seule ligne doit être remplacée, je recommande d'ajouter le fichier lui-même avec la ligne de remplacement à l'aide de printf, de stocker cette première ligne dans sedl'espace de stockage de , et de la déposer si nécessaire. De cette façon, vous n'avez pas du tout à vous soucier des caractères spéciaux. (La seule hypothèse ici est que $VARcontient une seule ligne de texte sans aucun retour à la ligne, ce que vous avez déjà dit dans les commentaires.) À part les retours à la ligne, VAR peut contenir n'importe quoi et cela fonctionnerait malgré tout.

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/KEYWORD/g'

printf '%s\n'affichera le contenu de $VARsous forme de chaîne littérale, quel que soit son contenu, suivi d'une nouvelle ligne. ( echofera d'autres choses dans certains cas, par exemple si le contenu de $VARcommence par un trait d'union — il sera interprété comme un indicateur d'option transmis à echo.)

Les accolades sont utilisées pour ajouter la sortie de printfau contenu de somefilelors de sa transmission sed. Les espaces séparant les accolades par eux-mêmes sont importants ici, tout comme le point-virgule avant l'accolade fermante.

1{h;d;};en tant que sedcommande stockera la première ligne de texte dans sedl' espace d'attente de , puis dsupprimera la ligne (plutôt que de l'imprimer).

/KEYWORD/applique les actions suivantes à toutes les lignes qui contiennent KEYWORD. L'action est get, qui récupère le contenu de l'espace d'attente et le dépose à la place de l' espace de motif - en d'autres termes, la ligne actuelle entière. (Ce n'est pas pour remplacer seulement une partie d'une ligne.) L'espace de retenue n'est pas vidé, soit dit en passant, juste copié dans l'espace de motif, remplaçant tout ce qui s'y trouve.

Si vous souhaitez ancrer votre expression rationnelle afin qu'elle ne corresponde pas à une ligne qui contient simplement KEYWORD mais uniquement une ligne où il n'y a rien d'autre sur la ligne que KEYWORD, ajoutez un début d'ancre de ligne ( ^) et une ancre de fin de ligne ( $) à votre regex:

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/^KEYWORD$/g'
Caractère générique
la source
Semble très bien si votre VAR est d'une ligne. J'ai en fait mentionné dans les commentaires que VAR "peut avoir une longueur de 100 pages" plutôt qu'une ligne. Désolé pour la confusion.
Tal
0

Vous pouvez utiliser une barre oblique inverse pour échapper aux barres obliques dans votre chaîne de remplacement, en utilisant l'expansion du paramètre de substitution de modèle de Bash. C'est un peu compliqué car les barres obliques doivent également être échappées pour Bash.

$ var='a/b/c';var="${var//\//\\/}";echo 'this is a test' | sed "s/i/$var/g"

production

tha/b/cs a/b/cs a test

Vous pouvez mettre l'expansion des paramètres directement dans votre commande sed:

$ var='a/b/c';echo 'this is a test' | sed "s/i/${var//\//\\/}/g"

mais je pense que la première forme est un peu plus lisible. Et bien sûr, si vous allez réutiliser le même modèle de remplacement dans plusieurs commandes sed, il est logique de ne faire la conversion qu'une seule fois.

Une autre option serait d'utiliser un script écrit en awk, perl ou Python, ou un programme C, pour faire vos substitutions au lieu d'utiliser sed.


Voici un exemple simple en Python qui fonctionne si le mot-clé à remplacer est une ligne complète dans le fichier d'entrée (sans compter la nouvelle ligne). Comme vous pouvez le voir, il s'agit essentiellement du même algorithme que votre exemple Bash, mais il lit le fichier d'entrée plus efficacement.

import sys

#Get the keyword and replacement texts from the command line
keyword, replacement = sys.argv[1:]
for line in sys.stdin:
    #Strip any trailing whitespace
    line = line.rstrip()
    if line == keyword:
        line = replacement
    print(line)
PM 2Ring
la source
C'est juste une autre façon de purifier l'entrée, et pas une excellente à cela, car elle ne gère qu'un seul caractère spécifique ('/'). Comme l'a souligné Wildcard, il faut se méfier de plus que de la chaîne de délimitation.
Tal
Appel juste. Par exemple, si le texte de remplacement contient des séquences avec barre oblique inverse, elles seront interprétées, ce qui peut ne pas être souhaitable. Une solution consiste à convertir les caractères problématiques (ou le tout) en \xséquences d'échappement de style. Ou d'utiliser un programme qui peut gérer des entrées arbitraires, comme je l'ai mentionné dans mon dernier paragraphe.
PM 2Ring
@Tal: Je vais ajouter un simple exemple Python à ma réponse.
PM 2Ring
Le script python fonctionne très bien et semble faire exactement ce que fait ma fonction, mais beaucoup plus efficacement. Malheureusement, si le script principal est bash (comme c'est le cas dans mon cas), cela nécessite l'utilisation d'un script python externe secondaire.
Tal
-1

Voici comment je suis allé:

#Replaces a keyword with a long string
#
#This is normally done with sed, but sed
#tries to interpret the string you are
#replacing the keyword with too hard
#
#stdin - contents to look through
#Arg 1 - keyword to replace
#Arg 2 - what to replace keyword with
replace() {
        KEYWORD="$1"
        REPLACEMENT_STRING="$2"

        while IFS= read -r LINE
        do
                if [[ "$LINE" == "$KEYWORD" ]]
                then
                        printf "%s\n" "$REPLACEMENT_STRING"
                else
                        printf "%s\n" "$LINE"
                fi
        done < /dev/stdin
}

cela fonctionne très bien dans mon cas, car mon mot clé est sur une ligne à lui tout seul. Si le mot clé était aligné avec un autre texte, cela ne fonctionnerait pas.

J'aimerais toujours vraiment savoir s'il existe un moyen facile de le faire qui n'implique pas de coder ma propre solution.

Tal
la source
1
Si vous êtes vraiment préoccupé par les caractères spéciaux et la robustesse, vous ne devriez pas utiliser echodu tout. Utilisez printfplutôt. Et faire du traitement de texte dans une boucle shell est une mauvaise idée.
Wildcard
1
Il aurait été utile que vous mentionniez dans la question que le mot clé sera toujours une ligne complète. FWIW, bash readest plutôt lent. Il est destiné au traitement des entrées utilisateur interactives, pas au traitement des fichiers texte. C'est lent car il lit stdin char par char, faisant un appel système pour chaque char.
PM 2Ring
@PM 2Ring Ma question ne mentionne pas que le mot-clé est sur une ligne à part parce que je ne veux pas d'une réponse qui ne fonctionne que dans un nombre aussi limité de cas - je voulais quelque chose qui pourrait facilement fonctionner, peu importe où le mot-clé était. Je n'ai jamais dit non plus que mon code était efficace - s'il l'était, je ne chercherais pas d'alternative ...
Tal
@Wildcard Sauf s'il me manque quelque chose, printf interprète absolument les caractères spéciaux, et bien plus que ne le fait l'écho par défaut. printf "hi\n"fera printf imprimer une nouvelle ligne tout en l' echo "hi\n"imprimant tel quel.
Tal
@Tal, le "f" printfsignifie "format" - le premier argument de printfest un spécificateur de format . Si ce spécificateur est %s\n, ce qui signifie "chaîne suivie d'un saut de ligne", rien dans l'argument suivant ne sera interprété ou traduit printf du tout . (Le shell peut toujours l'interpréter, bien sûr; il est préférable de tout coller entre guillemets simples s'il s'agit d'une chaîne littérale, ou de guillemets doubles si vous voulez une expansion variable.) Voir ma réponse en utilisantprintf pour plus de détails.
Wildcard