From e0ec2ce60a7ebcb7d1c413a0b37089501d3453c2 Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sun, 21 Dec 2025 09:29:21 +0000 Subject: [PATCH] Add tab completion, update README, version bump 0.2.0 --- README.md | 10 +++++ chguard/cli.py | 118 ++++++++++++++++++++++++++++++++++++++++--------- pyproject.toml | 2 +- 3 files changed, 108 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1384f22..d6746a0 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,16 @@ Snapshots are stored in a local SQLite database containing: Usernames and permission strings are resolved only for display. +### TAB completion +Add this to your `.bashrc` +``` +eval "$(register-python-argcomplete chguard)" +``` +And then +``` +source ~/.bashrc +``` + ## pre-commit This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks). diff --git a/chguard/cli.py b/chguard/cli.py index 9fee496..fa4e1f3 100644 --- a/chguard/cli.py +++ b/chguard/cli.py @@ -1,6 +1,8 @@ from __future__ import annotations import argparse +import argcomplete +import importlib.metadata import os import sys import stat @@ -27,6 +29,13 @@ from chguard.restore import plan_restore, apply_restore from chguard.util import normalize_root +def get_version(): + try: + return importlib.metadata.version("chguard") + except importlib.metadata.PackageNotFoundError: + return "unknown" + + def _uid_to_name(uid: int) -> str: """Return username for uid, or uid as string if unknown.""" try: @@ -83,6 +92,19 @@ def _is_root() -> bool: return os.geteuid() == 0 +def complete_state_names(prefix, parsed_args, **kwargs): + try: + conn = connect( + Path(parsed_args.db).expanduser().resolve() + if parsed_args.db + else None + ) + rows = conn.execute("SELECT name FROM states").fetchall() + return [name for (name,) in rows if name.startswith(prefix)] + except Exception: + return [] + + def main() -> None: """ Entry point for the CLI. @@ -100,45 +122,99 @@ def main() -> None: ) actions = parser.add_mutually_exclusive_group(required=True) - actions.add_argument("--save", metavar="PATH", help="Save state for PATH") + + parser.add_argument( + "--version", + action="version", + version=f"chguard {get_version()}", + ) + actions.add_argument( - "--restore", action="store_true", help="Restore a saved state" - ) + "--save", + metavar="PATH", + help="Save state for PATH", + ).completer = argcomplete.FilesCompleter() + actions.add_argument( - "--list", action="store_true", help="List saved states" + "--restore", + action="store_true", + help="Restore a saved state", ) + actions.add_argument( - "--delete", metavar="STATE", help="Delete a saved state" + "--list", + action="store_true", + help="List saved states", + ) + + actions.add_argument( + "--delete", + metavar="STATE", + help="Delete a saved state", + ).completer = complete_state_names + + # positional STATE + parser.add_argument( + "state", + nargs="?", + help="State name (required with --restore)", + ).completer = complete_state_names + + parser.add_argument( + "--name", + help="State name (required with --save)", ) parser.add_argument( - "state", nargs="?", help="State name (required with --restore)" - ) - parser.add_argument("--name", help="State name (required with --save)") - parser.add_argument( - "--overwrite", action="store_true", help="Overwrite existing state" + "--overwrite", + action="store_true", + help="Overwrite existing state", ) parser.add_argument( - "--permissions", action="store_true", help="Restore MODE only" - ) - parser.add_argument( - "--owner", action="store_true", help="Restore OWNER only" + "--permissions", + action="store_true", + help="Restore MODE only", ) parser.add_argument( - "--dry-run", action="store_true", help="Preview only; do not apply" - ) - parser.add_argument( - "--yes", action="store_true", help="Apply without confirmation" + "--owner", + action="store_true", + help="Restore OWNER only", ) - parser.add_argument("--root", metavar="PATH", help="Override restore root") parser.add_argument( - "--exclude", action="append", default=[], help="Exclude path prefix" + "--dry-run", + action="store_true", + help="Preview only; do not apply", ) - parser.add_argument("--db", metavar="PATH", help="Override database path") + parser.add_argument( + "--yes", + action="store_true", + help="Apply without confirmation", + ) + + parser.add_argument( + "--root", + metavar="PATH", + help="Override restore root", + ).completer = argcomplete.FilesCompleter() + + parser.add_argument( + "--exclude", + action="append", + default=[], + help="Exclude path prefix", + ).completer = argcomplete.FilesCompleter() + + parser.add_argument( + "--db", + metavar="PATH", + help="Override database path", + ).completer = argcomplete.FilesCompleter() + + argcomplete.autocomplete(parser) args = parser.parse_args() console = Console() diff --git a/pyproject.toml b/pyproject.toml index 67919db..b2ab67a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chguard" -version = "0.1.0" +version = "0.2.0" description = "Safety-first tool to snapshot and restore filesystem ownership and permissions." authors = ["Marco D'Aleo "] license = "GPL-3.0-or-later"