I agree that this would be a lot more efficient in awk or perl or python, etc...
But to answer the question asked, yes, you can make this a lot more efficient with the tools you have. As mentioned, get rid of the time wasters. Your original code spawns unnecessary processes on practically every line.
Just have the code make one pass through the file to write all the individual sed substitution commands out to another script file (or accumulate them into a string as Jetchisel suggests) and then run that.
$ cat props
$foo=bar
$hello=world
^hello=goodbye
$ cat editme
This is a story about $hello. It starts at a $foo and ends in a park.
Bob said to Sally "^hello, see you soon"
$ cat editme.new
cat: editme.new: No such file or directory
$ cat script
#!/bin/bash
date +'Inital timestamp: %D %T %N' >&2
{ printf '%s\n' '#!/bin/bash' "time sed '"
date +'Starting read of props file: %D %T %N' >&2
while IFS='=' read -r k v;
do printf ' s`%q\\b`%q`g;\n' "$k" "$v"
done < props
date +'Closing sed command: %D %T %N' >&2
printf '%s\n' "' editme > editme.new"
} > editor
date +'Done writing sed script file: %D %T %N' >&2
cat editor
. editor
The two time outputs at the bottom are for the one run of sed and the whole script, respectively.
$ time ./script
Inital timestamp: 04/01/25 20:21:21 799559000
Starting read of props file: 04/01/25 20:21:21 812337100
Closing sed command: 04/01/25 20:21:21 824636300
Done writing sed script file: 04/01/25 20:21:21 837483000
#!/bin/bash
time sed '
s`\$foo\b`bar`g;
s`\$hello\b`world`g;
s`\^hello\b`goodbye`g;
' editme > editme.new
real 0m0.014s
user 0m0.000s
sys 0m0.016s
real 0m0.104s
user 0m0.075s
sys 0m0.046s
and afterwards -
$ cat editme.new
This is a story about world. It starts at a bar and ends in a park.
Bob said to Sally "goodbye, see you soon"
addendum
Most bash scripts benefit a lot from moving subshells to built-ins.
A simplified version of my sed-based script above:
$ cat script
#!/bin/bash
{ printf '%s\n' '#!/bin/bash' "sed '"
while IFS='=' read -r k v; do printf ' s`%q\\b`%q`g;\n' "$k" "$v"; done < props
printf '%s\n' "' editme > editme.new"
} > editor
. editor
$ time ./script
real 0m0.043s
user 0m0.000s
sys 0m0.031s
Using simple bash string processing for the whole thing -
$ cat v2
#!/bin/bash
text="$(<editme)"
while IFS='=' read -r k v;
do while [[ "$text" =~ "$k" ]]; do text="${text//$k/$v}"; done
done < props
echo "$text" > editme.2
$ time ./v2
real 0m0.011s
user 0m0.015s
sys 0m0.000s
$ diff editme.new editme.2
This performs horribly on a large file, though, for a lot of reasons.
I made a file of nearly 400MB and the sed script handled it in about 12.5s.
I broke the all-in-memory all-bash version just under 3m.
bashthen consider eliminating the 6 subshells that are invoked on each pass through the loop; for starters use thereadto split the data on the=delimiter (eg,while IFS='=' read -r name value, you'll get much better performance; for even better performance, regardless of system load, consider using a different tool (eg,awk,perl,python, etc), especially since these tools can perform the updates with a single pass through the source (as opposed to the repeated passes with the current code)sedto repeatedly scan and update a (lengthy) variable is going to be silly slow; for large volumes of replacements you really need to consider a different tool that only scans/updates the source once (eg,awk,perl,python), etc.$foo=$helloinstead of$foo=bar?