diff --git a/cli/src/cli.rs b/cli/src/cli.rs index c3e30d5172f057..04c2c468a4716d 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -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}, @@ -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; @@ -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()); @@ -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) @@ -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::*; @@ -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); @@ -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"); diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index df5ea834847cb1..27f730d20f904d 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -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; @@ -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(); diff --git a/cli/src/main.rs b/cli/src/main.rs index a6e91adad43371..7a9c21bce2d66e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,18 +1,16 @@ use clap::{ - crate_description, crate_name, value_t_or_exit, AppSettings, Arg, ArgGroup, ArgMatches, - SubCommand, + crate_description, crate_name, value_t_or_exit, ArgMatches }; use console::style; use solana_clap_utils::{ - input_validators::{is_url, is_url_or_moniker, normalize_to_url_if_moniker}, - keypair::{CliSigners, DefaultSigner, SKIP_SEED_PHRASE_VALIDATION_ARG}, - DisplayError, + input_validators::{normalize_to_url_if_moniker}, + keypair::{CliSigners, DefaultSigner}, + DisplayError }; use solana_cli::cli::{ - app, parse_command, process_command, CliCommandInfo, CliConfig, SettingType, - DEFAULT_CONFIRM_TX_TIMEOUT_SECONDS, DEFAULT_RPC_TIMEOUT_SECONDS, + get_clap_app, parse_command, process_command, CliCommandInfo, CliConfig, SettingType }; -use solana_cli_config::{Config, CONFIG_FILE}; +use solana_cli_config::{Config}; use solana_cli_output::{display::println_name_value, OutputFormat}; use solana_client::rpc_config::RpcSendTransactionConfig; use solana_remote_wallet::remote_wallet::RemoteWalletManager; @@ -249,179 +247,11 @@ pub fn parse_args<'a>( fn main() -> Result<(), Box> { solana_logger::setup_with_default("off"); - let matches = app( + let matches = get_clap_app( crate_name!(), crate_description!(), solana_version::version!(), - ) - .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"), - ), - ), - ) - .get_matches(); + ).get_matches(); do_main(&matches).map_err(|err| DisplayError::new_as_boxed(err).into()) } diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs index d027f3a227a1da..074d9cd8c5c133 100644 --- a/cli/src/nonce.rs +++ b/cli/src/nonce.rs @@ -576,7 +576,7 @@ pub fn process_withdraw_from_nonce_account( #[cfg(test)] mod tests { use super::*; - use crate::cli::{app, parse_command}; + use crate::cli::{get_clap_app, parse_command}; use solana_sdk::{ account::Account, account_utils::StateMut, @@ -596,7 +596,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(); diff --git a/cli/src/program.rs b/cli/src/program.rs index 8e5f1b77bcaf9c..c7e0610f797416 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -2118,7 +2118,7 @@ fn send_and_confirm_transactions_with_spinner( #[cfg(test)] mod tests { use super::*; - use crate::cli::{app, parse_command, process_command}; + use crate::cli::{get_clap_app, parse_command, process_command}; use serde_json::Value; use solana_cli_output::OutputFormat; use solana_sdk::signature::write_keypair_file; @@ -2140,7 +2140,7 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] fn test_cli_parse_deploy() { - let test_commands = app("test", "desc", "version"); + let test_commands = get_clap_app("test", "desc", "version"); let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); @@ -2348,7 +2348,7 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] fn test_cli_parse_write_buffer() { - let test_commands = app("test", "desc", "version"); + let test_commands = get_clap_app("test", "desc", "version"); let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); @@ -2496,7 +2496,7 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] fn test_cli_parse_set_upgrade_authority() { - let test_commands = app("test", "desc", "version"); + let test_commands = get_clap_app("test", "desc", "version"); let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); @@ -2604,7 +2604,7 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] fn test_cli_parse_set_buffer_authority() { - let test_commands = app("test", "desc", "version"); + let test_commands = get_clap_app("test", "desc", "version"); let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); @@ -2661,7 +2661,7 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] fn test_cli_parse_show() { - let test_commands = app("test", "desc", "version"); + let test_commands = get_clap_app("test", "desc", "version"); let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); @@ -2760,7 +2760,7 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] fn test_cli_parse_close() { - let test_commands = app("test", "desc", "version"); + let test_commands = get_clap_app("test", "desc", "version"); let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index e28597e87609f5..77021145530dda 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -2358,7 +2358,7 @@ pub fn is_stake_program_v2_enabled( #[cfg(test)] mod tests { use super::*; - use crate::cli::{app, parse_command}; + use crate::cli::{get_clap_app, parse_command}; use solana_client::blockhash_query; use solana_sdk::{ hash::Hash, @@ -2376,7 +2376,7 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] 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(); diff --git a/cli/src/validator_info.rs b/cli/src/validator_info.rs index 6ac5f239b95a4d..03a4df9d284fea 100644 --- a/cli/src/validator_info.rs +++ b/cli/src/validator_info.rs @@ -408,7 +408,7 @@ pub fn process_get_validator_info( #[cfg(test)] mod tests { use super::*; - use crate::cli::app; + use crate::cli::get_clap_app; use bincode::{serialize, serialized_size}; use serde_json::json; @@ -432,7 +432,7 @@ mod tests { #[test] fn test_parse_args() { - let matches = app("test", "desc", "version").get_matches_from(vec![ + let matches = get_clap_app("test", "desc", "version").get_matches_from(vec![ "test", "validator-info", "publish", diff --git a/cli/src/vote.rs b/cli/src/vote.rs index ce27c55cf11cec..56039f697b709c 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -880,7 +880,7 @@ pub fn process_withdraw_from_vote_account( #[cfg(test)] mod tests { use super::*; - use crate::cli::{app, parse_command}; + use crate::cli::{get_clap_app, parse_command}; use solana_sdk::signature::{read_keypair_file, write_keypair, Keypair, Signer}; use tempfile::NamedTempFile; @@ -891,7 +891,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 keypair = Keypair::new(); let pubkey = keypair.pubkey(); let pubkey_string = pubkey.to_string();