From 40dc99132c1cac17d49449153a5dc7b6caab1008 Mon Sep 17 00:00:00 2001 From: a584-droid Date: Thu, 12 Mar 2026 16:52:55 +0300 Subject: [PATCH] Index only directories and validate alias targets --- README.md | 72 ++++++++++++++++++++++++++++++++++++ pathindex/cli.py | 81 +++++++++++++++++++++++++++++++++++++++++ pathindex/collectors.py | 4 +- 3 files changed, 155 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c48d9d..73e210f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - Индексация путей из переменных окружения - Индексация директорий из `PATH` - Пользовательские алиасы через `aliases.json` +- В индекс попадают только существующие директории (не файлы и не отдельные объекты) - SQLite-хранилище индекса - Поиск по `name`, `keywords`, `path` - Преобразование абсолютного пути в псевдопуть (`$VAR/...`) по самому длинному префиксу @@ -20,6 +21,58 @@ source .venv/bin/activate pip install -e . ``` +## Установка через pipx (глобальная команда `pathi`) + +Если нужно, чтобы `pathi` была доступна глобально (вне конкретного venv), используйте `pipx`. + +На дистрибутивах с PEP 668 (например, Arch) команда +`python3 -m pip install --user pipx` может падать с `externally-managed-environment`. +В этом случае сначала установите `pipx` через пакетный менеджер: + +```bash +# Arch +sudo pacman -S python-pipx + +# Debian/Ubuntu +sudo apt install pipx + +# Fedora +sudo dnf install pipx +``` + +После этого: + +```bash +pipx ensurepath +# перезапустите терминал +pipx install /полный/путь/к/EnvPathShortcutTool +``` + +Если `pipx` всё ещё не установлен и у вас НЕ PEP668-окружение, можно использовать fallback: + +```bash +python3 -m pip install --user pipx +python3 -m pipx ensurepath +``` + +Пример для вашего случая: + +```bash +pipx install ~/Sources/EnvPathShortcutTool-main +``` + +Обновить установленную версию после `git pull`: + +```bash +pipx upgrade pathindex +``` + +Удалить: + +```bash +pipx uninstall pathindex +``` + ## Использование ```bash @@ -28,8 +81,27 @@ pathi search conf pathi pseudo /home/user/.config/nvim pathi alias add projects /home/user/projects pathi open nvim +pathi env sync --source alias --normalize --file ~/.config/pathi/environment --rebuild ``` +### Экспорт индекса в переменные окружения + +Чтобы превращать записи индекса в переменные среды (например, для быстрого доступа к `$STEAM`), +используйте: + +```bash +pathi env sync --source alias --normalize --print-only +``` + +Для записи в файл окружения: + +```bash +pathi env sync --source alias --normalize --file /etc/environment +``` + +> Для `/etc/environment` обычно требуются права root (запуск через `sudo`). +> В файл добавляется/обновляется только блок, помеченный как `pathi managed`. + ## Хранилище По умолчанию: diff --git a/pathindex/cli.py b/pathindex/cli.py index 37f1c7d..38f9522 100644 --- a/pathindex/cli.py +++ b/pathindex/cli.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import re from pathlib import Path from .actions import copy_to_clipboard, open_path, open_terminal @@ -8,6 +9,33 @@ 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() @@ -100,6 +128,9 @@ def cmd_alias_add(args: argparse.Namespace) -> int: if not Path(p).is_absolute(): print("Alias path must be absolute") return 2 + if not Path(p).is_dir(): + print("Alias path must point to an existing directory") + return 2 aliases[args.name] = p save_aliases(alias_file, aliases) print(f"Added alias {args.name} -> {p}") @@ -114,6 +145,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") + 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 +225,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 diff --git a/pathindex/collectors.py b/pathindex/collectors.py index bdc37a6..64f3938 100644 --- a/pathindex/collectors.py +++ b/pathindex/collectors.py @@ -10,7 +10,7 @@ def path_like(value: str) -> bool: if not value: return False p = Path(value).expanduser() - return p.is_absolute() and p.exists() + return p.is_absolute() and p.is_dir() def keywords_for(path: str, name: str) -> str: @@ -45,6 +45,6 @@ def collect_alias_entries(aliases: dict[str, str]) -> list[IndexEntry]: entries: list[IndexEntry] = [] for name, raw in aliases.items(): p = str(Path(raw).expanduser()) - if Path(p).is_absolute() and Path(p).exists(): + if Path(p).is_absolute() and Path(p).is_dir(): entries.append(IndexEntry(name, str(Path(p).resolve()), keywords_for(p, name), "alias")) return entries