feat(cli): subcommands + stdin/stdout filter mode (ergonomics 2-3/3)#36
Merged
Conversation
Add `toggle`/`scan`/`check`/`list`/`insert`/`remove` subcommands, each exposing only the flags that apply to that operation. The layer is purely additive: the legacy flat-flag interface is unchanged, and each subcommand translates itself to the equivalent legacy argv (`Commands::to_legacy_argv`) which is re-parsed through the same `build_command()` path. clap therefore stays the single source of truth for defaults, validation, and binary-name aliasing, making subcommand/flat-flag drift structurally impossible. The legacy flat-flag form emits a one-line, TTY-only deprecation nudge (suppressed under --json, non-TTY stderr, and meta/no-op invocations) so scripts and pipes are unaffected. Adds 15 parity tests asserting each subcommand is behavior-identical to its flat-flag equivalent, including a defaulted `--remove-mode` case that guards against silent default drift, plus scoping and help/man-render checks. Refs P10-T02..T04, P10-TS01.
Add a single stdin->stdout filter mode with three spellings — a `-` path, `--stdin`, or `--stdout` — for the writer operations (toggle/insert/remove). Input is read from stdin and the transformed result is written to stdout; the file is never touched. Per the design, this is one mode (stdin in, stdout out), not an input x output matrix: a real file path combined with --stdin/--stdout is rejected. Comment style for the pathless stream resolves from --comment-style if given, else defaults to Python `#` (a synthetic `<stdin>.py` path); pipe other languages with --comment-style. Filter mode rejects flags that collide with stdout output (--json, --atomic, --backup, --interactive, --dry-run, -R) and the read-only operations. Detection runs after the meta/recover/journal short-circuits and before the file-oriented arity checks, so a stdin stream is never rejected as a missing file path. Adds 14 tests: stdin-equals-file byte parity for each writer op, no-op byte-identity preserving exact trailing-newline handling both ways, spelling equivalence, and the full rejection set. Insert's subcommand path becomes optional so `insert --stdin` needs no positional. New io helpers `read_stdin_encoded` / `write_stdout_encoded` mirror the file-encoded variants. Refs P10-T05, P10-TS02. Completes P10.
Previously `--stdout` was a pure alias of stdin filter mode. It now also accepts a real file path: `toggle file.py --stdout -S feat` reads that file, applies the transform, and writes the result to stdout without modifying the file — the prettier/clang-format model, useful for editor integration and previewing. Because a real file has a real extension, its comment style resolves normally (e.g. a `.js` file uses `//`), unlike piped stdin which falls back to the synthetic `<stdin>.py` Python default. Filter mode remains a single stream to stdout: mixing `-`/`--stdin` with a file path, multiple files, or a directory is rejected. Adds 7 tests: file→stdout byte parity with the in-place result, file-unmodified, real-extension comment style (.js → //), and the new rejection cases. Updates README and --stdout help text. Refs P10-T05.
…slash) Run both forms against the same file instead of normalizing two temp paths textually; JSON escapes \ on Windows so the raw-path replace never matched. All callers are non-mutating (scan/check/list/--dry-run), so sharing one file is safe and makes paths byte-identical.
There was a problem hiding this comment.
Pull request overview
Completes the CLI ergonomics refactor by adding an additive subcommand front-end and a stdin→stdout “filter mode” for writer operations, while keeping the legacy flat-flag pipeline as the execution path.
Changes:
- Add
Commandssubcommands that translate to legacy argv and re-parse through the canonical clapbuild_command()path, plus a TTY-only legacy deprecation nudge. - Implement stdin/stdout filter mode (
-/--stdin/--stdout) for toggle/insert/remove, backed by new lib I/O helpers. - Add dedicated integration tests for subcommand≡legacy parity and filter-mode behavior; update docs/project notes accordingly.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents new subcommands and filter mode usage. |
| PROJECTS.md | Records completion of Project P10 (CLI ergonomics refactor). |
| crates/togl-lib/src/io.rs | Adds stdin read + stdout write helpers with encoding support. |
| crates/togl-cli/src/cli.rs | Introduces subcommand CLI surface (Commands, GlobalArgs, FilterArgs) and legacy argv translation. |
| crates/togl-cli/src/main.rs | Adds subcommand bridge, legacy deprecation notice, and filter-mode execution path. |
| crates/togl-cli/tests/subcommands.rs | Parity tests to pin subcommand behavior to legacy flat-flag behavior. |
| crates/togl-cli/tests/filter.rs | Tests for stdin/stdout filter mode, parity, and rejection constraints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| | Subcommand | Flat-flag equivalent | | ||
| |---|---| | ||
| | `toggle <paths> -S id` | `toggle <paths> -S id` | |
Comment on lines
+48
to
+49
| Run `toggle <subcommand> --help` to see its scoped flags. The flat-flag form | ||
| still works and is supported, but is deprecated in favor of the subcommands. |
Comment on lines
+215
to
+217
| /// Flags shared by every subcommand. Flattened into each variant so the legacy | ||
| /// definitions on `Cli` stay untouched (additive, not a restructure). | ||
| #[derive(clap::Args, Debug)] |
Comment on lines
+441
to
+442
| /// Single file to modify. Omit (with --stdin) or pass `-` to read stdin. | ||
| path: Option<PathBuf>, |
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.
Summary
Completes the CLI ergonomics refactor (P10) — options 2 and 3 of 3, stacked on the just-merged declarative-constraints PR (#35).
Option 1 — additive subcommands
Adds
toggle/scan/check/list/insert/remove, each exposing only the flags relevant to that operation. Additive and zero-drift by construction: the legacy flat-flag interface is untouched; each subcommand translates itself to the equivalent legacy argv (Commands::to_legacy_argv) and is re-parsed through the samebuild_command()path, so clap stays the single source of truth for defaults, validation, and binary-name aliasing. The legacy form emits a one-line, TTY-only deprecation nudge (silent under--json, pipes, and meta/no-op invocations).Option 3 — stdin/stdout filter mode
A single stream-to-stdout transform for the writer ops (
toggle/insert/remove), never modifying a file:-path,--stdin, or--stdoutwith no path.toggle file.py --stdout -S featreads the file (real extension → real comment style), transforms, and prints to stdout, leaving disk untouched — the prettier/clang-format model.Filter mode rejects flags/inputs that collide with single-stream stdout output (
--json,--atomic,--backup,--dry-run,--interactive,-R, multiple files, directories, mixing-with a file path) and the read-only operations.Tests
+88 net tests (303 total green), built on a parity discipline:
--remove-modecase (guards default drift) and an--atomic -Rmulti-file case through the bridge..js→//), no-op byte identity (trailing newline both ways), and the full rejection set.--help/--man/--completionsrender subcommands under the aliased bin name.Notes
cargo test,cargo clippy --all-targets -- -D warnings, andcargo fmt --checkall clean.Refs P10.