From 95f277eda74a776be459613c4caf5dd4a8fefc80 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sun, 19 Jul 2026 13:01:06 +0800 Subject: [PATCH] core/tools: shell confirm argv uses $(command); full -c body indented --- src/plyngent/tools/danger.py | 55 ++++++++++++++++++++++++--------- tests/test_tools/test_danger.py | 23 ++++++++++++-- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/plyngent/tools/danger.py b/src/plyngent/tools/danger.py index 20e97b4..3f5bd7d 100644 --- a/src/plyngent/tools/danger.py +++ b/src/plyngent/tools/danger.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import shlex from typing import TYPE_CHECKING, cast from plyngent.tools.workspace import WorkspaceError, resolve_path @@ -8,8 +9,6 @@ from plyngent.tools.workspace import WorkspaceError, resolve_path if TYPE_CHECKING: from collections.abc import Mapping, Sequence -_DISPLAY_ARGV_MAX = 400 -_CODE_PREVIEW_MAX = 240 _SHELL_BASENAMES: frozenset[str] = frozenset( { @@ -81,30 +80,58 @@ def _find_dash_c_code(argv: Sequence[str]) -> str | None: return None +def _indent_block(text: str, *, prefix: str = " ") -> str: + """Indent every line of *text* (for multi-line -c bodies in confirm prompts).""" + if not text: + return prefix + return "\n".join( + prefix + line if line else prefix.rstrip() for line in text.splitlines() + ) + + +def _format_argv_for_confirm(argv: Sequence[str], *, code: str | None) -> str: + """One-line argv summary; replace -c payload with ``$(command)`` when present.""" + if code is None: + return shlex.join(list(argv)) + parts: list[str] = [] + skip_next = False + for part in argv: + if skip_next: + skip_next = False + continue + if part == "-c": + parts.append("-c") + parts.append("$(command)") + skip_next = True + continue + parts.append(part) + return shlex.join(parts) + + def _shell_or_dash_c_reason(argv: Sequence[str], *, via: str) -> str | None: - """Confirm interactive shells/REPLs and ``-c`` one-liners so the user can inspect argv. + """Confirm interactive shells and ``*-c`` one-liners so the user can inspect argv. Multi-line reason (shown inside the CLI confirm box). ``via`` is a short - label (tool name), not repeated on every line. + label such as ``run_command`` or ``open_pty``. + + For ``-c`` scripts, argv shows ``$(command)`` instead of inlining the body; + the full script is printed below untruncated, with every line indented. """ if not argv: return None base = _basename(argv[0]) - display = " ".join(argv) - if len(display) > _DISPLAY_ARGV_MAX: - display = display[:_DISPLAY_ARGV_MAX] + "…" - code = _find_dash_c_code(argv) + display = _format_argv_for_confirm(argv, code=code) + if code is not None: - preview = code if len(code) <= _CODE_PREVIEW_MAX else code[:_CODE_PREVIEW_MAX] + "…" - return f"{via}: {base} -c (review code before allow)\nargv:\n {display}\n-c code:\n {preview}" + body = _indent_block(code, prefix=" ") + return f"{via}: {base} -c (review code before allow)\n argv: {display}\n command:\n{body}" if base in _SHELL_BASENAMES and len(argv) == 1: - return f"{via}: interactive {base!r} (review before allow)\nargv:\n {display}" + return f"{via}: interactive {base!r} (review before allow)\n argv: {display}" - # e.g. python -i, bash --login without -c still needs a glance. - if base in _SHELL_BASENAMES: - return f"{via}: shell/runtime {base!r} (review before allow)\nargv:\n {display}" + if base in _SHELL_BASENAMES and "-c" not in argv[1:]: + return f"{via}: shell/runtime {base!r} without -c (review before allow)\n argv: {display}" return None diff --git a/tests/test_tools/test_danger.py b/tests/test_tools/test_danger.py index 8d7f75c..a4f5d31 100644 --- a/tests/test_tools/test_danger.py +++ b/tests/test_tools/test_danger.py @@ -14,9 +14,7 @@ def test_classify_delete_and_move() -> None: def test_classify_copy() -> None: # Without overwrite flag, copy is not soft-confirmed. assert classify_danger("copy_path", {"src": "a", "dst": "b"}) is None - assert classify_danger( - "copy_path", {"src": "a", "dst": "b", "overwrite": False} - ) is None + assert classify_danger("copy_path", {"src": "a", "dst": "b", "overwrite": False}) is None def test_classify_write_overwrite_only(tmp_path: Path) -> None: @@ -115,3 +113,22 @@ async def test_confirm_deny_with_comment() -> None: assert "denied" in out assert "user comment:" in out assert "too destructive" in out + +def test_shell_confirm_formats_command_placeholder() -> None: + script = "line1" + " +" + "line2" + " +" + "line3" + reason = classify_danger( + "run_command", + {"command": ["bash", "-c", script]}, + ) + assert reason is not None + assert "$(command)" in reason + assert "line1" in reason and "line2" in reason and "line3" in reason + argv_line = next(ln for ln in reason.splitlines() if "argv:" in ln) + assert "line1" not in argv_line + lines = reason.splitlines() + idx = lines.index(" command:") + body = lines[idx + 1 :] + assert body + assert all(ln.startswith(" ") for ln in body)