-
Notifications
You must be signed in to change notification settings - Fork 0
Add env sync CLI to export indexed paths as environment variables and document pipx installation
#2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,41 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
| from .actions import copy_to_clipboard, open_path, open_terminal | ||
| from .collectors import collect_alias_entries, collect_env_entries, collect_path_entries | ||
| from .search import to_pseudo_path | ||
| from .storage import ensure_storage, insert_entries, load_aliases, reset_index, search_entries, save_aliases | ||
|
|
||
| MANAGED_ENV_BEGIN = "# >>> pathi managed variables >>>" | ||
| MANAGED_ENV_END = "# <<< pathi managed variables <<<" | ||
|
|
||
|
|
||
| def normalize_env_name(name: str) -> str: | ||
| normalized = re.sub(r"[^A-Za-z0-9_]", "_", name.upper()) | ||
| if not normalized: | ||
| return "PI_PATH" | ||
| if normalized[0].isdigit(): | ||
| return f"PI_{normalized}" | ||
| return normalized | ||
|
|
||
|
|
||
| def is_env_name(name: str) -> bool: | ||
| return bool(re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name)) | ||
|
|
||
|
|
||
| def replace_managed_block(content: str, block: str) -> str: | ||
| pattern = re.compile( | ||
| rf"\n?{re.escape(MANAGED_ENV_BEGIN)}\n.*?\n{re.escape(MANAGED_ENV_END)}\n?", | ||
| re.DOTALL, | ||
| ) | ||
| cleaned = re.sub(pattern, "\n", content).rstrip("\n") | ||
| if cleaned: | ||
| return f"{cleaned}\n\n{block}\n" | ||
| return f"{block}\n" | ||
|
|
||
|
|
||
| def build_index() -> int: | ||
| root = ensure_storage() | ||
|
|
@@ -114,6 +142,45 @@ def cmd_alias_list(_: argparse.Namespace) -> int: | |
| return 0 | ||
|
|
||
|
|
||
| def cmd_env_sync(args: argparse.Namespace) -> int: | ||
| root = ensure_storage() | ||
| db = root / "index.sqlite" | ||
| if args.rebuild: | ||
| build_index() | ||
|
|
||
| entries = search_entries(db, "", 1_000_000) | ||
| selected = [e for e in entries if args.source == "all" or e.source == args.source] | ||
|
|
||
| out: dict[str, str] = {} | ||
| skipped: list[str] = [] | ||
| for entry in selected: | ||
| key = normalize_env_name(entry.name) if args.normalize else entry.name | ||
| if not is_env_name(key): | ||
| skipped.append(entry.name) | ||
| continue | ||
| out.setdefault(key, entry.path) | ||
|
|
||
| lines = [MANAGED_ENV_BEGIN] | ||
| for key in sorted(out): | ||
| lines.append(f'{key}="{out[key]}"') | ||
| lines.append(MANAGED_ENV_END) | ||
| block = "\n".join(lines) | ||
|
|
||
| if args.print_only: | ||
| print(block) | ||
| else: | ||
| target = Path(args.file).expanduser() | ||
| existing = target.read_text(encoding="utf-8") if target.exists() else "" | ||
| updated = replace_managed_block(existing, block) | ||
| target.write_text(updated, encoding="utf-8") | ||
| print(f"Updated {target}") | ||
|
|
||
| print(f"Exported {len(out)} variables") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| if skipped: | ||
| print(f"Skipped invalid names: {len(skipped)}") | ||
| return 0 | ||
|
|
||
|
|
||
| def build_parser() -> argparse.ArgumentParser: | ||
| p = argparse.ArgumentParser(prog="pathi", description="Path Index CLI") | ||
| sub = p.add_subparsers(dest="command", required=True) | ||
|
|
@@ -155,6 +222,17 @@ def build_parser() -> argparse.ArgumentParser: | |
| a = alias_sub.add_parser("list", help="List aliases") | ||
| a.set_defaults(func=cmd_alias_list) | ||
|
|
||
| env = sub.add_parser("env", help="Export index entries as environment variables") | ||
| env_sub = env.add_subparsers(dest="env_cmd", required=True) | ||
|
|
||
| e = env_sub.add_parser("sync", help="Sync variables to an env file (e.g. /etc/environment)") | ||
| e.add_argument("--file", default="/etc/environment") | ||
| e.add_argument("--source", choices=["all", "alias", "env", "path"], default="all") | ||
| e.add_argument("--normalize", action="store_true", help="Convert names to valid ENV keys") | ||
| e.add_argument("--rebuild", action="store_true", help="Rebuild index before export") | ||
| e.add_argument("--print-only", action="store_true", help="Print managed block instead of writing file") | ||
| e.set_defaults(func=cmd_env_sync) | ||
|
|
||
| return p | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The export line
f'{key}="{out[key]}"'writes raw paths into quoted values without escaping, so any indexed path containing a double quote (valid on POSIX filesystems, e.g. an alias path like/tmp/a"b) produces an invalid assignment and breaks consumers of the generated block. Becauseenv syncis meant to generate reusable environment-variable definitions, values should be escaped or serialized in a format that preserves arbitrary path characters.Useful? React with 👍 / 👎.