Skip to content

Commit 22d9cc1

Browse files
committed
[Filesystem] Add a cross-platform readlink method
1 parent 8f3c06b commit 22d9cc1

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

src/Symfony/Component/Filesystem/Filesystem.php

+70
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,76 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false)
316316
}
317317
}
318318

319+
/**
320+
* Return the next direct target of a link.
321+
*
322+
* Tto recursively follow links until a final target
323+
* is reached, use @see Filesystem::realpath($path).
324+
*
325+
* @param string $path A link path.
326+
*
327+
* @return string Return the resolved link.
328+
*
329+
* @throws IOException When the link does not exist or is not readable.
330+
*/
331+
public function readlink($path)
332+
{
333+
if (!$this->exists($path)) {
334+
throw new IOException(sprintf('The link %s does not exist and cannot be read.', $path));
335+
}
336+
337+
if (!is_link($path)) {
338+
return $path;
339+
}
340+
341+
// On Windows, transitive links are resolved to the final target by
342+
// readlink(). realpath(), however, returns the target link on Windows,
343+
// but not on Unix.
344+
345+
// /link1 -> /link2 -> /file
346+
347+
// Windows: readlink(/link1) => /file
348+
// realpath(/link1) => /link2
349+
350+
// Unix: readlink(/link1) => /link2
351+
// realpath(/link1) => /file
352+
353+
if ('\\' === DIRECTORY_SEPARATOR) {
354+
return realpath($path);
355+
}
356+
357+
return readlink($path);
358+
}
359+
360+
/**
361+
* Return the final target of a link by recursively following links.
362+
*
363+
* To find only the direct next target of a link,
364+
* use @see Filesystem::readlink($path).
365+
*
366+
* @param string $path A link path.
367+
*
368+
* @return string Return the final target of the link.
369+
*
370+
* @throws IOException When the link does not exist or is not readable.
371+
*/
372+
public function realpath($path)
373+
{
374+
if (!$this->exists($path)) {
375+
throw new IOException(sprintf('The link %s does not exist and cannot be read.', $path));
376+
}
377+
378+
if (!is_link($path)) {
379+
return $path;
380+
}
381+
382+
if ('\\' === DIRECTORY_SEPARATOR) {
383+
return readlink($path);
384+
}
385+
386+
return realpath($path);
387+
}
388+
319389
/**
320390
* Given an existing path, convert it to a path relative to a given starting path.
321391
*

src/Symfony/Component/Filesystem/Tests/FilesystemTest.php

+72
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,78 @@ public function testSymlinkCreatesTargetDirectoryIfItDoesNotExist()
767767
$this->assertEquals($file, readlink($link2));
768768
}
769769

770+
public function testReadLink()
771+
{
772+
$this->markAsSkippedIfSymlinkIsMissing();
773+
774+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
775+
$link1 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'link';
776+
$link2 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'subdir'.DIRECTORY_SEPARATOR.'link';
777+
778+
touch($file);
779+
780+
$this->filesystem->symlink($file, $link1);
781+
$this->filesystem->symlink($link1, $link2);
782+
783+
$this->assertTrue(is_link($link1));
784+
$this->assertEquals($file, $this->filesystem->readlink($link1));
785+
$this->assertTrue(is_link($link2));
786+
$this->assertEquals($link1, $this->filesystem->readlink($link2));
787+
}
788+
789+
public function testReadLinkNotLink()
790+
{
791+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
792+
793+
touch($file);
794+
795+
$this->assertEquals($file, $this->filesystem->readlink($file));
796+
}
797+
798+
/**
799+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
800+
*/
801+
public function testReadLinkFails()
802+
{
803+
$this->filesystem->readlink($this->workspace.DIRECTORY_SEPARATOR.'invalid');
804+
}
805+
806+
public function testRealPath()
807+
{
808+
$this->markAsSkippedIfSymlinkIsMissing();
809+
810+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
811+
$link1 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'link';
812+
$link2 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'subdir'.DIRECTORY_SEPARATOR.'link';
813+
814+
touch($file);
815+
816+
$this->filesystem->symlink($file, $link1);
817+
$this->filesystem->symlink($link1, $link2);
818+
819+
$this->assertTrue(is_link($link1));
820+
$this->assertEquals($file, $this->filesystem->realpath($link1));
821+
$this->assertTrue(is_link($link2));
822+
$this->assertEquals($file, $this->filesystem->realpath($link2));
823+
}
824+
825+
public function testRealPathNotLink()
826+
{
827+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
828+
829+
touch($file);
830+
831+
$this->assertEquals($file, $this->filesystem->realpath($file));
832+
}
833+
834+
/**
835+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
836+
*/
837+
public function testRealPathFails()
838+
{
839+
$this->filesystem->realpath($this->workspace.DIRECTORY_SEPARATOR.'invalid');
840+
}
841+
770842
/**
771843
* @dataProvider providePathsForMakePathRelative
772844
*/

0 commit comments

Comments
 (0)