diff --git a/crates/radicle-cli/examples/rad-help.md b/crates/radicle-cli/examples/rad-help.md index e4ac94c3..a2589de2 100644 --- a/crates/radicle-cli/examples/rad-help.md +++ b/crates/radicle-cli/examples/rad-help.md @@ -1,45 +1,52 @@ ``` $ rad --help -rad [..] -Radicle command line interface +Radicle is a sovereign code forge built on Git. -Usage: rad [--help] -Common `rad` commands used in various situations: - - auth Manage identities and profiles - block Block repositories or nodes from being seeded or followed - checkout Checkout a repository into the local directory - clone Clone a Radicle repository - config Manage your local Radicle configuration - debug Write out information to help debug your Radicle node remotely - fork Create a fork of a repository - help CLI help - id Manage repository identities - init Initialize a Radicle repository - inbox Manage your Radicle notifications - inspect Inspect a Radicle repository - issue Manage issues - ls List repositories - node Control and query the Radicle Node - patch Manage patches - path Display the Radicle home path - publish Publish a repository to the network - clean Remove all remotes from a repository - self Show information about your identity and device - seed Manage repository seeding policies - follow Manage node follow policies - unblock Unblock repositories or nodes to allow them to be seeded or followed - unfollow Unfollow a peer - unseed Remove repository seeding policies - remote Manage a repository's remotes - stats Displays aggregated repository and node metrics - sync Sync repositories to the network - watch Wait for some state to be updated - -See `rad --help` to learn about a specific command. +See `rad --help` to learn about a specific command. Do you have feedback? - - Chat[..] - - Mail[..] + - Chat + - Mail (Messages are automatically posted to the public #feedback channel on Zulip.) + +Usage: rad + +Commands: + auth Manage identities and profiles + block Block repositories or nodes from being seeded or followed + checkout Checkout a repository into the local directory + clean Remove all remotes from a repository + clone Clone a Radicle repository + config Manage your local Radicle configuration + debug Write out information to help debug your Radicle node remotely + follow Manage node follow policies + fork Create a fork of a repository + id Manage repository identities + inbox Manage your Radicle notifications + init Initialize a Radicle repository + inspect Inspect a Radicle repository + issue Manage issues + ls List repositories + node Control and query the Radicle Node + patch Manage patches + path Display the Radicle home path + publish Publish a repository to the network + remote Manage a repository's remotes + seed Manage repository seeding policies + self Show information about your identity and device + stats Displays aggregated repository and node metrics + sync Sync repositories to the network + unblock Unblock repositories or nodes to allow them to be seeded or followed + unfollow Unfollow a peer + unseed Remove repository seeding policies + watch Wait for some state to be updated + version Print the version information of the CLI + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version ``` diff --git a/crates/radicle-cli/src/main.rs b/crates/radicle-cli/src/main.rs index 944fa809..a82ef677 100644 --- a/crates/radicle-cli/src/main.rs +++ b/crates/radicle-cli/src/main.rs @@ -1,6 +1,7 @@ use std::ffi::OsString; -use std::io::{self, Write}; -use std::{io::ErrorKind, iter, process}; +use std::io; +use std::io::Write; +use std::{io::ErrorKind, process}; use anyhow::anyhow; use clap::builder::styling::AnsiColor; @@ -18,7 +19,16 @@ pub const RADICLE_VERSION: &str = env!("RADICLE_VERSION"); pub const RADICLE_VERSION_LONG: &str = concat!(env!("RADICLE_VERSION"), " (", env!("GIT_HEAD"), ")"); pub const DESCRIPTION: &str = "Radicle command line interface"; -pub const LONG_DESCRIPTION: &str = "Radicle is a sovereign code forge built on Git."; +pub const LONG_DESCRIPTION: &str = " +Radicle is a sovereign code forge built on Git. + +See `rad --help` to learn about a specific command. + +Do you have feedback? + - Chat <\x1b]8;;https://radicle.zulipchat.com\x1b\\radicle.zulipchat.com\x1b]8;;\x1b\\> + - Mail <\x1b]8;;mailto:feedback@radicle.xyz\x1b\\feedback@radicle.xyz\x1b]8;;\x1b\\> + (Messages are automatically posted to the public #feedback channel on Zulip.)\ +"; pub const TIMESTAMP: &str = env!("SOURCE_DATE_EPOCH"); pub const VERSION: Version = Version { name: NAME, @@ -36,15 +46,17 @@ const STYLES: Styles = Styles::styled() #[command(name = NAME)] #[command(version = RADICLE_VERSION)] #[command(long_version = RADICLE_VERSION_LONG)] +#[command(about = DESCRIPTION)] +#[command(long_about = LONG_DESCRIPTION)] #[command(propagate_version = true)] #[command(styles = STYLES)] struct CliArgs { #[command(subcommand)] - pub command: Option, + pub command: Command, } #[derive(Subcommand, Debug)] -enum Commands { +enum Command { Auth(auth::Args), Block(block::Args), Checkout(checkout::Args), @@ -54,23 +66,12 @@ enum Commands { Cob(cob::Args), Config(config::Args), Debug(debug::Args), - - /// This command is deprecated and delegates to `git diff`. - /// Even before it was deprecated, it was not printed by - /// `rad -h`, so it is also hidden. - /// - /// Since it is hidden, it makes no sense to add `about` - /// for the command listing, and since it is external, - /// `--help` will delegate to `git diff --help` it makes - /// no sense to add `long_about` for `rad diff --help`. - #[command(external_subcommand, hide = true)] - Diff(Vec), - Follow(follow::Args), Fork(fork::Args), Id(id::Args), Inbox(inbox::Args), Init(init::Args), + #[command(alias = ".")] Inspect(inspect::Args), Issue(issue::Args), Ls(ls::Args), @@ -88,13 +89,16 @@ enum Commands { Unfollow(unfollow::Args), Unseed(unseed::Args), Watch(watch::Args), -} -#[derive(Debug)] -enum Command { - Other(Vec), - Help, - Version { json: bool }, + /// Print the version information of the CLI + Version { + /// Print the version information in JSON format + #[arg(long)] + json: bool, + }, + + #[command(external_subcommand)] + External(Vec), } fn main() { @@ -113,291 +117,92 @@ fn main() { if let Err(e) = radicle::io::set_file_limit(4096) { log::warn!(target: "cli", "Unable to set open file limit: {e}"); } - match parse_args().map_err(Some).and_then(run) { + let CliArgs { command } = CliArgs::parse(); + match run(command, term::DefaultContext) { Ok(_) => process::exit(0), Err(err) => { - if let Some(err) = err { - term::error(format!("rad: {err}")); - } + term::error(format!("{err}")); process::exit(1); } } } -fn parse_args() -> anyhow::Result { - use lexopt::prelude::*; - - let mut parser = lexopt::Parser::from_env(); - let mut command = None; - let mut json = false; - - while let Some(arg) = parser.next()? { - match arg { - Long("json") => { - json = true; - } - Long("help") | Short('h') => { - command = Some(Command::Help); - } - Long("version") => { - command = Some(Command::Version { json: false }); - } - Value(val) if command.is_none() => { - if val == *"." { - command = Some(Command::Other(vec![OsString::from("inspect")])); - } else if val == "version" { - command = Some(Command::Version { json: false }); - } else { - let args = iter::once(val) - .chain(iter::from_fn(|| parser.value().ok())) - .collect(); - - command = Some(Command::Other(args)) - } - } - _ => anyhow::bail!(arg.unexpected()), - } +fn write_version(as_json: bool) -> anyhow::Result<()> { + let mut stdout = io::stdout(); + if as_json { + VERSION.write_json(&mut stdout)?; + writeln!(&mut stdout)?; + Ok(()) + } else { + VERSION.write(&mut stdout)?; + Ok(()) } - if let Some(Command::Version { json: j }) = &mut command { - *j = json; - } - Ok(command.unwrap_or_else(|| Command::Other(vec![]))) } -fn print_help() -> anyhow::Result<()> { - VERSION.write(&mut io::stdout())?; - println!("{DESCRIPTION}"); - println!(); - - help::run(Default::default(), term::DefaultContext) -} - -fn run(command: Command) -> Result<(), Option> { +fn run(command: Command, ctx: impl term::Context) -> Result<(), anyhow::Error> { match command { - Command::Version { json } => { - let mut stdout = io::stdout(); - if json { - VERSION - .write_json(&mut stdout) - .map_err(|e| Some(e.into()))?; - writeln!(&mut stdout).ok(); - } else { - VERSION.write(&mut stdout).map_err(|e| Some(e.into()))?; - } - } - Command::Help => { - print_help()?; - } - Command::Other(args) => { - let exe = args.first(); + Command::Auth(args) => term::run_command_fn(auth::run, args, ctx), + Command::Block(args) => term::run_command_fn(block::run, args, ctx), + Command::Checkout(args) => term::run_command_fn(checkout::run, args, ctx), + Command::Clean(args) => term::run_command_fn(clean::run, args, ctx), + Command::Clone(args) => term::run_command_fn(clone::run, args, ctx), + Command::Cob(args) => term::run_command_fn(cob::run, args, ctx), + Command::Config(args) => term::run_command_fn(config::run, args, ctx), + Command::Debug(args) => term::run_command_fn(debug::run, args, ctx), + Command::Follow(args) => term::run_command_fn(follow::run, args, ctx), + Command::Fork(args) => term::run_command_fn(fork::run, args, ctx), + Command::Id(args) => term::run_command_fn(id::run, args, ctx), + Command::Inbox(args) => term::run_command_fn(inbox::run, args, ctx), + Command::Init(args) => term::run_command_fn(init::run, args, ctx), + Command::Inspect(args) => term::run_command_fn(inspect::run, args, ctx), + Command::Issue(args) => term::run_command_fn(issue::run, args, ctx), + Command::Ls(args) => term::run_command_fn(ls::run, args, ctx), + Command::Node(args) => term::run_command_fn(node::run, args, ctx), + Command::Patch(args) => term::run_command_fn(patch::run, args, ctx), + Command::Path(args) => term::run_command_fn(path::run, args, ctx), + Command::Publish(args) => term::run_command_fn(publish::run, args, ctx), + Command::Remote(args) => term::run_command_fn(remote::run, args, ctx), + Command::Seed(args) => term::run_command_fn(seed::run, args, ctx), + Command::RadSelf(args) => term::run_command_fn(rad_self::run, args, ctx), + Command::Stats(args) => term::run_command_fn(stats::run, args, ctx), + Command::Sync(args) => term::run_command_fn(sync::run, args, ctx), + Command::Unblock(args) => term::run_command_fn(unblock::run, args, ctx), + Command::Unfollow(args) => term::run_command_fn(unfollow::run, args, ctx), + Command::Unseed(args) => term::run_command_fn(unseed::run, args, ctx), + Command::Watch(args) => term::run_command_fn(watch::run, args, ctx), + Command::Version { json } => write_version(json), + Command::External(mut args) => { + let exe = args.remove(0); - if let Some(Some(exe)) = exe.map(|s| s.to_str()) { - run_other(exe, &args[1..])?; - } else { - print_help()?; + // This command is deprecated and delegates to `git diff`. + // Even before it was deprecated, it was not printed by + // `rad -h`. + // + // Since it is external, `--help` will delegate to `git diff --help`. + if exe == "diff" { + return diff::run(args); } - } - } - Ok(()) -} - -/// Runs a `rad` command. `exe` expects the commands' name, e.g. `issue`, -/// `args` expects all other arguments. -/// -/// For commands that are already migrated to `clap`, we need to parse the -/// arguments again. This needs to be done for each migrated command -/// individually, otherwise `clap` would fail to parse on an non-migrated and -/// therefore unknown command. -pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option> { - match exe { - "auth" => { - if let Some(Commands::Auth(args)) = CliArgs::parse().command { - term::run_command_fn(auth::run, args); - } - } - "block" => { - if let Some(Commands::Block(args)) = CliArgs::parse().command { - term::run_command_fn(block::run, args); - } - } - "checkout" => { - if let Some(Commands::Checkout(args)) = CliArgs::parse().command { - term::run_command_fn(checkout::run, args); - } - } - "clone" => { - if let Some(Commands::Clone(args)) = CliArgs::parse().command { - term::run_command_fn(clone::run, args); - } - } - "cob" => { - if let Some(Commands::Cob(args)) = CliArgs::parse().command { - term::run_command_fn(cob::run, args); - } - } - "config" => { - if let Some(Commands::Config(args)) = CliArgs::parse().command { - term::run_command_fn(config::run, args); - } - } - "diff" => { - if let Some(Commands::Diff(mut args)) = CliArgs::parse().command { - debug_assert_eq!(args[0], "diff"); - args.remove(0); - return diff::run(args).map_err(Some); - } - } - "debug" => { - if let Some(Commands::Debug(args)) = CliArgs::parse().command { - term::run_command_fn(debug::run, args); - } - } - "follow" => { - if let Some(Commands::Follow(args)) = CliArgs::parse().command { - term::run_command_fn(follow::run, args); - } - } - "fork" => { - if let Some(Commands::Fork(args)) = CliArgs::parse().command { - term::run_command_fn(fork::run, args); - } - } - "help" => { - term::run_command_args::(help::HELP, help::run, args.to_vec()); - } - "id" => { - if let Some(Commands::Id(args)) = CliArgs::parse().command { - term::run_command_fn(id::run, args); - } - } - "inbox" => { - if let Some(Commands::Inbox(args)) = CliArgs::parse().command { - term::run_command_fn(inbox::run, args) - } - } - "init" => { - if let Some(Commands::Init(args)) = CliArgs::parse().command { - term::run_command_fn(init::run, args); - } - } - "inspect" => { - let reconstructed_args = { - // This is a horrible workaround to reconstruct the original - // args after having them mangled by our `lexopt`-style parser - // in `parse_args()` in case they were `rad .`. - // TODO: Remove this, when `rad` is fully migrated to `clap`. - vec!["rad", "inspect"] - .into_iter() - .map(OsString::from) - .chain(args.iter().cloned()) - }; - - if let Some(Commands::Inspect(args)) = CliArgs::parse_from(reconstructed_args).command { - term::run_command_fn(inspect::run, args); - } - } - "issue" => { - if let Some(Commands::Issue(args)) = CliArgs::parse().command { - term::run_command_fn(issue::run, args); - } - } - "ls" => { - if let Some(Commands::Ls(args)) = CliArgs::parse().command { - term::run_command_fn(ls::run, args); - } - } - "node" => { - if let Some(Commands::Node(args)) = CliArgs::parse().command { - term::run_command_fn(node::run, args); - } - } - "patch" => { - if let Some(Commands::Patch(args)) = CliArgs::parse().command { - term::run_command_fn(patch::run, args); - } - } - "path" => { - if let Some(Commands::Path(args)) = CliArgs::parse().command { - term::run_command_fn(path::run, args); - } - } - "publish" => { - if let Some(Commands::Publish(args)) = CliArgs::parse().command { - term::run_command_fn(publish::run, args); - } - } - "clean" => { - if let Some(Commands::Clean(args)) = CliArgs::parse().command { - term::run_command_fn(clean::run, args); - } - } - "self" => { - if let Some(Commands::RadSelf(args)) = CliArgs::parse().command { - term::run_command_fn(rad_self::run, args) - } - } - "sync" => { - if let Some(Commands::Sync(args)) = CliArgs::parse().command { - term::run_command_fn(sync::run, args); - } - } - "seed" => { - if let Some(Commands::Seed(args)) = CliArgs::parse().command { - term::run_command_fn(seed::run, args); - } - } - "unblock" => { - if let Some(Commands::Unblock(args)) = CliArgs::parse().command { - term::run_command_fn(unblock::run, args); - } - } - "unfollow" => { - if let Some(Commands::Unfollow(args)) = CliArgs::parse().command { - term::run_command_fn(unfollow::run, args); - } - } - "unseed" => { - if let Some(Commands::Unseed(args)) = CliArgs::parse().command { - term::run_command_fn(unseed::run, args); - } - } - "remote" => { - if let Some(Commands::Remote(args)) = CliArgs::parse().command { - term::run_command_fn(remote::run, args); - } - } - "stats" => { - if let Some(Commands::Stats(args)) = CliArgs::parse().command { - term::run_command_fn(stats::run, args); - } - } - "watch" => { - if let Some(Commands::Watch(args)) = CliArgs::parse().command { - term::run_command_fn(watch::run, args); - } - } - other => { - let exe = format!("{NAME}-{exe}"); - let status = process::Command::new(exe).args(args).status(); + let exe = format!("{NAME}-{exe:?}"); + let status = process::Command::new(&exe).args(&args).status(); match status { Ok(status) => { if !status.success() { - return Err(None); + return Err(anyhow!("`{exe}` exited with an error.")); } + Ok(()) } Err(err) => { if let ErrorKind::NotFound = err.kind() { - return Err(Some(anyhow!( - "`{other}` is not a command. See `rad --help` for a list of commands.", - ))); + Err(anyhow!( + "`{exe}` is not a command. See `rad --help` for a list of commands.", + )) } else { - return Err(Some(err.into())); + Err(err.into()) } } } } } - Ok(()) } diff --git a/crates/radicle-cli/src/terminal.rs b/crates/radicle-cli/src/terminal.rs index 574eb5b4..dbad96b1 100644 --- a/crates/radicle-cli/src/terminal.rs +++ b/crates/radicle-cli/src/terminal.rs @@ -57,11 +57,12 @@ where /// Execute a function `cmd` that runs a command with parsed the `args` /// and a default context. -pub fn run_command_fn(cmd: F, args: P) -> ! +pub fn run_command_fn(cmd: F, args: P, ctx: C) -> ! where - F: FnOnce(P, DefaultContext) -> anyhow::Result<()>, + F: FnOnce(P, C) -> anyhow::Result<()>, + C: Context, { - match cmd(args, DefaultContext) { + match cmd(args, ctx) { Ok(()) => process::exit(0), Err(err) => { // First parameter is not used and can just be empty.