Valentin Tanasescu
Valentin Tanasescu

Reputation: 106

Custom realpath() using regex

I want to create my personal realpath() function which uses regex and doesn't expect that file exists.

What I did so far

function my_realpath (string $path): string {
    if ($path[0] != '/') {
        $path = __DIR__.'/../../'.$path;
    }
    
    $path = preg_replace("~/\./~", '', $path);
    $path = preg_replace("~\w+/\.\./~", '', $path); // removes ../ from path

    return $path;
}

What is not correct

The problem is if I have this string:

"folders/folder1/folder5/../../folder2"

it removes only first occurence (folder5/../):

"folders/folder1/../folder2"

Question

How to I remove (with regex) all folders followed by same number of "../" after them?

Examples

"folders/folder1/folder5/../../folder2" -> "folders/folder2"

"folders/folder1/../../../folder2" -> "../folder2"

"folders/folder1/folder5/../folder2" -> "folders/folder1/folder2"

Can we tell regex that: "~(\w+){n}/(../){n}~", n being greedy but same in both groups?

Upvotes: 1

Views: 98

Answers (2)

Jan
Jan

Reputation: 43169

You could as well use a non-regex approach:

<?php
    
$strings = ["folders/folder1/folder5/../../folder2", "folders/folder1/../../../folder2", "folders/folder1/folder5/../folder2"];
    
function make_path($string) {
    $parts = explode("/", $string);
    $new_folder = [];
    for ($i=0; $i<count($parts); $i++) {
        if (($parts[$i] == "..") and count($new_folder) >= 1) {
            array_pop($new_folder);
        } else {
            $new_folder[] = $parts[$i];
        }
    }
    return implode("/", $new_folder);
}
    
$new_folders = array_map('make_path', $strings);
print_r($new_folders);
?>

This yields

Array
(
    [0] => folders/folder2
    [1] => ../folder2
    [2] => folders/folder1/folder2
)

See a demo on ideone.com.

Upvotes: 1

Wiktor Stribiżew
Wiktor Stribiżew

Reputation: 626932

You can use a recursion-based pattern like

preg_replace('~(?<=/|^)(?!\.\.(?![^/]))[^/]+/(?R)?\.\.(?:/|$)~', '', $url)

See the regex demo. Details:

  • (?<=/|^) - immediately to the left, there must be / or start of string (if the strings are served as separate strings, eqqual to a more efficient (?<![^/]))
  • (?!\.\.(?![^/])) - immediately to the right, there should be no .. that are followed with / or end of string
  • [^/]+ - one or more chars other than /
  • / - a / char
  • (?R)? - recurse the whole pattern, optionally
  • \.\.(?:/|$) - .. followed with a / char or end of string.

See the PHP demo:

$strings = ["folders/folder1/folder5/../../folder2", "folders/folder1/../../../folder2", "folders/folder1/folder5/../folder2"];
foreach ($strings as $url) {
    echo preg_replace('~(?<=/|^)(?!\.\.(?![^/]))[^/\n]+/(?R)?\.\.(?:/|$)~', '', $url) . PHP_EOL;
}
// => folders/folder2, ../folder2, folders/folder1/folder2

Alternatively, you can use

(?<![^/])(?!\.\.(?![^/]))[^/]+/\.\.(?:/|$)

See the regex demo. Details:

  • (?<![^/]) - immediately to the left, there must be start of string or a / char
  • (?!\.\.(?![^/])) - immediately to the right, there should be no .. that are followed with / or end of string
  • [^/]+ - one or more chars other than /
  • /\.\. - /.. substring followed with...
  • (?:/|$) - / or end of string.

See the PHP demo:

$strings = ["folders/folder1/folder5/../../folder2", "folders/folder1/../../../folder2", "folders/folder1/folder5/../folder2"];
foreach ($strings as $url) {
    $count = 0;
    do {
        $url = preg_replace('~(?<![^/])(?!\.\.(?![^/]))[^/]+/\.\.(?:/|$)~', '', $url, -1, $count);
    } while ($count > 0);
    echo "$url" . PHP_EOL;
}

The $count argument in preg_replace('~(?<![^/])(?!\.\.(?![^/]))[^/]+/\.\.(?:/|$)~', '', $url, -1, $count) keeps the number of replacements, and the replacing goes on until no match is found.

Output:

folders/folder2
../folder2
folders/folder1/folder2

Upvotes: 1

Related Questions