如何在 PHP 中连接文件系统路径字符串?

PHP 中是否有一个内置函数来智能地连接路径字符串?给定 abc/de//fg/x.php作为参数的函数应该返回 abc/de/fg/x.php; 使用 abc/defg/x.php作为该函数的参数应该得到相同的结果。

如果没有,是否有一个可用的类?它对于分割路径或删除部分路径也很有价值。如果您已经编写了一些代码,可以在这里共享您的代码吗?

总是使用 /是可以的,我只是为 Linux 编写代码。

在 Python 中有 os.path.join,这很棒。

70469 次浏览

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)

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

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

@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);
}

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;
}

A different way of attacking this one:

function joinPaths() {
$paths = array_filter(func_get_args());
return preg_replace('#/{2,}#', '/', implode('/', $paths));
}

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.

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'

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.

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'

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')

Result: 'http://example.com/parent/path/subdir/file.html'

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);
}

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.

Best solution found:

function joinPaths($leftHandSide, $rightHandSide) {
return rtrim($leftHandSide, '/') .'/'. ltrim($rightHandSide, '/');
}

NOTE: Copied from the comment by user89021

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
);
}

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);
}

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.

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);
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

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;
}

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);
}

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);
}
}
}

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.

$args = [sys_get_temp_dir(), "path1","path2", "filename.pdf"];
$filename = implode( DIRECTORY_SEPARATOR, $args);
// output "C:\Users\User\AppData\Local\Temp\path1\path2\filename.pdf"

Note OP is asking for something slightly different from https://docs.python.org/3/library/os.path.html#os.path.join which does more than just join paths with the right number of separators.

While what they have asked for has been answered, for anyone skim reading the Q&A, there will be the following differences and ambiguous cases between what was asked for and os.path.join():

Many of the above solutions don't work for the root only case ['/'] => '/'

os.path.join drop all args to the left of the rightmost absolute path e.g. ['a', 'b', '/c'] => '/c' which to be fair is probably not the behaviour you want if you are refactoring existing php which has a lot of path segments appear like they are absolute paths.

Another difference with os.path.join is it won't drop additional separators within a single string ['a///', 'b', 'c'] => 'a///b/c'

Another special case is one or more empty strings resulting in a trailing slash for os.path.join: ['a', ''] or ['a', '', ''] => 'a/'