diff --git a/Cargo.lock b/Cargo.lock index 5bcddf59149..f19705551c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -739,6 +739,16 @@ dependencies = [ "clap 4.5.0", ] +[[package]] +name = "clap_complete_fig" +version = "3.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed37b4c0c1214673eba6ad8ea31666626bf72be98ffb323067d973c48b4964b9" +dependencies = [ + "clap 3.2.25", + "clap_complete 3.2.5", +] + [[package]] name = "clap_derive" version = "3.2.25" @@ -1965,6 +1975,7 @@ dependencies = [ "anyhow", "clap 3.2.25", "clap_complete 3.2.5", + "clap_complete_fig", "forc-pkg", "forc-test", "forc-tracing 0.50.0", @@ -2073,7 +2084,7 @@ name = "forc-doc" version = "0.50.0" dependencies = [ "anyhow", - "clap 4.5.0", + "clap 3.2.25", "colored", "comrak", "dir_indexer", @@ -2224,7 +2235,6 @@ dependencies = [ "regex", "serde", "serde_json", - "serial_test", "sway-core", "sway-error", "sway-types", @@ -6132,31 +6142,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial_test" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" -dependencies = [ - "dashmap", - "futures", - "lazy_static", - "log", - "parking_lot 0.12.1", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "sha2" version = "0.9.9" diff --git a/forc-plugins/forc-doc/Cargo.toml b/forc-plugins/forc-doc/Cargo.toml index 9cc5cac148c..7339d16dc45 100644 --- a/forc-plugins/forc-doc/Cargo.toml +++ b/forc-plugins/forc-doc/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true [dependencies] anyhow = "1.0.65" -clap = { version = "4.0.18", features = ["derive"] } +clap = { version = "3", features = ["derive"] } colored = "2.0.0" comrak = "0.16" forc-pkg = { version = "0.50.0", path = "../../forc-pkg" } diff --git a/forc-plugins/forc-tx/src/lib.rs b/forc-plugins/forc-tx/src/lib.rs index 3e5281efaae..088fe8808f8 100644 --- a/forc-plugins/forc-tx/src/lib.rs +++ b/forc-plugins/forc-tx/src/lib.rs @@ -13,6 +13,9 @@ use std::path::PathBuf; use thiserror::Error; forc_util::cli_examples! { + { + None + } { // This parser has a custom parser super::Command::try_parse_from_args diff --git a/forc-util/Cargo.toml b/forc-util/Cargo.toml index e47e782532b..df52858cbf5 100644 --- a/forc-util/Cargo.toml +++ b/forc-util/Cargo.toml @@ -21,8 +21,7 @@ hex = "0.4.3" paste = "1.0.14" regex = "1.10.2" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.73" -serial_test = "3.0.0" +serde_json = "1.0" sway-core = { version = "0.50.0", path = "../sway-core" } sway-error = { version = "0.50.0", path = "../sway-error" } sway-types = { version = "0.50.0", path = "../sway-types" } diff --git a/forc-util/src/cli.rs b/forc-util/src/cli.rs index f8f6ed5662e..8e110dcd8af 100644 --- a/forc-util/src/cli.rs +++ b/forc-util/src/cli.rs @@ -1,9 +1,145 @@ +use clap::{ArgAction, Command}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CommandInfo { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub long_help: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub subcommands: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub args: Vec, +} + +impl CommandInfo { + pub fn new(cmd: &Command) -> Self { + CommandInfo { + name: cmd.get_name().to_owned(), + long_help: cmd.get_after_long_help().map(|s| s.to_string()), + description: cmd.get_about().map(|s| s.to_string()), + subcommands: Self::get_subcommands(cmd), + args: Self::get_args(cmd), + } + } + + pub fn to_clap(&self) -> clap::App<'_> { + let mut cmd = Command::new(self.name.as_str()); + if let Some(desc) = &self.description { + cmd = cmd.about(desc.as_str()); + } + if let Some(long_help) = &self.long_help { + cmd = cmd.after_long_help(long_help.as_str()); + } + for subcommand in &self.subcommands { + cmd = cmd.subcommand(subcommand.to_clap()); + } + for arg in &self.args { + cmd = cmd.arg(arg.to_clap()); + } + cmd + } + + fn get_subcommands(cmd: &Command) -> Vec { + cmd.get_subcommands() + .map(|subcommand| CommandInfo::new(subcommand)) + .collect::>() + } + + fn arg_conflicts(cmd: &Command, arg: &clap::Arg) -> Vec { + let mut res = vec![]; + + for conflict in cmd.get_arg_conflicts_with(arg) { + if let Some(s) = conflict.get_short() { + res.push(format!("-{}", s.to_string())); + } + + if let Some(l) = conflict.get_long() { + res.push(format!("--{}", l)); + } + } + + res + } + + fn get_args(cmd: &Command) -> Vec { + cmd.get_arguments() + .map(|opt| ArgInfo { + name: opt.get_name().to_string(), + short: opt.get_short_and_visible_aliases(), + aliases: opt + .get_long_and_visible_aliases() + .map(|c| c.iter().map(|x| x.to_string()).collect::>()) + .unwrap_or_default(), + help: opt.get_help().map(|s| s.to_string()), + long_help: opt.get_long_help().map(|s| s.to_string()), + conflicts: Self::arg_conflicts(cmd, opt), + is_repeatable: matches!( + opt.get_action(), + ArgAction::Set | ArgAction::Append | ArgAction::Count, + ), + }) + .collect::>() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ArgInfo { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub short: Option>, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub aliases: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub help: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub long_help: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub conflicts: Vec, + pub is_repeatable: bool, +} + +impl ArgInfo { + pub fn to_clap(&self) -> clap::Arg<'_> { + let mut arg = clap::Arg::with_name(self.name.as_str()); + if let Some(short) = &self.short { + arg = arg.short(short[0]); + } + if let Some(help) = &self.help { + arg = arg.help(help.as_str()); + } + if let Some(long_help) = &self.long_help { + arg = arg.long_help(long_help.as_str()); + } + if self.is_repeatable { + arg = arg.multiple(true); + } + arg + } +} + #[macro_export] // Let the user format the help and parse it from that string into arguments to create the unit test macro_rules! cli_examples { ($st:path { $( [ $($description:ident)* => $command:stmt ] )* }) => { forc_util::cli_examples! { { + $crate::paste::paste! { + use clap::IntoApp; + Some($st::into_app()) + } + } { $crate::paste::paste! { use clap::Parser; $st::try_parse_from @@ -13,15 +149,14 @@ macro_rules! cli_examples { } } }; - ( $code:block { $( [ $($description:ident)* => $command:stmt ] )* }) => { + ( $into_app:block $parser:block { $( [ $($description:ident)* => $command:stmt ] )* }) => { $crate::paste::paste! { #[cfg(test)] mod cli_parsing { $( #[test] fn [<$($description:lower _)*:snake example>] () { - - let cli_parser = $code; + let cli_parser = $parser; let mut args = parse_args($command); if cli_parser(args.clone()).is_err() { // Failed to parse, it maybe a plugin. To execute a plugin the first argument needs to be removed, `forc`. @@ -101,8 +236,27 @@ macro_rules! cli_examples { } } + mod autocomplete { + pub(crate) fn dump() { + if let Some(mut cmd) = $into_app { + forc_util::serde_json::to_writer_pretty( + std::io::stdout(), + &forc_util::cli::CommandInfo::new(&cmd) + ).unwrap(); + std::process::exit(0); + } + } + } + + static DUMPING_CLI_DEFINITION: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); fn help() -> &'static str { + use std::sync::atomic::Ordering; + if std::env::var("CLI_DUMP_DEFINITION") == Ok("1".to_string()) { + if DUMPING_CLI_DEFINITION.compare_exchange_weak(false, true, Ordering::SeqCst, Ordering::Relaxed) == Ok(false) { + autocomplete::dump(); + } + } Box::leak(format!("{}\n{}", forc_util::ansi_term::Colour::Yellow.paint("EXAMPLES:"), examples()).into_boxed_str()) } diff --git a/forc-util/src/lib.rs b/forc-util/src/lib.rs index c23181e9d32..a801ea65a5a 100644 --- a/forc-util/src/lib.rs +++ b/forc-util/src/lib.rs @@ -34,7 +34,7 @@ pub mod cli; pub use ansi_term; pub use paste; pub use regex::Regex; -pub use serial_test; +pub use serde_json; pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out"; pub const DEFAULT_ERROR_EXIT_CODE: u8 = 1; @@ -156,7 +156,8 @@ pub mod tx_utils { pub struct Salt { /// Added salt used to derive the contract ID. /// - /// By default, this is `0x0000000000000000000000000000000000000000000000000000000000000000`. + /// By default, this is + /// `0x0000000000000000000000000000000000000000000000000000000000000000`. #[clap(long = "salt")] pub salt: Option, } diff --git a/forc/Cargo.toml b/forc/Cargo.toml index bab719c300d..8d09034869c 100644 --- a/forc/Cargo.toml +++ b/forc/Cargo.toml @@ -22,6 +22,7 @@ ansi_term = "0.12" anyhow = "1.0.41" clap = { version = "3.1", features = ["cargo", "derive", "env"] } clap_complete = "3.1" +clap_complete_fig = "3.1" forc-pkg = { version = "0.50.0", path = "../forc-pkg" } forc-test = { version = "0.50.0", path = "../forc-test" } forc-tracing = { version = "0.50.0", path = "../forc-tracing" } @@ -30,7 +31,7 @@ fs_extra = "1.2" fuel-asm = { workspace = true } hex = "0.4.3" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.73" +serde_json = "1" sway-core = { version = "0.50.0", path = "../sway-core" } sway-error = { version = "0.50.0", path = "../sway-error" } sway-types = { version = "0.50.0", path = "../sway-types" } diff --git a/forc/src/cli/commands/completions.rs b/forc/src/cli/commands/completions.rs index a52d3cf5b16..aa4b706a41c 100644 --- a/forc/src/cli/commands/completions.rs +++ b/forc/src/cli/commands/completions.rs @@ -1,7 +1,52 @@ -use clap::Command as ClapCommand; +use clap::{Command as ClapCommand, ValueEnum}; use clap::{CommandFactory, Parser}; -use clap_complete::{generate, Generator, Shell}; +use clap_complete::{generate, Generator, Shell as BuiltInShell}; +use forc_util::cli::CommandInfo; use forc_util::ForcResult; +use std::collections::HashMap; +use std::str::FromStr; + +use crate::cli::plugin::find_all; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +enum Shell { + BuiltIn(BuiltInShell), + Fig, +} + +impl FromStr for Shell { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "fig" => Ok(Shell::Fig), + other => Ok(Shell::BuiltIn(::from_str( + other, + )?)), + } + } +} + +impl ValueEnum for Shell { + fn value_variants<'a>() -> &'a [Self] { + &[ + Shell::BuiltIn(BuiltInShell::Bash), + Shell::BuiltIn(BuiltInShell::Elvish), + Shell::BuiltIn(BuiltInShell::Fish), + Shell::BuiltIn(BuiltInShell::PowerShell), + Shell::BuiltIn(BuiltInShell::Zsh), + Shell::Fig, + ] + } + + fn to_possible_value<'a>(&self) -> Option> { + match self { + Shell::BuiltIn(shell) => shell.to_possible_value(), + Shell::Fig => Some(clap::PossibleValue::new("fig")), + } + } +} /// Generate tab-completion scripts for your shell #[derive(Debug, Parser)] @@ -16,8 +61,35 @@ pub struct Command { } pub(crate) fn exec(command: Command) -> ForcResult<()> { - let mut cmd = super::super::Opt::command(); - print_completions(command.shell, &mut cmd); + let mut cmd = CommandInfo::new(&super::super::Opt::command()); + + let mut plugins = HashMap::new(); + find_all().for_each(|path| { + let mut proc = std::process::Command::new(path.clone()); + proc.env("CLI_DUMP_DEFINITION", "1"); + if let Ok(proc) = proc.output() { + if let Ok(mut command_info) = serde_json::from_slice::(&proc.stdout) { + command_info.name = if let Some(name) = command_info.name.strip_prefix("forc-") { + name.to_string() + } else { + command_info.name + }; + plugins.insert(command_info.name.to_owned(), command_info); + } + } + }); + + let mut plugins = plugins + .into_iter() + .map(|(_, plugin)| plugin) + .collect::>(); + cmd.subcommands.append(&mut plugins); + + let mut cmd = cmd.to_clap(); + match command.shell { + Shell::Fig => print_completions(clap_complete_fig::Fig, &mut cmd), + Shell::BuiltIn(shell) => print_completions(shell, &mut cmd), + } Ok(()) }