Skip to content

Commit

Permalink
Add ability to generate completions for subcommands
Browse files Browse the repository at this point in the history
The subcommands may generate completions code optionally.

Fixes #5670
  • Loading branch information
crodas committed Feb 26, 2024
1 parent 0a8c66d commit 7c95355
Show file tree
Hide file tree
Showing 15 changed files with 237 additions and 14 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion forc-plugins/forc-client/src/bin/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use clap::Parser;
use clap::{IntoApp, Parser};
use forc_tracing::{init_tracing_subscriber, println_error};

#[tokio::main]
async fn main() {
forc_util::cli::register(forc_client::cmd::Deploy::into_app());
init_tracing_subscriber(Default::default());
let command = forc_client::cmd::Deploy::parse();
if let Err(err) = forc_client::op::deploy(command).await {
Expand Down
3 changes: 2 additions & 1 deletion forc-plugins/forc-client/src/bin/run.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use clap::Parser;
use clap::{IntoApp, Parser};
use forc_tracing::{init_tracing_subscriber, println_error};

#[tokio::main]
async fn main() {
forc_util::cli::register(forc_client::cmd::Run::into_app());
init_tracing_subscriber(Default::default());
let command = forc_client::cmd::Run::parse();
if let Err(err) = forc_client::op::run(command).await {
Expand Down
3 changes: 2 additions & 1 deletion forc-plugins/forc-client/src/bin/submit.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use clap::Parser;
use clap::{IntoApp, Parser};
use forc_tracing::{init_tracing_subscriber, println_error};

#[tokio::main]
async fn main() {
forc_util::cli::register(forc_client::cmd::Submit::into_app());
init_tracing_subscriber(Default::default());
let command = forc_client::cmd::Submit::parse();
if let Err(err) = forc_client::op::submit(command).await {
Expand Down
3 changes: 2 additions & 1 deletion forc-plugins/forc-crypto/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use anyhow::Result;
use atty::Stream;
use clap::Parser;
use clap::{IntoApp, Parser};
use forc_tracing::{init_tracing_subscriber, println_error};
use std::{
default::Default,
Expand Down Expand Up @@ -57,6 +57,7 @@ fn main() {
}

fn run() -> Result<()> {
forc_util::cli::register(Command::into_app());
let app = Command::parse();
let content = match app {
Command::Keccak256(arg) => keccak256::hash(arg)?,
Expand Down
1 change: 1 addition & 0 deletions forc-plugins/forc-debug/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ anyhow = "1.0" # Used by the examples and for conversion only
clap = { version = "3", features = ["env", "derive"] }
dap = "0.4.1-alpha1"
forc-pkg = { version = "0.51.1", path = "../../forc-pkg" }
forc-util = { version = "0.51.1", path = "../../forc-util" }
forc-test = { version = "0.51.1", path = "../../forc-test" }
fuel-core-client = { workspace = true }
fuel-types = { workspace = true, features = ["serde"] }
Expand Down
3 changes: 2 additions & 1 deletion forc-plugins/forc-debug/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::Parser;
use clap::{IntoApp, Parser};
use forc_debug::{
names::{register_index, register_name},
server::DapServer,
Expand All @@ -20,6 +20,7 @@ pub struct Opt {

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
forc_util::cli::register(Opt::into_app());
let config = Opt::parse();

if config.serve {
Expand Down
2 changes: 1 addition & 1 deletion forc-plugins/forc-doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repository.workspace = true

[dependencies]
anyhow = "1.0.65"
clap = { version = "4.0.18", features = ["derive"] }
clap = { version = "3.1", features = ["derive"] }
colored = "2.0.0"
comrak = "0.16"
forc-pkg = { version = "0.51.1", path = "../../forc-pkg" }
Expand Down
3 changes: 2 additions & 1 deletion forc-plugins/forc-doc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
search::write_search_index,
};
use anyhow::{bail, Result};
use clap::Parser;
use clap::{IntoApp, Parser};
use cli::Command;
use colored::*;
use forc_pkg as pkg;
Expand Down Expand Up @@ -51,6 +51,7 @@ struct ProgramInfo<'a> {
}

pub fn main() -> Result<()> {
forc_util::cli::register(Command::into_app());
let build_instructions = Command::parse();

let (doc_path, pkg_manifest) = compile_html(&build_instructions, &get_doc_dir)?;
Expand Down
3 changes: 2 additions & 1 deletion forc-plugins/forc-fmt/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! A `forc` plugin for running the Sway code formatter.

use anyhow::{bail, Result};
use clap::Parser;
use clap::{IntoApp, Parser};
use forc_pkg::{
manifest::{GenericManifestFile, ManifestFile},
WorkspaceManifestFile,
Expand Down Expand Up @@ -66,6 +66,7 @@ fn main() {
}

fn run() -> Result<()> {
forc_util::cli::register(App::into_app());
let app = App::parse();

let dir = match app.path.as_ref() {
Expand Down
2 changes: 1 addition & 1 deletion forc-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +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"
serde_json = "1"
serial_test = "3.0.0"
sway-core = { version = "0.51.1", path = "../sway-core" }
sway-error = { version = "0.51.1", path = "../sway-error" }
Expand Down
183 changes: 183 additions & 0 deletions forc-util/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,186 @@
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub long_help: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub subcommands: Vec<CommandInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub args: Vec<ArgInfo>,
}

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<CommandInfo> {
cmd.get_subcommands()
.map(|subcommand| CommandInfo::new(subcommand))
.collect::<Vec<_>>()
}

fn arg_conflicts(cmd: &Command, arg: &clap::Arg) -> Vec<String> {
cmd.get_arg_conflicts_with(arg)
.iter()
.flat_map(|conflict| {
vec![
conflict.get_short().map(|s| format!("-{}", s)),
conflict.get_long().map(|l| format!("--{}", l)),
]
})
.flatten()
.collect()
}

fn arg_possible_values(arg: &clap::Arg<'_>) -> Vec<PossibleValues> {
arg.get_possible_values()
.map(|possible_values| {
possible_values
.iter()
.map(|x| PossibleValues {
name: x.get_name().to_owned(),
help: x.get_help().unwrap_or_default().to_owned(),
})
.collect::<Vec<_>>()
})
.unwrap_or_default()
}

fn arg_alias(arg: &clap::Arg<'_>) -> Vec<String> {
arg.get_long_and_visible_aliases()
.map(|c| c.iter().map(|x| format!("--{}", x)).collect::<Vec<_>>())
.unwrap_or_default()
}

fn get_args(cmd: &Command) -> Vec<ArgInfo> {
cmd.get_arguments()
.map(|arg| ArgInfo {
name: if arg.get_long().is_some() {
format!("--{}", arg.get_name())
} else {
arg.get_name().to_string()
},
possible_values: Self::arg_possible_values(arg),
short: arg.get_short_and_visible_aliases(),
aliases: Self::arg_alias(arg),
help: arg.get_help().map(|s| s.to_string()),
long_help: arg.get_long_help().map(|s| s.to_string()),
conflicts: Self::arg_conflicts(cmd, arg),
is_repeatable: matches!(
arg.get_action(),
ArgAction::Set | ArgAction::Append | ArgAction::Count,
),
})
.collect::<Vec<_>>()
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PossibleValues {
pub name: String,
pub help: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ArgInfo {
pub name: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub possible_values: Vec<PossibleValues>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub short: Option<Vec<char>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub aliases: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub long_help: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub conflicts: Vec<String>,
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.possible_values.is_empty() {
arg = arg.possible_values(
self.possible_values
.iter()
.map(|pv| clap::PossibleValue::new(pv.name.as_str()).help(pv.help.as_str()))
.collect::<Vec<_>>(),
);
}
if self.is_repeatable {
arg = arg.multiple(true);
}
arg
}
}

/// Registers the current command to print the CLI definition, if the `--cli-definition` argument is
/// passed.
///
/// The existance of --cli-definition is arbitrary, a convention that is used by forc and is
/// probably not defined inside the clap struct. Because of this, the `--cli-definition` argument,
/// the function should be called *before* the `clap::App` is built to parse the arguments.
pub fn register(cmd: clap::App<'_>) {
std::env::args().skip(1).for_each(|arg| {
if arg == "--cli-definition" {
let cmd_info = CommandInfo::new(&cmd);
serde_json::to_writer_pretty(std::io::stdout(), &cmd_info).unwrap();
std::process::exit(0);
}
});
}

#[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 {
Expand Down
2 changes: 1 addition & 1 deletion forc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,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.51.1", path = "../sway-core" }
sway-error = { version = "0.51.1", path = "../sway-error" }
sway-types = { version = "0.51.1", path = "../sway-types" }
Expand Down
35 changes: 33 additions & 2 deletions forc/src/cli/commands/completions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::collections::HashMap;

use clap::{Command as ClapCommand, CommandFactory, Parser};
use clap_complete::{generate, Generator, Shell};
use forc_util::ForcResult;
use forc_util::{cli::CommandInfo, ForcResult};

use crate::cli::plugin::find_all;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
enum Target {
Expand Down Expand Up @@ -44,7 +48,34 @@ pub struct Command {
}

pub(crate) fn exec(command: Command) -> ForcResult<()> {
let mut cmd = super::super::Opt::command();
let mut cmd = CommandInfo::new(&super::super::Opt::command());
let mut plugins = HashMap::new();
find_all().for_each(|path| {
if let Ok(proc) = std::process::Command::new(path.clone())
.arg("--cli-definition")
.output()
{
if let Ok(mut command_info) = serde_json::from_slice::<CommandInfo>(&proc.stdout) {
command_info.name = if let Some(name) = path.file_name().and_then(|x| {
x.to_string_lossy()
.strip_prefix("forc-")
.map(|x| x.to_owned())
}) {
name
} else {
command_info.name
};
if !plugins.contains_key(&command_info.name) {
plugins.insert(command_info.name.to_owned(), command_info);
}
}
}
});

let mut plugins = plugins.into_values().collect::<Vec<_>>();
cmd.subcommands.append(&mut plugins);
let mut cmd = cmd.to_clap();

match command.target {
Target::Fig => print_completions(clap_complete_fig::Fig, &mut cmd),
Target::Bash => print_completions(Shell::Bash, &mut cmd),
Expand Down
2 changes: 1 addition & 1 deletion test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ prettydiff = "0.6"
rand = "0.8"
regex = "1.7"
revm = "2.3.1"
serde_json = "1.0.73"
serde_json = "1"
sway-core = { path = "../sway-core" }
sway-error = { path = "../sway-error" }
sway-ir = { path = "../sway-ir" }
Expand Down

0 comments on commit 7c95355

Please sign in to comment.