diff --git a/miri-script/Cargo.lock b/miri-script/Cargo.lock index 0c0fe477cd..4f025e2a07 100644 --- a/miri-script/Cargo.lock +++ b/miri-script/Cargo.lock @@ -2,6 +2,55 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.80" @@ -20,6 +69,39 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "directories" version = "5.0.1" @@ -89,6 +171,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.11.0" @@ -137,6 +225,7 @@ name = "miri-script" version = "0.1.0" dependencies = [ "anyhow", + "clap", "directories", "dunce", "itertools", @@ -278,6 +367,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.50" @@ -328,6 +423,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.4.0" @@ -362,7 +463,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/miri-script/Cargo.toml b/miri-script/Cargo.toml index 5b31d5a6ff..db88bda8b1 100644 --- a/miri-script/Cargo.toml +++ b/miri-script/Cargo.toml @@ -25,3 +25,4 @@ dunce = "1.0.4" directories = "5" serde_json = "1" tempfile = "3.13.0" +clap = "4.5.21" diff --git a/miri-script/src/args.rs b/miri-script/src/args.rs deleted file mode 100644 index 55d9de4233..0000000000 --- a/miri-script/src/args.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::{env, iter}; - -use anyhow::{Result, bail}; - -pub struct Args { - args: iter::Peekable, - /// Set to `true` once we saw a `--`. - terminated: bool, -} - -impl Args { - pub fn new() -> Self { - let mut args = Args { args: env::args().peekable(), terminated: false }; - args.args.next().unwrap(); // skip program name - args - } - - /// Get the next argument without any interpretation. - pub fn next_raw(&mut self) -> Option { - self.args.next() - } - - /// Consume a `-$f` flag if present. - pub fn get_short_flag(&mut self, flag: char) -> Result { - if self.terminated { - return Ok(false); - } - if let Some(next) = self.args.peek() { - if let Some(next) = next.strip_prefix("-") { - if let Some(next) = next.strip_prefix(flag) { - if next.is_empty() { - self.args.next().unwrap(); // consume this argument - return Ok(true); - } else { - bail!("`-{flag}` followed by value"); - } - } - } - } - Ok(false) - } - - /// Consume a `--$name` flag if present. - pub fn get_long_flag(&mut self, name: &str) -> Result { - if self.terminated { - return Ok(false); - } - if let Some(next) = self.args.peek() { - if let Some(next) = next.strip_prefix("--") { - if next == name { - self.args.next().unwrap(); // consume this argument - return Ok(true); - } - } - } - Ok(false) - } - - /// Consume a `--$name val` or `--$name=val` option if present. - pub fn get_long_opt(&mut self, name: &str) -> Result> { - assert!(!name.is_empty()); - if self.terminated { - return Ok(None); - } - let Some(next) = self.args.peek() else { return Ok(None) }; - let Some(next) = next.strip_prefix("--") else { return Ok(None) }; - let Some(next) = next.strip_prefix(name) else { return Ok(None) }; - // Starts with `--flag`. - Ok(if let Some(val) = next.strip_prefix("=") { - // `--flag=val` form - let val = val.into(); - self.args.next().unwrap(); // consume this argument - Some(val) - } else if next.is_empty() { - // `--flag val` form - self.args.next().unwrap(); // consume this argument - let Some(val) = self.args.next() else { bail!("`--{name}` not followed by value") }; - Some(val) - } else { - // Some unrelated flag, like `--flag-more` or so. - None - }) - } - - /// Consume a `--$name=val` or `--$name` option if present; the latter - /// produces a default value. (`--$name val` is *not* accepted for this form - /// of argument, it understands `val` already as the next argument!) - pub fn get_long_opt_with_default( - &mut self, - name: &str, - default: &str, - ) -> Result> { - assert!(!name.is_empty()); - if self.terminated { - return Ok(None); - } - let Some(next) = self.args.peek() else { return Ok(None) }; - let Some(next) = next.strip_prefix("--") else { return Ok(None) }; - let Some(next) = next.strip_prefix(name) else { return Ok(None) }; - // Starts with `--flag`. - Ok(if let Some(val) = next.strip_prefix("=") { - // `--flag=val` form - let val = val.into(); - self.args.next().unwrap(); // consume this argument - Some(val) - } else if next.is_empty() { - // `--flag` form - self.args.next().unwrap(); // consume this argument - Some(default.into()) - } else { - // Some unrelated flag, like `--flag-more` or so. - None - }) - } - - /// Returns the next free argument or uninterpreted flag, or `None` if there are no more - /// arguments left. `--` is returned as well, but it is interpreted in the sense that no more - /// flags will be parsed after this. - pub fn get_other(&mut self) -> Option { - if self.terminated { - return self.args.next(); - } - let next = self.args.next()?; - if next == "--" { - self.terminated = true; // don't parse any more flags - // This is where our parser is special, we do yield the `--`. - } - Some(next) - } - - /// Return the rest of the aguments entirely unparsed. - pub fn remainder(self) -> Vec { - self.args.collect() - } -} diff --git a/miri-script/src/commands.rs b/miri-script/src/commands.rs index 21029d0b5b..6de6e23d0a 100644 --- a/miri-script/src/commands.rs +++ b/miri-script/src/commands.rs @@ -1,6 +1,6 @@ use std::ffi::{OsStr, OsString}; use std::io::Write; -use std::ops::{Not, Range}; +use std::ops::Not; use std::path::PathBuf; use std::time::Duration; use std::{env, net, process}; @@ -502,7 +502,7 @@ impl Command { fn run( dep: bool, verbose: bool, - many_seeds: Option>, + many_seeds: Option, target: Option, edition: Option, flags: Vec, @@ -562,7 +562,7 @@ impl Command { }; // Run the closure once or many times. if let Some(seed_range) = many_seeds { - e.run_many_times(seed_range, |e, seed| { + e.run_many_times(seed_range.0, |e, seed| { eprintln!("Trying seed: {seed}"); run_miri(e, Some(format!("-Zmiri-seed={seed}"))).inspect_err(|_| { eprintln!("FAILING SEED: {seed}"); diff --git a/miri-script/src/main.rs b/miri-script/src/main.rs index a329f62790..46fc37e16f 100644 --- a/miri-script/src/main.rs +++ b/miri-script/src/main.rs @@ -1,13 +1,29 @@ #![allow(clippy::needless_question_mark)] -mod args; mod commands; mod coverage; mod util; use std::ops::Range; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow}; +use clap::{Arg, ArgAction, Command as ClapCommand}; + +#[derive(Clone, Debug)] +pub struct MiriScriptRange(Range); + +fn parse_range(val: &str) -> anyhow::Result { + let (from, to) = val + .split_once("..") + .ok_or_else(|| anyhow!("invalid format for `--many-seeds`: expected `from..to`"))?; + let from: u32 = if from.is_empty() { + 0 + } else { + from.parse().context("invalid `from` in `--many-seeds=from..to")? + }; + let to: u32 = to.parse().context("invalid `to` in `--many-seeds=from..to")?; + Ok(MiriScriptRange(from..to)) +} #[derive(Clone, Debug)] pub enum Command { @@ -45,7 +61,7 @@ pub enum Command { Run { dep: bool, verbose: bool, - many_seeds: Option>, + many_seeds: Option, target: Option, edition: Option, /// Flags that are passed through to `miri`. @@ -87,171 +103,233 @@ pub enum Command { RustcPush { github_user: String, branch: String }, } -const HELP: &str = r#" COMMANDS - -./miri build : -Just build miri. are passed to `cargo build`. - -./miri check : -Just check miri. are passed to `cargo check`. - -./miri test [--bless] [--target ] : -Build miri, set up a sysroot and then run the test suite. - are passed to the test harness. - -./miri run [--dep] [-v|--verbose] [--many-seeds|--many-seeds=..to|--many-seeds=from..to] : -Build miri, set up a sysroot and then run the driver with the given . -(Also respects MIRIFLAGS environment variable.) -If `--many-seeds` is present, Miri is run many times in parallel with different seeds. -The range defaults to `0..64`. - -./miri fmt : -Format all sources and tests. are passed to `rustfmt`. - -./miri clippy : -Runs clippy on all sources. are passed to `cargo clippy`. - -./miri cargo : -Runs just `cargo ` with the Miri-specific environment variables. -Mainly meant to be invoked by rust-analyzer. - -./miri install : -Installs the miri driver and cargo-miri. are passed to `cargo -install`. Sets up the rpath such that the installed binary should work in any -working directory. Note that the binaries are placed in the `miri` toolchain -sysroot, to prevent conflicts with other toolchains. - -./miri bench [--target ] : -Runs the benchmarks from bench-cargo-miri in hyperfine. hyperfine needs to be installed. - can explicitly list the benchmarks to run; by default, all of them are run. - -./miri toolchain : -Update and activate the rustup toolchain 'miri' to the commit given in the -`rust-version` file. -`rustup-toolchain-install-master` must be installed for this to work. Any extra -flags are passed to `rustup-toolchain-install-master`. - -./miri rustc-pull : -Pull and merge Miri changes from the rustc repo. Defaults to fetching the latest -rustc commit. The fetched commit is stored in the `rust-version` file, so the -next `./miri toolchain` will install the rustc that just got pulled. +impl Command { + fn add_remainder(&mut self, remainder: Vec) { + match self { + Self::Install { flags } + | Self::Build { flags } + | Self::Check { flags } + | Self::Doc { flags } + | Self::Fmt { flags } + | Self::Clippy { flags } + | Self::Run { flags, .. } + | Self::Toolchain { flags, .. } + | Self::Test { flags, .. } => + if !remainder.is_empty() { + flags.push("--".into()); + flags.extend(remainder); + }, + Self::Bench { .. } | Self::RustcPull { .. } | Self::RustcPush { .. } => (), + } + } +} -./miri rustc-push []: -Push Miri changes back to the rustc repo. This will pull a copy of the rustc -history into the Miri repo, unless you set the RUSTC_GIT env var to an existing -clone of the rustc repo. The branch defaults to `miri-sync`. +fn parse_many_seeds(arg: &str) -> Result { + parse_range(arg).map_err(|e| e.to_string()) +} - ENVIRONMENT VARIABLES +fn build_cli() -> ClapCommand { + ClapCommand::new("miri") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + ClapCommand::new("install").about("Installs the miri driver and cargo-miri.").arg( + Arg::new("flags") + .help("Flags that are passed through to `cargo install`.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("build").about("Just build miri.").arg( + Arg::new("flags") + .help("Flags that are passed through to `cargo build`.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("check").about("Just check miri.").arg( + Arg::new("flags") + .help("Flags that are passed through to `cargo check`.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("test") + .about("Build miri, set up a sysroot and then run the test suite.") + .arg(Arg::new("bless").long("bless").action(ArgAction::SetTrue)) + .arg( + Arg::new("target") + .long("target") + .help("The cross-interpretation target. If none, the host is the target."), + ) + .arg( + Arg::new("coverage") + .long("coverage") + .help("Produce coverage report if set.") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("flags") + .help("Flags that are passed through to the test harness.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("run") + .about("Run the driver with the given flags.") + .arg(Arg::new("dep").long("dep").action(ArgAction::SetTrue)) + .arg(Arg::new("verbose").long("verbose").short('v').action(ArgAction::SetTrue)) + .arg( + Arg::new("many_seeds") + .long("many-seeds") + .help("Specify a range for many seeds.") + .value_parser(parse_many_seeds), + ) + .arg(Arg::new("target").long("target")) + .arg(Arg::new("edition").long("edition")) + .arg( + Arg::new("flags") + .help("Flags that are passed through to `miri`.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("doc").about("Build documentation.").arg( + Arg::new("flags") + .help("Flags that are passed through to `cargo doc`.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("fmt").about("Format all sources and tests.").arg( + Arg::new("flags") + .help("Flags that are passed through to `rustfmt`.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("clippy").about("Runs clippy on all sources.").arg( + Arg::new("flags") + .help("Flags that are passed through to `cargo clippy`.") + .action(ArgAction::Append) + .trailing_var_arg(true) + .allow_hyphen_values(true), + ), + ) + .subcommand( + ClapCommand::new("bench") + .about("Runs benchmarks with hyperfine.") + .arg(Arg::new("target").long("target")) + .arg( + Arg::new("benches") + .help("List of benchmarks to run.") + .action(ArgAction::Append), + ), + ) + .subcommand( + ClapCommand::new("toolchain") + .about("Update and activate the rustup toolchain 'miri'.") + .arg(Arg::new("flags").action(ArgAction::Append).trailing_var_arg(true)) + .allow_hyphen_values(true), + ) + .subcommand( + ClapCommand::new("rustc-pull") + .about("Pull and merge Miri changes from the rustc repo.") + .arg(Arg::new("commit").help("The commit hash to fetch.")), + ) + .subcommand( + ClapCommand::new("rustc-push") + .about("Push Miri changes back to the rustc repo.") + .arg(Arg::new("github_user").help("GitHub user for the push.")) + .arg(Arg::new("branch").help("Branch for the push.")), + ) +} -MIRI_SYSROOT: -If already set, the "sysroot setup" step is skipped. +fn main() -> Result<()> { + let miri_args: Vec<_> = + std::env::args().take_while(|x| *x != "--").filter(|x| *x != "--").collect(); + let remainder: Vec<_> = std::env::args().skip_while(|x| *x != "--").skip(1).collect(); -CARGO_EXTRA_FLAGS: -Pass extra flags to all cargo invocations. (Ignored by `./miri cargo`.)"#; + let matches = build_cli().get_matches_from(miri_args); -fn main() -> Result<()> { - // We are hand-rolling our own argument parser, since `clap` can't express what we need - // (https://github.com/clap-rs/clap/issues/5055). - let mut args = args::Args::new(); - let command = match args.next_raw().as_deref() { - Some("build") => Command::Build { flags: args.remainder() }, - Some("check") => Command::Check { flags: args.remainder() }, - Some("doc") => Command::Doc { flags: args.remainder() }, - Some("test") => { - let mut target = None; - let mut bless = false; - let mut flags = Vec::new(); - let mut coverage = false; - loop { - if args.get_long_flag("bless")? { - bless = true; - } else if args.get_long_flag("coverage")? { - coverage = true; - } else if let Some(val) = args.get_long_opt("target")? { - target = Some(val); - } else if let Some(flag) = args.get_other() { - flags.push(flag); - } else { - break; - } - } - Command::Test { bless, flags, target, coverage } - } - Some("run") => { - let mut dep = false; - let mut verbose = false; - let mut many_seeds = None; - let mut target = None; - let mut edition = None; - let mut flags = Vec::new(); - loop { - if args.get_long_flag("dep")? { - dep = true; - } else if args.get_long_flag("verbose")? || args.get_short_flag('v')? { - verbose = true; - } else if let Some(val) = args.get_long_opt_with_default("many-seeds", "0..64")? { - let (from, to) = val.split_once("..").ok_or_else(|| { - anyhow!("invalid format for `--many-seeds`: expected `from..to`") - })?; - let from: u32 = if from.is_empty() { - 0 - } else { - from.parse().context("invalid `from` in `--many-seeds=from..to")? - }; - let to: u32 = to.parse().context("invalid `to` in `--many-seeds=from..to")?; - many_seeds = Some(from..to); - } else if let Some(val) = args.get_long_opt("target")? { - target = Some(val); - } else if let Some(val) = args.get_long_opt("edition")? { - edition = Some(val); - } else if let Some(flag) = args.get_other() { - flags.push(flag); - } else { - break; - } - } - Command::Run { dep, verbose, many_seeds, target, edition, flags } - } - Some("fmt") => Command::Fmt { flags: args.remainder() }, - Some("clippy") => Command::Clippy { flags: args.remainder() }, - Some("install") => Command::Install { flags: args.remainder() }, - Some("bench") => { - let mut target = None; - let mut benches = Vec::new(); - loop { - if let Some(val) = args.get_long_opt("target")? { - target = Some(val); - } else if let Some(flag) = args.get_other() { - benches.push(flag); - } else { - break; - } - } - Command::Bench { target, benches } - } - Some("toolchain") => Command::Toolchain { flags: args.remainder() }, - Some("rustc-pull") => { - let commit = args.next_raw(); - if args.next_raw().is_some() { - bail!("Too many arguments for `./miri rustc-pull`"); - } - Command::RustcPull { commit } - } - Some("rustc-push") => { - let github_user = args.next_raw().ok_or_else(|| { - anyhow!("Missing first argument for `./miri rustc-push GITHUB_USER [BRANCH]`") - })?; - let branch = args.next_raw().unwrap_or_else(|| "miri-sync".into()); - if args.next_raw().is_some() { - bail!("Too many arguments for `./miri rustc-push GITHUB_USER BRANCH`"); - } - Command::RustcPush { github_user, branch } - } - _ => { - eprintln!("Unknown or missing command. Usage:\n\n{HELP}"); - std::process::exit(1); - } + let mut command = match matches.subcommand() { + Some(("install", sub_m)) => + Command::Install { + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("build", sub_m)) => + Command::Build { + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("check", sub_m)) => + Command::Check { + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("test", sub_m)) => + Command::Test { + bless: sub_m.get_flag("bless"), + target: sub_m.get_one::("target").cloned(), + coverage: sub_m.get_flag("coverage"), + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("run", sub_m)) => + Command::Run { + dep: sub_m.get_flag("dep"), + verbose: sub_m.get_flag("verbose"), + many_seeds: sub_m.get_one::("many_seeds").cloned(), + target: sub_m.get_one::("target").cloned(), + edition: sub_m.get_one::("edition").cloned(), + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("doc", sub_m)) => + Command::Doc { + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("fmt", sub_m)) => + Command::Fmt { + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("clippy", sub_m)) => + Command::Clippy { + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("bench", sub_m)) => + Command::Bench { + target: sub_m.get_one::("target").cloned(), + benches: sub_m.get_many::("benches").unwrap_or_default().cloned().collect(), + }, + Some(("toolchain", sub_m)) => + Command::Toolchain { + flags: sub_m.get_many::("flags").unwrap_or_default().cloned().collect(), + }, + Some(("rustc-pull", sub_m)) => + Command::RustcPull { commit: sub_m.get_one::("commit").cloned() }, + Some(("rustc-push", sub_m)) => + Command::RustcPush { + github_user: sub_m + .get_one::("github_user") + .context("missing GitHub user")? + .to_string(), + branch: sub_m.get_one::("branch").context("missing branch")?.to_string(), + }, + _ => unreachable!("Unhandled subcommand"), }; + + command.add_remainder(remainder); command.exec()?; Ok(()) }