Échangez proprement toutes les occurrences de deux chaînes à l'aide de sed

13

Supposons que j'ai un fichier qui contient plusieurs occurrences de StringA et StringB. Je veux remplacer toutes les occurrences de StringA par StringB et (simultanément) toutes les occurrences de StringB par StringA.

En ce moment, je fais quelque chose comme

cat file.txt | sed 's/StringB/StringC/g' | sed 's/StringA/StringB/g' | sed 's/StringC/StringA/g'

Le problème avec cette approche est qu'elle suppose que StringC ne se produit pas dans le fichier. Bien que ce ne soit pas un problème dans la pratique, cette solution semble toujours sale - c'est-à-dire qu'elle ressemble à une opportunité d'apprendre plus de magie unix. :)

Seth
la source

Réponses:

11

Si StringBet StringAne peut pas apparaître sur la même ligne d'entrée, vous pouvez dire à sed d'effectuer le remplacement dans un sens, et d'essayer dans l'autre sens uniquement s'il n'y a pas eu d'occurrences de la première chaîne recherchée.

<file.txt sed -e 's/StringA/StringB/g' -e t -e 's/StringB/StringA/g'

Dans le cas général, je ne pense pas qu'il existe une méthode simple dans sed. Soit dit en passant, la spécification est ambiguë si StringAet StringBpeut se chevaucher. Voici une solution Perl, qui remplace l'occurrence la plus à gauche de l'une ou l'autre chaîne et se répète.

<file.txt perl -pe 'BEGIN {%r = ("StringA" => "StringB", "StringB" => "StringA")}
                    s/(StringA|StringB)/$r{$1}/ge'

Si vous voulez vous en tenir aux outils POSIX, awk est le chemin à parcourir. Awk n'a pas de primitive pour les remplacements paramétrés généraux, vous devez donc lancer le vôtre.

<file.txt awk '{
    while (match($0, /StringA|StringB/)) {
        printf "%s", substr($0, 1, RSTART-1);
        $0 = substr($0, RSTART);
        printf "%s", /^StringA/ ? "StringB" : "StringA";
        $0 = substr($0, 1+RLENGTH)
    }
    print
}'
Gilles 'SO- arrête d'être méchant'
la source
Lorsque j'exécute la première commande, sed me le dit sed: can't read s/StringB/StringA/g: No such file or directory. Cela ne semble -e t PATTERNpas bien compris.
Gyscos
1
@Gyscos Il manquait -eavant la deuxième scommande. J'ai corrigé ma réponse.
Gilles 'SO- arrête d'être méchant'
8

En ce moment, je fais quelque chose comme
...............
Le problème avec cette approche est qu'elle suppose que StringC ne se produit pas dans le fichier.

Je pense que votre approche est bonne, vous devez simplement utiliser quelque chose d'autre au lieu d'une chaîne, quelque chose qui ne peut pas se produire dans une ligne (dans l'espace de motif). Le meilleur candidat est la \newline.
Normalement, aucune ligne d'entrée dans l'espace de motif ne contiendra ce caractère donc, pour permuter toutes les occurrences de THISet THATdans un fichier, vous pouvez exécuter:

sed 's/THIS/\
/g
s/THAT/THIS/g
s/\n/THAT/g' infile

ou, si votre sed prend également \nen charge la RHS:

sed 's/THIS/\n/g;s/THAT/THIS/g;s/\n/THAT/g' infile
don_crissti
la source
1
C'est beau. J'ai pleuré un peu. Une autre façon de faire les nouvelles lignes RHS est les variables shell - que le sedsupporte ou non certaines échappées devient beaucoup moins important si vous préparez quelques macros au préalable. Comme set /THIS /THAT "$(printf \\n/)"; sed "s/$2/\\$4g;s/$3$2/g;s/\\n$3/g"- un peu stupide ici, certes, mais cela a beaucoup plus de sens à d'autres moments - en particulier pour les classes de chars et similaires.
mikeserv
Et alors, mec. Il y a même une réponse à ce sujet. Était-ce là quand j'ai fait le commentaire? Je viens de voir la chose apparaître sur la liste récemment éditée (peut-être) et la première ligne de la première réponse était un peu décalée (si vous ne vous souciez que de Linux non intégré, je suppose) . Je préfère la suggestion de Gilles là-bas - à moins que vous ne fassiez une longue course sed, la surcharge constante de la fourche avec eest un cauchemar pour kinduva. Sur une note différente - je joue avec pastedepuis une journée entière. J'ai fait une columnsorte de parseur d'option . Il suffit de tirets pour les chaînes d'entrée et les chaînes ensemble.
mikeserv
3

Je pense qu'il est parfaitement valable d'utiliser une chaîne "nonce" pour échanger deux mots. Si vous voulez une solution plus générale, vous pouvez faire quelque chose comme:

sed 's/_/__/g; s/you/x_x/g; s/me/you/g; s/x_x/me/g; s/__/_/g' <<<"say you say me"

Cela donne

say me say you

Notez que vous avez besoin des deux substitutions supplémentaires ici pour éviter de les remplacer x_xsi vous avez des chaînes "x_x". Mais même cela semble encore plus simple que la awksolution pour moi.

David Ongaro
la source
Cela semble être ce que l'Asker a dit qu'ils faisaient déjà.
roaima
1
Oui, j'ai ignoré cela au début (voir l'historique des modifications) mais ma solution donnée est différente car elle fonctionne même lorsque la chaîne de remplacement (ici "x_x") se trouve dans la chaîne d'origine, d'où sa plus générale.
David Ongaro
Intelligent, mais il y a un hic. Si StringA ou StringB contient _, il faut ajuster _lui - même (choisir un autre caractère) ou la chaîne gênante (jouer s/_/__/gdessus à l'avance, semble mieux). Votre solution, telle qu'elle est, ne peut pas être appliquée aveuglément pour permuter des chaînes arbitraires.
Kamil Maciorowski,
@KamilMaciorowski Je ne comprends pas ce que tu veux dire? J'applique en fait un s/_/__/gpréalable. Peut-être juste montrer un test qui échoue.
David Ongaro
@KamilMaciorowski ah je pense que je comprends maintenant. Vous voulez dire que si les chaînes de remplacement elles-mêmes contiennent un _, alors dites remplacer y_oupar me. Oui, c'est vrai, il faut être conscient de cela et mettre y__oudans l'expression. Un script qui prend le remplacement comme paramètres d'entrée doit également en tenir compte.
David Ongaro