Skip to content

Commit

Permalink
Auto-generate shell completions for solana-cli (issue solana-labs#8879
Browse files Browse the repository at this point in the history
…and solana-labs#14005)

Implement `completion` SubCommand for solana-cli, which outputs
completion script to stdout and exits the process. The script generation
handled completely by clap.

In order to implement the generation, one minor design change was
necessary regarding the creation of clap `App`.

Previously: One part of App initialization was in the `app` function,
and some other arguments and subcommands were added later directly in
the `main` function.

Now: The whole construction of App was moved to `get_clap_app` function.

P.S. I wasn't sure if constructing App separately had visual importance,
so both constructing parts are still separate in `base_clap_app` and
`final_clap_app` functions. But they sure could be in one single
function.
  • Loading branch information
theonekeyg committed Jul 27, 2021
1 parent 6980d42 commit 8ae0a81
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 201 deletions.
234 changes: 228 additions & 6 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ use crate::{
cluster_query::*, feature::*, inflation::*, memo::*, nonce::*, program::*, spend_utils::*,
stake::*, validator_info::*, vote::*,
};
use clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand};
use clap::{
crate_description, crate_name, value_t_or_exit, App, AppSettings, Arg, ArgMatches,
SubCommand, Shell, ArgGroup
};
use log::*;
use num_traits::FromPrimitive;
use serde_json::{self, Value};
use solana_account_decoder::{UiAccount, UiAccountEncoding};
use solana_cli_config::CONFIG_FILE;
use solana_clap_utils::{
self,
fee_payer::{fee_payer_arg, FEE_PAYER_ARG},
Expand Down Expand Up @@ -53,8 +57,8 @@ use solana_sdk::{
use solana_transaction_status::{EncodedTransaction, UiTransactionEncoding};
use solana_vote_program::vote_state::VoteAuthorize;
use std::{
collections::HashMap, error, fmt::Write as FmtWrite, fs::File, io::Write, str::FromStr,
sync::Arc, time::Duration,
collections::HashMap, error, fmt::Write as FmtWrite, fs::File, io::Write, io::stdout,
str::FromStr, sync::Arc, time::Duration,
};
use thiserror::Error;

Expand Down Expand Up @@ -949,6 +953,37 @@ pub fn parse_command(
signers: vec![],
})
}
("completion", Some(matches)) => {
match matches.value_of("shell") {
Some("bash") =>
get_clap_app(
crate_name!(), crate_description!(),
solana_version::version!()
).gen_completions_to("solana", Shell::Bash, &mut stdout()),
Some("fish") =>
get_clap_app(
crate_name!(), crate_description!(),
solana_version::version!()
).gen_completions_to("solana", Shell::Fish, &mut stdout()),
Some("zsh") =>
get_clap_app(
crate_name!(), crate_description!(),
solana_version::version!()
).gen_completions_to("solana", Shell::Zsh, &mut stdout()),
Some("powershell") =>
get_clap_app(
crate_name!(), crate_description!(),
solana_version::version!()
).gen_completions_to("solana", Shell::PowerShell, &mut stdout()),
Some("elvish") =>
get_clap_app(
crate_name!(), crate_description!(),
solana_version::version!()
).gen_completions_to("solana", Shell::Elvish, &mut stdout()),
_ => unreachable!() // This is safe, since we assign default_value
}
std::process::exit(0);
}
//
("", None) => {
eprintln!("{}", matches.usage());
Expand Down Expand Up @@ -2040,7 +2075,189 @@ where
}
}

pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, 'v> {
fn finalize_clap_app<'ab, 'v>(app: App<'ab, 'v>) -> App<'ab, 'v> {
app.arg({
let arg = Arg::with_name("config_file")
.short("C")
.long("config")
.value_name("FILEPATH")
.takes_value(true)
.global(true)
.help("Configuration file to use");
if let Some(ref config_file) = *CONFIG_FILE {
arg.default_value(config_file)
} else {
arg
}
})
.arg(
Arg::with_name("json_rpc_url")
.short("u")
.long("url")
.value_name("URL_OR_MONIKER")
.takes_value(true)
.global(true)
.validator(is_url_or_moniker)
.help(
"URL for Solana's JSON RPC or moniker (or their first letter): \
[mainnet-beta, testnet, devnet, localhost]",
),
)
.arg(
Arg::with_name("websocket_url")
.long("ws")
.value_name("URL")
.takes_value(true)
.global(true)
.validator(is_url)
.help("WebSocket URL for the solana cluster"),
)
.arg(
Arg::with_name("keypair")
.short("k")
.long("keypair")
.value_name("KEYPAIR")
.global(true)
.takes_value(true)
.help("Filepath or URL to a keypair"),
)
.arg(
Arg::with_name("commitment")
.long("commitment")
.takes_value(true)
.possible_values(&[
"processed",
"confirmed",
"finalized",
"recent", // Deprecated as of v1.5.5
"single", // Deprecated as of v1.5.5
"singleGossip", // Deprecated as of v1.5.5
"root", // Deprecated as of v1.5.5
"max", // Deprecated as of v1.5.5
])
.value_name("COMMITMENT_LEVEL")
.hide_possible_values(true)
.global(true)
.help("Return information at the selected commitment level [possible values: processed, confirmed, finalized]"),
)
.arg(
Arg::with_name("verbose")
.long("verbose")
.short("v")
.global(true)
.help("Show additional information"),
)
.arg(
Arg::with_name("no_address_labels")
.long("no-address-labels")
.global(true)
.help("Do not use address labels in the output"),
)
.arg(
Arg::with_name("output_format")
.long("output")
.value_name("FORMAT")
.global(true)
.takes_value(true)
.possible_values(&["json", "json-compact"])
.help("Return information in specified output format"),
)
.arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
.global(true)
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
)
.arg(
Arg::with_name("rpc_timeout")
.long("rpc-timeout")
.value_name("SECONDS")
.takes_value(true)
.default_value(DEFAULT_RPC_TIMEOUT_SECONDS)
.global(true)
.hidden(true)
.help("Timeout value for RPC requests"),
)
.arg(
Arg::with_name("confirm_transaction_initial_timeout")
.long("confirm-timeout")
.value_name("SECONDS")
.takes_value(true)
.default_value(DEFAULT_CONFIRM_TX_TIMEOUT_SECONDS)
.global(true)
.hidden(true)
.help("Timeout value for initial transaction status"),
)
.subcommand(
SubCommand::with_name("config")
.about("Solana command-line tool configuration settings")
.aliases(&["get", "set"])
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("get")
.about("Get current config settings")
.arg(
Arg::with_name("specific_setting")
.index(1)
.value_name("CONFIG_FIELD")
.takes_value(true)
.possible_values(&[
"json_rpc_url",
"websocket_url",
"keypair",
"commitment",
])
.help("Return a specific config setting"),
),
)
.subcommand(
SubCommand::with_name("set")
.about("Set a config setting")
.group(
ArgGroup::with_name("config_settings")
.args(&["json_rpc_url", "websocket_url", "keypair", "commitment"])
.multiple(true)
.required(true),
),
)
.subcommand(
SubCommand::with_name("import-address-labels")
.about("Import a list of address labels")
.arg(
Arg::with_name("filename")
.index(1)
.value_name("FILENAME")
.takes_value(true)
.help("YAML file of address labels"),
),
)
.subcommand(
SubCommand::with_name("export-address-labels")
.about("Export the current address labels")
.arg(
Arg::with_name("filename")
.index(1)
.value_name("FILENAME")
.takes_value(true)
.help("YAML file to receive the current address labels"),
),
),
)
.subcommand(
SubCommand::with_name("completion")
.about("Generate completion scripts for various shells")
.arg(
Arg::with_name("shell")
.long("shell")
.short("s")
.takes_value(true)
.possible_values(&["bash", "fish", "zsh", "powershell", "elvish"])
.default_value("bash")
)
)
}

fn base_clap_app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, 'v> {
App::new(name)
.about(about)
.version(version)
Expand Down Expand Up @@ -2308,6 +2525,11 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, '
.vote_subcommands()
}

pub fn get_clap_app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, 'v> {
let app = base_clap_app(name, about, version);
return finalize_clap_app(app);
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -2417,7 +2639,7 @@ mod tests {
#[test]
#[allow(clippy::cognitive_complexity)]
fn test_cli_parse_command() {
let test_commands = app("test", "desc", "version");
let test_commands = get_clap_app("test", "desc", "version");

let pubkey = solana_sdk::pubkey::new_rand();
let pubkey_string = format!("{}", pubkey);
Expand Down Expand Up @@ -2954,7 +3176,7 @@ mod tests {

#[test]
fn test_parse_transfer_subcommand() {
let test_commands = app("test", "desc", "version");
let test_commands = get_clap_app("test", "desc", "version");

let default_keypair = Keypair::new();
let default_keypair_file = make_tmp_path("keypair_file");
Expand Down
4 changes: 2 additions & 2 deletions cli/src/cluster_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2143,7 +2143,7 @@ pub fn process_calculate_rent(
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{app, parse_command};
use crate::cli::{get_clap_app, parse_command};
use solana_sdk::signature::{write_keypair, Keypair};
use std::str::FromStr;
use tempfile::NamedTempFile;
Expand All @@ -2155,7 +2155,7 @@ mod tests {

#[test]
fn test_parse_command() {
let test_commands = app("test", "desc", "version");
let test_commands = get_clap_app("test", "desc", "version");
let default_keypair = Keypair::new();
let (default_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap();
Expand Down
Loading

0 comments on commit 8ae0a81

Please sign in to comment.