Skip to content

Instantly share code, notes, and snippets.

@stefanschmidt
Last active August 13, 2023 09:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stefanschmidt/c74676a9406588eb43aec7ea385c13f8 to your computer and use it in GitHub Desktop.
Save stefanschmidt/c74676a9406588eb43aec7ea385c13f8 to your computer and use it in GitHub Desktop.
Resolve symbolic link chains to files and containing folders step-by-step

Gradually resolving symbolic link chains

Let's say one wants to know where pdflatex is located.

$ type pdflatex
pdflatex is /Library/TeX/texbin/pdflatex

We can see that this path is a symbolic link to pdftex.

$ ls -ld /Library/TeX/texbin/pdflatex
lrwxr-xr-x  1 root  wheel  6 Oct  3  2022 /Library/TeX/texbin/pdflatex -> pdftex

Further we can see that the containing folder is also a symbolic link

$ ls -ld /Library/TeX/texbin
lrwxr-xr-x  1 root  wheel  29 Oct  3  2022 /Library/TeX/texbin -> Distributions/Programs/texbin

...which points to another symbolic link

$ ls -ld /Library/TeX/Distributions/Programs/texbin
lrwxr-xr-x  1 root  wheel  39 Jun 19  2014 /Library/TeX/Distributions/Programs/texbin -> ../.DefaultTeX/Contents/Programs/texbin

...which points to yet another symbolic link

$ ls -ld /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/texbin
lrwxr-xr-x  1 root  wheel  9 Oct  3  2022 /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/texbin -> universal

...which points to one more symbolic link which appears to point to a location within /usr/local.

$ ls -ld /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/universal
lrwxr-xr-x  1 root  wheel  64 Oct  3  2022 /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/universal -> ../../../../../../../usr/local/texlive/2022/bin/universal-darwin

The number of relative path segments is quite numerous so manually resolving them would be a tedious and error-prone procedure.

As a workaround for the appearant lack of builtin functionality we'll temporarily switch the working directory to resolve the relative path segments without resolving the complete remaining symbolic link chain.

$ path=/Library/TeX/Distributions/.DefaultTeX/Contents/Programs/universal

$ pushd $path/$(readlink $path) > /dev/null && pwd && popd > /dev/null
/usr/local/texlive/2022/bin/universal-darwin

We now have arrived at the final destination!

$ ls -ld /usr/local/texlive/2022/bin/universal-darwin
drwxr-xr-x  476 root  wheel  15232 Oct  3  2022 /usr/local/texlive/2022/bin/universal-darwin

Summarizing we have the following symbolic link chains:

/Library/TeX/texbin/pdflatex
-> /Library/TeX/texbin/pdftex

/Library/TeX/texbin
-> /Library/TeX/Distributions/Programs/texbin
-> /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/texbin
-> /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/universal
-> /usr/local/texlive/2022/bin/universal-darwin

It may seem obvious, but while realpath and readlink allow to resolve all symbolic link chains of a path, apparently neither of them has an option to do this step-by-step.

$ greadlink -f /Library/TeX/texbin/pdflatex
/usr/local/texlive/2022/bin/universal-darwin/pdftex

$ realpath /Library/TeX/texbin/pdflatex
/usr/local/texlive/2022/bin/universal-darwin/pdftex

This is where realpath_verbose.py comes in, which provides a merged view for symbolic link chains pointing to directories and/or files.

$ ./realpath_verbose.py /Library/TeX/texbin/pdflatex
-> /Library/TeX/texbin/pdftex
-> /Library/TeX/Distributions/Programs/texbin/pdftex
-> /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/texbin/pdftex
-> /Library/TeX/Distributions/.DefaultTeX/Contents/Programs/universal/pdftex
-> /usr/local/texlive/2022/bin/universal-darwin/pdftex

This leaves the question remaining how meaningful such rather lengthy symbolic chains are and assuming one approves the idea if there is something like a maximum chain length.

#!/usr/bin/env python
from os import sep, readlink
from os.path import islink, join, normpath, dirname, basename
import sys
def resolve_symlinks(path):
path_list = []
while islink(path):
resolved = readlink(path)
path = normpath(join(dirname(path), resolved))
path_list.append(path)
return path_list
def resolve_path(path):
path_list = []
base_name = basename(path)
parts = path.split(sep)
for i in range(len(parts), 0, -1):
partial_path = sep.join(parts[:i])
path_remainder = sep.join(parts[i:])
if islink(partial_path):
path_list.extend(resolve_symlinks(partial_path))
path_list = [path + sep + base_name for path in path_list]
break
return path_list
if len(sys.argv) == 2:
path = normpath(sys.argv[1])
else:
print('Usage: python realpath_verbose.py <path>')
sys.exit()
path_list = resolve_symlinks(path)
if path_list:
path_list.extend(resolve_path(path_list[-1]))
for path in path_list:
print('-> ' + str(path))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment