15

Suppose I have a file that contains multiple occurrences of both StringA and StringB. I want to replace all occurrences of StringA with StringB, and (simultaneously) all occurrences of StringB with StringA.

Right now, I'm doing something like

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

The problem with this approach is that it assumes StringC doesn't occur in the file. While this isn't an issue in practice, this solution still feels dirty -- that is, it feels like an opportunity to learn more unix magic. :)

4 Answers 4

15

If StringB and StringA can't appear on the same input line, then you can tell sed to perform the replacement one way, and only try the other way if there were no occurrences of the first searched string.

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

In the general case, I don't think there is an easy method in sed. By the way, note that the specification is ambiguous if StringA and StringB can overlap. Here's a Perl solution, which replaces the leftmost occurrence of either string, and repeats.

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

If you want to stick with POSIX tools, awk is the way to go. Awk doesn't have a primitive for general parametrized replacements, so you need to roll your own.

<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
}'
2
  • When I run the first command, sed tells me sed: can't read s/StringB/StringA/g: No such file or directory. It seems -e t PATTERN isn't well understood.
    – Gyscos
    Oct 1, 2017 at 18:16
  • 1
    @Gyscos There was a missing -e before the second s command. I've fixed my answer. Oct 1, 2017 at 18:49
8

Right now, I'm doing something like
...............
The problem with this approach is that it assumes StringC doesn't occur in the file.

I think your approach is fine, you should just use something else instead of a string, something that cannot occur in a line (in the pattern space). The best candidate is the \newline.
Normally, no input line in the pattern space will contain that character so, to swap all occurrences of THIS and THAT in a file, you could run:

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

or, if your sed supports \n in the RHS too:

sed 's/THIS/\n/g;s/THAT/THIS/g;s/\n/THAT/g' infile
2
  • 1
    This is beautiful. I cried a little. Another way to do the RHS newlines is shell variables - whether the sed supports certain escapes or not becomes a lot less important if you prep a few macros beforehand. Like set /THIS /THAT "$(printf \\n/)"; sed "s/$2/\\$4g;s/$3$2/g;s/\\n$3/g" - kinda stupid here, admittedly, but it makes a lot more sense when some other times - especially for char classes and similar.
    – mikeserv
    Apr 3, 2015 at 17:37
  • Well how about that, man. There's even an answer there about it. Was it there when I made the comment? I just saw the thing pop up on the recently edited list (maybe) and the top line of the top answer was a little off (if you only care about non-embedded linux, I guess). I prefer Gilles's suggestion there - unless you're doing a long-running sed, the constant fork overhead with e is kinduva nightmare. On a different note - I've playing with paste for a whole day. I made an option parser - like column kind of. It just gens dashes for input strings and strings stuff together.
    – mikeserv
    Apr 4, 2015 at 3:23
4

I think it's perfectly valid to use a "nonce" string to swap two words. If you want a more general solution you can do something like:

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

That yields

say me say you

Note that you need the two additional substitution here to avoid replacing x_x if you happen to have "x_x" strings. But even that still seems simpler than the awk solution for me.

5
  • That seems to be what the Asker said they were already doing. May 9, 2016 at 14:16
  • 1
    Yes, I overlooked that at first (see editing history) but my given solution is different since it works even when the replacement string (here "x_x") occurs in the original string, hence its more general. May 9, 2016 at 23:43
  • Smart, but there's a catch. If StringA or StringB contains _, one needs to adjust the _ itself (choose another character) or the troublesome string (perform s/_/__/g on it beforehand, seems better). Your solution, as it is, cannot be blindly applied to swap arbitrary strings. Sep 10, 2018 at 16:56
  • @KamilMaciorowski I don't understand what you mean? I actually apply a s/_/__/g beforehand. Maybe just show a testcase which fails. Sep 11, 2018 at 17:46
  • @KamilMaciorowski ah I think I understand now. You mean if the replacement strings itself contain a _, so say replacing y_ou with me. Yes it's true one have to be aware of that and put y__ou into the expression. A script which takes the replacement as input parameters also have to take that into account. Sep 11, 2018 at 17:56
0

I know this is from ages ago but in some cases you may use:

echo foobar | sed -e 's/bar/foo/' -e 's/foo/bar/'

This works because it replaces the first occurrence of bar first, and then the first occurrence of foo, leaving the second foo untouched. This assumes knowledge of the order in which they occur though, and only one occurrence.

This is a more agnostic version:

echo foobar | sed 's/foo/tmp/g' | sed -e 's/bar/foo/g' -e 's/tmp/bar/g'
$ echo barbarfoobar | sed 's/foo/tmp/g' | sed -e 's/bar/foo/g' -e 's/tmp/bar/g'
foofoobarfoo

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .