Bash – Replace string in a file with another where both are saved in variables

bashquotingsedshell-scriptvariable

I want to replace a string found in a file with another string, but both have a special character (in this example it is a . char) e.g 1.0 and 2.0.

So this is the command currently used:

sed -i 's/1\.0/2\.0/g' /home/user1/file1.txt

What if 1.0 and 2.0 were saved in variables $i and $j? where i has the value 1.0 and j has the value 2.0, how can I still replace i with j?

Best Answer

You can just use sed to do the fixup for you:

printf '%s\n' "$i" "$j" |
sed 's/[]\$*&/.^[]/\\&/g;H;$!d
     x;y|\n|/|;s|.*|s&/g|' |
sed -f - /path/to/infile

So this s///ubstitution will escape any/all BRE metacharacters in input:

s/[]\$*&/.^[]/\\&/g

...by prefixing each with a backslash. The first sed then saves a copy of the first line - $i - in Hold space by prepending a \newline character to it. The $i line is then deleted because it is !not the $last. The next line - the $j line - is also the last, and after it gets the same treatment as the first, it is not deleted. Instead, it exchanges the hold and pattern buffers and operates on the concatenated results. At this point pattern space looks like:

\n1\.0\n2\.0

...so we y/// translate all \newlines for / slashes, s///ubstitute .*all of pattern-space for &itself plus a prepended s and an appended /g which gets us:

s/1\.0/2\.0/g

This is then autoprinted to the second sed which is reading stdin - or -f - - as its script. When the first sed is finished and closes the pipe between them, the second sed starts applying...

s/1\.0/2\.0/g

...to every line in its named input file - which is here /path/to/infile.

I wrote your file like this:

printf '%04s%04s%04s%04s\n' \
        0 0 -1 0 1 0 0 0 0 -1\
        0 0 1.5 2.0 1.0 0 >/tmp/temp

Which got me a file like...

   0   0  -1   0
   1   0   0   0
   0  -1   0   0
 1.5 2.0 1.0   0

I then wrote a different version of your script like:

ii=0.0
for i in        1.0 2.0 3.0 4.0
do      str2=$i
        printf '\033[41m## %s \033[0m\n' \
                "str2 = $str2" "$ii $str2"
        printf %s\\n "$ii" "$str2"
        ii=$str2
done | 
sed '   s/[]\$^&*./[]/\\&/g;H;x
        s|^\(\n\)\(.*\)\n\(.*\)\n\(.*\)\n\(.*\)|\
        bs\5\1:i\5\1i\\\1\2\\\1\3\1:s\5\1s/\4/\5/gp;ti\5|p
        s|||;h;d' |
sed -f - /tmp/temp

Which uses the shell only to generate the strings, but allows sed to do all of the data processing. Notice that though two seds are called each is only ever called once.

When I run it the results are:

   0   0  -1   0
   1   0   0   0
   0  -1   0   0
 1.5 2.0 2.0   0
## str2 = 2.0 
## 1.0 2.0 
 1.5 3.0 3.0   0
## str2 = 3.0 
## 2.0 3.0 
 1.5 4.0 4.0   0
## str2 = 4.0 
## 3.0 4.0 
 1.5 4.0 4.0   0

The lines beginning with # are colored red as I expect you mean them to be. sed only writes them when a s///ubstitution is successful. The script that the first sed writes for the second looks like:

                bs1\.0
:i1\.0
i\
[41m## str2 = 1\.0 [0m\
[41m## 0\.0 1\.0 [0m
:s1\.0
s/0\.0/1\.0/gp;ti1\.0

                bs2\.0
:i2\.0
i\
[41m## str2 = 2\.0 [0m\
[41m## 1\.0 2\.0 [0m
:s2\.0
s/1\.0/2\.0/gp;ti2\.0

                bs3\.0
:i3\.0
i\
[41m## str2 = 3\.0 [0m\
[41m## 2\.0 3\.0 [0m
:s3\.0
s/2\.0/3\.0/gp;ti3\.0

                bs4\.0
:i4\.0
i\
[41m## str2 = 4\.0 [0m\
[41m## 3\.0 4\.0 [0m
:s4\.0
s/3\.0/4\.0/gp;ti4\.0

Note though that that though it appears the [ strings are not escaped this is just the effect of my terminal on the output - which winds up eating the char immediately following \033. When the second sed receives the script the input is like \033\[... but the output it inserts to stdout is \033[...

Related Question