Skip to content

Add shell completions for the psfmt CLI #4

Open

Description

@coderabbitai

Summary

Add shell completion support for the psfmt CLI.

The CLI currently uses a hand-rolled argument parser. The completion support can use static scripts or a completion generator. The implementation must not require a large runtime dependency unless the project accepts that trade-off.

Rationale

psfmt has a stable set of options. Shell completions improve CLI discoverability and reduce input errors while preserving the current CLI behavior.

Affected areas

  • cli/psfmt/src/main.rs
  • CLI documentation and release/package artifacts, if they distribute completion scripts
  • CLI end-to-end tests, if required to validate completion generation or installation

References

Acceptance criteria

  • Provide completions for the supported target shells.
  • Cover all documented psfmt options and positional file arguments.
  • Document how users install or load the completions.
  • Preserve the existing CLI usage text, option semantics, and exit codes.
  • Add automated coverage where practical.

Activity

  1. coderabbitai commented on Aug 16, 2026

    @coderabbitai
    Author
    🔗 Related PRs

    kjanat/runner#3 - fix: suppress zsh NOMATCH errors in completion function [merged]
    kjanat/runner#16 - fix(completions): accept shell path or pwsh for explicit arg [merged]
    kjanat/runner#42 - fix(completions): fall back to $PSModulePath for pwsh shell detection [closed]
    kjanat/dreamcli#43 - Add eager --completions flag and .help() config builder [merged]
    #1 - Production PowerShell formatter: lexer, engine, CLI, dprint plugin, WASM package [open]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. coderabbitai commented on Aug 16, 2026

    @coderabbitai
    Author

    Coding Plan

    Summary

    • First, add a real clap-based argument parser to psfmt, because the ticket's premise of an existing parser is false. Keep the default stdin→stdout filter and exit codes intact.
    • Expose the existing FormatOptions fields as stable flags plus optional file positionals, so the completions have real options to cover.
    • Add a runtime psfmt completions <shell> subcommand backed by clap_complete, since clap is already a runtime dependency and this makes install, docs, and tests straightforward.
    • Document per-shell installation, verify generation in CI, and add integration tests for the filter behaviour and completion output.

    ⚓ When the tests go green, that be the sweetest sound on the seven seas.

    Design Choices
    Design Choice 1: How should we generate completions and control the dependency footprint?

    Options Considered:

    1. Add clap and clap_complete as runtime dependencies with a psfmt completions <shell> subcommand that prints a script to stdout.
    2. Keep a hand-rolled parser and hand-write static completion scripts for each shell.
    3. Use clap (runtime) plus clap_complete as a build-dependency that writes static scripts to the build output directory (OUT_DIR).

    Chosen Option: 1

    Rationale: clap is already a required runtime dependency for parsing, so clap_complete adds only a small extra footprint. A runtime completions subcommand is easy to install (psfmt completions bash > ...), easy to document, and easy to test directly, and it avoids the awkward OUT_DIR discovery problem of the build-script approach.

    Design Choice 2: Should we expose `FormatOptions` as CLI flags?

    Options Considered:

    1. Expose the existing FormatOptions fields as stable flags plus optional file positionals.
    2. Add only --help/--version and an optional file positional, leaving formatting hard-coded.

    Chosen Option: 1

    Rationale: The acceptance criteria require completions to cover all documented psfmt options and positional file arguments, yet none exist today. Option 1 reuses the existing FormatOptions fields, makes the completions meaningful, and satisfies the acceptance criteria without inventing new features.

    💡 User Tips

    Regenerate the plan with different choices with @coderabbitai <feedback>.

    Implementation Steps

    Phase 1: Give psfmt a proper CLI surface

    This phase builds the argument parser that everything else depends on. The goal is a stable, well-defined option set that maps onto the existing FormatOptions. The old default behaviour must survive untouched: with no arguments, psfmt still reads stdin, formats, and writes stdout.

    Task 1: Add the argument parser and define the command

    Introduce clap and declare the option surface as the single source of truth for both parsing and completion generation.

    • Add clap (derive feature) to cli/psfmt/Cargo.toml as a runtime dependency. Add it directly to the crate. Do not create a [workspace.dependencies] table for a single consumer.
    • Define the command in a small, shareable module, for example cli/psfmt/src/cli.rs, so that both the parse path and the completions path use the same clap::Command.
    • Declare flags that map to FormatOptions: --indent-width, --use-tabs, --line-width, and --brace-style (with a fixed value set matching BraceStyle).
    • Declare an optional positional file argument set (zero or more paths). When paths are absent, the tool reads stdin.
    • Declare a completions subcommand with one required argument that selects the shell. Restrict the shell argument to the supported set so it also completes itself.
    • Rely on clap for --help and --version text and take the version from workspace.package.
    Task 2: Wire the parser into main while preserving current behaviour

    Refactor the entry point to parse arguments, dispatch the completions subcommand, and keep the exact filter behaviour when no options are given.

    • Update cli/psfmt/src/main.rs to parse arguments through the module from Task 1.1.
    • If the completions subcommand is present, generate the script for the requested shell (see Task 2.1), write it to stdout, and exit with code 0.
    • If no subcommand and no file positionals are present, keep the current path: read all of stdin, format, write diagnostics to stderr, write output to stdout.
    • If file positionals are present, format each file to stdout in order.
    • Map parsed flags onto FormatOptions. When a flag is absent, use the current default value for that field.
    • Preserve exit codes: 0 on success and non-zero on I/O or formatting failure. Note that clap returns exit code 2 for usage errors and 0 for --help/--version. This is additive and does not change the existing 0/1 filter path.
    🤖 Prompt for AI agents
    Implement a proper CLI argument surface for `psfmt`, replacing the current
    zero-argument stdin→stdout filter, while keeping that default filter behaviour
    intact.
    
    - Add `clap` (with the derive feature) as a runtime dependency directly in
    `cli/psfmt/Cargo.toml`. Do not create a `[workspace.dependencies]` table for a
    single consumer.
    - Create a shareable module, for example `cli/psfmt/src/cli.rs`, that defines
    the `clap::Command` for `psfmt`. Both the parse path and the completions path
    must use this same command definition.
    - In this command, declare flags mapping to the existing `FormatOptions` fields:
    `--indent-width`, `--use-tabs`, `--line-width`, and `--brace-style` (restrict
    `--brace-style` to a fixed set of values matching `BraceStyle`).
    - Declare an optional positional argument accepting zero or more file paths.
    When no paths are given, `psfmt` must read from stdin.
    - Declare a `completions` subcommand with one required argument that selects the
    shell. Restrict the shell argument to the supported set so the argument itself
    completes correctly.
    - Let `clap` supply `--help` and `--version` output, using the version from
    `workspace.package`.
    - Update `cli/psfmt/src/main.rs` to parse arguments using the new module.
    - If the `completions` subcommand is present, generate the script for the
    requested shell, write it to stdout, and exit with code `0`.
    - When no subcommand and no file positionals are provided, preserve the current
    behaviour exactly: read all of stdin, format it, write diagnostics to stderr,
    and write formatted output to stdout.
    - When file positionals are provided, format each file in order and write output
    to stdout.
    - Map each parsed flag to the corresponding `FormatOptions` field; when a flag
    is not supplied, fall back to the current default value for that field.
    - Preserve exit codes: `0` on success, non-zero on I/O or formatting failure.
    Note that `clap` itself returns exit code `2` for usage errors and `0` for
    `--help`/`--version`; treat this as additive behaviour that does not alter the
    existing `0`/`1` filter exit codes.
    

    Phase 2: Generate the completion scripts

    This phase turns the option set into completion scripts for the target shells. Generation runs at runtime through the completions subcommand, so users can install scripts directly and tests can assert the output.

    Task 1: Implement the completions subcommand

    Generate a completion script for the requested shell from the shared clap::Command.

    • Add clap_complete to cli/psfmt/Cargo.toml as a runtime dependency.
    • Implement the completions handler in the module from Task 1.1 (for example a generate_completions function that takes the shell and a writer).
    • Build the command from the shared definition, then call clap_complete::generate to write the script.
    • Support these shells: Bash, Zsh, Fish, and PowerShell. PowerShell matters because psfmt formats PowerShell.
    • Write the script to stdout so a user can redirect it to the correct location for the shell.
    Task 2: Verify generation in CI

    Make sure completion generation does not silently break.

    • Update .github/workflows/ci.yml to build the CLI on the existing three-OS matrix.
    • Add a lightweight step that runs psfmt completions <shell> for each supported shell and confirms the output is non-empty.
    • Keep this additive. Do not add a full release or publish pipeline. That is out of scope for this ticket.
    🤖 Prompt for AI agents
    Implement a runtime `completions` subcommand that generates a completion script
    for a requested shell, using the shared `clap::Command` from Phase 1.
    
    - Add `clap_complete` to `cli/psfmt/Cargo.toml` as a runtime dependency.
    - Implement the `completions` handler inside the module from Phase 1 (for
    example, a `generate_completions` function that accepts the shell and a writer).
    - Build the command from the shared definition, then call
    `clap_complete::generate` to write the completion script.
    - Support these target shells: Bash, Zsh, Fish, and PowerShell. Include
    PowerShell because `psfmt` itself formats PowerShell.
    - Write the generated script to stdout so a user can redirect it to the correct
    load location for their shell.
    
    Then verify this behaviour in CI:
    - Update `.github/workflows/ci.yml` so the CLI is built on the existing three-OS
    matrix.
    - Add a lightweight step that runs `psfmt completions <shell>` for each
    supported shell and checks the output is non-empty.
    - Keep this change additive only. Do not add a full release or publish pipeline;
    that work is out of scope for this ticket.
    

    Phase 3: Documentation and automated coverage

    This phase tells users how to load the completions and locks the behaviour with tests. Everything documented, everything checked. That be how a perfectionist sleeps at night.

    Task 1: Document options and completion installation

    Extend the documentation without contradicting the existing filter description.

    • Update README.md under the "CLI contract" section. Add a "Shell completions" subsection.
    • Document the new options and the optional file positionals.
    • Give per-shell instructions that run psfmt completions <shell> and redirect the output to the correct load path for Bash, Zsh, Fish, and PowerShell.
    • Keep the existing filter example (psfmt < script.ps1 > formatted.ps1) accurate and unchanged.
    Task 2: Add automated tests

    Add practical coverage for both the CLI surface and completion generation.

    • Add an integration test under cli/psfmt/tests/ that runs the built binary through the CARGO_BIN_EXE_psfmt environment variable, so no extra test dependency is required.
    • Assert the preserved filter behaviour: piping source to stdin with no arguments yields formatted output on stdout and a 0 exit code.
    • Assert that the shared clap::Command passes Command::debug_assert to catch invalid option definitions.
    • Assert that psfmt completions <shell> produces non-empty output for each supported shell and exits with code 0.
    🤖 Prompt for AI agents
    Document the new CLI options and the completion installation steps, and add
    automated tests locking in both the CLI behaviour and completion generation.
    
    Documentation:
    - Update `README.md` under the existing "CLI contract" section.
    - Add a new "Shell completions" subsection.
    - Document the new flags (`--indent-width`, `--use-tabs`, `--line-width`,
    `--brace-style`) and the optional file positionals.
    - Provide per-shell instructions that run `psfmt completions <shell>` and
    redirect the output to the correct load path, covering Bash, Zsh, Fish, and
    PowerShell.
    - Keep the existing filter example (`psfmt < script.ps1 > formatted.ps1`)
    accurate and unchanged; do not contradict the existing filter description.
    
    Testing:
    - Add an integration test under `cli/psfmt/tests/` that runs the compiled binary
    via the `CARGO_BIN_EXE_psfmt` environment variable, avoiding any extra test
    dependency.
    - Assert that the preserved filter behaviour still works: piping source into
    stdin with no arguments produces formatted output on stdout and exits with code
    `0`.
    - Assert that the shared `clap::Command` passes `Command::debug_assert`, to
    catch invalid option definitions.
    - Assert that `psfmt completions <shell>` produces non-empty output for each
    supported shell (Bash, Zsh, Fish, PowerShell) and exits with code `0`.
    
    Research

    Arrr, heed this warning before ye set sail, matey! The ticket claims psfmt already carries a hand-rolled argument parser. It does not! The exploration proves the binary be a bare stdin→stdout filter with no argument surface at all. So we must first give psfmt a proper option set, then let a shell complete it. This plan does both, and it does not break the old behaviour. Sloppy premises make me itch.

    Codebase Summary

    The workspace is a Rust edition-2024 project (rust-version = "1.85"). The native CLI lives in cli/psfmt/src/main.rs. It reads all of stdin, calls powershell_formatter::format with FormatOptions::default(), writes diagnostics to stderr, and writes formatted output to stdout. It parses no arguments. FormatOptions (in crates/powershell-formatter/src/lib.rs) exposes brace_style, indent_width, use_tabs, and line_width. No CLI-parsing crate exists anywhere in the workspace. The root Cargo.toml has no [workspace.dependencies] table. README.md documents psfmt only as a Unix-style filter with no flags. .github/workflows/ci.yml runs fmt, clippy, and test on a three-OS matrix and has no build, release, or artefact steps.


    🚀 Next Steps

    🤖 All AI agent prompts combined
    Task: 1
    
    Implement a proper CLI argument surface for `psfmt`, replacing the current
    zero-argument stdin→stdout filter, while keeping that default filter behaviour
    intact.
    
    - Add `clap` (with the derive feature) as a runtime dependency directly in
    `cli/psfmt/Cargo.toml`. Do not create a `[workspace.dependencies]` table for a
    single consumer.
    - Create a shareable module, for example `cli/psfmt/src/cli.rs`, that defines
    the `clap::Command` for `psfmt`. Both the parse path and the completions path
    must use this same command definition.
    - In this command, declare flags mapping to the existing `FormatOptions` fields:
    `--indent-width`, `--use-tabs`, `--line-width`, and `--brace-style` (restrict
    `--brace-style` to a fixed set of values matching `BraceStyle`).
    - Declare an optional positional argument accepting zero or more file paths.
    When no paths are given, `psfmt` must read from stdin.
    - Declare a `completions` subcommand with one required argument that selects the
    shell. Restrict the shell argument to the supported set so the argument itself
    completes correctly.
    - Let `clap` supply `--help` and `--version` output, using the version from
    `workspace.package`.
    - Update `cli/psfmt/src/main.rs` to parse arguments using the new module.
    - If the `completions` subcommand is present, generate the script for the
    requested shell, write it to stdout, and exit with code `0`.
    - When no subcommand and no file positionals are provided, preserve the current
    behaviour exactly: read all of stdin, format it, write diagnostics to stderr,
    and write formatted output to stdout.
    - When file positionals are provided, format each file in order and write output
    to stdout.
    - Map each parsed flag to the corresponding `FormatOptions` field; when a flag
    is not supplied, fall back to the current default value for that field.
    - Preserve exit codes: `0` on success, non-zero on I/O or formatting failure.
    Note that `clap` itself returns exit code `2` for usage errors and `0` for
    `--help`/`--version`; treat this as additive behaviour that does not alter the
    existing `0`/`1` filter exit codes.
    ===============================================================================
    
    Task: 2
    
    Implement a runtime `completions` subcommand that generates a completion script
    for a requested shell, using the shared `clap::Command` from Phase 1.
    
    - Add `clap_complete` to `cli/psfmt/Cargo.toml` as a runtime dependency.
    - Implement the `completions` handler inside the module from Phase 1 (for
    example, a `generate_completions` function that accepts the shell and a writer).
    - Build the command from the shared definition, then call
    `clap_complete::generate` to write the completion script.
    - Support these target shells: Bash, Zsh, Fish, and PowerShell. Include
    PowerShell because `psfmt` itself formats PowerShell.
    - Write the generated script to stdout so a user can redirect it to the correct
    load location for their shell.
    
    Then verify this behaviour in CI:
    - Update `.github/workflows/ci.yml` so the CLI is built on the existing three-OS
    matrix.
    - Add a lightweight step that runs `psfmt completions <shell>` for each
    supported shell and checks the output is non-empty.
    - Keep this change additive only. Do not add a full release or publish pipeline;
    that work is out of scope for this ticket.
    ===============================================================================
    
    Task: 3
    
    Document the new CLI options and the completion installation steps, and add
    automated tests locking in both the CLI behaviour and completion generation.
    
    Documentation:
    - Update `README.md` under the existing "CLI contract" section.
    - Add a new "Shell completions" subsection.
    - Document the new flags (`--indent-width`, `--use-tabs`, `--line-width`,
    `--brace-style`) and the optional file positionals.
    - Provide per-shell instructions that run `psfmt completions <shell>` and
    redirect the output to the correct load path, covering Bash, Zsh, Fish, and
    PowerShell.
    - Keep the existing filter example (`psfmt < script.ps1 > formatted.ps1`)
    accurate and unchanged; do not contradict the existing filter description.
    
    Testing:
    - Add an integration test under `cli/psfmt/tests/` that runs the compiled binary
    via the `CARGO_BIN_EXE_psfmt` environment variable, avoiding any extra test
    dependency.
    - Assert that the preserved filter behaviour still works: piping source into
    stdin with no arguments produces formatted output on stdout and exits with code
    `0`.
    - Assert that the shared `clap::Command` passes `Command::debug_assert`, to
    catch invalid option definitions.
    - Assert that `psfmt completions <shell>` produces non-empty output for each
    supported shell (Bash, Zsh, Fish, PowerShell) and exits with code `0`.
    
    💡 Iterate on the plan with: @coderabbitai <feedback>
    Example Feedback
    - @coderabbitai You can skip phase 3. Add a simple unit test case for phase 2.
    - @coderabbitai For design choice 1 go ahead with option 3 and replan.
    

    💬 Have feedback or questions? Drop into our discord!

  3. kjanat commented on Aug 16, 2026

    @kjanat
    Owner

    @coderabbitai Check if we can implement something similar to what I've done in envctl's complete.c, module.c & Co. Markdown docs accompanying that lives here: https://github.com/kjanat/envctl/blob/master/docs/shells.md

  4. coderabbitai commented on Aug 16, 2026

    @coderabbitai
    Author

    I've updated the plan above based on your feedback. Ready to proceed when you are!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions