Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,54 @@ tasks --usage"`, so task names are meant to come from running that. usage-argv d
once in usage-argv and called from both places. A facade test asserts the two
halves agree: the page `help(spec, &["build"], Page::Long)` renders is
byte-for-byte the one `ex build --help` produces.
- [x] **Dispatch** — the `match` from the parsed enum to the code that carries the
command out, which every adopter writes and nobody varies: 210 arms of pure
routing at mise's size, none of them checkable, since every arm has the same
shape. `usage_argv::Run`, `RunWith<Ctx>`, `RunAsync` and `RunAsyncWith<Ctx>` are
the traits a command implements, and `#[usage(run)]`, `#[usage(run_with)]`,
`#[usage(run_async)]` and `#[usage(run_async_with)]` on the enum generate the
match. **Nothing reaches the spec** — which Rust function runs a command is not
part of what the CLI _is_, and a spec recording it could be read by nothing but
the program that wrote it, so this is `#[usage(skip)]`'s rule rather than a new
spec node. Proved on usage-cli itself: both its matches are gone and
`usage --usage-spec` is byte-identical, so no manpage, reference or completion
changed. Decisions, each because the alternative is a wrong program rather than
a missing one:
**a trait per shape, not one with defaults** — four of them, differing only in
whether a command is handed a context and whether it is awaited: `Run`,
`RunWith<Ctx>`, `RunAsync`, `RunAsyncWith<Ctx>`, asked for by the matching
attribute. A defaulted context would make a hundred commands needing nothing
shared each carry `fn run(self, _: ())`; the `With` pair is generic over `Ctx`,
so `&Config`, `&mut App` and an owned handle all work from one emission. An enum
may declare several, which is what a CLI part-way through adopting a context or a
runtime needs.
**The async pair declares `-> impl Future` rather than `async fn`**, which is the
same signature to implement against and imposes no `Send` bound: a CLI that spawns
gets `Send` by inference out of the concrete commands, and one on a single-threaded
runtime keeps a future holding an `Rc` across an await. `-> impl Future + Send`
would buy the ability to _demand_ `Send` in generic code at the cost of the second,
and there is no way to have both without a fifth trait. The sync pair can still
carry a boxed future as its `Output`, which is what to reach for when the future
has to be a value; the async traits exist so that neither the box nor the name is
necessary.
**The output is the first variant's**, with the others bound to agree, so a
command returning something else is reported on the command rather than inside
a generated arm.
**Opt-in**, because the generated impl is the only one an enum can have: a CLI
that wants to act between the parse and the dispatch keeps its match, and asking
is what makes an undispatchable variant an error where it is declared.
**A `run` struct forwards and does nothing else**, so it holds one field, its
subcommands, not in an `Option` — a container like `usage generate` or mise's
`config`. A struct with arguments of its own has to decide what becomes of them,
and an `Option` has a state nothing generated can decide about; both implement
the trait by hand, which is the root's usual case.
**A variant that holds nothing — bare, inline-fields, or `external_subcommand` —
cannot be dispatched.** The first two are served by a struct the derive writes
under a name nothing else can name, and the third holds argv rather than a
command, so there is no type to implement the trait for. Naming the `Args` struct
is the fix, and is where `effect` belongs anyway. If an adopter wants the bare
spelling dispatched, the shape is a per-variant `#[usage(run = path::to::fn)]`;
not built, because one mechanism covers the fleet.

### What clap can say that we cannot

Expand Down
5 changes: 5 additions & 0 deletions argv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,16 @@ macro_rules! __usage_needs_complete_feature {
}
#[cfg(feature = "spec")]
pub mod help;
// Behind no feature: two traits and no code, so there is nothing here for a binary that
// does not dispatch to pay for, and a hand-written CLI on the bare runtime can use them.
pub mod run;
#[cfg(feature = "spec")]
pub mod spec;
#[cfg(feature = "spec")]
pub mod warn;

pub use run::{Run, RunAsync, RunAsyncWith, RunWith};

/// How deep a command tree this parser will descend.
///
/// The ancestor chain is kept in a fixed-size array so that a parse allocates
Expand Down
177 changes: 177 additions & 0 deletions argv/src/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//! Dispatch: handing a parsed command to the code that carries it out.
//!
//! A parse ends with a value — an enum whose selected variant holds the command's own
//! struct — and every CLI then writes the same thing: a `match` over that enum, one arm per
//! command, each arm calling the one function that command exists to call. At mise's size
//! that match is 210 arms of pure routing, and the compiler cannot tell that an arm calling
//! the wrong function is wrong, because every arm has the same shape.
//!
//! So the derive writes it. A command implements [`Run`] (or [`RunWith`], when the CLI hands
//! its commands shared state), the enum says `#[usage(run)]`, and the match is generated from
//! the same declaration the parser and the spec come from. Nothing about it reaches the
//! spec: which Rust function carries out a command is not part of what the CLI *is*, and a
//! spec that recorded it could not be read by anything that is not this program. It is the
//! same rule `#[usage(skip)]` follows.
//!
//! Every one of these traits takes `self` by value. A command is finished when it has run, and
//! the values it parsed are its own — taking them by reference would mean every handler
//! borrowing what nothing else can want.
//!
//! # Which one
//!
//! | | no context | a context |
//! | -------------- | ---------- | -------------------- |
//! | **sync** | [`Run`] | [`RunWith`] |
//! | **async** | [`RunAsync`] | [`RunAsyncWith`] |
//!
//! The context is whatever the CLI has to give — a resolved config, an output handle, a
//! database connection — and the `With` traits are generic over it, so `RunWith<&mut App>` and
//! `RunAsyncWith<Arc<Ctx>>` are ordinary implementations rather than shapes this crate has to
//! anticipate.
//!
//! A context is a separate trait rather than one defaulted to `()` because the noise otherwise
//! falls on the wrong side of a CLI: a hundred commands that need no context would each carry
//! `fn run(self, _: ())`, which says nothing and cannot be left out. One type may implement
//! several of these, and one enum may dispatch several, which is what a CLI part-way through
//! adopting a context — or an async runtime — needs.
//!
//! # Async commands
//!
//! [`RunAsync`] and [`RunAsyncWith`] are the async pair: an implementation writes `async fn`,
//! and the generated dispatch is an `async fn` that awaits the selected command.
//!
//! ```
//! use usage_argv::RunAsync;
//!
//! struct Install {
//! force: bool,
//! }
//!
//! impl RunAsync for Install {
//! type Output = Result<(), String>;
//! async fn run_async(self) -> Self::Output {
//! // .await here
//! Ok(())
//! }
//! }
//! ```
//!
//! The trait declares `-> impl Future<Output = Self::Output>` rather than `async fn`, which is
//! the same thing on the implementing side and **deliberately imposes no `Send` bound**: a CLI
//! on a single-threaded runtime keeps futures that hold an `Rc` across an await, and one that
//! spawns gets `Send` by inference, since it leaks out of the concrete commands the dispatch
//! reaches. What this cannot do is *demand* `Send` in generic code, which is the trade the
//! alternative — `-> impl Future + Send` in the trait — makes in the other direction, and
//! there is no way to have both without duplicating the trait.
//!
//! The sync pair can carry a future too, since [`Output`](Run::Output) is whatever the command
//! produces: a boxed `Pin<Box<dyn Future<Output = T>>>` (plus `+ Send` if the CLI wants it) is
//! a value like any other. That costs an allocation and names a type; the async traits exist so
//! that neither is necessary.
//!
//! # An example
//!
//! ```
//! use usage_argv::Run;
//!
//! struct Install {
//! force: bool,
//! }
//! struct Sponsors;
//!
//! impl Run for Install {
//! type Output = Result<(), String>;
//! fn run(self) -> Self::Output {
//! if self.force {
//! Ok(())
//! } else {
//! Err("refusing without --force".into())
//! }
//! }
//! }
//!
//! impl Run for Sponsors {
//! type Output = Result<(), String>;
//! fn run(self) -> Self::Output {
//! println!("thanks");
//! Ok(())
//! }
//! }
//!
//! // What `#[usage(run)]` on the subcommand enum generates, written out.
//! enum Command {
//! Install(Install),
//! Sponsors(Sponsors),
//! }
//!
//! impl Run for Command
//! where
//! Install: Run,
//! Sponsors: Run<Output = <Install as Run>::Output>,
//! {
//! type Output = <Install as Run>::Output;
//! fn run(self) -> Self::Output {
//! match self {
//! Command::Install(inner) => Run::run(inner),
//! Command::Sponsors(inner) => Run::run(inner),
//! }
//! }
//! }
//!
//! assert!(Command::Install(Install { force: true }).run().is_ok());
//! ```

/// A command that can be carried out with nothing but what it parsed.
///
/// The output is the implementation's own: `Result<(), E>` for a CLI whose commands can
/// fail, `()` for one whose commands cannot, [`ExitCode`](std::process::ExitCode) for one
/// that decides its own status. A generated dispatcher takes its output from the first
/// command it routes to and requires the rest to agree, since a `match` has one type.
pub trait Run {
/// What running the command produces.
type Output;

/// Carry out the command.
fn run(self) -> Self::Output;
}

/// A command that is handed something shared when it runs.
///
/// `Ctx` is whatever the CLI has to give: `&Config`, `&mut App`, an owned handle. It is a
/// parameter of the trait rather than of the method so that one command may be runnable with
/// several — a leaf that needs only a config can implement `RunWith<&Config>` while its
/// siblings implement `RunWith<&mut App>`, as long as the enum dispatching them agrees on
/// one.
pub trait RunWith<Ctx> {
/// What running the command produces.
type Output;

/// Carry out the command, with `ctx`.
fn run_with(self, ctx: Ctx) -> Self::Output;
}

/// An async command: [`Run`], awaited.
///
/// The signature is `-> impl Future` rather than `async fn` so that no `Send` bound is implied
/// either way — an implementation still writes `async fn run_async(self)`, and whether its
/// future is `Send` is decided by what the command does rather than by this trait. See the
/// [module docs](self#async-commands).
pub trait RunAsync {
/// What running the command produces, once awaited.
type Output;

/// Carry out the command.
fn run_async(self) -> impl core::future::Future<Output = Self::Output>;
}

/// An async command that is handed something shared when it runs: [`RunWith`], awaited.
///
/// A borrowed context is the ordinary case, and the future borrows it for as long as it runs:
/// `impl<'a> RunAsyncWith<&'a App> for Install`.
pub trait RunAsyncWith<Ctx> {
/// What running the command produces, once awaited.
type Output;

/// Carry out the command, with `ctx`.
fn run_async_with(self, ctx: Ctx) -> impl core::future::Future<Output = Self::Output>;
}
84 changes: 44 additions & 40 deletions cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,46 +128,6 @@ pub fn answer(
}

impl CompleteWord {
pub fn run(&self) -> miette::Result<()> {
let spec = generate::file_or_spec(&self.file, &self.spec)?;
let choices = self.complete_word(&spec)?;
let shell = self.shell.as_ref();
let any_descriptions = choices.iter().any(|(_, d)| !d.is_empty());
for (c, description) in choices {
match shell {
"bash" => println!("{c}"),
"fish" | "nu" | "powershell" => {
if any_descriptions {
println!("{c}\t{description}")
} else {
println!("{c}")
}
}
"zsh" => {
// Three tab-separated columns per line:
// 1. The raw value (used as the menu display label).
// 2. The description (may be empty).
// 3. The shell-quoted form that `compadd -Q` should
// insert verbatim — wrapped in single quotes when
// the value contains shell metacharacters, raw
// otherwise.
// The generated zsh script builds the formatted display
// (`value -- description`) from columns 1 and 2 and uses
// column 3 as the inserted match. Keeping these as three
// distinct fields avoids the `\:`-escaping acrobatics
// that `_describe`'s `value:description` format required.
let insert = zsh_shell_quote(&c);
println!("{c}\t{description}\t{insert}")
}
_ => {
miette::bail!("unsupported shell: {}", shell);
}
}
}

Ok(())
}

pub fn complete_word(&self, spec: &Spec) -> miette::Result<Vec<(String, String)>> {
Ok(self.complete_word_answer(spec)?.candidates)
}
Expand Down Expand Up @@ -783,6 +743,50 @@ impl CompleteWord {
}
}

impl usage_rs::Run for CompleteWord {
type Output = miette::Result<()>;

fn run(self) -> Self::Output {
let spec = generate::file_or_spec(&self.file, &self.spec)?;
let choices = self.complete_word(&spec)?;
let shell = self.shell.as_ref();
let any_descriptions = choices.iter().any(|(_, d)| !d.is_empty());
for (c, description) in choices {
match shell {
"bash" => println!("{c}"),
"fish" | "nu" | "powershell" => {
if any_descriptions {
println!("{c}\t{description}")
} else {
println!("{c}")
}
}
"zsh" => {
// Three tab-separated columns per line:
// 1. The raw value (used as the menu display label).
// 2. The description (may be empty).
// 3. The shell-quoted form that `compadd -Q` should
// insert verbatim — wrapped in single quotes when
// the value contains shell metacharacters, raw
// otherwise.
// The generated zsh script builds the formatted display
// (`value -- description`) from columns 1 and 2 and uses
// column 3 as the inserted match. Keeping these as three
// distinct fields avoids the `\:`-escaping acrobatics
// that `_describe`'s `value:description` format required.
let insert = zsh_shell_quote(&c);
println!("{c}\t{description}\t{insert}")
}
_ => {
miette::bail!("unsupported shell: {}", shell);
}
}
}

Ok(())
}
}

/// Existing directories described by a possibly abbreviated path.
///
/// Exact parents keep the old single-directory fast path. When one does not exist, resolve its
Expand Down
18 changes: 11 additions & 7 deletions cli/src/cli/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ pub struct Exec {
}

impl Exec {
pub fn run(&mut self) -> miette::Result<()> {
pub fn help(&self, spec: &Spec, args: &[String], long: bool) -> miette::Result<()> {
let parsed = usage::parse::parse_partial(spec, args)?;
println!("{}", usage::docs::cli::render_help(spec, &parsed.cmd, long));
Ok(())
}
}

impl usage_rs::Run for Exec {
type Output = miette::Result<()>;

fn run(self) -> Self::Output {
let parent = self
.bin
.parent()
Expand Down Expand Up @@ -85,10 +95,4 @@ impl Exec {

Ok(())
}

pub fn help(&self, spec: &Spec, args: &[String], long: bool) -> miette::Result<()> {
let parsed = usage::parse::parse_partial(spec, args)?;
println!("{}", usage::docs::cli::render_help(spec, &parsed.cmd, long));
Ok(())
}
}
6 changes: 4 additions & 2 deletions cli/src/cli/generate/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ pub struct Completion {
usage_cmd: Option<String>,
}

impl Completion {
pub fn run(&self) -> miette::Result<()> {
impl usage_rs::Run for Completion {
type Output = miette::Result<()>;

fn run(self) -> Self::Output {
// TODO: refactor this
let spec = match &self.file {
Some(file) => parse_file_or_stdin(file)?,
Expand Down
Loading