5

I'm trying to use sed to replace template strings in files of the form %XXX% with the value of a variable called XXX in my shell script.

e.g. The following works perfectly

sed "s/%user_home%/$user_home/gi"

So if user_home=fred the following,

NameVirtualHost *:80

<VirtualHost *:80>
  ServerName %server_name%

  ErrorLog /var/log/apache2/%user_home%_webapp_error.log
  CustomLog /var/log/apache2/%user_home%_webapp.log common

  DocumentRoot /home/%user_home%/web_app/public
</VirtualHost>

becomes,

NameVirtualHost *:80

<VirtualHost *:80>
  ServerName %server_name%

  ErrorLog /var/log/apache2/fred_webapp_error.log
  CustomLog /var/log/apache2/fred_webapp.log common

  DocumentRoot /home/fred/web_app/public
</VirtualHost>

The problem is that I want to run the sed command without explicitly knowing the template strings and their variables up front. That is, it looks for %XXX% and then replaces that with the contents of $XXX without caring what the actual name of the variable is.

I know its got something to do with back-references but I can't figure out how to use the content of a back-reference as the variable name.

I tried,

sed "s/%\([a-z_]\)%/$(\1)/gi"

but this failed to work because it seems to be a looking for a variable called $\1.

2
  • 1
    Backticks won't work there. Variables are expanded by the shell before sending the argument to sed. Commented Oct 25, 2013 at 5:07
  • 1
    Perl would be better for this. It can use $ENV{$variable} to get an environment variable after setting $variable to the string between %. Commented Oct 25, 2013 at 5:09

4 Answers 4

3

The problem here is that by the time the sed command is actually run (and therefore by the time it retrieves the variable-name), the sed command must have been fully assembled (including substituting the Bash variable's value into the replacement string); so everything happens in the wrong order.

Or, taking a higher-level view, the problem is that sed doesn't know about Bash variables, so you need Bash to provide the details of the variables, but Bash doesn't know about sed replacements, so it doesn't have any way of knowing what variables you need the details of.

The fix, as long as you want to use Bash variables, is to use more Bash: you need to identify the relevant variable-names before you first call sed. The below shows how you can do that.


To get the list of all variable-names in your file, you can write something like this:

grep -o '%[a-z_][a-z_]*%' FILE | grep -o '[a-z_][a-z_]*' | sort -u

(The first grep gets all expressions of the form %...%. The second grep filters out the percent-signs; or you can use sed for that, if you prefer. The sort -u eliminates the duplicates, since you only need the list of distinct variable-names.)

Armed with that, you can assemble a sed command that performs all the necessary replacements:

sed_args=()
while read varname ; do
    sed_args+=(-e "s/%$varname%/${!varname}/g")
done < <(grep -o '%[a-z_][a-z_]*%' FILE | grep -o '[a-z_][a-z_]*' | sort -u)
sed "${sed_args[@]}" FILE

(Note the use of ${!varname} to mean "take the value of $varname as a variable-name, and return the value of that variable." This is what §3.5.3 "Shell Parameter Expansion" of the Bash Reference Manual calls "indirect expansion".)

You can wrap this in a function:

function replace_bash_variables () {
    local file="$1"
    local sed_args=()
    local varname
    while read varname ; do
        sed_args+=(-e "s/%$varname%/${!varname}/g")
    done < <(grep -o '%[a-z_][a-z_]*%' "$file" | grep -o '[a-z_][a-z_]*' | sort -u)
    if [[ "${#sed_args[@]}" = 0 ]] ; then
        # if no variables to replace, just cat the file:
        cat -- "$file"
    else
        sed "${sed_args[@]}" -- "$file"
    fi
}

replace_bash_variables OLD_FILE > NEW_FILE

You can also adjust the above to do line-by-line processing, so that it doesn't need to read the file twice. (That gives you more flexibility, since reading the file twice means you have to pass in the actual file, and can't (say) apply this to the output of a pipeline.)

Sign up to request clarification or add additional context in comments.

2 Comments

This is a great solution and solves the problem but I can't get it to work as is. I can only get it running by redirecting the output of the grep into a temp file and then redirecting that file into the while loop. Running the above code gives me -bash: syntax error near unexpected token `<(grep -o '%[a-z_][a-z_]*%' "$file" | grep -o '[a-z_][a-z_]*' | sort -u)'.
@reagleton: Oops, sorry, stupid mistake. For my process substitution, I used <(...) when I needed to write < <(...). (I make this mistake embarrassingly often. <(...) really looks like it's redirecting standard-input, but it isn't, it just gets replaced with the name of a FIFO or whatnot.)
0

Use this:

sed -E "s/%(\w+)%/\$\1/g"

For example this:

echo "abcdef %variable% blah" | sed -E "s/%(\w+)%/\$\1/g"

prints:

abcdef $variable blah

Comments

0

Using awk you can do this

awk '{gsub(/%user_home%/,"${user_home}")}1' file
NameVirtualHost *:80

<VirtualHost *:80>
  ServerName %server_name%

  ErrorLog /var/log/apache2/${user_home}_webapp_error.log
  CustomLog /var/log/apache2/${user_home}_webapp.log common

  DocumentRoot /home/${user_home}/web_app/public
</VirtualHost>

This replace the %user_home% to the variable ${user_home}

Comments

0

try with 1 sed but still need previously to catch the "set" content to know variables name and value

#!/bin/ksh
# YourFilename contain the file name of your file to treat (here passed as 1st parameter to a script)
YourFileName=$1

(set | sed 's/.*/#V0r:&:r0V#/'; cat ${YourFileName}) | sed -n "
s/$/²/
H

$  {
   x
   s/^\(\n *\)*//
# also reset t flag
   t varxs

:varxs
   s/^#V0r:\([a-zA-Z0-9_]\{1,\}\)=\([^²]*\):r0V#²\(\n.*\)%\1%/#V0r:\1=\2:r0V#²\3\2/
   t varxs
: tmpb

# clean the line when no more occurance in text
#   s/^#V0r:\([a-zA-Z0-9_]\{1,\}\)=\([^²]*\):r0V#²\n//
   s/^[^²]*:r0V#²\n//

# and next
   t varxs


# clean the  marker
   s/²\(\n\)/\1/g
   s/²$//

# display the result
   p
   }
"
  • limitation here due to the use of char "²" not escaped so if ² appear in the file, could be annoying (so change this char as marker or translate it in the file)
  • #V0r: and :r0V# are marker also and could be changed without problem

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.