Skip to content

path-utils

Cross-platform path resolver: Windows, macOS, Linux, WSL.

Metadata

  • Author: ropean, Claude Sonnet (Anthropic)
  • Version: 1.0.0
  • Dependencies: pathlib, platform
  • See Also: md-files/help/script-template.md

Code

python
#!/usr/bin/env python3
"""
@title path-utils
@description Cross-platform path resolver: Windows, macOS, Linux, WSL.
@author ropean, Claude Sonnet (Anthropic)
@version 1.0.0

Parses and resolves file paths from any of the following formats:
  - Windows absolute:   C:\\Users\\foo\\file.env   or   C:/Users/foo/file.env
  - UNC:                \\\\server\\share\\file.env
  - WSL mount:          /mnt/c/Users/foo/file.env
  - Unix absolute:      /home/foo/file.env
  - Tilde-expanded:     ~/projects/file.env
  - Relative:           ../file.env  or  .dev.vars

Can be used as a module (import resolve_path) or run directly for testing.

@example
    # As a module:
    from path_utils import resolve_path
    p = resolve_path("C:/Users/foo/.dev.vars")

    # Self-test:
    python path-utils.py --test

@requires pathlib, platform
@see md-files/help/script-template.md
"""

import os
import platform
import re
import sys
from pathlib import Path, PurePosixPath, PureWindowsPath

# ════════════════════════════════════════════════════════════
#  PLATFORM DETECTION
# ════════════════════════════════════════════════════════════

def _running_platform() -> str:
    """Return 'windows', 'wsl', 'macos', or 'linux'."""
    if sys.platform == "win32":
        return "windows"
    uname = platform.uname()
    if "microsoft" in uname.release.lower() or "microsoft" in uname.version.lower():
        return "wsl"
    if sys.platform == "darwin":
        return "macos"
    return "linux"

CURRENT_PLATFORM = _running_platform()

# ════════════════════════════════════════════════════════════
#  PATH CLASSIFIER
# ════════════════════════════════════════════════════════════

_WIN_ABS   = re.compile(r'^[A-Za-z]:[/\\]')   # C:\ or C:/
_UNC       = re.compile(r'^[/\\]{2}[^/\\]')    # \\server or //server
_WSL_MOUNT = re.compile(r'^/mnt/[a-z]/')       # /mnt/c/...

def _classify(path_str: str) -> str:
    s = path_str.strip()
    if _WIN_ABS.match(s):
        return "windows_abs"
    if _UNC.match(s):
        return "unc"
    if _WSL_MOUNT.match(s):
        return "wsl_mount"
    if s.startswith("~"):
        return "tilde"
    if s.startswith("/") or s.startswith("./") or s.startswith("../"):
        return "unix"
    # bare relative (no leading slash or dot-slash)
    return "relative"


# ════════════════════════════════════════════════════════════
#  CONVERSION HELPERS
# ════════════════════════════════════════════════════════════

def _win_abs_to_path(s: str) -> Path:
    """C:/Users/foo or C:\\Users\\foo → Path on any OS."""
    if CURRENT_PLATFORM in ("windows",):
        return Path(s)
    # On WSL/Linux/macOS, convert to /mnt/c/... style
    drive = s[0].lower()
    rest  = s[2:].replace("\\", "/").lstrip("/")
    return Path(f"/mnt/{drive}/{rest}")


def _unc_to_path(s: str) -> Path:
    """\\\\server\\share\\path → Path (Windows only; warn elsewhere)."""
    if CURRENT_PLATFORM == "windows":
        return Path(s)
    # UNC not natively supported on Linux/macOS — return as-is and let the
    # caller handle the resulting error
    return Path(s.replace("\\", "/"))


def _wsl_mount_to_path(s: str) -> Path:
    """
    /mnt/c/Users/foo  →  C:\\Users\\foo on Windows,
                      →  /mnt/c/Users/foo on WSL/Linux (already valid).
    """
    if CURRENT_PLATFORM == "windows":
        # /mnt/c/Users/foo → C:\Users\foo
        parts = PurePosixPath(s).parts  # ('/', 'mnt', 'c', 'Users', 'foo')
        drive  = parts[2].upper() + ":\\"
        rest   = "\\".join(parts[3:])
        return Path(drive + rest)
    return Path(s)


# ════════════════════════════════════════════════════════════
#  PUBLIC API
# ════════════════════════════════════════════════════════════

def resolve_path(path_str: str) -> Path:
    """
    Parse *path_str* into a fully-resolved, absolute :class:`pathlib.Path`
    on the current OS.

    Supports:
      - Windows absolute  (C:/…  or  C:\\…)
      - UNC               (\\\\server\\share\\…)
      - WSL mount         (/mnt/c/…)
      - Tilde             (~/…)
      - Unix absolute     (/home/…)
      - Relative          (./…  ../…  or  bare name)

    Returns the Path object; does **not** assert existence so you can use it
    for output paths too.
    """
    s = path_str.strip()
    kind = _classify(s)

    if kind == "windows_abs":
        p = _win_abs_to_path(s)
    elif kind == "unc":
        p = _unc_to_path(s)
    elif kind == "wsl_mount":
        p = _wsl_mount_to_path(s)
    elif kind == "tilde":
        p = Path(s).expanduser()
    else:  # unix absolute or relative
        p = Path(s)

    return p.resolve()


# ════════════════════════════════════════════════════════════
#  SELF-TEST
# ════════════════════════════════════════════════════════════

_TEST_CASES = [
    # (label, path_str, expected_fragment_on_platform)
    # expected_fragment is a substring we look for in the resolved string
    ("Windows abs (forward slash)",  "C:/Users/test/.env",         None),
    ("Windows abs (backslash)",      r"C:\Users\test\.env",        None),
    ("WSL mount path",               "/mnt/c/Users/test/.env",     None),
    ("Tilde expansion",              "~/.env",                     None),
    ("Unix absolute",                "/tmp/test/.env",             None),
    ("Relative dot-slash",           "./.env",                     None),
    ("Relative bare",                ".env",                       None),
]


def _color(code: int, text: str) -> str:
    if not sys.stdout.isatty():
        return text
    return f"\033[{code}m{text}\033[0m"


def run_tests() -> None:
    print(f"\nPlatform detected: {_color(36, CURRENT_PLATFORM)}\n")
    pad = max(len(lbl) for lbl, _, _ in _TEST_CASES) + 2
    passes = 0

    for label, raw, _ in _TEST_CASES:
        try:
            result = resolve_path(raw)
            status = _color(32, "PASS")
            detail = str(result)
            passes += 1
        except Exception as exc:
            status = _color(31, "FAIL")
            detail = f"ERROR: {exc}"

        print(f"  {status}  {label:<{pad}}  {_color(33, repr(raw))}")
        print(f"         -> {detail}\n")

    total = len(_TEST_CASES)
    summary = _color(32 if passes == total else 31, f"{passes}/{total} passed")
    print(f"Results: {summary}\n")


# ════════════════════════════════════════════════════════════
#  CLI ENTRY
# ════════════════════════════════════════════════════════════

def main() -> None:
    import argparse
    parser = argparse.ArgumentParser(
        description="Cross-platform path resolver utility.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="Run --test to execute the built-in test suite.",
    )
    parser.add_argument("path", nargs="?", help="Path string to resolve")
    parser.add_argument("--test", action="store_true", help="Run self-test suite")
    args = parser.parse_args()

    if args.test:
        run_tests()
        return

    if args.path:
        result = resolve_path(args.path)
        print(result)
        return

    parser.print_help()


if __name__ == "__main__":
    main()

File Information

  • Filename: path_utils.py
  • Category: python
  • Language: PYTHON

View on GitHub