5443
Comment:
|
5513
|
Deletions are marked like this. | Additions are marked like this. |
Line 2: | Line 2: |
== How can i perform a substitution (s/foo/bar/) safely, without treating either value as a regular expression? == Sed is not the right tool for this. At best, it will be an escaping nightmare, and extremely prone to bugs. |
== How can i perform a substitution with arbitrary values ("s/$foo/$bar/") safely, without treating either value as a regular expression or worrying about other special characters? == Sed is not the right tool for this. Nor is ed. At best, attempting either will result in an escaping nightmare, and will be extremely prone to bugs. |
Line 9: | Line 9: |
echo "${var//some/another}" | search=some rep=another printf '%s\n' "${var//"$search"/$rep}" |
Line 17: | Line 19: |
search=foo rep=bar |
|
Line 19: | Line 24: |
printf '%s\n' "${line//foo/bar}" done < file |
printf '%s\n' "${line//"$search"/$rep}" done < "$file" |
Line 24: | Line 29: |
printf '%s\n' "${line//foo/bar}" | printf '%s\n' "${line//"$search"/$rep}" |
Line 28: | Line 33: |
printf '%s\n' "${line//foo/bar}" | printf '%s\n' "${line//"$search"/$rep}" |
Line 31: | Line 36: |
Note that the last example creates a subshell. See [[BashFAQ/024|Faq #24]] for more information on that. | |
Line 32: | Line 38: |
The second of the two command examples there creates a subshell. See [[BashFAQ/024|Faq #24]] for more information on that. | Both of the above examples print to stdout; neither actually edits the file in place. Of course this could be resolved with something like: {{{ # create a temp file, die on failure tmp=$(mktemp) || exit |
Line 34: | Line 43: |
Both of the above examples print to stdout. Neither actually edits the file in place. Of course this could be resolved with something like: {{{ |
|
Line 37: | Line 44: |
printf '%s\n' "${line//foo/bar}" done < file > new_file && mv new_file file |
printf '%s\n' "${line//"$search"/$rep}" done < "$file" > "$tmp" && mv "$tmp" "$file" |
Line 43: | Line 50: |
The first two here are similar to sed 's/STR/REP/', they only replaces the first instance on each line. The first function operates on stdin and writes to stdout, the second overwrites FILE. |
|
Line 45: | Line 56: |
# Replaces the first instance (on each line) of STR with REP, treating them as # literal strings and not regexes. Reads stdin and writes to stdout. # Similar to sed 's/STR/REP/' |
|
Line 69: | Line 76: |
# Replaces the first instance (on each line) of STR with REP in FILE, treating # them as literal strings and not regexes. # Similar to sed -i 's/STR/REP/' FILE |
|
Line 80: | Line 83: |
trap 'rm -rf "$tmp"' RETURN tmp=$(mktemp) && cp "$3" "$tmp" || return |
tmp=$(mktemp) || return trap 'rm -f "$tmp"' RETURN |
Line 98: | Line 101: |
' "$tmp" > "$3" | ' "$3" > "$tmp" && mv "$tmp" "$3" |
Line 100: | Line 103: |
}}} | |
Line 101: | Line 105: |
The next two functions are similar to 's/STR/REP/g', replacing every instance. Just like above, the first reads stdin and writes to stdout, the second actually edits FILE. | |
Line 102: | Line 107: |
{{{ | |
Line 103: | Line 109: |
# Replaces all instances of STR with REP, treating them as literal strings # and not regexes. Reads stdin and writes to stdout # Similar to sed 's/STR/REP/g' |
|
Line 138: | Line 140: |
# Replaces all instances of STR with REP in FILE, treating them as literal # strings and not regexes. # Similar to sed -i 's/STR/REP/g' FILE |
|
Line 149: | Line 147: |
trap 'rm -rf "$tmp"' RETURN tmp=$(mktemp) && cp "$3" "$tmp" || return |
tmp=$(mktemp) || return trap 'rm -f "$tmp"' RETURN |
Line 178: | Line 176: |
' "$tmp" > "$3" | ' "$3" > "$tmp" && mv "$tmp" "$3" |
Line 181: | Line 179: |
For more information on how these work, and awk in general, i recommend the #awk channel on freenode. | |
Line 182: | Line 181: |
The mktemp(1) command used in the *_f functions above is not completely portable. While it will work on most systems, more information on safely creating temp files can be found in [[BashFAQ/062|Faq #62.]] | The mktemp(1) command used in some of the examples above is not completely portable. While it will work on most systems, more information on safely creating temp files can be found in [[BashFAQ/062|Faq #62]]. |
How can i perform a substitution with arbitrary values ("s/$foo/$bar/") safely, without treating either value as a regular expression or worrying about other special characters?
Sed is not the right tool for this. Nor is ed. At best, attempting either will result in an escaping nightmare, and will be extremely prone to bugs.
First, what are we performing the substitution on? If it's a string, it can be done very simply with a parameter expansion.
var='some string' search=some rep=another printf '%s\n' "${var//"$search"/$rep}"
This is discussed in more detail in Faq #100.
If it's a file or stream, things get a bit trickier. One way to accomplish this would be to combine the previous method with Faq #1.
search=foo rep=bar # file while IFS= read -r line; do printf '%s\n' "${line//"$search"/$rep}" done < "$file" # command output while IFS= read -r line; do printf '%s\n' "${line//"$search"/$rep}" done < <(my_command) my_command | while IFS= read -r line; do printf '%s\n' "${line//"$search"/$rep}" done
Note that the last example creates a subshell. See Faq #24 for more information on that.
Both of the above examples print to stdout; neither actually edits the file in place. Of course this could be resolved with something like:
# create a temp file, die on failure tmp=$(mktemp) || exit while IFS= read -r line; do printf '%s\n' "${line//"$search"/$rep}" done < "$file" > "$tmp" && mv "$tmp" "$file"
On large data sets, you'll notice that this is quite slow. The following functions use awk, and are quite a bit faster:
The first two here are similar to sed 's/STR/REP/', they only replaces the first instance on each line. The first function operates on stdin and writes to stdout, the second overwrites FILE.
# usage: sub_literal STR REP sub_literal() { # string manip needed to escape '\'s, so awk doesn't expand '\n' and such awk -v str="${1//\\/\\\\}" -v rep="${2//\\/\\\\}" ' # get the length of the search string BEGIN { len = length(str); } # if the search string is in the line (i = index($0, str)) { # replace the first occurance with rep $0 = substr($0, 1, i-1) rep substr($0, i + len); } # print each line 1 ' } # usage: sub_literal_f STR REP FILE sub_literal_f() { local tmp if ! [[ -f $3 && -r $3 && -w $3 ]]; then printf '%s does not exist or is not readable or writable\n' "$3" >&2 return 1 fi tmp=$(mktemp) || return trap 'rm -f "$tmp"' RETURN # string manip needed to escape '\'s, so awk doesn't expand '\n' and such awk -v str="${1//\\/\\\\}" -v rep="${2//\\/\\\\}" ' # get the length of the search string BEGIN { len = length(str); } # if the search string is in the line (i = index($0, str)) { # replace the first occurance with rep $0 = substr($0, 1, i-1) rep substr($0, i + len); } # print each line 1 ' "$3" > "$tmp" && mv "$tmp" "$3" }
The next two functions are similar to 's/STR/REP/g', replacing every instance. Just like above, the first reads stdin and writes to stdout, the second actually edits FILE.
# usage: gsub_literal STR REP gsub_literal() { # string manip needed to escape '\'s, so awk doesn't expand '\n' and such awk -v str="${1//\\/\\\\}" -v rep="${2//\\/\\\\}" ' # get the length of the search string BEGIN { len = length(str); } { # empty the output string out = ""; # continue looping while the search string is in the line while (i = index($0, str)) { # append everything up to the search string, and the replacement string out = out substr($0, 1, i-1) rep; # remove everything up to and including the first instance of the # search string from the line $0 = substr($0, i + len); } # append whatever is left out = out $0; print out; } ' } # usage: gsub_literal_f STR REP FILE gsub_literal_f() { local tmp if ! [[ -f $3 && -r $3 && -w $3 ]]; then printf '%s does not exist or is not readable or writable\n' "$3" >&2 return 1 fi tmp=$(mktemp) || return trap 'rm -f "$tmp"' RETURN # string manip needed to escape '\'s, so awk doesn't expand '\n' and such awk -v str="${1//\\/\\\\}" -v rep="${2//\\/\\\\}" ' # get the length of the search string BEGIN { len = length(str); } { # empty the output string out = ""; # continue looping while the search string is in the line while (i = index($0, str)) { # append everything up to the search string, and the replacement string out = out substr($0, 1, i-1) rep; # remove everything up to and including the first instance of the # search string from the line $0 = substr($0, i + len); } # append whatever is left out = out $0; print out; } ' "$3" > "$tmp" && mv "$tmp" "$3" }
For more information on how these work, and awk in general, i recommend the #awk channel on freenode.
The mktemp(1) command used in some of the examples above is not completely portable. While it will work on most systems, more information on safely creating temp files can be found in Faq #62.