How to join filesystem path strings in PHP?
PhpStringFilePhp Problem Overview
Is there a builtin function in PHP to intelligently join path strings? The function, given abc/de/
and /fg/x.php
as arguments, should return abc/de/fg/x.php
; the same result should be given using abc/de
and fg/x.php
as arguments for that function.
If not, is there an available class? It could also be valuable for splitting paths or removing parts of them. If you have written something, may you share your code here?
It is ok to always use /
, I am coding for Linux only.
In Python there is os.path.join
, which is great.
Php Solutions
Solution 1 - Php
function join_paths() {
$paths = array();
foreach (func_get_args() as $arg) {
if ($arg !== '') { $paths[] = $arg; }
}
return preg_replace('#/+#','/',join('/', $paths));
}
My solution is simpler and more similar to the way Python os.path.join works
Consider these test cases
array my version @deceze @david_miller @mark
['',''] '' '' '/' '/'
['','/'] '/' '' '/' '/'
['/','a'] '/a' 'a' '//a' '/a'
['/','/a'] '/a' 'a' '//a' '//a'
['abc','def'] 'abc/def' 'abc/def' 'abc/def' 'abc/def'
['abc','/def'] 'abc/def' 'abc/def' 'abc/def' 'abc//def'
['/abc','def'] '/abc/def' 'abc/def' '/abc/def' '/abc/def'
['','foo.jpg'] 'foo.jpg' 'foo.jpg' '/foo.jpg' '/foo.jpg'
['dir','0','a.jpg'] 'dir/0/a.jpg' 'dir/a.jpg' 'dir/0/a.jpg' 'dir/0/a.txt'
Solution 2 - Php
> Since this seems to be a popular question and the comments are filling with "features suggestions" or "bug reports"... All this code snippet does is join two strings with a slash without duplicating slashes between them. That's all. No more, no less. It does not evaluate actual paths on the hard disk nor does it actually keep the beginning slash (add that back in if needed, at least you can be sure this code always returns a string without starting slash).
join('/', array(trim("abc/de/", '/'), trim("/fg/x.php", '/')));
The end result will always be a path with no slashes at the beginning or end and no double slashes within. Feel free to make a function out of that.
EDIT: Here's a nice flexible function wrapper for above snippet. You can pass as many path snippets as you want, either as array or separate arguments:
function joinPaths() {
$args = func_get_args();
$paths = array();
foreach ($args as $arg) {
$paths = array_merge($paths, (array)$arg);
}
$paths = array_map(create_function('$p', 'return trim($p, "/");'), $paths);
$paths = array_filter($paths);
return join('/', $paths);
}
echo joinPaths(array('my/path', 'is', '/an/array'));
//or
echo joinPaths('my/paths/', '/are/', 'a/r/g/u/m/e/n/t/s/');
:o)
Solution 3 - Php
@deceze's function doesn't keep the leading / when trying to join a path that starts with a Unix absolute path, e.g. joinPaths('/var/www', '/vhosts/site');
.
function unix_path() {
$args = func_get_args();
$paths = array();
foreach($args as $arg) {
$paths = array_merge($paths, (array)$arg);
}
foreach($paths as &$path) {
$path = trim($path, '/');
}
if (substr($args[0], 0, 1) == '/') {
$paths[0] = '/' . $paths[0];
}
return join('/', $paths);
}
Solution 4 - Php
My take:
function trimds($s) {
return rtrim($s,DIRECTORY_SEPARATOR);
}
function joinpaths() {
return implode(DIRECTORY_SEPARATOR, array_map('trimds', func_get_args()));
}
I'd have used an anonymous function for trimds
, but older versions of PHP don't support it.
Example:
join_paths('a','\\b','/c','d/','/e/','f.jpg'); // a\b\c\d\e\f.jpg (on Windows)
Updated April 2013 March 2014 May 2018:
function join_paths(...$paths) {
return preg_replace('~[/\\\\]+~', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, $paths));
}
This one will correct any slashes to match your OS, won't remove a leading slash, and clean up and multiple slashes in a row.
Solution 5 - Php
If you know the file/directory exists, you can add extra slashes (that may be unnecessary), then call realpath, i.e.
realpath(join('/', $parts));
This is of course not quite the same thing as the Python version, but in many cases may be good enough.
Solution 6 - Php
As a fun project, I created yet another solution. Should be universal for all operating systems.
For PHP 7.2+:
<?php
/**
* Join string into a single URL string.
*
* @param string $parts,... The parts of the URL to join.
* @return string The URL string.
*/
function join_paths(...$parts) {
if (sizeof($parts) === 0) return '';
$prefix = ($parts[0] === DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
$processed = array_filter(array_map(function ($part) {
return rtrim($part, DIRECTORY_SEPARATOR);
}, $parts), function ($part) {
return !empty($part);
});
return $prefix . implode(DIRECTORY_SEPARATOR, $processed);
}
For PHP version before 7.2:
/**
* Join string into a single URL string.
*
* @param string $parts,... The parts of the URL to join.
* @return string The URL string.
*/
function join_paths() {
$parts = func_get_args();
if (sizeof($parts) === 0) return '';
$prefix = ($parts[0] === DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
$processed = array_filter(array_map(function ($part) {
return rtrim($part, DIRECTORY_SEPARATOR);
}, $parts), function ($part) {
return !empty($part);
});
return $prefix . implode(DIRECTORY_SEPARATOR, $processed);
}
Some test case for its behaviour.
// relative paths
var_dump(join_paths('hello/', 'world'));
var_dump(join_paths('hello', 'world'));
var_dump(join_paths('hello', '', 'world'));
var_dump(join_paths('', 'hello/world'));
echo "\n";
// absolute paths
var_dump(join_paths('/hello/', 'world'));
var_dump(join_paths('/hello', 'world'));
var_dump(join_paths('/hello/', '', 'world'));
var_dump(join_paths('/hello', '', 'world'));
var_dump(join_paths('', '/hello/world'));
var_dump(join_paths('/', 'hello/world'));
Results:
string(11) "hello/world"
string(11) "hello/world"
string(11) "hello/world"
string(11) "hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
Update: Added a version that supports PHP before 7.2.
Solution 7 - Php
An alternative is using implode()
and explode()
.
$a = '/a/bc/def/';
$b = '/q/rs/tuv/path.xml';
$path = implode('/',array_filter(explode('/', $a . $b)));
echo $path; // -> a/bc/def/q/rs/tuv/path.xml
Solution 8 - Php
The solution below uses the logic proposed by @RiccardoGalli, but is improved to avail itself of the DIRECTORY_SEPARATOR
constant, as @Qix and @FélixSaparelli suggested, and, more important, to trim each given element to avoid space-only folder names appearing in the final path (it was a requirement in my case).
Regarding the escape of directory separator inside the preg_replace()
pattern, as you can see I used the preg_quote()
function which does the job fine.
Furthermore, I would replace mutiple separators only (RegExp quantifier {2,}
).
// PHP 7.+
function paths_join(string ...$parts): string {
$parts = array_map('trim', $parts);
$path = [];
foreach ($parts as $part) {
if ($part !== '') {
$path[] = $part;
}
}
$path = implode(DIRECTORY_SEPARATOR, $path);
return preg_replace(
'#' . preg_quote(DIRECTORY_SEPARATOR) . '{2,}#',
DIRECTORY_SEPARATOR,
$path
);
}
}
Solution 9 - Php
Elegant Python-inspired PHP one-liner way to join path.
This code doesn't use unnecessary array.
Multi-platform
function os_path_join(...$parts) {
return preg_replace('#'.DIRECTORY_SEPARATOR.'+#', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, array_filter($parts)));
}
Unix based systems
function os_path_join(...$parts) {
return preg_replace('#/+#', '/', implode('/', array_filter($parts)));
}
Unix based system without REST parameters (don't respect explicit PEP8 philosophy) :
function os_path_join() {
return preg_replace('#/+#', '/', implode('/', array_filter(func_get_args())));
}
Usage
$path = os_path_join("", "/", "mydir/", "/here/");
Bonus : if you want really follow Python os.path.join(). First argument is required :
function os_path_join($path=null, ...$paths) {
if (!is_null($path)) {
throw new Exception("TypeError: join() missing 1 required positional argument: 'path'", 1);
}
$path = rtrim($path, DIRECTORY_SEPARATOR);
foreach ($paths as $key => $current_path) {
$paths[$key] = $paths[$key] = trim($current_path, DIRECTORY_SEPARATOR);
}
return implode(DIRECTORY_SEPARATOR, array_merge([$path], array_filter($paths)));
}
Check os.path.join() source if you want : https://github.com/python/cpython/blob/master/Lib/ntpath.py
Warning : This solution is not suitable for urls.
Solution 10 - Php
for getting parts of paths you can use pathinfo http://nz2.php.net/manual/en/function.pathinfo.php
for joining the response from @deceze looks fine
Solution 11 - Php
A different way of attacking this one:
function joinPaths() {
$paths = array_filter(func_get_args());
return preg_replace('#/{2,}#', '/', implode('/', $paths));
}
Solution 12 - Php
This is a corrected version of the function posted by deceze. Without this change, joinPaths('', 'foo.jpg') becomes '/foo.jpg'
function joinPaths() {
$args = func_get_args();
$paths = array();
foreach ($args as $arg)
$paths = array_merge($paths, (array)$arg);
$paths2 = array();
foreach ($paths as $i=>$path)
{ $path = trim($path, '/');
if (strlen($path))
$paths2[]= $path;
}
$result = join('/', $paths2); // If first element of old path was absolute, make this one absolute also
if (strlen($paths[0]) && substr($paths[0], 0, 1) == '/')
return '/'.$result;
return $result;
}
Solution 13 - Php
This seems to be work quite well, and looks reasonably neat to me.
private function JoinPaths() {
$slash = DIRECTORY_SEPARATOR;
$sections = preg_split(
"@[/\\\\]@",
implode('/', func_get_args()),
null,
PREG_SPLIT_NO_EMPTY);
return implode($slash, $sections);
}
Solution 14 - Php
Best solution found:
function joinPaths($leftHandSide, $rightHandSide) {
return rtrim($leftHandSide, '/') .'/'. ltrim($rightHandSide, '/');
}
NOTE: Copied from the comment by user89021
Solution 15 - Php
OS-independent version based on the answer by mpen but encapsulated into a single function and with the option to add a trailing path separator.
function joinPathParts($parts, $trailingSeparator = false){
return implode(
DIRECTORY_SEPARATOR,
array_map(
function($s){
return rtrim($s,DIRECTORY_SEPARATOR);
},
$parts)
)
.($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}
Or for you one-liner lovers:
function joinPathParts($parts, $trailingSeparator = false){
return implode(DIRECTORY_SEPARATOR, array_map(function($s){return rtrim($s,DIRECTORY_SEPARATOR);}, $parts)).($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}
Simply call it with an array of path parts:
// No trailing separator - ex. C:\www\logs\myscript.txt
$logFile = joinPathParts([getcwd(), 'logs', 'myscript.txt']);
// Trailing separator - ex. C:\www\download\images\user1234\
$dir = joinPathParts([getcwd(), 'download', 'images', 'user1234'], true);
Solution 16 - Php
Here's a function that behaves like Node's path.resolve
:
function resolve_path() {
$working_dir = getcwd();
foreach(func_get_args() as $p) {
if($p === null || $p === '') continue;
elseif($p[0] === '/') $working_dir = $p;
else $working_dir .= "/$p";
}
$working_dir = preg_replace('~/{2,}~','/', $working_dir);
if($working_dir === '/') return '/';
$out = [];
foreach(explode('/',rtrim($working_dir,'/')) as $p) {
if($p === '.') continue;
if($p === '..') array_pop($out);
else $out[] = $p;
}
return implode('/',$out);
}
Test cases:
resolve_path('/foo/bar','./baz') # /foo/bar/baz
resolve_path('/foo/bar','/tmp/file/') # /tmp/file
resolve_path('/foo/bar','/tmp','file') # /tmp/file
resolve_path('/foo//bar/../baz') # /foo/baz
resolve_path('/','foo') # /foo
resolve_path('/','foo','/') # /
resolve_path('wwwroot', 'static_files/png/', '../gif/image.gif')
# __DIR__.'/wwwroot/static_files/gif/image.gif'
Solution 17 - Php
From the great answer of Ricardo Galli, a bit of improvement to avoid killing the protocol prefix.
The idea is to test for the presence of a protocol in one argument, and maintain it into the result. WARNING: this is a naive implementation!
For example:
array("http://domain.de","/a","/b/")
results to (keeping protocol)
"http://domain.de/a/b/"
instead of (killing protocol)
"http:/domain.de/a/b/"
But http://codepad.org/hzpWmpzk needs a better code writing skill.
Solution 18 - Php
I love Riccardo's answer and I think it is the best answer.
I am using it to join paths in url building, but with one small change to handle protocols' double slash:
function joinPath () {
$paths = array();
foreach (func_get_args() as $arg) {
if ($arg !== '') { $paths[] = $arg; }
}
// Replace the slash with DIRECTORY_SEPARATOR
$paths = preg_replace('#/+#', '/', join('/', $paths));
return preg_replace('#:/#', '://', $paths);
}
Solution 19 - Php
function path_combine($paths) {
for ($i = 0; $i < count($paths); ++$i) {
$paths[$i] = trim($paths[$i]);
}
$dirty_paths = explode(DIRECTORY_SEPARATOR, join(DIRECTORY_SEPARATOR, $paths));
for ($i = 0; $i < count($dirty_paths); ++$i) {
$dirty_paths[$i] = trim($dirty_paths[$i]);
}
$unslashed_paths = array();
for ($i = 0; $i < count($dirty_paths); ++$i) {
$path = $dirty_paths[$i];
if (strlen($path) == 0) continue;
array_push($unslashed_paths, $path);
}
$first_not_empty_index = 0;
while(strlen($paths[$first_not_empty_index]) == 0) {
++$first_not_empty_index;
}
$starts_with_slash = $paths[$first_not_empty_index][0] == DIRECTORY_SEPARATOR;
return $starts_with_slash
? DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $unslashed_paths)
: join(DIRECTORY_SEPARATOR, $unslashed_paths);
}
Example usage:
$test = path_combine([' ', '/cosecheamo', 'pizze', '///// 4formaggi', 'GORGONZOLA']);
echo $test;
Will output:
/cosecheamo/pizze/4formaggi/GORGONZOLA
Solution 20 - Php
Here is my solution:
function joinPath(): string {
$path = '';
foreach (func_get_args() as $numArg => $arg) {
$arg = trim($arg);
$firstChar = substr($arg, 0, 1);
$lastChar = substr($arg, -1);
if ($numArg != 0 && $firstChar != '/') {
$arg = '/'.$arg;
}
# Eliminamos el slash del final
if ($lastChar == '/') {
$arg = rtrim($arg, '/');
}
$path .= $arg;
}
return $path;
}
Solution 21 - Php
Hmmm most seem a bit over complicated. Dunno, this is my take on it:
// Takes any amount of arguments, joins them, then replaces double slashes
function join_urls() {
$parts = func_get_args();
$url_part = implode("/", $parts);
return preg_replace('/\/{1,}/', '/', $url_part);
}
Solution 22 - Php
For people who want a join function that does the Windows backslash and the Linux forward slash.
Usage:
<?php
use App\Util\Paths
echo Paths::join('a','b'); //Prints 'a/b' on *nix, or 'a\\b' on Windows
Class file:
<?php
namespace App\Util;
class Paths
{
public static function join_with_separator($separator, $paths) {
$slash_delimited_path = preg_replace('#\\\\#','/', join('/', $paths));
$duplicates_cleaned_path = preg_replace('#/+#', $separator, $slash_delimited_path);
return $duplicates_cleaned_path;
}
public static function join() {
$paths = array();
foreach (func_get_args() as $arg) {
if ($arg !== '') { $paths[] = $arg; }
}
return Paths::join_with_separator(DIRECTORY_SEPARATOR, $paths);
}
}
Here's the test function:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use App\Util\Paths;
class PathsTest extends TestCase
{
public function testWindowsPaths()
{
$TEST_INPUTS = [
[],
['a'],
['a','b'],
['C:\\','blah.txt'],
['C:\\subdir','blah.txt'],
['C:\\subdir\\','blah.txt'],
['C:\\subdir','nested','1/2','blah.txt'],
];
$EXPECTED_OUTPUTS = [
'',
'a',
'a\\b',
'C:\\blah.txt',
'C:\\subdir\\blah.txt',
'C:\\subdir\\blah.txt',
'C:\\subdir\\nested\\1\\2\\blah.txt',
];
for ($i = 0; $i < count($TEST_INPUTS); $i++) {
$actualPath = Paths::join_with_separator('\\', $TEST_INPUTS[$i]);
$expectedPath = $EXPECTED_OUTPUTS[$i];
$this->assertEquals($expectedPath, $actualPath);
}
}
public function testNixPaths()
{
$TEST_INPUTS = [
[],
['a'],
['a','b'],
['/home','blah.txt'],
['/home/username','blah.txt'],
['/home/username/','blah.txt'],
['/home/subdir','nested','1\\2','blah.txt'],
];
$EXPECTED_OUTPUTS = [
'',
'a',
'a/b',
'/home/blah.txt',
'/home/username/blah.txt',
'/home/username/blah.txt',
'/home/subdir/nested/1/2/blah.txt',
];
for ($i = 0; $i < count($TEST_INPUTS); $i++) {
$actualPath = Paths::join_with_separator('/', $TEST_INPUTS[$i]);
$expectedPath = $EXPECTED_OUTPUTS[$i];
$this->assertEquals($expectedPath, $actualPath);
}
}
}
Solution 23 - Php
I liked several solutions presented. But those who does replacing all '/+' into '/' (regular expressions) are forgetting that os.path.join() from python can handle this kind of join:
os.path.join('http://example.com/parent/path', 'subdir/file.html')