Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,35 @@ source .venv/bin/activate
pip install -e .
```

## Установка через pipx (глобальная команда `pathi`)

Если нужно, чтобы `pathi` была доступна глобально (вне конкретного venv), используйте `pipx`:

```bash
python3 -m pip install --user pipx
python3 -m pipx ensurepath
# перезапустите терминал
pipx install /полный/путь/к/EnvPathShortcutTool
```

Пример для вашего случая:

```bash
pipx install ~/Sources/EnvPathShortcutTool-main
```

Обновить установленную версию после `git pull`:

```bash
pipx upgrade pathindex
```

Удалить:

```bash
pipx uninstall pathindex
```

## Использование

```bash
Expand All @@ -28,8 +57,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`.

## Хранилище

По умолчанию:
Expand Down
78 changes: 78 additions & 0 deletions pathindex/cli.py
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()
Expand Down Expand Up @@ -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]}"')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape env values before emitting assignments

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. Because env sync is meant to generate reusable environment-variable definitions, values should be escaped or serialized in a format that preserves arbitrary path characters.

Useful? React with 👍 / 👎.

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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor --print-only by avoiding extra stdout lines

--print-only is documented and described as printing the managed block, but the command always prints Exported ... (and optionally skipped counts) afterward. This makes the output unsuitable for piping directly into env files or shell evaluation, because the trailing status text is not part of the managed block and can invalidate downstream parsing.

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)
Expand Down Expand Up @@ -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


Expand Down
Loading