Created
September 25, 2024 18:53
-
-
Save moreati/8d37001063e90181788c389b0a92fd51 to your computer and use it in GitHub Desktop.
tracerealpath - Like traceroute, but for symlinks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Copyright (c) 2001 - 2023 Python Software Foundation | |
# Copyright (c) 2024 Alex Willmer <[email protected]> | |
# License: Python Software Foundation License Version 2 | |
# Derived from https://github.com/python/cpython/blob/v3.12.6/Lib/posixpath.py#L423-L492 | |
""" | |
Show the route taken through symlinks to a resolved file path | |
""" | |
import os | |
import stat | |
def trace_realpath(filename:os.PathLike, *, strict:bool=False) -> tuple[os.fspath, list[os.PathLike]]: | |
""" | |
Return the canonical path of the specified filename, and a trace of | |
the route taken, eliminating any symbolic links encountered in the path. | |
""" | |
filename = os.fspath(filename) | |
path, trace, ok = _join_tracepath(filename[:0], filename, strict, seen={}, trace=[]) | |
return os.path.abspath(path), trace | |
def _join_tracepath( | |
path: str, | |
rest: str, | |
strict: bool, | |
seen: dict[str, str|None], | |
trace: list[str], | |
) -> tuple[str, list[str], bool]: | |
""" | |
Join two paths, normalizing and eliminating any symbolic links encountered | |
in the second path. | |
""" | |
trace.append(rest) | |
if isinstance(path, bytes): | |
sep = b'/' | |
curdir = b'.' | |
pardir = b'..' | |
else: | |
sep = '/' | |
curdir = '.' | |
pardir = '..' | |
if os.path.isabs(rest): | |
rest = rest[1:] | |
path = sep | |
while rest: | |
name, _, rest = rest.partition(sep) | |
if not name or name == curdir: | |
# current dir | |
continue | |
if name == pardir: | |
# parent dir | |
if path: | |
path, name = os.path.split(path) | |
if name == pardir: | |
path = os.path.join(path, pardir, pardir) | |
else: | |
path = pardir | |
continue | |
newpath = os.path.join(path, name) | |
try: | |
st = os.lstat(newpath) | |
except OSError: | |
if strict: | |
raise | |
is_link = False | |
else: | |
is_link = stat.S_ISLNK(st.st_mode) | |
if not is_link: | |
path = newpath | |
continue | |
# Resolve the symbolic link | |
if newpath in seen: | |
# Already seen this path | |
path = seen[newpath] | |
if path is not None: | |
# use cached value | |
continue | |
# The symlink is not resolved, so we must have a symlink loop. | |
if strict: | |
# Raise OSError(errno.ELOOP) | |
os.stat(newpath) | |
else: | |
# Return already resolved part + rest of the path unchanged. | |
return os.path.join(newpath, rest), trace, False | |
seen[newpath] = None # not resolved symlink | |
path, trace, ok = _join_tracepath(path, os.readlink(newpath), strict, seen, trace) | |
if not ok: | |
return os.path.join(path, rest), False | |
seen[newpath] = path # resolved symlink | |
return path, trace, True | |
__all__ = [ | |
trace_realpath.__name__, | |
] | |
if __name__ == '__main__': | |
import argparse | |
parser = argparse.ArgumentParser(description=__doc__) | |
parser.add_argument( | |
'path', nargs='?', default='.', | |
help="Path to resolve (default: current working directory)", | |
) | |
args = parser.parse_args() | |
path, trace = trace_realpath(args.path) | |
print(f"{len(trace)} hops to {path}") | |
for hop, path in enumerate(trace, start=1): | |
print(hop, os.path.abspath(path), sep=' ') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Almost entirely untested, except for a few simple cases.