Skip to content

Remove plain run and improve plain shell - #75

Merged
davegaeddert merged 6 commits into
masterfrom
claude/plain-run-shell-ux-wdcwrh
Jul 15, 2026
Merged

Remove plain run and improve plain shell#75
davegaeddert merged 6 commits into
masterfrom
claude/plain-run-shell-ux-wdcwrh

Conversation

@davegaeddert

@davegaeddert davegaeddert commented Jul 9, 2026

Copy link
Copy Markdown
Member

Remove the plain run <script> command and make plain shell the 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:

import plain.runtime
plain.runtime.setup()

and runs under plain python script.py with normal sys.argv behavior (the old plain run didn't forward arguments at all). Scripts worth keeping graduate to registered CLI commands. The CLI README documents both patterns.

Along the way, plain shell gets a real redesign and fixes:

  • -c and piped-stdin code now executes in-process, as a real module registered as sys.modules["__main__"] — the CLI has already run plain.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.argv matches python -c, pickle/multiprocessing of user-defined objects works, exit codes propagate, and tracebacks start at the user's code just like python -c.
  • The REPL keeps its subprocess (PYTHONSTARTUP is only honored interactively) but now runs under sys.executable instead of bare python, and Plain's PYTHONSTARTUP (banner + SHELL_IMPORT) wins over a user-exported PYTHONSTARTUP — the env merge order was backwards.
  • The piped-stdin setup path (plain shell: piping a script via stdin skips runtime.setup() (opaque PackageRegistryNotReady) #66) keeps its regression test.

Intermediate commits on this branch unified everything into a python-interpreter-compatible plain python command; later commits remove it. Owning interpreter compatibility meant servicing an open-ended parity contract (-m, argv grammar, sys.path semantics) 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

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
davegaeddert force-pushed the claude/plain-run-shell-ux-wdcwrh branch from c9c1c56 to 4d6767b Compare July 11, 2026 02:30
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
@davegaeddert davegaeddert changed the title Unify shell/run into a single plain python command Remove plain run and improve plain shell Jul 15, 2026
Executing 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
davegaeddert enabled auto-merge (squash) July 15, 2026 03:52
@davegaeddert
davegaeddert merged commit 7e55115 into master Jul 15, 2026
8 checks passed
@davegaeddert
davegaeddert deleted the claude/plain-run-shell-ux-wdcwrh branch July 15, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants