"""
OS Helper — argparse-based command-line interface.
Thin wrapper around the pure functions exported by :mod:`os_helper` that
exposes the toolkit as subcommands under a single ``os-helper`` entry
point. Written with :mod:`argparse` from the standard library so the CLI
works out of the box on any Python install that has the package
installed — no extra runtime dependency required.
The CLI is deliberately *additive*: it exists on top of the library API,
never replaces it. Every existing public function stays importable from
``os_helper`` unchanged; each subcommand here is a shell-friendly wrapper
that translates flags into keyword arguments.
Subcommand groups
-----------------
- ``os`` — operating-system detection (``system`` / ``unix`` /
``linux`` / ``macos`` / ``windows`` / ``pid`` /
``workers`` / ``run`` / ``open``)
- ``path`` — path predicates and helpers (``exists`` / ``dir-exists``
/ ``join`` / ``abs`` / ``rel`` / ``no-home`` /
``size`` / ``split`` / ``glob`` / ``mkdir`` /
``rmdir`` / ``rm`` / ``cp``)
- ``hash`` — hashing (``string`` / ``file`` / ``folder``)
- ``str`` — string utilities (``empty`` / ``ascii``)
- ``config`` — configuration loading (``get``)
- ``temp`` — temporary file helpers (``file`` / ``folder``)
- ``misc`` — misc utilities (``now`` / ``format-size`` / ``describe``
/ ``url-ok`` / ``zip`` / ``download`` / ``time2str`` /
``str2time`` / ``ip``)
- ``prof`` — profiling (``wall`` / ``cpu`` / ``gpu``)
Usage Example
-------------
>>> # os-helper os system
>>> # os-helper path exists ~/somefile.txt
>>> # os-helper hash string hello --size 8
>>> # os-helper hash file ./pyproject.toml
>>> # os-helper misc now --fmt filename
>>> # os-helper misc format-size 12345678
>>> # os-helper temp folder --prefix demo
>>> # os-helper prof wall -- sleep 0.1
Author
------
Warith Harchaoui, Ph.D. — https://linkedin.com/in/warith-harchaoui/
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from collections.abc import Callable, Sequence
# Import the pure functions once here — every handler is a thin dispatch
# on top of these, no logic duplication with the library.
from . import (
absolute2relative_path,
asciistring,
checkfile,
copyfile,
dir_exists,
download_file,
emptystring,
file_exists,
folder_description,
folder_name_ext,
format_size,
get_config,
get_nb_workers,
get_user_ip,
getpid,
hash_string,
hashfile,
hashfolder,
is_working_url,
join,
linux,
macos,
make_directory,
now_string,
openfile,
path_without_home,
recursive_glob,
relative2absolute_path,
remove_directory,
remove_files,
size_file,
str2time,
system,
temporary_filename,
temporary_folder,
time2str,
unix,
windows,
zip_folder,
)
# ---------------------------------------------------------------------------
# Small utilities
# ---------------------------------------------------------------------------
def _emit(text: object) -> None:
"""Write one result line to stdout (the CLI's data channel).
Parameters
----------
text : object
Value to print; stringified then followed by a single newline.
Notes
-----
This is the CLI's *data* output surface, deliberately distinct from the
package logging surface (rule 6). A CLI must be able to write its computed
result to stdout so shell pipelines can consume it; routing that through a
single helper (rather than scattering bare ``print`` calls) keeps the
output channel in one place and byte-for-byte identical to ``print(text)``.
"""
# Mirror ``print(text)`` exactly: stringify, then append one newline.
sys.stdout.write(f"{text}\n")
def _emit_err(text: object) -> None:
"""Write one diagnostic line to stderr, away from the piped data stream.
Parameters
----------
text : object
Value to print to stderr; stringified then followed by a newline.
"""
# Diagnostics (timings, notices) go to fd 2 so they never pollute the
# stdout data channel that callers may be piping elsewhere.
sys.stderr.write(f"{text}\n")
def _echo_json(payload: object) -> None:
"""Emit a structured payload as pretty-printed, sorted JSON on stdout.
Parameters
----------
payload : object
Any JSON-serializable value (dict, list, scalar).
"""
# Structured outputs use JSON so shell pipelines can `| jq` on them;
# ``sort_keys`` makes the output deterministic across runs.
_emit(json.dumps(payload, indent=2, sort_keys=True))
# ---------------------------------------------------------------------------
# os — operating-system probes and process helpers
# ---------------------------------------------------------------------------
def _handle_os_system(_: argparse.Namespace) -> int:
"""Print the current operating-system short name on stdout.
Parameters
----------
_ : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# `system` here refers to platform.system(); print a short name.
if windows():
_emit("windows")
elif macos():
_emit("macos")
elif linux():
_emit("linux")
else:
_emit("unknown")
return 0
def _handle_os_flag(fn: Callable[[], bool]) -> Callable[[argparse.Namespace], int]:
"""Build a handler that prints ``true``/``false`` for a boolean OS probe.
Parameters
----------
fn : Callable[[], bool]
A zero-argument predicate such as :func:`os_helper.unix` whose result
should be surfaced to the shell.
Returns
-------
Callable[[argparse.Namespace], int]
A subcommand handler that emits the stringified boolean and returns 0.
"""
# Factory: build a handler that prints "true" / "false" for a bool probe.
def _run(_: argparse.Namespace) -> int:
"""Emit the probe result as ``true``/``false``.
Parameters
----------
_ : argparse.Namespace
Unused parsed CLI arguments (probes take no options).
Returns
-------
int
Always 0 — the boolean value is carried on stdout, not the exit code.
"""
_emit("true" if fn() else "false")
return 0
return _run
def _handle_os_pid(_: argparse.Namespace) -> int:
"""Print the current process ID on stdout.
Parameters
----------
_ : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(getpid())
return 0
def _handle_os_workers(ns: argparse.Namespace) -> int:
"""Resolve and print a worker count (sklearn n_jobs convention).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# Mirrors sklearn's n_jobs convention.
_emit(get_nb_workers(ns.n))
return 0
def _handle_os_run(ns: argparse.Namespace) -> int:
"""Run a shell-style command and stream its captured stdout/stderr.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# Execute a shell-style command via os_helper.system() and print stdout.
result = system(
cmd=ns.cmd,
expected_output=ns.expected or "",
check_exitcode=not ns.no_check_exitcode,
check_empty=ns.check_empty,
)
# Return the captured stdout verbatim; stderr goes to fd 2 so it can be
# inspected but does not pollute the piped output.
if result["out"]:
sys.stdout.write(result["out"])
if result["err"]:
sys.stderr.write(result["err"])
return 0
def _handle_os_open(ns: argparse.Namespace) -> int:
"""Open a file in the platform's default application.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# Open a file in the platform's default application.
openfile(ns.path)
return 0
# ---------------------------------------------------------------------------
# path — filesystem predicates and manipulations
# ---------------------------------------------------------------------------
def _handle_path_exists(ns: argparse.Namespace) -> int:
"""Report whether a file exists (exit 0 when it does).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
ok = file_exists(ns.path, check_empty=ns.non_empty)
_emit("true" if ok else "false")
return 0 if ok else 1
def _handle_path_dir_exists(ns: argparse.Namespace) -> int:
"""Report whether a directory exists (exit 0 when it does).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
ok = dir_exists(ns.path, check_empty=ns.non_empty)
_emit("true" if ok else "false")
return 0 if ok else 1
def _handle_path_join(ns: argparse.Namespace) -> int:
"""Join path components into one normalized absolute path.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# `join` normalizes and returns an absolute path; ideal for shell pipelines.
_emit(join(*ns.parts))
return 0
def _handle_path_abs(ns: argparse.Namespace) -> int:
"""Print the absolute form of a path.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(relative2absolute_path(ns.path, checkpath=ns.check))
return 0
def _handle_path_rel(ns: argparse.Namespace) -> int:
"""Print a path relative to a base directory.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(absolute2relative_path(ns.path, base_path=ns.base))
return 0
def _handle_path_no_home(ns: argparse.Namespace) -> int:
"""Print a path with the home-directory prefix collapsed to '~'.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(path_without_home(ns.path))
return 0
def _handle_path_size(ns: argparse.Namespace) -> int:
"""Print a file's size in bytes (-1 when it does not exist).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# -1 signals "does not exist" — that's what the library returns.
_emit(size_file(ns.path))
return 0
def _handle_path_split(ns: argparse.Namespace) -> int:
"""Print a path decomposed into folder/name/ext as JSON.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
folder, name, ext = folder_name_ext(ns.path, checkpath=ns.check)
_echo_json({"folder": folder, "name": name, "ext": ext})
return 0
def _handle_path_glob(ns: argparse.Namespace) -> int:
"""Print every file matching a glob pattern recursively.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
for match in recursive_glob(ns.root, ns.pattern):
_emit(match)
return 0
def _handle_path_mkdir(ns: argparse.Namespace) -> int:
"""Create a directory (and any missing parents).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
make_directory(ns.path, exist_ok=not ns.strict)
return 0
def _handle_path_rmdir(ns: argparse.Namespace) -> int:
"""Recursively remove a directory (missing is a no-op).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
remove_directory(ns.path)
return 0
def _handle_path_rm(ns: argparse.Namespace) -> int:
"""Remove a list of files on a best-effort basis.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
remove_files(ns.paths)
return 0
def _handle_path_cp(ns: argparse.Namespace) -> int:
"""Copy a file, preserving metadata.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
copyfile(ns.source, ns.dest)
return 0
def _handle_path_check(ns: argparse.Namespace) -> int:
"""Assert a file exists (and optionally is non-empty).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# Assertion-flavored: raises if the file is missing / empty.
checkfile(ns.path, msg=ns.msg or "", check_empty=ns.non_empty)
return 0
# ---------------------------------------------------------------------------
# hash — hashing strings, files, folders
# ---------------------------------------------------------------------------
def _handle_hash_string(ns: argparse.Namespace) -> int:
"""Print the hash of a string (optionally truncated).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(hash_string(ns.value, size=ns.size))
return 0
def _handle_hash_file(ns: argparse.Namespace) -> int:
"""Print the hash of a file's content and/or date.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(hashfile(ns.path, hash_content=not ns.path_only, date=ns.date))
return 0
def _handle_hash_folder(ns: argparse.Namespace) -> int:
"""Print the hash of a folder's contents and/or path/date.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(
hashfolder(
ns.path,
hash_content=not ns.no_content,
hash_path=ns.include_path,
date=ns.date,
)
)
return 0
# ---------------------------------------------------------------------------
# str — string utilities
# ---------------------------------------------------------------------------
def _handle_str_empty(ns: argparse.Namespace) -> int:
"""Report whether a string is empty/whitespace (exit 0 when it is).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
result = emptystring(ns.value)
_emit("true" if result else "false")
return 0 if result else 1
def _handle_str_ascii(ns: argparse.Namespace) -> int:
"""Print an ASCII-safe slug derived from the input string.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(
asciistring(
ns.value,
replacement_char=ns.replacement,
lower=not ns.preserve_case,
allow_digits=not ns.no_digits,
)
)
return 0
# ---------------------------------------------------------------------------
# config — configuration loader
# ---------------------------------------------------------------------------
def _handle_config_get(ns: argparse.Namespace) -> int:
"""Load configuration keys and print the result as JSON.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# `get_config` raises RuntimeError when nothing is found; let it propagate
# so the exit code is non-zero and the reason lands on stderr.
result = get_config(
keys=ns.keys,
config_type=ns.name,
path=ns.path,
env_files=ns.env_files or None,
)
_echo_json(result)
return 0
# ---------------------------------------------------------------------------
# temp — temporary file / folder scratch space
# ---------------------------------------------------------------------------
def _handle_temp_file(ns: argparse.Namespace) -> int:
"""Create a temporary file and print its path.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# We deliberately do NOT auto-delete when the CLI creates a scratch
# path — the caller decides. Print the path so shell pipelines can
# chain on it. When `--keep` is not set, the file is created and
# immediately deleted (proof-of-life mode).
with temporary_filename(
suffix=ns.suffix or "",
mode=ns.mode,
prefix=ns.prefix or "",
delete=not ns.keep,
) as path:
_emit(path)
return 0
def _handle_temp_folder(ns: argparse.Namespace) -> int:
"""Create a temporary directory and print its path.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
with temporary_folder(prefix=ns.prefix or "", delete=not ns.keep) as path:
_emit(path)
return 0
# ---------------------------------------------------------------------------
# misc — grab-bag utilities
# ---------------------------------------------------------------------------
def _handle_misc_now(ns: argparse.Namespace) -> int:
"""Print a formatted current timestamp.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(now_string(ns.fmt))
return 0
def _handle_misc_format_size(ns: argparse.Namespace) -> int:
"""Print a byte count as a human-readable size.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(format_size(ns.bytes))
return 0
def _handle_misc_describe(ns: argparse.Namespace) -> int:
"""Describe a folder's contents and print the mapping as JSON.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# Emit the raw {path: size} mapping as JSON. Companion index.html and
# description.json are written into the folder when flags allow.
result = folder_description(
ns.path,
recursive=not ns.flat,
index_html=not ns.no_html,
with_size=not ns.no_size,
description_json=not ns.no_json,
)
_echo_json(result)
return 0
def _handle_misc_url_ok(ns: argparse.Namespace) -> int:
"""Report whether a URL is valid and reachable (exit 0 when so).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
ok = is_working_url(ns.url)
_emit("true" if ok else "false")
return 0 if ok else 1
def _handle_misc_zip(ns: argparse.Namespace) -> int:
"""Zip a folder, skipping hidden files.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
zip_folder(ns.folder, zip_file_path=ns.output or "")
return 0
def _handle_misc_download(ns: argparse.Namespace) -> int:
"""Download a URL to a local file.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
download_file(ns.url, file_path=ns.output or "")
return 0
def _handle_misc_time2str(ns: argparse.Namespace) -> int:
"""Convert a seconds value to a readable duration string.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(time2str(ns.seconds, no_space=ns.no_space))
return 0
def _handle_misc_str2time(ns: argparse.Namespace) -> int:
"""Parse a duration string into seconds and print it.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_emit(str2time(ns.value))
return 0
def _handle_misc_ip(_: argparse.Namespace) -> int:
"""Fetch and print the caller's public IP addresses as JSON.
Parameters
----------
_ : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
_echo_json(get_user_ip())
return 0
# ---------------------------------------------------------------------------
# prof — profiling of an arbitrary command
# ---------------------------------------------------------------------------
def _run_subprocess(argv: list[str]) -> int:
"""Run a caller-provided command as a child process, inheriting stdio.
Parameters
----------
argv : list of str
The command and its arguments (e.g. ``["sleep", "1"]``).
Returns
-------
int
The subprocess's own exit code, so the wrapper is transparent.
"""
# ``subprocess.call`` wires the child to the parent's stdio, so the command
# behaves exactly as if run directly (`os-helper prof wall -- sleep 1`).
return subprocess.call(argv)
def _handle_prof_wall(ns: argparse.Namespace) -> int:
"""Time a subprocess by wall-clock and print the seconds on stderr.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
start = time.perf_counter()
rc = _run_subprocess(list(ns.argv))
elapsed = time.perf_counter() - start
_emit_err(f"{elapsed:.6f}")
return rc
def _handle_prof_cpu(ns: argparse.Namespace) -> int:
"""Time a subprocess by child CPU time and print seconds on stderr.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# process_time() only captures Python-side CPU; wrapping a subprocess
# here would report ~0 s. We measure via os.times() which sums children.
import os as _os
before = _os.times()
rc = _run_subprocess(list(ns.argv))
after = _os.times()
cpu_s = (after.children_user + after.children_system) - (
before.children_user + before.children_system
)
_emit_err(f"{cpu_s:.6f}")
return rc
def _handle_prof_gpu(ns: argparse.Namespace) -> int:
"""GPU-profile a subprocess (falls back to wall-clock timing).
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success).
"""
# Not meaningful for a subprocess wrapper, but keeping the surface
# symmetric with wall / cpu. Prints a clear notice on stderr and
# falls back to wall-clock timing.
_emit_err("gpu profiling of an external subprocess is not supported; using wall-clock timing")
return _handle_prof_wall(ns)
# ---------------------------------------------------------------------------
# gui — optional Tree Radar treemap dashboard (needs the [gui] extra)
# ---------------------------------------------------------------------------
def _handle_gui(ns: argparse.Namespace) -> int:
"""Launch the optional Tree Radar treemap GUI.
The GUI and its web stack (FastAPI/uvicorn) live behind the optional
``os-helper[gui]`` extra, so the import happens *here*, inside the
handler — never at module load. This keeps the plain CLI (and the
library) usable without the web dependencies.
Parameters
----------
ns : argparse.Namespace
Parsed CLI arguments for this subcommand.
Returns
-------
int
Process exit code (0 on success, 1 if the ``gui`` extra is missing).
"""
# Import lazily so `os-helper` (and `import os_helper`) never require
# FastAPI. A missing extra becomes a clear message + exit 1, not a stack.
try:
from .gui import run as _run_gui
except ImportError as exc: # pragma: no cover — only without the extra
_emit_err(f"GUI unavailable: {exc}")
return 1
# run() blocks until Ctrl-C; it raises a friendly ImportError itself if
# FastAPI/uvicorn are absent, which we catch and surface as a clean exit.
try:
_run_gui(root=ns.root, host=ns.host, port=ns.port)
except ImportError as exc:
_emit_err(f"GUI unavailable: {exc}")
return 1
return 0
def _add_gui_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``gui`` subcommand to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this command on.
"""
# A single verb (no sub-actions): `os-helper gui --root ~/Downloads`.
p = sub.add_parser(
"gui",
help="Launch the optional Tree Radar treemap dashboard (needs the [gui] extra).",
)
p.add_argument(
"--root", default=None, help="Folder to pre-fill and scan (default: current directory)."
)
p.add_argument(
"--host", default="127.0.0.1", help="Bind interface (default: 127.0.0.1, localhost only)."
)
p.add_argument("--port", type=int, default=8017, help="Port to serve on (default: 8017).")
p.set_defaults(func=_handle_gui)
# ---------------------------------------------------------------------------
# Parser construction — one helper per subcommand group keeps this readable
# ---------------------------------------------------------------------------
def _add_os_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``os`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("os", help="Operating-system detection and process helpers.")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
# Each action registers its handler via set_defaults(func=...); main()
# then dispatches on that attribute, so no manual command table is needed.
s.add_parser(
"system", help="Print the current OS name (macos / linux / windows / unknown)."
).set_defaults(func=_handle_os_system)
s.add_parser("unix", help="Print 'true' if the current OS is Unix-based.").set_defaults(
func=_handle_os_flag(unix)
)
s.add_parser("linux", help="Print 'true' if the current OS is Linux.").set_defaults(
func=_handle_os_flag(linux)
)
s.add_parser("macos", help="Print 'true' if the current OS is macOS.").set_defaults(
func=_handle_os_flag(macos)
)
s.add_parser("windows", help="Print 'true' if the current OS is Windows.").set_defaults(
func=_handle_os_flag(windows)
)
s.add_parser("pid", help="Print the current process ID.").set_defaults(func=_handle_os_pid)
p = s.add_parser(
"workers", help="Resolve a worker count following sklearn's n_jobs convention."
)
p.add_argument(
"--n",
type=int,
default=-1,
help="0 = full pool; >0 = exact count; <0 = pool+n+1 (default -1 = all).",
)
p.set_defaults(func=_handle_os_workers)
p = s.add_parser("run", help="Run a shell-style command and capture stdout / stderr.")
p.add_argument("cmd", help="Command line (parsed with shlex; no real shell involved).")
p.add_argument(
"--expected", default=None, help="File or directory expected to exist after success."
)
p.add_argument(
"--check-empty", action="store_true", help="Also require --expected to be non-empty."
)
p.add_argument("--no-check-exitcode", action="store_true", help="Do not assert exit code == 0.")
p.set_defaults(func=_handle_os_run)
p = s.add_parser("open", help="Open a file in the platform's default application.")
p.add_argument("path", help="File path to open.")
p.set_defaults(func=_handle_os_open)
def _add_path_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``path`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("path", help="Filesystem predicates and helpers.")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
# Predicate commands (exists / dir-exists) additionally encode their result
# in the process exit code; mutating commands (mkdir / rm / cp) return 0.
p = s.add_parser("exists", help="Check if a file exists (exit 0 = yes).")
p.add_argument("path")
p.add_argument(
"--non-empty", action="store_true", help="Also require the file to be non-empty."
)
p.set_defaults(func=_handle_path_exists)
p = s.add_parser("dir-exists", help="Check if a directory exists (exit 0 = yes).")
p.add_argument("path")
p.add_argument(
"--non-empty", action="store_true", help="Also require the directory to be non-empty."
)
p.set_defaults(func=_handle_path_dir_exists)
p = s.add_parser("join", help="Join components into a normalized absolute path.")
p.add_argument("parts", nargs="+", help="Path components to join.")
p.set_defaults(func=_handle_path_join)
p = s.add_parser("abs", help="Convert a path to absolute (with optional existence check).")
p.add_argument("path")
p.add_argument("--check", action="store_true", help="Assert the resulting path exists.")
p.set_defaults(func=_handle_path_abs)
p = s.add_parser("rel", help="Convert a path to relative from --base (default cwd).")
p.add_argument("path")
p.add_argument(
"--base", default=None, help="Reference path (default: current working directory)."
)
p.set_defaults(func=_handle_path_rel)
p = s.add_parser("no-home", help="Replace the user's home prefix with '~'.")
p.add_argument("path")
p.set_defaults(func=_handle_path_no_home)
p = s.add_parser("size", help="Print a file's size in bytes (-1 when it does not exist).")
p.add_argument("path")
p.set_defaults(func=_handle_path_size)
p = s.add_parser("split", help="Decompose a path into folder/name/ext as JSON.")
p.add_argument("path")
p.add_argument("--check", action="store_true")
p.set_defaults(func=_handle_path_split)
p = s.add_parser("glob", help="Recursively glob for files matching PATTERN under ROOT.")
p.add_argument("root")
p.add_argument("pattern")
p.set_defaults(func=_handle_path_glob)
p = s.add_parser("mkdir", help="Create a directory (and parents).")
p.add_argument("path")
p.add_argument("--strict", action="store_true", help="Fail if the directory already exists.")
p.set_defaults(func=_handle_path_mkdir)
p = s.add_parser("rmdir", help="Recursively remove a directory (missing = no-op).")
p.add_argument("path")
p.set_defaults(func=_handle_path_rmdir)
p = s.add_parser("rm", help="Remove files best-effort (missing entries skipped).")
p.add_argument("paths", nargs="+")
p.set_defaults(func=_handle_path_rm)
p = s.add_parser("cp", help="Copy a file, preserving metadata.")
p.add_argument("source")
p.add_argument("dest")
p.set_defaults(func=_handle_path_cp)
p = s.add_parser("check", help="Assert a file exists (and optionally is non-empty).")
p.add_argument("path")
p.add_argument("--msg", default=None)
p.add_argument("--non-empty", action="store_true")
p.set_defaults(func=_handle_path_check)
def _add_hash_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``hash`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("hash", help="Hashing helpers (RIPEMD-160 / BLAKE2b fallback).")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
p = s.add_parser("string", help="Hash a string; --size N truncates to N chars.")
p.add_argument("value")
p.add_argument("--size", type=int, default=-1)
p.set_defaults(func=_handle_hash_string)
p = s.add_parser("file", help="Hash a file's content (and optionally the current date).")
p.add_argument("path")
p.add_argument(
"--path-only", action="store_true", help="Hash the file path instead of its content."
)
p.add_argument("--date", action="store_true", help="Mix the current date into the hash.")
p.set_defaults(func=_handle_hash_file)
p = s.add_parser("folder", help="Hash a folder's contents (and optionally path / date).")
p.add_argument("path")
p.add_argument("--no-content", action="store_true")
p.add_argument("--include-path", action="store_true")
p.add_argument("--date", action="store_true")
p.set_defaults(func=_handle_hash_folder)
def _add_str_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``str`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("str", help="String utilities.")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
# ``empty`` doubles as a shell test: its exit code mirrors the boolean.
p = s.add_parser("empty", help="Check if a string is None / whitespace-only (exit 0 = yes).")
p.add_argument("value")
p.set_defaults(func=_handle_str_empty)
p = s.add_parser("ascii", help="Normalize a string to a filesystem-safe ASCII slug.")
p.add_argument("value")
p.add_argument("--replacement", default="-", help="Character used to replace disallowed chars.")
p.add_argument("--preserve-case", action="store_true", help="Keep case (default: lowercase).")
p.add_argument("--no-digits", action="store_true", help="Disallow digits in the result.")
p.set_defaults(func=_handle_str_ascii)
def _add_config_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``config`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("config", help="Configuration loading (JSON / YAML / .env / env vars).")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
# ``--keys`` takes one-or-more names; the loader fails (non-zero) if any
# requested key is missing from every configured source.
p = s.add_parser("get", help="Load a set of keys, returning JSON on stdout.")
p.add_argument("--name", required=True, help="Human-readable label used in log messages.")
p.add_argument("--keys", required=True, nargs="+", help="Keys to load.")
p.add_argument("--path", default=None, help="Config file or directory to search.")
p.add_argument(
"--env-files", nargs="*", default=None, help="Extra .env files to merge into os.environ."
)
p.set_defaults(func=_handle_config_get)
def _add_temp_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``temp`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("temp", help="Temporary file / folder helpers.")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
# ``--keep`` flips the library's auto-delete off so the printed path
# survives past the CLI process for downstream shell steps.
p = s.add_parser("file", help="Create a temporary file and print its path.")
p.add_argument("--suffix", default=None)
p.add_argument("--prefix", default=None)
p.add_argument("--mode", default="wt")
p.add_argument("--keep", action="store_true", help="Do not delete on exit.")
p.set_defaults(func=_handle_temp_file)
p = s.add_parser("folder", help="Create a temporary directory and print its path.")
p.add_argument("--prefix", default=None)
p.add_argument("--keep", action="store_true", help="Do not delete on exit.")
p.set_defaults(func=_handle_temp_folder)
def _add_misc_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``misc`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("misc", help="Miscellaneous utilities.")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
# A grab-bag group: some commands print scalars, others emit JSON so the
# richer results (describe / ip) stay machine-parseable.
p = s.add_parser("now", help="Print a formatted timestamp.")
p.add_argument(
"--fmt", default="log", choices=["log", "filename"], help="Format (default 'log')."
)
p.set_defaults(func=_handle_misc_now)
p = s.add_parser("format-size", help="Format a byte count as a human-readable string.")
p.add_argument("bytes", type=int)
p.set_defaults(func=_handle_misc_format_size)
p = s.add_parser(
"describe", help="Describe a folder's contents as JSON (relative path -> size)."
)
p.add_argument("path")
p.add_argument("--flat", action="store_true", help="Do not descend into subdirectories.")
p.add_argument("--no-html", action="store_true", help="Do not write index.html.")
p.add_argument("--no-json", action="store_true", help="Do not write description.json.")
p.add_argument("--no-size", action="store_true", help="Hide the size column in the HTML index.")
p.set_defaults(func=_handle_misc_describe)
p = s.add_parser("url-ok", help="Check whether a URL is syntactically valid + reachable.")
p.add_argument("url")
p.set_defaults(func=_handle_misc_url_ok)
p = s.add_parser("zip", help="Zip a folder (skipping hidden files).")
p.add_argument("folder")
p.add_argument("--output", default=None, help="Output archive path (default: folder + '.zip').")
p.set_defaults(func=_handle_misc_zip)
p = s.add_parser("download", help="Download a URL to a local file.")
p.add_argument("url")
p.add_argument("--output", default=None)
p.set_defaults(func=_handle_misc_download)
p = s.add_parser("time2str", help="Convert seconds to a readable duration string.")
p.add_argument("seconds", type=float)
p.add_argument("--no-space", action="store_true")
p.set_defaults(func=_handle_misc_time2str)
p = s.add_parser("str2time", help="Parse a duration string into seconds.")
p.add_argument("value")
p.set_defaults(func=_handle_misc_str2time)
p = s.add_parser("ip", help="Fetch the caller's public IPv4 / IPv6 addresses.")
p.set_defaults(func=_handle_misc_ip)
def _add_prof_group(sub: argparse._SubParsersAction) -> None:
"""Attach the ``prof`` subcommand group to the parser.
Parameters
----------
sub : argparse._SubParsersAction
The top-level subparser action to register this group on.
"""
g = sub.add_parser("prof", help="Profile an arbitrary subcommand.")
s = g.add_subparsers(dest="action", metavar="ACTION")
s.required = True
# The three prof subcommands share identical wiring (only the handler
# differs), so build them from a table instead of copy-pasting three blocks.
for name, handler, doc in (
("wall", _handle_prof_wall, "Wall-clock elapsed time (seconds on stderr)."),
("cpu", _handle_prof_cpu, "CPU time consumed by the child subprocess."),
("gpu", _handle_prof_gpu, "GPU timing (falls back to wall-clock for subprocesses)."),
):
p = s.add_parser(name, help=doc)
# ``REMAINDER`` swallows everything after the subcommand verbatim, so
# the wrapped command's own flags (e.g. ``ffmpeg -i ...``) are not
# mis-parsed as flags for os-helper itself.
p.add_argument(
"argv",
nargs=argparse.REMAINDER,
help="Command to run (put after '--' to avoid flag collisions).",
)
p.set_defaults(func=handler)
[docs]
def build_parser() -> argparse.ArgumentParser:
"""
Assemble the top-level ``os-helper`` argument parser.
Returns
-------
argparse.ArgumentParser
Fully wired parser with every subcommand group attached.
"""
parser = argparse.ArgumentParser(
prog="os-helper",
description=(
"OS Helper — cross-platform utility CLI (OS detection, path "
"manipulation, hashing, config loading, temp files, "
"profiling)."
),
)
# Every non-trivial CLI benefits from `--version` — cheap to add and
# oncall people always look for it. We resolve it lazily so a broken
# importlib.metadata does not break the whole CLI.
try:
from importlib.metadata import version as _pkg_version
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {_pkg_version('os-helper')}",
)
except Exception: # pragma: no cover — never fatal
pass
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
subparsers.required = True
_add_os_group(subparsers)
_add_path_group(subparsers)
_add_hash_group(subparsers)
_add_str_group(subparsers)
_add_config_group(subparsers)
_add_temp_group(subparsers)
_add_misc_group(subparsers)
_add_prof_group(subparsers)
_add_gui_group(subparsers)
return parser
[docs]
def main(argv: Sequence[str] | None = None) -> int:
"""
Entry point invoked by ``os-helper`` (see ``[project.scripts]``).
Parameters
----------
argv : sequence of str, optional
Arguments to parse. Defaults to ``sys.argv[1:]`` when None.
Returns
-------
int
Process exit code (``0`` on success).
"""
parser = build_parser()
args = parser.parse_args(argv)
# Every subparser sets ``func`` via ``set_defaults`` — no dispatch
# table needed, argparse resolved it for us.
return int(args.func(args))
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())