Як приєднати рядки шляху до файлової системи у PHP?


76

Чи існує вбудована функція в PHP для розумного приєднання рядків шляху? Функція, наведена abc/de/та /fg/x.phpяк аргументи, повинна повернутися abc/de/fg/x.php; той самий результат слід давати, використовуючи abc/deта fg/x.phpяк аргументи для цієї функції.

Якщо ні, чи є доступний клас? Це також може бути цінним для розбиття шляхів або видалення їх частин. Якщо ви щось написали, можете поділитися своїм кодом тут?

Це нормально завжди використовувати /, я кодую лише для Linux.

У Python є os.path.join, що чудово.


5
Ого, я щойно прийшов сюди з Google. Не можу повірити, що PHP такий.
Дмитро Міньковський

4
Зверніть увагу, що ваш приклад є досить оманливим, оскільки os.path.join('some/relative/path, '/an/absolute/path')завжди буде повертатися /an/absolute/path. Отже, ви або шукаєте os.path.joinзаміну (потім виправляєте свій приклад), або щось близьке до неї, за винятком того, що абсолютні шляхи, що йдуть на друге місце (або n-те), розглядаються як відносні шляхи.

2
@Tibo та прихильники його коментаря: ні, приклад взагалі не вводить в оману, за винятком випадків, коли ігнорується суть питання та наполягає на надмірному акцентуванні на посиланні на Python, що є маргінальним зауваженням для орієнтації. Питання абсолютно чітке щодо того, чого він хоче, а це не зовсім так os.path.join. Читайте: "Функція, подана abc/de/та /fg/x.phpаргументи, повинна повернутися abc/de/fg/x.php".
Sz.

Огидно, що PHP не має вбудованої функції для цього
Silidrone

Відповіді:


54

Оскільки це, здається, популярне запитання, і коментарі наповнюються "пропозиціями щодо функцій" або "звітами про помилки" ... Все, що робить цей фрагмент коду, це поєднання двох рядків косою рисою, не дублюючи між ними скісних рисок. Це все. Ні більше, ні менше. Він не оцінює фактичні шляхи на жорсткому диску, а також фактично не зберігає початкову косу риску (додайте це, якщо потрібно, принаймні, ви можете бути впевнені, що цей код завжди повертає рядок без стартової скісної риски).

join('/', array(trim("abc/de/", '/'), trim("/fg/x.php", '/')));

Кінцевим результатом завжди буде шлях без косих рисок на початку або в кінці та подвійних скісних рисок всередині. Не соромтеся робити з цього функцію.

EDIT: Ось приємна гнучка обгортка функцій для наведеного вище фрагмента. Ви можете передавати скільки завгодно фрагментів шляху, як масив, так і окремі аргументи:

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)


5
функція pj ($ a, $ b) {return rtrim ($ a, '/'). '/'. ltrim ($ b, '/'); }
user89021 07

2
Це не завжди працює, як описано. joinPaths ('', 'foo.jpg') стає '/foo.jpg'. Я помітив це після того, як мій менеджер файлів php почав писати оновлені користувачами файли до кореня файлової системи! Виправлена ​​версія повинна видалити всі шляхи, які є порожніми рядками.
EricP

35
Чи слід тут використовувати DIRECTORY_SEPARATOR замість '/'?
Дейв

2
@fe_ Це зовсім інша функція, ніж те, про що було запитання.
обдурити

3
Я не згоден, оскільки людина чітко повідомила, що звикла до результатів, наданих python, os.path.joinякі дають цей результат і які він вважає чудовими. Тому я не вірю, що це інша функція. Так само, як join('/a/b','../c')слід повернутися /a/cбез жодної зовнішньої нормалізації.
fe_lix_

130
function join_paths() {
    $paths = array();

    foreach (func_get_args() as $arg) {
        if ($arg !== '') { $paths[] = $arg; }
    }

    return preg_replace('#/+#','/',join('/', $paths));
}

Моє рішення простіше і більше схоже на те, як працює Python os.path.join

Розглянемо ці тестові приклади

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'

5
Це найкраща відповідь, оскільки вона найкраще відповідає питанню - вона найближча os.path.joinі розумно приєднує рядки шляхів . Відповідь можна покращити, додавши реалізацію "посилання" os.path.joinта вказавши специфіку OP, яка порушує правило (тестовий випадок ['abc','/def']неправильний wrt os.path.join, але правильний відповідно до запитання).

@qix чому? Windows чудово розуміє похилі риски вперед
Ріккардо Галлі

4
@qix дивись, я розумію твою думку, зазвичай я б погодився з тобою, але реально PHP не збирається працювати на платформах, які не використовують скісні риски як роздільник шляху найближчим часом, і використовувати константу preg_replace () стане справжнім безладом (вам потрібно буде уникнути роздільника шляху в регулярному виразі), тому я вибираю цей компроміс.
Ріккардо Галлі

4
Отже, ви скорочуєте кути, бо ледачі, хоча інтерполяція рядків констант у PHP майже безкоштовна? Цк.
Qix - МОНІКУ ПОМИЛИЛО

3
Як примітка, моя команда щойно виявила випадок, коли /роздільники не працюють (стара PHP5.3.4, інстальована на Windows Server 2012 за допомогою оболонки msys git).
Фелікс Сапареллі,

17

Функція @ deceze не утримує провідну / при спробі приєднатися до шляху, який починається з абсолютного шляху Unix, наприклад 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);
}

16

Моя думка:

function trimds($s) {
    return rtrim($s,DIRECTORY_SEPARATOR);
}

function joinpaths() {
    return implode(DIRECTORY_SEPARATOR, array_map('trimds', func_get_args()));
}

Я б використовував анонімну функцію trimds, але старіші версії PHP її не підтримують.

Приклад:

join_paths('a','\\b','/c','d/','/e/','f.jpg'); // a\b\c\d\e\f.jpg (on Windows)

Оновлено Квітень 2013 р Березень 2014 р Травень 2018 :

function join_paths(...$paths) {
    return preg_replace('~[/\\\\]+~', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, $paths));
}

Цей виправить будь-які скісні риски відповідно до вашої ОС, не видалить провідну скісну риску, а також очистить і кілька похилих схилів поспіль.


13
це завжди створює абсолютний шлях, але принаймні хтось згадав DIRECTORY_SEPARATOR ...
Karoly Horvath

7

Якщо ви знаєте, що файл / каталог існує , ви можете додати зайві скісні риски (що може бути непотрібним), а потім викликати realpath , тобто

realpath(join('/', $parts));

Звичайно, це не зовсім те саме, що версія Python, але в багатьох випадках може бути досить хорошою.


4

Альтернативою є використання implode()та 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

Спробуйте URL /offset/0/limit/1.
Данон,

3

Рішення, наведене нижче, використовує логіку, запропоновану @RiccardoGalli, але вдосконалено, щоб скористатися DIRECTORY_SEPARATORконстантою, як пропонували @Qix та @ FélixSaparelli, і, що ще важливіше, обрізати кожен даний елемент, щоб уникнути імен папок, що містять лише пробіл у фіналі шлях (у моєму випадку це була вимога).

Що стосується екранування роздільника каталогів усередині preg_replace()шаблону, як ви бачите, я використовував preg_quote()функцію, яка чудово виконує роботу.
Крім того, я б замінив лише різні розділювачі (квантор RegExp {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
    );
}


2

Інший спосіб атаки на цей:

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

1

Це виправлена ​​версія функції, опублікованої decese. Без цієї зміни joinPaths ('', 'foo.jpg') стає '/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;
}

1

Здається, це працює досить добре, і мені здається досить акуратним.

private function JoinPaths() {
  $slash = DIRECTORY_SEPARATOR;
  $sections = preg_split(
          "@[/\\\\]@",
          implode('/', func_get_args()),
          null,
          PREG_SPLIT_NO_EMPTY);
  return implode($slash, $sections);
}

1

Знайдено найкраще рішення:

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

ПРИМІТКА: Скопійовано з коментаря користувачем89021


1

Незалежна від ОС версія, заснована на відповіді mpen, але інкапсульована в єдину функцію та з можливістю додати роздільник кінцевого шляху.

function joinPathParts($parts, $trailingSeparator = false){
    return implode(
        DIRECTORY_SEPARATOR, 
        array_map(
            function($s){
                return rtrim($s,DIRECTORY_SEPARATOR);
            }, 
            $parts)
        )
        .($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}

Або для тих, хто любить один лайнер:

function joinPathParts($parts, $trailingSeparator = false){
    return implode(DIRECTORY_SEPARATOR, array_map(function($s){return rtrim($s,DIRECTORY_SEPARATOR);}, $parts)).($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}

Просто назвіть це з масивом частин шляху:

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

1

Елегантний PHP-натхненний PHP однолінійний спосіб приєднатися до шляху.

Цей код не використовує непотрібний масив.

Багатоплатформна

function os_path_join(...$parts) {
  return preg_replace('#'.DIRECTORY_SEPARATOR.'+#', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, array_filter($parts)));
}

Системи на основі Unix

function os_path_join(...$parts) {
  return preg_replace('#/+#', '/', implode('/', array_filter($parts)));
}

Система на основі Unix без параметрів REST (не дотримуйтесь явної філософії PEP8):

function os_path_join() {
  return preg_replace('#/+#', '/', implode('/', array_filter(func_get_args())));
}

Використання

$path = os_path_join("", "/", "mydir/", "/here/");

Бонус: якщо ви дійсно хочете слідувати Python os.path.join (). Потрібний перший аргумент:

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

Перевірте джерело os.path.join (), якщо хочете: https://github.com/python/cpython/blob/master/Lib/ntpath.py

Попередження: Це рішення не підходить для URL-адрес.


1

Як цікавий проект я створив ще одне рішення. Має бути універсальним для всіх операційних систем:

<?php

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

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

Результати:

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"

+1 Велике спасибі @Koala Yeung. Ваше рішення сумісне з PHP-версіями вище v7.2. Код із прийнятої відповіді використовує create_function (), який застарів у версіях вище 7.2 - у 2013 році ми щойно мали PHP v5.5.
JackLeEmmerdeur

0

Ось функція, яка працює як Nodepath.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);
}

Тестові кейси:

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'

0

З чудової відповіді Рікардо Галлі, трохи вдосконалення, щоб уникнути вбивства префікса протоколу.

Ідея полягає в тому, щоб перевірити наявність протоколу в одному аргументі та підтримати його в результаті. ПОПЕРЕДЖЕННЯ: це наївна реалізація!

Наприклад:

array("http://domain.de","/a","/b/")

результати до (ведення протоколу)

"http://domain.de/a/b/"

замість (протокол вбивства)

"http:/domain.de/a/b/"

Але http://codepad.org/hzpWmpzk потребує кращої навички написання коду.


0

Мені подобається відповідь Ріккардо, і я вважаю, що це найкраща відповідь.

Я використовую його для об’єднання шляхів у побудові url , але з однією невеликою зміною для обробки подвійної косої риски протоколів:

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

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

Приклад використання:

$test = path_combine([' ', '/cosecheamo', 'pizze', '///// 4formaggi', 'GORGONZOLA']);
echo $test;

Виведе:

/cosecheamo/pizze/4formaggi/GORGONZOLA

0

Ось моє рішення:

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

0

Хммм, більшість здаються дещо надто складними. Не знаю, це мій погляд на це:

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

0

Для людей, які хочуть функцію об’єднання, яка робить зворотну косу риску Windows і скісну риску Linux.

Використання:

<?php
use App\Util\Paths
echo Paths::join('a','b'); //Prints 'a/b' on *nix, or 'a\\b' on Windows

Файл класу:

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

Ось функція тесту:

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

-7

Мені сподобалось кілька представлених рішень. Але ті, хто замінює всі '/ +' на '/' (регулярні вирази), забувають, що os.path.join () з python може обробляти такий тип об'єднання:

os.path.join('http://example.com/parent/path', 'subdir/file.html')

Результат: ' http://example.com/parent/path/subdir/file.html '


1
Запитання для PHP, відповідь для Python
Toilal
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.