Getting the target of a symbolic link with pathlib

Question:

Is there a way to get the target of a symbolic link using pathlib? I know that this can be done using os.readlink().

I want to create a dictionary composed by links and their target files.

links = [link for link in root.rglob('*') if link.is_symlink()]
files = [Path(os.readlink(str(pointed_file))) for pointed_file in links]

Edit … and I want to filter all paths that are not absoulute

    link_table = {link : pointed_file for link, pointed_file in zip(links, files) if pointed_file.is_absolute()}
Asked By: Elvis Dukaj

||

Answers:

Update: Python 3.9 introduced Path.readlink() method, so explanations below apply to earlier releases only.

Nope, it’s currently not possible to get the results from pathlib that os.readlink() gives. Path.resolve() doesn’t work for broken links and either raises FileNotFoundError (Python <3.5) or returns potentially bizarre path (Python 3.6). One can still get old behaviour with Path.resolve(True), though that means incompatibility between versions (not mentioned in the package documentation nor Porting section of Py3.6 release document by the way).

Also regarding updated question about absolute paths, os.readlink() is the only way to check whether symbolic link is absolute or relative one. Path.relative_to() as name says, transforms existing Path to relative one, without checking whether symbolic link destination path starts with ‘/’ or not. The same applies for Path.is_absolute(). Finally Path.resolve() for existing targets, transforms destination path so it destroys required information on the way.

And by bizarre path above I mean, let’s say in /home/test there’s symlink .myapp pointing to .config/myapp/config. If .config/myapp doesn’t exist and the code is written in Py<=3.5, Path.resolve() would raise an exception and the app could inform a user about the missing file. Now if called from Py3.6 without code changes, it resolves to ~/.config/myapp, the exception is not thrown, so check around .resolve() passes, but then probably another exception will be thrown later when the app will try to open the file for reading, so the user may get message that ~/config/myapp file is not found, though that’s not really the one missing here. It may be even worse – when app would do Path('/home/test/.myapp').resolve().open('w') (not necessarily in one step, but let’s say as part of some sanitization process), then simply wrong file is created. Of course next time the app is called, path resolves one level deeper, to /home/test/.config/myapp/config (as Path.resolve() doesn’t check if myapp is a directory or not), and both reading and writing will fail with a NotADirectoryError exception (with a little misleading "Not a directory: /home/test/.config/myapp/config" as a description…).

Answered By: RobertT

Seemed useful to say how it’s possible since 3.9.

This was a bit harder than expected. You have to check is_symlink(), readlink and then is_absolute to check if the result of readlink is itself an absolute path.

readlink says:

Return the path to which the symbolic link points (as returned by os.readlink()):

i.e. Path("./file1").readlink() => PosixPath('../files/file1') for my relative symlink in my test files.

The code itself:

from pathlib import Path

cwd = Path(".")

#is it a link?
links = [pa for pa in (cwd / "links").glob("*") if pa.is_symlink()]

#you have to read the link and check if readlink's result is absolute
di_abs = { str(link) : str(v)  for link in links if (v:=link.readlink()).is_absolute()}

for k,v in di_abs.items():
    print(f" {k:20.20} => {v}"

The output, assuming I understood the requirements:

 links/file3          => /Users/me/kds2/wk/explore/test_430_readlink.py/files/file3
 links/file2          => /Users/me/kds2/wk/explore/test_430_readlink.py/files/file2

The tree of the files used to test

├── files
│   ├── file1
│   ├── file2
│   └── file3
├── links           `readlink()` result
│   ├── file1 -> ../files/file1
│   ├── file2 -> /Users/me/kds2/wk/explore/test_430_readlink.py/files/file2
│   ├── file3 -> /Users/me/kds2/wk/explore/test_430_readlink.py/files/file3
│   └── realfile4

the shell to setup the test

(needs realpath which you’ll need to install on macos)

rm -rf links files

mkdir files
cd files
echo file1contents > file1
echo file2contents > file2
echo file3contents > file3

cd ..
mkdir links
cd links

ln -s ../files/file1
ln -s $(realpath ../files/file2)
ln -s $(realpath ../files/file3)

echo realfile4contents > realfile4

cd ..


Answered By: JL Peyret
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.