136

I need to substitute some text inside a text file with a replacement. Usually I would do something like

sed -i 's/text/replacement/g' path/to/the/file

The problem is that both text and replacement are complex strings containing dashes, slashes, blackslashes, quotes and so on. If I escape all necessary characters inside text the thing becomes quickly unreadable. On the other hand I do not need the power of regular expressions: I just need to substitute the text literally.

Is there a way to do text substitution without using regular expressions with some bash command?

It would be rather trivial to write a script that does this, but I figure there should exist something already.

Hennes
  • 65,804
  • 7
  • 115
  • 169
Andrea
  • 1,555

20 Answers20

24
export FIND='find this'
export REPLACE='replace with this'
ruby -p -i -e "gsub(ENV['FIND'], ENV['REPLACE'])" path/to/file

This is the only 100% safe solution here, because:

  • It's a static substition, not a regexp, no need to escape anything (thus, superior to using sed)
  • It won't break if your string contains } char (thus, superior to a submitted Perl solution)
  • It won't break with any character, because ENV['FIND'] is used, not $FIND. With $FIND or your text inlined in Ruby code, you could hit a syntax error if your string contained an unescaped '.
Nowaker
  • 1,646
  • 14
  • 13
15

When you don't need the power of regular expressions, don't use it. That is fine.
But, this is not really a regular expression.

sed 's|literal_pattern|replacement_string|g'

So, if / is your problem, use | and you don't need to escape the former.

PS: About the comments, also see this Stackoverflow answer on Escape a string for sed search pattern.


Update: If you are fine using Perl try it with \Q and \E like this,

 perl -pe 's|\Qliteral_pattern\E|replacement_string|g'

@RedGrittyBrick has also suggested a similar trick with stronger Perl syntax in a comment here or here

peterh
  • 2,782
nik
  • 57,042
14

The replace command will do this.

https://linux.die.net/man/1/replace

Change in place:

replace text replacement -- path/to/the/file

To stdout:

replace text replacement < path/to/the/file

Example:

$ replace '.*' '[^a-z ]{1,3}' <<EOF
> r1: /.*/g
> r2: /.*/gi
> EOF
r1: /[^a-z ]{1,3}/g
r2: /[^a-z ]{1,3}/gi

The replace command comes with MySQL or MariaDB.

7

You can do it converting the patterns to their escaped form automatically. Like this:

keyword_raw=$'1\n2\n3'
keyword_regexp="$(printf '%s' "$keyword_raw" | sed -e 's/[]\/$*.^|[]/\\&/g' | sed ':a;N;$!ba;s,\n,\\n,g')"
# keyword_regexp is now '1\/2\/3'

replacement_raw=$'2\n3\n4' replacement_regexp="$(printf '%s' "$replacement_raw" | sed -e 's/[/&]/\&/g' | sed ':a;N;$!ba;s,\n,\n,g')"

replacement_regexp is now '2/3/4'

echo $'a/b/c/1\n2\n3/d/e/f' | sed -e "s/$keyword_regexp/$replacement_regexp/"

the last command will print 'a/b/c/2\n3\n4/d/e/f'

Credits for this solutions goes here: https://stackoverflow.com/questions/407523/escape-a-string-for-a-sed-replace-pattern

Note1: this only works for non-empty keywords. Empty keywords are not accepted by sed (sed -e 's//replacement/').

Note2: unfortunately, I don't know a popular tool that would NOT use regexp-s to solve the problem. You can write such a tool in Rust or C, but it's not there by default.

VasyaNovikov
  • 3,656
6

You could also use perl's \Q mechanism to "quote (disable) pattern metacharacters"

perl -pe 'BEGIN {$text = q{your */text/?goes"here"}} s/\Q$text\E/replacement/g'
glenn jackman
  • 27,524
4

This is an enhancement of Hashbrown’s answer (and wef’s answer to a very similar question).

We can remove the issue of the special meaning of various special characters and strings (^, ., [, *, $, \(, \), \{, \}, \+, \?, &, \1, …, whatever, and the / delimiter) by removing the special characters. Specifically, we can convert everything to hex; then we have only 0-9 and a-f to deal with.  This example demonstrates the principle:

$ echo -n '3.14' | xxd
0000000: 332e 3134                                3.14

$ echo -n 'pi' | xxd 0000000: 7069 pi

$ echo '3.14 is a transcendental number. 3614 is an integer.' | xxd 0000000: 332e 3134 2069 7320 6120 7472 616e 7363 3.14 is a transc 0000010: 656e 6465 6e74 616c 206e 756d 6265 722e endental number. 0000020: 2020 3336 3134 2069 7320 616e 2069 6e74 3614 is an int 0000030: 6567 6572 2e0a eger..

$ echo "3.14 is a transcendental number. 3614 is an integer." | xxd -p | sed 's/332e3134/7069/g' | xxd -p -r pi is a transcendental number. 3614 is an integer.

whereas, of course, sed 's/3.14/pi/g' would also change 3614.

The above is a slight oversimplification; it doesn’t account for boundaries.  Consider this (somewhat contrived) example:

$ echo -n 'E' | xxd
0000000: 45                                       E

$ echo -n 'g' | xxd 0000000: 67 g

$ echo '$Q Eak!' | xxd 0000000: 2451 2045 616b 210a $Q Eak!.

$ echo '$Q Eak!' | xxd -p | sed 's/45/67/g' | xxd -p -r &q gak!

Because $ (24) and Q (51) combine to form 2451, the s/45/67/g command rips it apart from the inside.  It changes 2451 to 2671, which is &q (26 + 71).  We can prevent that by separating the bytes of data in the search text, the replacement text and the file with spaces.  Here’s a stylized solution:

encode() {
        xxd -p    -- "$@" | sed 's/../& /g' | tr -d '\n'
}
decode() {
        xxd -p -r -- "$@"
}
left=$( printf '%s' "$search"      | encode)
right=$(printf '%s' "$replacement" | encode)
encode path/to/the/file | sed "s/$left/$right/g" | decode

I defined an encode function because I used that functionality three times, and then I defined decode for symmetry.  If you don’t want to define a decode function, just change the last line to

encode path/to/the/file | sed "s/$left/$right/g" | xxd -p –r

Note that the encode function triples the size of the data (text) in the file, and then sends it through sed as a single line — without even having a newline at the end.  GNU sed seems to be able to handle this; other versions might not be able to.  Also, this does not modify the file in place; you’ll need to write the output to a temporary file and then copy it over the original file (or one of the other tricks for doing that).

As an added bonus, this solution handles multi-line search and replace (in other words, search and replacement strings that contain newline(s)).

4

check out my Perl script. it do exactly what you need without implicit or explicit use of regular expression :

https://github.com/Samer-Al-iraqi/Linux-str_replace

str_replace Search Replace File # replace in File in place

STDIN | str_replace Search Replace # to STDOUT

very handy right? I had to learn Perl to do it. because I really really need it.

Samer Ata
  • 141
3

You can use php's str_replace:

php -R 'echo str_replace("\|!£$%&/()=?^\"'\''","replace",$argn),PHP_EOL;'<input.txt >output.txt

Note: You would still need to escape single quotes ' and double quotes ", though.

simlev
  • 3,912
3

Working on an alpine docker container, i wasn't keen to install python / pearl / ruby / python just to do the very simple operation of a find and replace. All these solutions are horribly complex!!

There are two viable solutions to this:

  1. Use a different find + replace from elsewhere (e.g. python/pearl/etc)
  2. Escape all the regex metacharacters. We can use sed for this purpose.

I cannot do the first in my case as I am working in a minimal docker container.
This solution can be used for the second

In my case i had a known string in my file: {{replace_me}} and a user input. Lets call it $replace_text.

sed -i "s/{{replace_me}}/$(sed 's/[&/\]/\\&/g' <<<"$replace_text")/g" path/to/file

How does it work?

We use sed -i for an inplace conversion. Here i use the \ as a delimiter as I am specifically escaping this in my replacement line. This protects against the user putting down my\string.

The $(sed 's/[&/\]/\\&/g' <<<"$replace_text") bit is explained in detail here in a great answer where this solution is derived from. In this case I am using it as a one liner

In answer to OPs initial question, here is a sed one liner that should do the trick:

sed -i "s/$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$search_text")/$(sed 's/[&/\]/\\&/g' <<<"$replace_text")/g" path/to/file

but i guess he probably no longer cares given that it has been 7 years!

3

Replace (literal) strings in a bash variable

Given bash variables containing:

  • the original data,
  • text to be replaced, and
  • the replacement text

literal string replacement can be done with:

modified=${original/"$text"/"$replacement"}

The double-quotes are required.

See Shell Parameter Expansion for the nitty-gritty.

The complicated part is getting everything into the variables without any escaping.

Loading text into a bash variable without escaping

To avoid any quoting, it must be possible to choose a delimiter that cannot appear in the string. This is not possible in general but escaping can be minimised.

Single Quotes

If single-quotes (') are the delimiter, then any character except single quote may appear in the string:

var='this is a variable
containing lots of stuff !@#$%^&*()-=_+[]{};\:"|,./<>?
as well as trailing (and other) newlines:

'

Unfortunately single-quotes must still be escaped and included separately. For example:

var='anything except single quote ('\'')'

Here-docs

Another method is to read into the variable from a here-doc. In this case, the delimiter sequence cannot appear (EOD below). Be sure to use quotes around the delimiter, otherwise the input is treated like a double-quoted string and may have expansions performed.

var=$(cat <<'EOD'
this is a variable
containing lots of stuff !@#$%^&*()-=_+[]{};\:"|,./<>? 
including single-quotes: '''
The delimiter (`EOD`) cannot appear alone on a line.

EOD )

However note that with this method any trailing newlines are not stored. To include them mapfile can be used if available (bash 4+) or a sentinel may be appended and stripped:

mapfile -d '' var <<'EOD'
Anything allowed (except `EOD` alone on a line).
Trailing newlines are retained:

EOD

tmpvar=$(cat <<'EOD'
String containing anything (except `EOD` alone on a line).
Trailing newlines can't exist because of the sentinel:


sentinel
EOD
)
var=${tmpvar%sentinel}

Actual files

If content for a variable is stored in an actual file, the delimiter can be the null character (which cannot be stored in a bash variable and so is guaranteed not to appear). See below.

Loading data from file

If the original data is stored in a file rather than in a variable, it must first be read in. On versions of bash that have mapfile, this can be safely done with:

mapfile -d '' var <"$path_to_file"

Otherwise the sentinel trick can be used:

tmpvar=$(cat "$path_to_file"; echo sentinel)
var=${tmpvar%sentinel}

If trailing newlines don't exist (or should be elided), bash provides a slightly faster alternative to cat to read from a file:

var=$(< "$path_to_file")

Note that these methods can also be used to load the text and replacement variables with arbitrary text stored in files.

Writing data out to file

Saving the modified data can be done with printf:

printf '%s' "$modified" >"$path_to_file"

printf is more reliable than echo because different implementations of echo behave differently and the string printed may get mangled. For example, consider modified='-n'; echo "$modified" or if modified should not end with a newline for some reason.

See: https://unix.stackexchange.com/q/65803/333919

Putting it all together

mapfile -d '' text <<'EOD'
...anything (except EOD alone on a line)...
...(or use sentinel trick (without a sentinel!) if text does not
... end with a newline; or a single-quoted string if that is simpler)
EOD
mapfile -d '' replacement <<'EOD'
...anything else (except EOD alone on a line)..
...(again, use sentinel trick if newline handling not required)
EOD

mapfile -d '' original <"$path_to_file" printf '%s' "${original/"$text"/"$replacement"}" >"$path_to_file"

jhnc
  • 192
2

You can do this in sh without any script (though putting this "one-liner" into a script would be better) or non-standard external program (I reeeally liked @Nowaker's answer thanks to it's safety against injection, but this old CentOS box I needed this on didn't have ruby!). as long as perl isn't non-standard for you

Without attempting to escape the string (and account for issues with doing it correctly syntactically, knowing all the special characters, et cetera), we can just blanket encode all the strings so that nothing has the possibility of being special.

cat path/to/the/file | xxd -p | tr -d '\n' \
| perl -pe "s/$(printf '%s' 'text' | xxd -p | tr -d '\n')(?=(?:.{2})*\$)/$(printf '%s' 'replacement' | xxd -p | tr -d '\n')/g" \
| xxd -p -r

This was just to match the asker's example, other users can obviously replace 'text' with "$text" if using a variable, or cat path/to/the/file with printf '%s' "$input" if not using a file.

You can even replace the /g with / to make it replace-once, or otherwise edit the regex outside the $() to "escape" only portions of the matcher (say, add a ^ after s/ to make it match only the start of the file).
If in the above you need ^/$ to match ends-of-lines again you'll need unencode those:

cat path/to/the/file | xxd -p | tr -d '\n' | sed 's/0a/\n/g'\
| perl -pe "s/^$(printf '%s' 'text' | xxd -p | tr -d '\n')(?=(?:.{2})*\$)/$(printf '%s' 'replacement' | xxd -p | tr -d '\n')/g" \
| sed 's/\n/0a/g' | xxd -p -r

Which'll replace all lines in the file begining with 'text' to instead start with 'replacement'


Test:

Within ^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}, replace literal ^/.[a]|$0\\{7} with literally $0\\

printf '%s' '^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}' \
| xxd -p | tr -d '\n' \
| perl -pe "s/$(printf '%s' '^/.[a]|$0\\{7}' | xxd -p | tr -d '\n')(?=(?:.{2})*\$)/$(printf '%s' '$0\\' | xxd -p | tr -d '\n')/g" \
| xxd -p -r

Output:

$0\\!!$0\\!!$0\\
Hashbrown
  • 3,338
  • 4
  • 39
  • 51
2

After reading through the many answers here and not finding a straightforward way to do find + replace with string literals (not regular expressions) with sed / git grep:

I wrote a small CLI tool to do this:

go install -v github.com/paralin/git-find-replace@main
cd ./my/git/repo
git find-replace 'SearchString' 'ReplaceString'

The source code for the tool is available on GitHub.

2

I pieced together a few other answers and came up with this:

function unregex {
   # This is a function because dealing with quotes is a pain.
   # http://stackoverflow.com/a/2705678/120999
   sed -e 's/[]\/()$*.^|[]/\\&/g' <<< "$1"
}
function fsed {
   local find=$(unregex "$1")
   local replace=$(unregex "$2")
   shift 2
   # sed -i is only supported in GNU sed.
   #sed -i "s/$find/$replace/g" "$@"
   perl -p -i -e "s/$find/$replace/g" "$@"
}
1

Node.JS equivalent of @Nowaker:

export FNAME='moo.txt'
export FIND='search'
export REPLACE='rpl'
node -e 'fs=require("fs");fs.readFile(process.env.FNAME,"utf8",(err,data)=>{if(err!=null)throw err;fs.writeFile(process.env.FNAME,data.replace(process.env.FIND,process.env.REPLACE),"utf8",e=>{if(e!=null)throw e;});});'
A T
  • 821
  • 1
  • 12
  • 26
1

Heres one more "almost" working way.

Use vi or vim.

Create a textfile with your substitution in it:

:%sno/my search string \\"-:#2;g('.j');\\">/my replacestring=\\"bac)(o:#46;\\">/
:x

then execute vi or vim from the commandline:

vi -S commandfile.txt path/to/the/file

:%sno is the vi command to do search and replace without magic.

/ is my chosen separator.

:x saves and exits vi.

You need to escape backslashes '\' the forwardslash '/' may be replaced with e.g. a questionmark '?' or something else that is not in your search or replace-string, pipe '|' did not work for me tho.

ref: https://stackoverflow.com/questions/6254820/perform-a-non-regex-search-replace-in-vim https://vim.fandom.com/wiki/Search_without_need_to_escape_slash http://linuxcommand.org/lc3_man_pages/vim1.html

1

Using a Simple Python Script

Most systems have python ready to go these days. So here's a simple script that'll work for ya:

# replace.py
# USAGE: python replace.py bad-word good-word target-file.txt
#
import sys

search_term = sys.argv[1]
replace_term = sys.argv[2]
target_file = sys.argv[3]

with open(target_file, 'r') as file:
        content = file.read()

content = content.replace(sys.argv[1], sys.argv[2])

with open(target_file, 'w') as file:
        file.write(content)

One Caveat: This works great if your good/bad words are already in system/environment variables. Just make sure you use double-quotes to wrap the variables when passing to the script.

For example:

python replace.py "$BAD_WORD" "$GOOD_WORD" target-file.txt

However, these will not work:

# This breaks on $ or " characters
BAD_WORD="your-artibrary-string"

# This breaks on ' characters
BAD_WORD='your-artibrary-string'

# This breaks on spaces plus a variety of characters
BAD_WORD=your-artibrary-string

Handling Arbitrary Literal Characters

1. Write the Chars to Disk

If I need to provide a arbitrary literal value to a script (skipping any escaping), I generally write it to disk using this method:

head -c -1 << 'CRAZY_LONG_EOF_MARKER' | tee /path/to/file > /dev/null
arbitrary-one-line-string
CRAZY_LONG_EOF_MARKER

... where:

  • We're employing the Here Document mechanism to write literal text
  • We're using head and tee to delete the trailing newline that Here Docs create
  • We're preventing evalution of variables inside the Here Doc by quoting the EOL marker string

Here's a quick demo with tricky chars:

head -c -1 << 'CRAZY_LONG_EOF_MARKER' | tee /path/to/file > /dev/null
1"2<3>4&5'6$7 # 8
CRAZY_LONG_EOF_MARKER

2. Use Modified Python Script

Here's an updated script that reads from word files:

# replace.py
# USAGE: python replace.py bad-word.txt good-word.txt target-file.txt
#
import sys

search_term_file = sys.argv[1]
replace_term_file = sys.argv[2]
target_file = sys.argv[3]

print [search_term_file, replace_term_file, target_file]

with open(search_term_file, 'r') as file:
        search_term = file.read()
with open(replace_term_file, 'r') as file:
        replace_term = file.read()
with open(target_file, 'r') as file:
        content = file.read()

print [search_term, replace_term]
content = content.replace(search_term, replace_term)

with open(target_file, 'w') as file:
        file.write(content)
Ryan
  • 121
1

here is an fsed implementation with grep and dd (via)

limitation: the pattern can match only single lines = the pattern cannot contain \n

this version will replace only the first match. to replace more matches, remove -m 1 and loop all matches

fixedReplaceFirst(){ # aka fsed (fixed string editor)
  input="$1"
  pattern="$2"
  replace="$3"
  match="$(echo "$input" | grep -b -m 1 -o -E "$pattern")"
  offset1=$(echo "$match" | cut -d: -f1)
  match="$(echo "$match" | cut -d: -f2-)"
  matchLength=${#match}
  offset2=$(expr $offset1 + $matchLength)
  echo "$input" | dd bs=1 status=none count=$offset1
  echo -n "$replace"
  echo "$input" | dd bs=1 status=none skip=$offset2
}

read -d '' input <<EOF #%replace_me

here, #%replace_me is not replaced EOF

read -d '' replace <<EOF a=1 b=2 EOF

fixedReplaceFirst "$input" "^#%replace_me$" "$replace"

there is rpl (python), but it requires a regular file, so it does not work to replace stdin to stdout

$ echo $'[(\n.*' >temp.txt
$ rpl $'[(\n.*' 'yep'
$ cat temp.txt
yep

there is replace (C, nixpkgs), but it fails on newlines

$ replace-literal '[(.*' 'yep' <<< '[(.*'
yep

$ replace-literal $'[(\n.' 'yep' <<< $'[(\n.' [( .*

milahu
  • 297
1

just in case I will need this in the future again

subject=' http://a\tbc/ef.png?&;wert="3$3*4%5"#eft def '
search='http://a\tbc/ef.png?&;wert="3$3*4%5"#eft'
replace='e&*$%\tf.png'
echo "$subject" | awk -v srch=$(printf "%s" "$search"| sed -e 's/\\/\\\\\\\\/g' -e 's/[?&*.$]/\\\\\0/g' ) -v replc=$(printf "%s" "$replace"| sed -e 's/\\/\\\\/g'  -e 's/[?&]/\\\\\0/g'  ) '{gsub(srch, replc, $0); print}' 2> /dev/null

srch and replc are escaped in as subshell not sure if it actually adds value here, but this should get around mostly all special chars

0

!!! ONE-LINER !!!

All Bash quoted strings (single quotes ('...'), double quotes ("..."), or dollar sign single quotes ($'...')) are not interpreted as regex, but instead taken literally.

zResult=${zInput//"$zMatch"/"replacement"}

@see comment by jhnc.

The above one-liner is much better than the following loop I originally imagined which also takes advantage of bash quoting . This example works, but like every other answer, compared to the one-liner, remains only for a hardship exercise:

zInput='string .* to .* search'
zMatch='.*'
zResult=''
while :; do
    [[ "$zInput" =~ (.*)("$zMatch")(.*) ]] || break
    zResult="replacement${BASH_REMATCH[3]}$zResult"
    zInput="${BASH_REMATCH[1]}"
done
zResult="$zInput$zResult"
echo "$zResult"

Note: Result is built backwards because leftmost .* is greedy.

Paul
  • 124
0

There was ruby solution for replacing text in files purely without any regexps. Here is another one with python directly in bash

PYTHON_CODE=$(cat <<PY_END
##################
import os
f_path = "./"
patch_pairs=[ ('AAA', 'BBB') ] # AAA -> BBB
patch_pairs=patch_pairs+[ ('"CCC"', '"DDD"') ] # "CCC" -> "DDD"
# etc...
patch_exts= [".py"]
for r, d, f in os.walk(f_path):
    for file_name in f:
        for ext in patch_exts:
            if file_name.endswith(ext):
                fpath = os.path.join(r, file_name)
                fpath = os.path.abspath(fpath)
                try:
                    anychange = 0
                    with open(fpath, "r+") as text_file:
                        texts = text_file.read()
                        for pp in patch_pairs:
                            if pp[0] in texts:
                                anychange = anychange+1
                                texts = texts.replace(pp[0], pp[1])
                    if anychange>0:
                        with open(fpath, "w") as text_file:
                            text_file.write(texts)
                            print("// patched",fpath)
                except Exception as e:
                    print("// failed to patch file, skipping", fpath, e)

################## PY_END ) python -c "$PYTHON_CODE"

IPv6
  • 101