Remove plain run and improve plain shell - #75
Merged
Conversation
Collapse `plain shell` (interactive REPL + `-c` + piped stdin) and
`plain run <script>` into one command that mirrors the `python`
interpreter, but always runs `plain.runtime.setup()` first:
plain python # interactive REPL (banner + SHELL_IMPORT)
plain python -c "..." # execute a string, then exit
plain python script.py # run a file as __main__
plain python - # execute stdin, then exit
echo ... | plain python # execute piped input
Enrichment (banner + SHELL_IMPORT auto-imports) stays confined to the
interactive REPL via PYTHONSTARTUP, matching how the interpreter only
honors PYTHONSTARTUP for interactive sessions — `-c`, file, and stdin
modes run code as-is. Args are a flat positional vector like the
interpreter's own argv, so `plain python script.py -c foo` forwards
`-c foo` to the script rather than capturing plain's own option, and
Plain's startup file wins over any user-exported PYTHONSTARTUP. All
interpreter modes run under `sys.executable`.
`plain shell` is kept as a thin shortcut to the interactive REPL (and,
like `plain python`, runs piped stdin with the app configured).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ln7AFVxkkJTY78DfRfqWV
davegaeddert
force-pushed
the
claude/plain-run-shell-ux-wdcwrh
branch
from
July 11, 2026 02:30
c9c1c56 to
4d6767b
Compare
Hand-parse the interpreter-style argv instead of using click's option parser, which kept matching option-shaped tokens after the -c payload (`plain python -c '...' --interface python` silently consumed both tokens instead of forwarding them as sys.argv). Also: - Add `plain python -m module` via runpy.run_module - Execute -c/stdin code in a fresh namespace so wrapper imports (plain.runtime, sys) don't leak into user globals - Put the script's directory on sys.path for file mode, like `python script.py` - Clean error + exit 2 for a nonexistent script file instead of a runpy traceback - Set sys.argv before plain.runtime.setup() in the child so setup sees the real argv - Document the supported modes explicitly rather than claiming full interpreter parity Claude-Session: https://claude.ai/code/session_014CgGYyfFCV32412TkQW3x7
Owning a python-interpreter-compatible command meant servicing an open-ended parity contract (-m, argv grammar, sys.path semantics, fresh globals) for little payoff: Plain's app setup is self-serve, so a standalone script just starts with `import plain.runtime; plain.runtime.setup()` and runs under the plain interpreter. The wrapper command only saved those two lines. `plain shell` is now the single command: interactive REPL (enriched via SHELL_IMPORT), -c for one-off code, and piped stdin. The real fixes from the python command are kept — sys.executable instead of bare "python", Plain's PYTHONSTARTUP winning over the user's, fresh globals for -c/stdin so wrapper imports don't leak, sys.argv set in the child, and the #66 piped-stdin regression guard. The old `plain run` stays deleted; scripts self-configure or graduate to registered CLI commands, which the docs now describe. Claude-Session: https://claude.ai/code/session_014CgGYyfFCV32412TkQW3x7
plain python commandExecuting one-off code in a detached globals dict broke __main__ identity: objects defined in -c/stdin code got __module__ == '__main__' without being resolvable through sys.modules['__main__'], so pickle (and multiprocessing spawn) raised PicklingError for code that works under python -c. The subprocess + generated-code-string layer existed only to run plain.runtime.setup() in a child — but the CLI already runs setup in this process before any command executes. So execute -c/stdin code in-process instead: compile it, register a fresh module as sys.modules['__main__'], and exec into that module's namespace. That keeps the namespace clean (no wrapper imports to leak), makes pickle and __main__ semantics match python -c, sets sys.argv the same way, and drops a whole interpreter start + duplicate setup from every -c invocation. Tracebacks are trimmed to start at the user's code, like python -c. The REPL keeps its subprocess since PYTHONSTARTUP is only honored interactively. Claude-Session: https://claude.ai/code/session_014CgGYyfFCV32412TkQW3x7
Inline the single-caller _default_interface helper, derive the -i choices from _INTERFACES instead of a duplicate list, and drop the docstring's multiprocessing claim (spawn can't reimport a synthetic __main__ — same limitation as python -c). Claude-Session: https://claude.ai/code/session_014CgGYyfFCV32412TkQW3x7
davegaeddert
enabled auto-merge (squash)
July 15, 2026 03:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Remove the
plain run <script>command and makeplain shellthe single way to execute code with the app configured: interactive REPL (banner + SHELL_IMPORT enrichment),-c "..."for one-off code, and piped stdin.There's deliberately no wrapper command for script files. Plain's app setup is self-serve — a standalone script starts with:
and runs under plain
python script.pywith normalsys.argvbehavior (the oldplain rundidn't forward arguments at all). Scripts worth keeping graduate to registered CLI commands. The CLI README documents both patterns.Along the way,
plain shellgets a real redesign and fixes:-cand piped-stdin code now executes in-process, as a real module registered assys.modules["__main__"]— the CLI has already runplain.runtime.setup(), so the old subprocess + generated-code-string layer (and its duplicate interpreter start + setup) is gone. Namespaces are clean (no wrapper imports leak into user globals),sys.argvmatchespython -c, pickle/multiprocessing of user-defined objects works, exit codes propagate, and tracebacks start at the user's code just likepython -c.sys.executableinstead of barepython, and Plain's PYTHONSTARTUP (banner + SHELL_IMPORT) wins over a user-exported PYTHONSTARTUP — the env merge order was backwards.Intermediate commits on this branch unified everything into a
python-interpreter-compatibleplain pythoncommand; later commits remove it. Owning interpreter compatibility meant servicing an open-ended parity contract (-m, argv grammar,sys.pathsemantics) for a command that only saved two lines over self-setup. The history is kept as the record of that exploration — squash-merge recommended.https://claude.ai/code/session_014CgGYyfFCV32412TkQW3x7