6

I use preg_replace and I want to include a url inside the replacement string. How do I quote that string? It appears preg_quote is only for the search pattern.

$replace = '\1'.addslashes($url).'\3'.addslashes($title).'\4';
4
  • 5
    What are you attempting to do? The replace string is not passed through the regex engine and therefore does not need to be escaped. Any back-references should be referenced by $1, $2, etc. Commented Dec 2, 2011 at 17:38
  • You could add more of an example to your question so it's clear what you do. Commented Dec 2, 2011 at 18:08
  • 2
    @hakre, obviously OP wants to know how to properly escape replacement strings in preg_replace so that the replacement is exactly what is originally in the string (no matter its content). What more would you want to know? Commented Dec 3, 2011 at 1:35
  • @Qtax: What is $subject? What is $url? What is $title?. Just the basics, it's not clear from the question and would be good to have to give examples according to the question. To properly escape both the matches from $subject as well as the variable values, I added an answer that deals with the deficiencies of any escape function that is applied prior replacement (namely the problem of double-escaping). Commented Dec 3, 2011 at 2:00

5 Answers 5

5
  1. Escapes are needed
  2. addslashes is not sufficient
  3. preg_quote escapes too much

See this demo.

As Mario commented you can use addcslashes($str, "\\$").

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

1 Comment

Can you add code from "this demo" directly to your post, please? The link has rotted and no longer works.
0

Unfortunately there is no generic way to do this, but addslashes should be enough in most cases.

For maximum safety you can use the ${1} syntax. E.g.

$replace = '${1}'.addslashes($url).'${3}'.addslashes($title).'${4}';

If you really want to be totally bulletproof, use a callback replacement function with preg_replace_callback(). The string returned from the callback function is used entirely as-is, so you don't have to worry about mixing replacement syntax with normal text.

Example with preg_replace_callback():

class URLReplacer {
    public $pattern = '/my regex/';
    public $url;
    public $title;
    function __construct($url, $title) {
        $this->url = $url;
        $this->title = $title;
    }
    function _callback($matches) {
        return $matches[1].$url.$matches[3].$title.$matches[4];
    }
    function replace($subject) {
        return preg_replace_callback($this->pattern, array($this, '_callback'), $subject);
    }
}
$repl = new URLReplacer($url, $title);
$replaced = $repl->replace($subject);

4 Comments

preg_replace_callback() seems like a good solution. Any idea how to pass other variables, such as the url+title to the callback function?
Use a method instead of a function as your callback, and put the extra variables you need as properties of the object. Then supply your callback to preg_replace_callback() like array($callbackobj, 'callbackmethodname')
@Tike: You could also use the lazy version with an /e expression as replacement string to pull them from the local scope.
@Tike: Something like preg_replace("/(..)(..)/e", '"$1".$url."$2"', $src) -- Okay, now that I look at it; it's actually awful. But would be a shortcut to the callback approach.
0

You did not provide an example, so I compiled one on my own. The working solution I came up with is using a simple callback function:

$url = 'http://example.com/';
$title = 'Make it Complex \4';

$subject = 'Call \\4 " $me an url';
$pattern = '/(.*)an()( )(url)/';

$replace = function($m) use ($url, $title)
{
    return "$m[1]$url$m[3]$title$m[4]";
};

$result = preg_replace_callback($pattern, $replace, $subject);

Result:

Call \4 " $me http://example.com/ Make it Complex \4url

The callback function is a so called anonymous function Docs which makes it easy to edit the code in place.

In case you need this more often you can put that into a function of your own, probably to make it more re-useable. You can even go to that far and create yourself your own pattern to replace subgroup matches and variables. For examle {\1} stands for the subpattern 1 match, {$2} for the second variable. Wrapping this into a function of it's own:

$patternf = function()
{
    $values = func_get_args();
    $mask = $values ? array_shift($values) : NULL;
    return function($matches) use ($mask, $values)
    {
        $parts = preg_split('/({[\\\\\\$][0-9]{1,3}})/', $mask, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
        foreach($parts as &$part)
            if (preg_match('/^{([\\\\\\$])([0-9]{1,3})}$/', $part, $p))             
                $part = $p[1] == '\\' ? $matches[(int)$p[2]] : $values[$p[2]-1];
        return implode('', $parts);
    };
};

Would allow you to make the replacement more handy:

$replace = $patternf('{\1}{$1}{\3}{$2}{\4}', $url, $title);

$result = preg_replace_callback($pattern, $replace, $subject);

Demo. Wrapping this into a function of it's own:

function preg_replace_subst($pattern, $replace, $subject)
{
    $values = func_get_args();
    $pattern = array_shift($values);
    $mask = array_shift($values);
    $subject = array_shift($values);
    $callback = function($matches) use ($mask, $values)
    {
        $parts = preg_split('/({[\\\\\\$][0-9]{1,3}})/', $mask, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
        foreach($parts as &$part)
            if (preg_match('/^{([\\\\\\$])([0-9]{1,3})}$/', $part, $p))             
                $part = $p[1] == '\\' ? $matches[(int)$p[2]] : $values[$p[2]-1];
        return implode('', $parts);
    };
    return preg_replace_callback($pattern, $callback, $subject);
}

Would give it an easy interface:

$url = 'http://example.com/';
$title = 'Make it Complex \4';

$subject = 'Call \\4 " $me an url';
$pattern = '/(.*)an()( )(url)/';

$replace = '{\1}{$1}{\3}{$2}{\4}';

$result = preg_replace_subst($pattern, $replace, $subject, $url, $title);

But with many substitution variables it should be possible to pass them as an array probably otherwise it gets a bit lengthy.

Using the e modifier with preg_replace (and why it does not work)

When using the e modifier, the matches get replaced in the replace string and it then get's evaluated. As other variables do not get escaped, the matches do interfere with PHP variable substitution, which is dangerous:

$url = 'http://example.com/';
$title = 'Make it Complex \4';

$subject = 'Call me an url.';

$pattern = '/(.*)an()( )(url)/e';
$replace = '"$1{$url}$3{$title}$4"';

$result = preg_replace($pattern, $replace, $subject);

Outputs:

Call me http://example.com/ Make it Complex \4url.

As written, the first e-modifier example is broken because $ won't get escape in $subject, so PHP would have looked for unset variables. That's dangerous, too. I came up with a variant, it solves that issue but it can't handle double-quotes in the subject:

$url = 'http://example.com/';
$title = 'Make it Complex \4';

$subject = 'Call \\4 " $me an url';

$pattern = '/(.*)an()( )(url)/e';
$replace = "'\$1'.\$url.'\$3'.\$title.'$4'";

Output:

Call \4 \" $me http://example.com/ Make it Complex \4url
        ^ problem, not in input.

So not really fool-proof, that's why it needs the callback function because it gets the matching sub-patterns unquoted.

12 Comments

This is a good solution too. I would use this for simple or one-off code, and preg_replace_callback with method callback for more complex replacements or code that needs to be maintained for a while.
Actually I just saw my code is broken if $subject contains $. I'm fiddling a bit, what I wrote about what get's escape by regex does not include $.
Oh, I didn't catch that. How unfortunate :(
That's PHP 5.3, yes. It's documented here: Anonymous Functions (PHP).
Why all this when a simple 20 char solution (addcslashes) works perfectly? What am I missing?
|
0

To make it obvious one is escaping any potential back-references in the $replacement parameter of preg_replace(), use a function:

function preg_quote_replacement($input) {
    return addcslashes($input, '\\$');
}

In OP's case:

$subject = preg_replace(
    $pattern,
    '\1'.preg_quote_replacement($url).'\3'.preg_quote_replacement($title).'\4',
     $subject
);

Comments

0

You should use Prepared Patterns. It works like Prepared Statements in SQL:

Pattern::inject("\1@url\2@url\3", ['url' => $input]);

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.