春江暮客

春江暮客的个人学习分享网站

Find Broken Symlinks with Python: Separate Missing Targets, Permission Errors, and Loops

2026-10-16 Technology
Find Broken Symlinks with Python: Separate Missing Targets, Permission Errors, and Loops

A link still appears in a directory listing, but Python raises FileNotFoundError: moving data or removing an old release can leave a link pointing at its former target. This tutorial builds a read-only standard-library tool that lists dangling links and distinguishes missing targets from checks that could not finish.

It targets beginner-to-intermediate readers who can run terminal commands. If the failure happens only in a scheduled job, also check Linux cron paths and working directories to confirm that the job is inspecting the same directory.

  1. Walk real subdirectories with os.walk(), set followlinks=False to avoid descending into directory symlinks, and collect directory listing errors through onerror.
  2. Use Path.lstat() to identify the entry itself as a symlink, then obtain its stored target string with os.readlink().
  3. Query the target with Path.stat(), which follows links. Classify a missing target path, or a file where a directory component is required, as broken. Record permission failures and link loops separately.

A relative target starts from the link’s parent directory, regardless of the script’s working directory. An existing external target is valid too, but the tool will not descend into an external directory through its symlink. The Python large-file ranking tool skips symlinks; this tool checks their targets instead.

This verifies target resolution, without validating contents, application formats, or readability. Use a quiet local project directory: the scan is not an atomic snapshot and cannot prevent directories from being replaced during traversal. Mount points inside real subdirectories may still be traversed.

2. Prepare the environment and repeatable sample

Use Linux, Python 3.9 or newer, and bash. No third-party packages are required. These commands were verified on Linux with Python 3.11.4.

  1. Create a separate directory in one bash session. Choose another directory name when repeating the exercise:
python3 --version
mkdir symlink-audit-demo
cd symlink-audit-demo
python3 - <<'PY_DATA'
from pathlib import Path
root = Path("sample")
(root / "nested").mkdir(parents=True)
(root / "data.txt").write_text("ready\n", encoding="utf-8")
(root / "good-file").symlink_to("data.txt")
(root / "good-dir").symlink_to("nested", target_is_directory=True)
(root / "missing-file").symlink_to("gone.txt")
(root / "missing-dir").symlink_to("gone-dir", target_is_directory=True)
PY_DATA
  1. Save the next section as check_symlinks.py in the current symlink-audit-demo directory, or download the complete script. The sample has four links: two valid targets, one missing file, and one missing directory.

3. Complete implementation: JSON and exit codes

"""Read-only symlink audit for a quiet local directory. Python 3.9+."""
import argparse
import json
import os
from pathlib import Path
import stat


def audit(root):
    broken = []
    errors = []
    symlinks = 0

    def record_error(path, exc):
        errors.append({"path": str(path), "errno": exc.errno,
                       "message": str(exc)})

    def walk_error(exc):
        record_error(exc.filename or root, exc)

    for folder, dirs, names in os.walk(root, onerror=walk_error,
                                       followlinks=False):
        dirs.sort()
        for name in sorted(dirs + names):
            path = Path(folder) / name
            try:
                mode = path.lstat().st_mode
                if not stat.S_ISLNK(mode):
                    continue
                symlinks += 1
                target = os.readlink(path)
            except OSError as exc:
                record_error(path, exc)
                continue
            try:
                path.stat()
            except (FileNotFoundError, NotADirectoryError):
                broken.append({"path": str(path.relative_to(root)),
                               "target": target})
            except OSError as exc:
                record_error(path, exc)

    return {"root": str(root), "symlinks": symlinks,
            "broken": sorted(broken, key=lambda item: item["path"]),
            "errors": sorted(errors, key=lambda item: item["path"]),
            "complete": not errors}


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("directory", type=Path, help="directory to inspect")
    args = parser.parse_args(argv)
    try:
        root = args.directory.resolve(strict=True)
        if not stat.S_ISDIR(root.stat().st_mode):
            parser.error("directory must refer to a directory")
    except (OSError, RuntimeError) as exc:
        parser.error(str(exc))

    report = audit(root)
    print(json.dumps(report, indent=2, sort_keys=True))
    if report["errors"]:
        return 2
    return 1 if report["broken"] else 0


if __name__ == "__main__":
    raise SystemExit(main())

The root resolves to its physical path first, so the supplied root itself may be a valid directory symlink. Inspecting both dirs and names includes directory links. Broken-link paths are relative to the root; error paths retain their absolute location. JSON escapes special characters, so spaces or newlines in names do not become extra records.

Exit code Meaning Next action
0 Scan completed with no broken links Continue the next task.
1 Scan completed and found broken links Review and restore targets, or review link paths separately.
2 Argument, root, or scanning error Read stderr or the report’s errors, fix the cause, and rerun.

Scanning errors take precedence and return 2, even when broken links were also found. complete: true means no scanning errors were recorded; broken links may still exist. Invalid arguments or roots produce stderr diagnostics without JSON. All broken links and errors remain in memory, making this suitable for troubleshooting project directories.

4. Validate missing targets and recovery

  1. Run the tool and capture its exit status. Save the report outside the inspected directory:
python3 check_symlinks.py --help
if python3 check_symlinks.py sample > report.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'exit=%s\n' "$scan_rc"
python3 - <<'PY_CHECK'
import json
from pathlib import Path
report = json.loads(Path("report.json").read_text())
assert report["symlinks"] == 4
assert report["complete"] is True
assert report["errors"] == []
assert report["broken"] == [
    {"path": "missing-dir", "target": "gone-dir"},
    {"path": "missing-file", "target": "gone.txt"},
]
print(f"symlinks={report['symlinks']} broken={len(report['broken'])} complete={report['complete']}")
PY_CHECK

The output should end with:

exit=1
symlinks=4 broken=2 complete=True
  1. Restore the two missing targets in this demo directory and rerun:
printf 'restored\n' > sample/gone.txt
mkdir sample/gone-dir
if python3 check_symlinks.py sample > repaired.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'exit=%s\n' "$scan_rc"

Expect exit=0, with empty broken and errors lists in repaired.json. The links themselves were neither removed nor recreated. In a real project, establish whether a target should be restored first; creating empty files merely to clear a warning can hide a problem.

  1. Add a pair of looping links to the sample after restoring its missing targets:
ln -s loop-b sample/loop-a
ln -s loop-a sample/loop-b
if python3 check_symlinks.py sample > loop-report.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'exit=%s\n' "$scan_rc"
python3 - <<'PY_LOOP'
import errno
import json
from pathlib import Path
report = json.loads(Path("loop-report.json").read_text())
assert report["complete"] is False
assert report["broken"] == []
assert len(report["errors"]) == 2
assert all(item["errno"] == errno.ELOOP for item in report["errors"])
print("loop errors=2 complete=False")
PY_LOOP

Expect exit=2 and loop errors=2 complete=False. A resolution loop must not look like a directory with no broken links. Additional checks covered empty directories, invalid roots, relative and absolute targets, Unicode names, a non-directory target component, and a real PermissionError in an unprivileged child process.

  1. For your own batch job, first prepare a writable report directory and replace the interpreter, script, data, and report paths below with your actual absolute paths. Do not add these example paths to a scheduler unchanged:
if /usr/bin/python3 /srv/tools/check_symlinks.py /srv/project/data > /srv/reports/symlinks.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'symlink audit exit=%s\n' "$scan_rc"
exit "$scan_rc"

This wrapper returns the tool’s status to its caller. The if also captures expected nonzero results when set -e is enabled. Redirection overwrites an earlier report; use a different report name for each run if you need history.

6. Troubleshooting and direct fixes

Symptom Action
Moving a target leaves a broken link Interpret target from the link’s parent directory. Restore the correct target or rebuild the link in a separate maintenance step, then rerun.
errors contains Permission denied Recheck directory read permission and search permission on each path component as the actual job account. Adjust permissions to the project’s needs; do not classify this as a missing target.
errors contains Too many levels of symbolic links Inspect the link chain, fix the loop or excessive chain length, and scan again.
The target exists but the application cannot read it This tool does not test content access. Check target permissions and format as the account running the application.
Broken links inside a directory symlink are absent Only that directory link’s target is checked. Run the tool again with the target directory as the root to inspect its contents.

7. Summary

Separating a link from its target and handling broken links and scan errors through exit codes makes missing-file problems repeatable to diagnose. The script only reads metadata and never deletes or repairs links automatically. To check whether ordinary data files have changed, continue with the SHA-256 dataset manifest tutorial.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它