diff --git a/Cargo.lock b/Cargo.lock index 3c698f08ee..761bb18e81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,7 +318,7 @@ dependencies = [ "serde_urlencoded", "socket2", "time 0.2.22", - "tinyvec 1.0.1", + "tinyvec", "url", ] @@ -374,15 +374,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "arrayref" version = "0.3.6" @@ -718,19 +709,36 @@ dependencies = [ [[package]] name = "clap" -version = "2.33.3" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" dependencies = [ - "ansi_term", "atty", "bitflags", - "strsim 0.8.0", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim 0.10.0", + "termcolor", "textwrap", "unicode-width", "vec_map", ] +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clicolors-control" version = "1.0.1" @@ -1714,7 +1722,7 @@ dependencies = [ [[package]] name = "ic-agent" version = "0.1.0" -source = "git+https://github.com/dfinity/agent-rs.git?branch=next#dd99c5c1d7f2ade92451d21eb20fccca59237b84" +source = "git+https://github.com/dfinity/agent-rs.git?branch=next#132d7995435aeaf8011ee02c0b4009c292c5c2cd" dependencies = [ "async-trait", "base32", @@ -1741,7 +1749,7 @@ dependencies = [ [[package]] name = "ic-types" version = "0.1.2" -source = "git+https://github.com/dfinity/agent-rs.git?branch=next#dd99c5c1d7f2ade92451d21eb20fccca59237b84" +source = "git+https://github.com/dfinity/agent-rs.git?branch=next#132d7995435aeaf8011ee02c0b4009c292c5c2cd" dependencies = [ "base32", "crc32fast", @@ -1753,7 +1761,7 @@ dependencies = [ [[package]] name = "ic-utils" version = "0.1.0" -source = "git+https://github.com/dfinity/agent-rs.git?branch=next#dd99c5c1d7f2ade92451d21eb20fccca59237b84" +source = "git+https://github.com/dfinity/agent-rs.git?branch=next#132d7995435aeaf8011ee02c0b4009c292c5c2cd" dependencies = [ "async-trait", "candid", @@ -2145,9 +2153,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a1cda389c26d6b88f3d2dc38aa1b750fe87d298cc5d795ec9e975f402f00372" +checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f" dependencies = [ "lazy_static", "libc", @@ -2282,9 +2290,9 @@ checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" [[package]] name = "once_cell" -version = "1.4.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" [[package]] name = "opaque-debug" @@ -2331,6 +2339,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + [[package]] name = "parking_lot" version = "0.11.0" @@ -2359,9 +2373,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7ae1a2180ed02ddfdb5ab70c70d596a26dd642e097bb6fe78b1bde8588ed97" +checksum = "7151b083b0664ed58ed669fcdd92f01c3d2fdbf10af4931a301474950b52bfa9" [[package]] name = "pem" @@ -3218,9 +3232,9 @@ checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" [[package]] name = "socket2" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44" +checksum = "7fd8b795c389288baa5f355489c65e71fd48a02104600d15c4cfbc561e9e429d" dependencies = [ "cfg-if 0.1.10", "libc", @@ -3307,15 +3321,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "strsim" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -3424,9 +3438,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" dependencies = [ "unicode-width", ] @@ -3518,12 +3532,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tinyvec" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" - [[package]] name = "tinyvec" version = "1.0.1" @@ -3541,9 +3549,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" +checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" dependencies = [ "bytes", "fnv", @@ -3738,11 +3746,11 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +checksum = "b7f98e67a4d84f730d343392f9bfff7d21e3fca562b9cb7a43b768350beeddc6" dependencies = [ - "tinyvec 0.3.4", + "tinyvec", ] [[package]] diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 4429e0ff4f..0f84899551 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -23,7 +23,7 @@ atty = "0.2.13" base64 = "0.11.0" candid = "0.6.7" chrono = "0.4.9" -clap = "2.33.0" +clap = "3.0.0-beta.2" console = "0.7.7" crossbeam = "0.7.3" ctrlc = { version = "3.1.6", features = [ "termination" ] } diff --git a/src/dfx/src/commands/bootstrap.rs b/src/dfx/src/commands/bootstrap.rs index 3c969a4735..139b4d0a0d 100644 --- a/src/dfx/src/commands/bootstrap.rs +++ b/src/dfx/src/commands/bootstrap.rs @@ -1,12 +1,11 @@ use crate::config::dfinity::{ConfigDefaults, ConfigDefaultsBootstrap}; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::network::network_descriptor::NetworkDescriptor; use crate::lib::provider::get_network_descriptor; use crate::lib::webserver::webserver; use crate::util::get_reusable_socket_addr; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use slog::info; use std::default::Default; use std::fs; @@ -17,53 +16,49 @@ use std::str::FromStr; use std::time::Duration; use url::Url; -/// Constructs a sub-command to run the bootstrap server. -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("bootstrap") - .about(UserMessage::BootstrapCommand.to_str()) - .arg( - Arg::with_name("ip") - .help(UserMessage::BootstrapIP.to_str()) - .long("ip") - .takes_value(true), - ) - .arg( - Arg::with_name("port") - .help(UserMessage::BootstrapPort.to_str()) - .long("port") - .takes_value(true), - ) - .arg( - Arg::with_name("network") - .help(UserMessage::CanisterComputeNetwork.to_str()) - .long("network") - .takes_value(true), - ) - .arg( - Arg::with_name("root") - .help(UserMessage::BootstrapRoot.to_str()) - .long("root") - .takes_value(true), - ) - .arg( - Arg::with_name("timeout") - .help(UserMessage::BootstrapTimeout.to_str()) - .long("timeout") - .takes_value(true), - ) +/// Starts the bootstrap server. +#[derive(Clap, Clone)] +#[clap(name("bootstrap"))] +pub struct BootstrapOpts { + /// Specifies the IP address that the bootstrap server listens on. Defaults to 127.0.0.1. + #[clap(long)] + ip: Option, + + /// Specifies the port number that the bootstrap server listens on. Defaults to 8081. + #[clap(long)] + port: Option, + + /// Override the compute network to connect to. By default, the local network is used. + #[clap(long)] + network: Option, + + /// Specifies the directory containing static assets served by the bootstrap server. + /// Defaults to $HOME/.cache/dfinity/versions/$DFX_VERSION/js-user-library/dist/bootstrap. + #[clap(long)] + root: Option, + + /// Specifies the maximum number of seconds that the bootstrap server + /// will wait for upstream requests to complete. Defaults to 30. + #[clap(long)] + timeout: Option, +} + +pub fn construct() -> App<'static> { + BootstrapOpts::into_app() } /// Runs the bootstrap server. -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: BootstrapOpts = BootstrapOpts::from_arg_matches(args); let logger = env.get_logger(); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; let config_defaults = get_config_defaults_from_file(env); let base_config_bootstrap = config_defaults.get_bootstrap().to_owned(); - let config_bootstrap = apply_arguments(&base_config_bootstrap, env, args)?; + let config_bootstrap = apply_arguments(&base_config_bootstrap, env, opts.clone())?; - let network_descriptor = get_network_descriptor(env, args)?; + let network_descriptor = get_network_descriptor(env, opts.network)?; let build_output_root = config.get_temp_path().join(network_descriptor.name.clone()); let build_output_root = build_output_root.join("canisters"); @@ -118,12 +113,12 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { fn apply_arguments( config: &ConfigDefaultsBootstrap, env: &dyn Environment, - args: &ArgMatches<'_>, + opts: BootstrapOpts, ) -> DfxResult { - let ip = get_ip(&config, args)?; - let port = get_port(&config, args)?; - let root = get_root(&config, env, args)?; - let timeout = get_timeout(&config, args)?; + let ip = get_ip(&config, opts.ip.as_deref())?; + let port = get_port(&config, opts.port.as_deref())?; + let root = get_root(&config, env, opts.root.as_deref())?; + let timeout = get_timeout(&config, opts.timeout.as_deref())?; Ok(ConfigDefaultsBootstrap { ip: Some(ip), port: Some(port), @@ -143,9 +138,8 @@ fn get_config_defaults_from_file(env: &dyn Environment) -> ConfigDefaults { /// Gets the IP address that the bootstrap server listens on. First checks if the IP address was /// specified on the command-line using --ip, otherwise checks if the IP address was specified in /// the dfx configuration file, otherise defaults to 127.0.0.1. -fn get_ip(config: &ConfigDefaultsBootstrap, args: &ArgMatches<'_>) -> DfxResult { - args.value_of("ip") - .map(|ip| ip.parse()) +fn get_ip(config: &ConfigDefaultsBootstrap, ip: Option<&str>) -> DfxResult { + ip.map(|ip| ip.parse()) .unwrap_or_else(|| { let default = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); Ok(config.ip.unwrap_or(default)) @@ -156,9 +150,8 @@ fn get_ip(config: &ConfigDefaultsBootstrap, args: &ArgMatches<'_>) -> DfxResult< /// Gets the port number that the bootstrap server listens on. First checks if the port number was /// specified on the command-line using --port, otherwise checks if the port number was specified /// in the dfx configuration file, otherise defaults to 8081. -fn get_port(config: &ConfigDefaultsBootstrap, args: &ArgMatches<'_>) -> DfxResult { - args.value_of("port") - .map(|port| port.parse()) +fn get_port(config: &ConfigDefaultsBootstrap, port: Option<&str>) -> DfxResult { + port.map(|port| port.parse()) .unwrap_or_else(|| { let default = 8081; Ok(config.port.unwrap_or(default)) @@ -182,10 +175,9 @@ fn get_providers(network_descriptor: &NetworkDescriptor) -> DfxResult, + root: Option<&str>, ) -> DfxResult { - args.value_of("root") - .map(|root| parse_dir(root)) + root.map(|root| parse_dir(root)) .unwrap_or_else(|| { config .root @@ -205,8 +197,8 @@ fn get_root( /// requests to complete. First checks if the timeout was specified on the command-line using /// --timeout, otherwise checks if the timeout was specified in the dfx configuration file, /// otherise defaults to 30. -fn get_timeout(config: &ConfigDefaultsBootstrap, args: &ArgMatches<'_>) -> DfxResult { - args.value_of("timeout") +fn get_timeout(config: &ConfigDefaultsBootstrap, timeout: Option<&str>) -> DfxResult { + timeout .map(|timeout| timeout.parse()) .unwrap_or_else(|| { let default = 30; diff --git a/src/dfx/src/commands/build.rs b/src/dfx/src/commands/build.rs index 46874001c3..ed6af01bfd 100644 --- a/src/dfx/src/commands/build.rs +++ b/src/dfx/src/commands/build.rs @@ -1,45 +1,39 @@ use crate::lib::builders::BuildConfig; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister::CanisterPool; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::provider::create_agent_environment; -use clap::{App, Arg, ArgMatches, SubCommand}; - -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("build") - .about(UserMessage::BuildCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .conflicts_with("all") - .help(UserMessage::BuildCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("all") - .long("all") - .conflicts_with("canister_name") - .help(UserMessage::BuildAll.to_str()) - .takes_value(false), - ) - .arg( - Arg::with_name("check") - .long("check") - .takes_value(false) - .help(UserMessage::BuildCheck.to_str()), - ) - .arg( - Arg::with_name("network") - .help(UserMessage::CanisterComputeNetwork.to_str()) - .long("network") - .takes_value(true), - ) +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; + +/// Builds all or specific canisters from the code in your project. By default, all canisters are built. +#[derive(Clap)] +#[clap(name("build"))] +pub struct CanisterBuildOpts { + /// Specifies the name of the canister to build. + /// You must specify either a canister name or the --all option. + canister_name: Option, + + /// Builds all canisters configured in the dfx.json file. + #[clap(long, conflicts_with("canister-name"))] + all: bool, + + /// Build canisters without creating them. This can be used to check that canisters build ok. + #[clap(long)] + check: bool, + + /// Override the compute network to connect to. By default, the local network is used. + #[clap(long)] + network: Option, +} + +pub fn construct() -> App<'static> { + CanisterBuildOpts::into_app() } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let env = create_agent_environment(env, args)?; +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterBuildOpts = CanisterBuildOpts::from_arg_matches(args); + let env = create_agent_environment(env, opts.network)?; let logger = env.get_logger(); @@ -52,19 +46,19 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { // already. env.get_cache().install()?; - let build_mode_check = args.is_present("check"); + let build_mode_check = opts.check; + let _all = opts.all; // Option can be None in which case --all was specified - let some_canister = args.value_of("canister_name"); let canister_names = config .get_config() - .get_canister_names_with_dependencies(some_canister)?; + .get_canister_names_with_dependencies(opts.canister_name.as_deref())?; // Get pool of canisters to build let canister_pool = CanisterPool::load(&env, build_mode_check, &canister_names)?; // Create canisters on the replica and associate canister ids locally. - if args.is_present("check") { + if build_mode_check { slog::warn!( env.get_logger(), "Building canisters to check they build ok. Canister IDs might be hard coded." diff --git a/src/dfx/src/commands/cache/delete.rs b/src/dfx/src/commands/cache/delete.rs index cf25df6af8..8669d9dcc4 100644 --- a/src/dfx/src/commands/cache/delete.rs +++ b/src/dfx/src/commands/cache/delete.rs @@ -1,18 +1,24 @@ use crate::config::cache::delete_version; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::message::UserMessage; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("delete") - .about(UserMessage::CacheDelete.to_str()) - .arg(Arg::with_name("version").takes_value(true)) +/// Deletes a specific versioned cache of dfx. +#[derive(Clap)] +#[clap(name("delete"))] +pub struct CacheDeleteOpts { + #[clap(long)] + version: Option, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - match args.value_of("version") { - Some(v) => delete_version(v).map(|_| {}), +pub fn construct() -> App<'static> { + CacheDeleteOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CacheDeleteOpts = CacheDeleteOpts::from_arg_matches(args); + match opts.version { + Some(v) => delete_version(v.as_str()).map(|_| {}), _ => env.get_cache().delete(), } } diff --git a/src/dfx/src/commands/cache/install.rs b/src/dfx/src/commands/cache/install.rs index 8d9b952ab4..1b69df7905 100644 --- a/src/dfx/src/commands/cache/install.rs +++ b/src/dfx/src/commands/cache/install.rs @@ -1,12 +1,16 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("install").about(UserMessage::CacheUnpack.to_str()) +/// Forces unpacking the cache from this dfx version. +#[derive(Clap)] +#[clap(name("install"))] +pub struct CacheInstall {} + +pub fn construct() -> App<'static> { + CacheInstall::into_app() } -pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, _args: &ArgMatches) -> DfxResult { env.get_cache().force_install() } diff --git a/src/dfx/src/commands/cache/list.rs b/src/dfx/src/commands/cache/list.rs index 4e8344dc46..cdeb9c070e 100644 --- a/src/dfx/src/commands/cache/list.rs +++ b/src/dfx/src/commands/cache/list.rs @@ -1,15 +1,19 @@ use crate::config::{cache, dfx_version}; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; use std::io::Write; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("list").about(UserMessage::CacheList.to_str()) +/// Lists installed and used version. +#[derive(Clap)] +#[clap(name("list"))] +pub struct CacheListOpts {} + +pub fn construct() -> App<'static> { + CacheListOpts::into_app() } -pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, _args: &ArgMatches) -> DfxResult { let mut current_printed = false; let current_version = env.get_version(); let mut all_versions = cache::list_versions()?; diff --git a/src/dfx/src/commands/cache/mod.rs b/src/dfx/src/commands/cache/mod.rs index ae692f25de..f415d2b5d8 100644 --- a/src/dfx/src/commands/cache/mod.rs +++ b/src/dfx/src/commands/cache/mod.rs @@ -1,8 +1,7 @@ use crate::commands::CliCommand; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; mod delete; mod install; @@ -18,16 +17,19 @@ fn builtins() -> Vec { ] } -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("cache") - .about(UserMessage::ManageCache.to_str()) - .subcommands(builtins().into_iter().map(|x| x.get_subcommand().clone())) +/// Manages the dfx version cache. +#[derive(Clap)] +#[clap(name("cache"))] +pub struct CacheOpts {} + +pub fn construct() -> App<'static> { + CacheOpts::into_app().subcommands(builtins().into_iter().map(|x| x.get_subcommand().clone())) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { let subcommand = args.subcommand(); - if let (name, Some(subcommand_args)) = subcommand { + if let Some((name, subcommand_args)) = subcommand { match builtins().into_iter().find(|x| name == x.get_name()) { Some(cmd) => cmd.execute(env, subcommand_args), None => Err(DfxError::UnknownCommand(format!( diff --git a/src/dfx/src/commands/cache/show.rs b/src/dfx/src/commands/cache/show.rs index 9132b20e02..25f99b11f4 100644 --- a/src/dfx/src/commands/cache/show.rs +++ b/src/dfx/src/commands/cache/show.rs @@ -1,14 +1,18 @@ use crate::config::cache; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("show").about(UserMessage::CacheShow.to_str()) +/// Shows the path of the cache used by this version. +#[derive(Clap)] +#[clap(name("show"))] +pub struct CacheShowOpts {} + +pub fn construct() -> App<'static> { + CacheShowOpts::into_app() } -pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, _args: &ArgMatches) -> DfxResult { let v = format!("{}", env.get_version()); println!("{}", cache::get_bin_cache(&v)?.as_path().display()); Ok(()) diff --git a/src/dfx/src/commands/canister/call.rs b/src/dfx/src/commands/canister/call.rs index 3fd2a2112c..c7ca361762 100644 --- a/src/dfx/src/commands/canister/call.rs +++ b/src/dfx/src/commands/canister/call.rs @@ -1,82 +1,62 @@ use crate::lib::canister_info::CanisterInfo; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::waiter::waiter_with_timeout; use crate::util::{blob_from_arguments, expiry_duration, get_candid_type, print_idl_blob}; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use ic_types::principal::Principal as CanisterId; use std::option::Option; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("call") - .about(UserMessage::CallCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .help(UserMessage::CanisterName.to_str()) - .required(true), - ) - .arg( - Arg::with_name("method_name") - .help(UserMessage::MethodName.to_str()) - .required(true), - ) - .arg( - Arg::with_name("async") - .help(UserMessage::AsyncResult.to_str()) - .long("async") - .takes_value(false), - ) - .arg( - Arg::with_name("query") - .help(UserMessage::QueryCanister.to_str()) - .long("query") - .conflicts_with("async") - .conflicts_with("update") - .takes_value(false), - ) - .arg( - Arg::with_name("update") - .help(UserMessage::UpdateCanisterArg.to_str()) - .long("update") - .conflicts_with("async") - .conflicts_with("query") - .takes_value(false), - ) - .arg( - Arg::with_name("type") - .help(UserMessage::ArgumentType.to_str()) - .long("type") - .takes_value(true) - .requires("argument") - .possible_values(&["idl", "raw"]), - ) - .arg( - Arg::with_name("output") - .help(UserMessage::OutputType.to_str()) - .long("output") - .takes_value(true) - .conflicts_with("async") - .possible_values(&["idl", "raw", "pp"]), - ) - .arg( - Arg::with_name("argument") - .help(UserMessage::ArgumentValue.to_str()) - .takes_value(true), - ) +/// Deletes a canister on the Internet Computer network. +#[derive(Clap)] +#[clap(name("call"))] +pub struct CanisterCallOpts { + /// Specifies the name of the canister to build. + /// You must specify either a canister name or the --all option. + canister_name: String, + + /// Specifies the method name to call on the canister. + method_name: String, + + /// Specifies not to wait for the result of the call to be returned by polling the replica. + /// Instead return a response ID. + #[clap(long)] + r#async: bool, + + /// Sends a query request to a canister. + #[clap(long, conflicts_with("async"))] + query: bool, + + /// Sends an update request to a canister. This is the default if the method is not a query method. + #[clap(long, conflicts_with("async"), conflicts_with("query"))] + update: bool, + + /// Specifies the argument to pass to the method. + argument: Option, + + /// Specifies the data type for the argument when making the call using an argument. + #[clap(long, requires("argument"), possible_values(&["idl", "raw"]))] + r#type: Option, + + /// Specifies the format for displaying the method's return result. + #[clap(long, conflicts_with("async"), + possible_values(&["idl", "raw", "pp"]))] + output: Option, +} + +pub fn construct() -> App<'static> { + CanisterCallOpts::into_app() } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterCallOpts = CanisterCallOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; - let canister_name = args.value_of("canister_name").unwrap(); - let method_name = args - .value_of("method_name") - .ok_or_else(|| DfxError::InvalidArgument("method_name".to_string()))?; + let canister_name = opts.canister_name.as_str(); + let method_name = opts.method_name.as_str(); let (canister_id, maybe_candid_path) = match CanisterId::from_text(canister_name) { Ok(id) => { @@ -100,16 +80,16 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { None => None, }; - let arguments: Option<&str> = args.value_of("argument"); - let arg_type: Option<&str> = args.value_of("type"); - let output_type: Option<&str> = args.value_of("output"); - let is_query = if args.is_present("async") { + let arguments = opts.argument.as_deref(); + let arg_type = opts.r#type.as_deref(); + let output_type = opts.output.as_deref(); + let is_query = if opts.r#async { false } else { match is_query_method { - Some(true) => !args.is_present("update"), + Some(true) => !opts.update, Some(false) => { - if args.is_present("query") { + if opts.query { return Err(DfxError::InvalidMethodCall(format!( "{} is not a query method", method_name @@ -118,7 +98,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { false } } - None => args.is_present("query"), + None => opts.query, } }; diff --git a/src/dfx/src/commands/canister/create.rs b/src/dfx/src/commands/canister/create.rs index 98bcc0d81b..72cda8011b 100644 --- a/src/dfx/src/commands/canister/create.rs +++ b/src/dfx/src/commands/canister/create.rs @@ -1,41 +1,38 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::operations::canister::create_canister; use crate::util::expiry_duration; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; -use clap::{App, Arg, ArgMatches, SubCommand}; +/// Creates an empty canister on the Internet Computer and +/// associates the Internet Computer assigned Canister ID to the canister name. +#[derive(Clap)] +#[clap(name("create"))] +pub struct CanisterCreateOpts { + /// Specifies the canister name. Either this or the --all flag are required. + canister_name: Option, -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("create") - .about(UserMessage::CreateCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .required_unless("all") - .help(UserMessage::CreateCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("all") - .long("all") - .required_unless("canister_name") - .help(UserMessage::CreateAll.to_str()) - .takes_value(false), - ) + /// Creates all canisters configured in dfx.json. + #[clap(long, required_unless_present("canister-name"))] + all: bool, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn construct() -> App<'static> { + CanisterCreateOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterCreateOpts = CanisterCreateOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; let timeout = expiry_duration(); - if let Some(canister_name) = args.value_of("canister_name") { - create_canister(env, canister_name, timeout)?; + if let Some(canister_name) = opts.canister_name { + create_canister(env, canister_name.as_str(), timeout)?; Ok(()) - } else if args.is_present("all") { + } else if opts.all { // Create all canisters. if let Some(canisters) = &config.get_config().canisters { for canister_name in canisters.keys() { diff --git a/src/dfx/src/commands/canister/delete.rs b/src/dfx/src/commands/canister/delete.rs index 750f24a8a2..a88658bcf0 100644 --- a/src/dfx/src/commands/canister/delete.rs +++ b/src/dfx/src/commands/canister/delete.rs @@ -1,11 +1,9 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::waiter::waiter_with_timeout; use crate::util::expiry_duration; - -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use ic_agent::Agent; use ic_utils::call::AsyncCall; use ic_utils::interfaces::ManagementCanister; @@ -13,23 +11,21 @@ use slog::info; use std::time::Duration; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("delete") - .about(UserMessage::DeleteCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .required_unless("all") - .help(UserMessage::DeleteCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("all") - .long("all") - .required_unless("canister_name") - .help(UserMessage::DeleteAll.to_str()) - .takes_value(false), - ) +/// Deletes a canister on the Internet Computer network. +#[derive(Clap)] +#[clap(name("delete"))] +pub struct CanisterDeleteOpts { + /// Specifies the name of the canister to delete. + /// You must specify either a canister name or the --all flag. + canister_name: Option, + + /// Deletes all of the canisters configured in the dfx.json file. + #[clap(long, required_unless_present("canister-name"))] + all: bool, +} + +pub fn construct() -> App<'static> { + CanisterDeleteOpts::into_app() } async fn delete_canister( @@ -58,7 +54,8 @@ async fn delete_canister( Ok(()) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterDeleteOpts = CanisterDeleteOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; @@ -70,10 +67,10 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let mut runtime = Runtime::new().expect("Unable to create a runtime"); - if let Some(canister_name) = args.value_of("canister_name") { - runtime.block_on(delete_canister(env, &agent, &canister_name, timeout))?; + if let Some(canister_name) = opts.canister_name.as_deref() { + runtime.block_on(delete_canister(env, &agent, canister_name, timeout))?; Ok(()) - } else if args.is_present("all") { + } else if opts.all { if let Some(canisters) = &config.get_config().canisters { for canister_name in canisters.keys() { runtime.block_on(delete_canister(env, &agent, &canister_name, timeout))?; diff --git a/src/dfx/src/commands/canister/id.rs b/src/dfx/src/commands/canister/id.rs index e4b7ea41d8..349c8af679 100644 --- a/src/dfx/src/commands/canister/id.rs +++ b/src/dfx/src/commands/canister/id.rs @@ -1,25 +1,27 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use ic_types::principal::Principal as CanisterId; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("id") - .about(UserMessage::IdCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .help(UserMessage::CanisterName.to_str()) - .required(true), - ) +/// Prints the identifier of a canister. +#[derive(Clap)] +#[clap(name("id"))] +pub struct CanisterIdOpts { + /// Specifies the name of the canister to stop. + /// You must specify either a canister name or the --all option. + canister_name: String, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn construct() -> App<'static> { + CanisterIdOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts = CanisterIdOpts::from_arg_matches(args); env.get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; - let canister_name = args.value_of("canister_name").unwrap(); + let canister_name = opts.canister_name.as_str(); let canister_id = CanisterIdStore::for_env(env)?.get(canister_name)?; println!("{}", CanisterId::to_text(&canister_id)); diff --git a/src/dfx/src/commands/canister/install.rs b/src/dfx/src/commands/canister/install.rs index fde7a91237..057ab40f1b 100644 --- a/src/dfx/src/commands/canister/install.rs +++ b/src/dfx/src/commands/canister/install.rs @@ -2,108 +2,65 @@ use crate::config::dfinity::ConfigInterface; use crate::lib::canister_info::CanisterInfo; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::operations::canister::install_canister; +use crate::util::clap::validators::{compute_allocation_validator, memory_allocation_validator}; use crate::util::{blob_from_arguments, expiry_duration, get_candid_init_type}; - -use clap::{App, Arg, ArgMatches, SubCommand}; -use humanize_rs::bytes::{Bytes, Unit}; - +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; +use humanize_rs::bytes::Bytes; use ic_utils::interfaces::management_canister::{ComputeAllocation, InstallMode, MemoryAllocation}; use std::convert::TryFrom; use std::str::FromStr; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("install") - .about(UserMessage::InstallCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .required_unless("all") - .help(UserMessage::InstallCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("all") - .long("all") - .required_unless("canister_name") - .help(UserMessage::InstallAll.to_str()) - .takes_value(false), - ) - .arg( - Arg::with_name("async") - .help(UserMessage::AsyncResult.to_str()) - .long("async") - .takes_value(false), - ) - .arg( - Arg::with_name("mode") - .help(UserMessage::InstallMode.to_str()) - .long("mode") - .short("m") - .possible_values(&["install", "reinstall", "upgrade"]) - .default_value("install") - .takes_value(true), - ) - .arg( - Arg::with_name("argument") - .help(UserMessage::ArgumentValue.to_str()) - .takes_value(true), - ) - .arg( - Arg::with_name("type") - .help(UserMessage::ArgumentType.to_str()) - .long("type") - .takes_value(true) - .requires("argument") - .possible_values(&["idl", "raw"]), - ) - .arg( - Arg::with_name("compute-allocation") - .help(UserMessage::InstallComputeAllocation.to_str()) - .long("compute-allocation") - .short("c") - .takes_value(true) - .validator(compute_allocation_validator), - ) - .arg( - Arg::with_name("memory-allocation") - .help(UserMessage::InstallMemoryAllocation.to_str()) - .long("memory-allocation") - .takes_value(true) - .validator(memory_allocation_validator), - ) -} - -fn compute_allocation_validator(compute_allocation: String) -> Result<(), String> { - if let Ok(num) = compute_allocation.parse::() { - if num <= 100 { - return Ok(()); - } - } - Err("Must be a percent between 0 and 100".to_string()) +/// Deploys compiled code as a canister on the Internet Computer. +#[derive(Clap, Clone)] +#[clap(name("install"))] +pub struct CanisterInstallOpts { + /// Specifies the canister name to deploy. You must specify either canister name or the --all option. + canister_name: Option, + + /// Deploys all canisters configured in the project dfx.json files. + #[clap(long, required_unless_present("canister-name"))] + all: bool, + + /// Specifies not to wait for the result of the call to be returned by polling the replica. Instead return a response ID. + #[clap(long)] + async_call: bool, + + /// Specifies the type of deployment. You can set the canister deployment modes to install, reinstall, or upgrade. + #[clap(long, short('m'), default_value("install"), + possible_values(&["install", "reinstall", "upgrade"]))] + mode: String, + + /// Specifies the argument to pass to the method. + #[clap(long)] + argument: Option, + + /// Specifies the data type for the argument when making the call using an argument. + #[clap(long, requires("argument"), possible_values(&["idl", "raw"]))] + argument_type: Option, + + /// Specifies the canister's compute allocation. This should be a percent in the range [0..100] + #[clap(long, short('c'), validator(compute_allocation_validator))] + compute_allocation: Option, + + /// Specifies how much memory the canister is allowed to use in total. + /// This should be a value in the range [0..256 TB] + #[clap(long, validator(memory_allocation_validator))] + memory_allocation: Option, } -fn memory_allocation_validator(memory_allocation: String) -> Result<(), String> { - let limit = Bytes::new(256, Unit::TByte).map_err(|_| "Parse Overflow.")?; - if let Ok(bytes) = memory_allocation.parse::() { - if bytes.size() <= limit.size() { - return Ok(()); - } - } - Err("Must be a value between 0..256 TB inclusive.".to_string()) +pub fn construct() -> App<'static> { + CanisterInstallOpts::into_app() } fn get_compute_allocation( - args: &ArgMatches<'_>, + compute_allocation: Option, config_interface: &ConfigInterface, canister_name: &str, ) -> DfxResult> { - Ok(args - .value_of("compute-allocation") - .map(|v| v.to_string()) + Ok(compute_allocation .or(config_interface.get_compute_allocation(canister_name)?) .map(|arg| { ComputeAllocation::try_from(arg.parse::().unwrap()) @@ -112,13 +69,11 @@ fn get_compute_allocation( } fn get_memory_allocation( - args: &ArgMatches<'_>, + memory_allocation: Option, config_interface: &ConfigInterface, canister_name: &str, ) -> DfxResult> { - Ok(args - .value_of("memory-allocation") - .map(|v| v.to_string()) + Ok(memory_allocation .or(config_interface.get_memory_allocation(canister_name)?) .map(|arg| { MemoryAllocation::try_from(u64::try_from(arg.parse::().unwrap().size()).unwrap()) @@ -126,7 +81,8 @@ fn get_memory_allocation( })) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterInstallOpts = CanisterInstallOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; @@ -139,24 +95,26 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let config_interface = config.get_config(); - let mode = InstallMode::from_str(args.value_of("mode").unwrap())?; + let mode = InstallMode::from_str(opts.mode.as_str())?; let mut runtime = Runtime::new().expect("Unable to create a runtime"); let canister_id_store = CanisterIdStore::for_env(env)?; - if let Some(canister_name) = args.value_of("canister_name") { + if let Some(canister_name) = opts.canister_name.as_deref() { let canister_id = canister_id_store.get(canister_name)?; let canister_info = CanisterInfo::load(&config, canister_name, Some(canister_id))?; let maybe_path = canister_info.get_output_idl_path(); let init_type = maybe_path.and_then(|path| get_candid_init_type(&path)); - let arguments: Option<&str> = args.value_of("argument"); - let arg_type: Option<&str> = args.value_of("type"); + let arguments = opts.argument.as_deref(); + let arg_type = opts.argument_type.as_deref(); let install_args = blob_from_arguments(arguments, arg_type, &init_type)?; - let compute_allocation = get_compute_allocation(args, config_interface, canister_name)?; - let memory_allocation = get_memory_allocation(args, config_interface, canister_name)?; + let compute_allocation = + get_compute_allocation(opts.compute_allocation, config_interface, canister_name)?; + let memory_allocation = + get_memory_allocation(opts.memory_allocation, config_interface, canister_name)?; runtime.block_on(install_canister( env, @@ -169,7 +127,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { timeout, ))?; Ok(()) - } else if args.is_present("all") { + } else if opts.all { // Install all canisters. if let Some(canisters) = &config.get_config().canisters { for canister_name in canisters.keys() { @@ -178,10 +136,16 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let install_args = []; - let compute_allocation = - get_compute_allocation(args, config_interface, canister_name)?; - let memory_allocation = - get_memory_allocation(args, config_interface, canister_name)?; + let compute_allocation = get_compute_allocation( + opts.compute_allocation.clone(), + config_interface, + canister_name, + )?; + let memory_allocation = get_memory_allocation( + opts.memory_allocation.clone(), + config_interface, + canister_name, + )?; runtime.block_on(install_canister( env, diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index 5ae18e87d0..9107d1d62b 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -1,9 +1,8 @@ use crate::commands::CliCommand; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::provider::create_agent_environment; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; mod call; mod create; @@ -31,23 +30,25 @@ fn builtins() -> Vec { ] } -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("canister") - .about(UserMessage::ManageCanister.to_str()) - .arg( - Arg::with_name("network") - .help(UserMessage::CanisterComputeNetwork.to_str()) - .long("network") - .takes_value(true), - ) - .subcommands(builtins().into_iter().map(|x| x.get_subcommand().clone())) +/// Manages canisters deployed on a network replica. +#[derive(Clap)] +#[clap(name("canister"))] +pub struct CanisterOpts { + // Override the compute network to connect to. By default, the local network is used. + #[clap(long)] + network: Option, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn construct() -> App<'static> { + CanisterOpts::into_app().subcommands(builtins().into_iter().map(|x| x.get_subcommand().clone())) +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterOpts = CanisterOpts::from_arg_matches(args); let subcommand = args.subcommand(); - let agent_env = create_agent_environment(env, args)?; + let agent_env = create_agent_environment(env, opts.network)?; - if let (name, Some(subcommand_args)) = subcommand { + if let Some((name, subcommand_args)) = subcommand { match builtins().into_iter().find(|x| name == x.get_name()) { Some(cmd) => cmd.execute(&agent_env, subcommand_args), None => Err(DfxError::UnknownCommand(format!( diff --git a/src/dfx/src/commands/canister/request_status.rs b/src/dfx/src/commands/canister/request_status.rs index b137a53cce..04cedf601a 100644 --- a/src/dfx/src/commands/canister/request_status.rs +++ b/src/dfx/src/commands/canister/request_status.rs @@ -1,35 +1,33 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::waiter::waiter_with_timeout; use crate::util::clap::validators; use crate::util::{expiry_duration, print_idl_blob}; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use delay::Waiter; use ic_agent::agent::{Replied, RequestStatusResponse}; use ic_agent::{AgentError, RequestId}; use std::str::FromStr; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("request-status") - .about(UserMessage::RequestCallStatus.to_str()) - .arg( - Arg::with_name("request_id") - .takes_value(true) - .help(UserMessage::RequestId.to_str()) - .required(true) - .validator(validators::is_request_id), - ) +/// Requests the status of a specified call from a canister. +#[derive(Clap)] +#[clap(name("request-status"))] +pub struct RequestStatusOpts { + /// Specifies the request identifier. + /// The request identifier is an hexadecimal string starting with 0x. + #[clap(validator(validators::is_request_id))] + request_id: String, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let request_id = RequestId::from_str( - &args - .value_of("request_id") - .ok_or_else(|| DfxError::InvalidArgument("request_id".to_string()))?[2..], - ) - .map_err(|_| DfxError::InvalidArgument("request_id".to_owned()))?; +pub fn construct() -> App<'static> { + RequestStatusOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts = RequestStatusOpts::from_arg_matches(args); + let request_id = RequestId::from_str(&opts.request_id[2..]) + .map_err(|_| DfxError::InvalidArgument("request_id".to_owned()))?; let agent = env .get_agent() diff --git a/src/dfx/src/commands/canister/set_controller.rs b/src/dfx/src/commands/canister/set_controller.rs index 35b4f65edc..73090c128a 100644 --- a/src/dfx/src/commands/canister/set_controller.rs +++ b/src/dfx/src/commands/canister/set_controller.rs @@ -1,46 +1,43 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::waiter::waiter_with_timeout; use crate::util::expiry_duration; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use ic_agent::Identity; use ic_types::principal::Principal as CanisterId; use ic_utils::call::AsyncCall; use ic_utils::interfaces::ManagementCanister; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("set-controller") - .about(UserMessage::SetController.to_str()) - .arg( - Arg::with_name("canister") - .takes_value(true) - .help(UserMessage::SetControllerCanister.to_str()) - .required(true), - ) - .arg( - Arg::with_name("new-controller") - .takes_value(true) - .help(UserMessage::NewController.to_str()) - .required(true), - ) +/// Sets the provided identity's name or its principal as the +/// new controller of a canister on the Internet Computer network. +#[derive(Clap)] +#[clap(name("set-controller"))] +pub struct SetControllerOpts { + /// Specifies the canister name or the canister identifier for the canister to be controlled. + canister: String, + + /// Specifies the identity name or the principal of the new controller. + new_controller: String, +} + +pub fn construct() -> App<'static> { + SetControllerOpts::into_app() } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let canister = args.value_of("canister").unwrap(); - let canister_id = match CanisterId::from_text(canister) { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: SetControllerOpts = SetControllerOpts::from_arg_matches(args); + let canister_id = match CanisterId::from_text(&opts.canister) { Ok(id) => id, - Err(_) => CanisterIdStore::for_env(env)?.get(canister)?, + Err(_) => CanisterIdStore::for_env(env)?.get(&opts.canister)?, }; - let new_controller = args.value_of("new-controller").unwrap(); - let controller_principal = match CanisterId::from_text(new_controller) { + let controller_principal = match CanisterId::from_text(&opts.new_controller) { Ok(principal) => principal, Err(_) => IdentityManager::new(env)? - .instantiate_identity_from_name(new_controller)? + .instantiate_identity_from_name(&opts.new_controller)? .sender()?, }; @@ -57,6 +54,9 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { .call_and_wait(waiter_with_timeout(timeout)), )?; - println!("Set {:?} as controller of {:?}.", new_controller, canister); + println!( + "Set {:?} as controller of {:?}.", + opts.new_controller, opts.canister + ); Ok(()) } diff --git a/src/dfx/src/commands/canister/start.rs b/src/dfx/src/commands/canister/start.rs index 76e6048c5e..8cf0a3d9b5 100644 --- a/src/dfx/src/commands/canister/start.rs +++ b/src/dfx/src/commands/canister/start.rs @@ -1,11 +1,9 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::waiter::waiter_with_timeout; use crate::util::expiry_duration; - -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use ic_agent::Agent; use ic_utils::call::AsyncCall; use ic_utils::interfaces::ManagementCanister; @@ -13,23 +11,20 @@ use slog::info; use std::time::Duration; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("start") - .about(UserMessage::StartCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .required_unless("all") - .help(UserMessage::StartCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("all") - .long("all") - .required_unless("canister_name") - .help(UserMessage::StartAll.to_str()) - .takes_value(false), - ) +/// Starts a canister on the Internet Computer network. +#[derive(Clap)] +#[clap(name("start"))] +pub struct CanisterStartOpts { + /// Specifies the name of the canister to start. You must specify either a canister name or the --all flag. + canister_name: Option, + + /// Starts all of the canisters configured in the dfx.json file. + #[clap(long, required_unless_present("canister-name"))] + all: bool, +} + +pub fn construct() -> App<'static> { + CanisterStartOpts::into_app() } async fn start_canister( @@ -58,7 +53,8 @@ async fn start_canister( Ok(()) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterStartOpts = CanisterStartOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; @@ -70,10 +66,10 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let timeout = expiry_duration(); - if let Some(canister_name) = args.value_of("canister_name") { + if let Some(canister_name) = opts.canister_name.as_deref() { runtime.block_on(start_canister(env, &agent, &canister_name, timeout))?; Ok(()) - } else if args.is_present("all") { + } else if opts.all { if let Some(canisters) = &config.get_config().canisters { for canister_name in canisters.keys() { runtime.block_on(start_canister(env, &agent, &canister_name, timeout))?; diff --git a/src/dfx/src/commands/canister/status.rs b/src/dfx/src/commands/canister/status.rs index 129d680d8c..e7fc63e4c5 100644 --- a/src/dfx/src/commands/canister/status.rs +++ b/src/dfx/src/commands/canister/status.rs @@ -1,11 +1,9 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::waiter::waiter_with_timeout; use crate::util::expiry_duration; - -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use ic_agent::Agent; use ic_utils::call::AsyncCall; use ic_utils::interfaces::ManagementCanister; @@ -13,23 +11,21 @@ use slog::info; use std::time::Duration; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("status") - .about(UserMessage::CanisterStatus.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .required_unless("all") - .help(UserMessage::StatusCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("all") - .long("all") - .required_unless("canister_name") - .help(UserMessage::StatusAll.to_str()) - .takes_value(false), - ) +/// Returns the current status of the canister on the Internet Computer network: Running, Stopping, or Stopped. +#[derive(Clap)] +#[clap(name("status"))] +pub struct CanisterStatusOpts { + /// Specifies the name of the canister to return information for. + /// You must specify either a canister name or the --all flag. + canister_name: Option, + + /// Returns status information for all of the canisters configured in the dfx.json file. + #[clap(long, required_unless_present("canister-name"))] + all: bool, +} + +pub fn construct() -> App<'static> { + CanisterStatusOpts::into_app() } async fn canister_status( @@ -52,7 +48,8 @@ async fn canister_status( Ok(()) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterStatusOpts = CanisterStatusOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; @@ -64,10 +61,10 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let timeout = expiry_duration(); - if let Some(canister_name) = args.value_of("canister_name") { + if let Some(canister_name) = opts.canister_name.as_deref() { runtime.block_on(canister_status(env, &agent, &canister_name, timeout))?; Ok(()) - } else if args.is_present("all") { + } else if opts.all { if let Some(canisters) = &config.get_config().canisters { for canister_name in canisters.keys() { runtime.block_on(canister_status(env, &agent, &canister_name, timeout))?; diff --git a/src/dfx/src/commands/canister/stop.rs b/src/dfx/src/commands/canister/stop.rs index 46b8d677d6..cb78e48e35 100644 --- a/src/dfx/src/commands/canister/stop.rs +++ b/src/dfx/src/commands/canister/stop.rs @@ -1,11 +1,9 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::waiter::waiter_with_timeout; use crate::util::expiry_duration; - -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use ic_agent::Agent; use ic_utils::call::AsyncCall; use ic_utils::interfaces::ManagementCanister; @@ -13,23 +11,21 @@ use slog::info; use std::time::Duration; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("stop") - .about(UserMessage::StopCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .required_unless("all") - .help(UserMessage::StopCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("all") - .long("all") - .required_unless("canister_name") - .help(UserMessage::StopAll.to_str()) - .takes_value(false), - ) +/// Stops a canister that is currently running on the Internet Computer network. +#[derive(Clap)] +#[clap(name("stop"))] +pub struct CanisterStopOpts { + /// Specifies the name of the canister to stop. + /// You must specify either a canister name or the --all option. + canister_name: Option, + + /// Stops all of the canisters configured in the dfx.json file. + #[clap(long, required_unless_present("canister-name"))] + all: bool, +} + +pub fn construct() -> App<'static> { + CanisterStopOpts::into_app() } async fn stop_canister( @@ -57,7 +53,8 @@ async fn stop_canister( Ok(()) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: CanisterStopOpts = CanisterStopOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; @@ -68,10 +65,10 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let mut runtime = Runtime::new().expect("Unable to create a runtime"); let timeout = expiry_duration(); - if let Some(canister_name) = args.value_of("canister_name") { + if let Some(canister_name) = opts.canister_name.as_deref() { runtime.block_on(stop_canister(env, &agent, &canister_name, timeout))?; Ok(()) - } else if args.is_present("all") { + } else if opts.all { if let Some(canisters) = &config.get_config().canisters { for canister_name in canisters.keys() { runtime.block_on(stop_canister(env, &agent, &canister_name, timeout))?; diff --git a/src/dfx/src/commands/config.rs b/src/dfx/src/commands/config.rs index 8de258deff..1c97563614 100644 --- a/src/dfx/src/commands/config.rs +++ b/src/dfx/src/commands/config.rs @@ -1,26 +1,33 @@ use crate::config::dfinity::Config; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use serde_json::value::Value; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("config") - .about(UserMessage::ConfigureOptions.to_str()) - .arg(Arg::with_name("config_path").help(UserMessage::OptionName.to_str())) - .arg(Arg::with_name("value").help(UserMessage::OptionValue.to_str())) - .arg( - Arg::with_name("format") - .help(UserMessage::OptionFormat.to_str()) - .long("format") - .takes_value(true) - .default_value("json") - .possible_values(&["json", "text"]), - ) +/// Configures project options for your currently-selected project. +#[derive(Clap)] +#[clap(name("config"))] +pub struct ConfigOpts { + /// Specifies the name of the configuration option to set or read. + /// Use the period delineated path to specify the option to set or read. + /// If this is not mentioned, outputs the whole configuration. + config_path: String, + + /// Specifies the new value to set. + /// If you don't specify a value, the command displays the current value of the option from the configuration file. + value: Option, + + /// Specifies the format of the output. By default, the output format is JSON. + #[clap(long, default_value("json"), possible_values(&["json", "text"]))] + format: String, +} + +pub fn construct() -> App<'static> { + ConfigOpts::into_app() } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: ConfigOpts = ConfigOpts::from_arg_matches(args); // Cannot use the `env` variable as we need a mutable copy. let mut config: Config = env .get_config() @@ -28,8 +35,8 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { .as_ref() .clone(); - let config_path = args.value_of("config_path").unwrap_or(""); - let format = args.value_of("format").unwrap_or("json"); + let config_path = opts.config_path.as_str(); + let format = opts.format.as_str(); // We replace `.` with `/` so the user can use `path.value.field` instead of forcing him // to use `path/value/field`. Since none of our keys have slashes or tildes in them it @@ -45,11 +52,11 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { config_path.clear() } - if let Some(arg_value) = args.value_of("value") { + if let Some(arg_value) = opts.value { // Try to parse the type of the value (which is a string from the arguments) as // JSON. By default we will just assume the type is string (if all parsing fails). - let value = serde_json::from_str::(arg_value) - .unwrap_or_else(|_| Value::String(arg_value.to_owned())); + let value = + serde_json::from_str::(&arg_value).unwrap_or_else(|_| Value::String(arg_value)); *config .get_mut_json() .pointer_mut(config_path.as_str()) diff --git a/src/dfx/src/commands/deploy.rs b/src/dfx/src/commands/deploy.rs index faadeb81fc..77ea672735 100644 --- a/src/dfx/src/commands/deploy.rs +++ b/src/dfx/src/commands/deploy.rs @@ -1,50 +1,43 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::message::UserMessage; use crate::lib::operations::canister::deploy_canisters; use crate::lib::provider::create_agent_environment; use crate::util::expiry_duration; -use clap::{App, Arg, ArgMatches, SubCommand}; - -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("deploy") - .about(UserMessage::DeployCanister.to_str()) - .arg( - Arg::with_name("canister_name") - .takes_value(true) - .help(UserMessage::DeployCanisterName.to_str()) - .required(false), - ) - .arg( - Arg::with_name("network") - .help(UserMessage::CanisterComputeNetwork.to_str()) - .long("network") - .takes_value(true), - ) - .arg( - Arg::with_name("argument") - .help(UserMessage::ArgumentValue.to_str()) - .long("argument") - .takes_value(true), - ) - .arg( - Arg::with_name("type") - .help(UserMessage::ArgumentType.to_str()) - .long("type") - .takes_value(true) - .requires("argument") - .possible_values(&["idl", "raw"]), - ) +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; + +/// Deploys all or a specific canister from the code in your project. By default, all canisters are deployed. +#[derive(Clap)] +#[clap(name("deploy"))] +pub struct DeployOpts { + /// Specifies the name of the canister you want to deploy. + /// If you don’t specify a canister name, all canisters defined in the dfx.json file are deployed. + canister_name: Option, + + /// Specifies the argument to pass to the method. + #[clap(long)] + argument: Option, + + /// Specifies the data type for the argument when making the call using an argument. + #[clap(long, requires("argument"), possible_values(&["idl", "raw"]))] + argument_type: Option, + + /// Override the compute network to connect to. By default, the local network is used. + #[clap(long)] + network: Option, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let env = create_agent_environment(env, args)?; +pub fn construct() -> App<'static> { + DeployOpts::into_app() +} - let timeout = expiry_duration(); - let canister = args.value_of("canister_name"); +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: DeployOpts = DeployOpts::from_arg_matches(args); + let env = create_agent_environment(env, opts.network)?; - let argument = args.value_of("argument"); - let argument_type = args.value_of("type"); + let timeout = expiry_duration(); + let canister_name = opts.canister_name.as_deref(); + let argument = opts.argument.as_deref(); + let argument_type = opts.argument_type.as_deref(); - deploy_canisters(&env, canister, argument, argument_type, timeout) + deploy_canisters(&env, canister_name, argument, argument_type, timeout) } diff --git a/src/dfx/src/commands/identity/list.rs b/src/dfx/src/commands/identity/list.rs index 680280bd3a..a066d4d754 100644 --- a/src/dfx/src/commands/identity/list.rs +++ b/src/dfx/src/commands/identity/list.rs @@ -1,15 +1,19 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; use std::io::Write; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("list").about(UserMessage::ListIdentities.to_str()) +/// Lists existing identities. +#[derive(Clap)] +#[clap(name("list"))] +pub struct ListOpts {} + +pub fn construct() -> App<'static> { + ListOpts::into_app() } -pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, _args: &ArgMatches) -> DfxResult { let mgr = IdentityManager::new(env)?; let identities = mgr.get_identity_names()?; let current_identity = mgr.get_selected_identity_name(); diff --git a/src/dfx/src/commands/identity/mod.rs b/src/dfx/src/commands/identity/mod.rs index 49ec291922..a9cba1c748 100644 --- a/src/dfx/src/commands/identity/mod.rs +++ b/src/dfx/src/commands/identity/mod.rs @@ -1,8 +1,7 @@ use crate::commands::CliCommand; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; mod list; mod new; @@ -12,6 +11,16 @@ mod rename; mod r#use; mod whoami; +/// Manages identities used to communicate with the Internet Computer network. +/// Setting an identity enables you to test user-based access controls. +#[derive(Clap)] +#[clap(name("identity"))] +pub struct IdentityOpt {} + +pub fn construct() -> App<'static> { + IdentityOpt::into_app().subcommands(builtins().into_iter().map(|x| x.get_subcommand().clone())) +} + fn builtins() -> Vec { vec![ CliCommand::new(list::construct(), list::exec), @@ -24,16 +33,10 @@ fn builtins() -> Vec { ] } -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("identity") - .about(UserMessage::ManageIdentity.to_str()) - .subcommands(builtins().into_iter().map(|x| x.get_subcommand().clone())) -} - -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { let subcommand = args.subcommand(); - if let (name, Some(subcommand_args)) = subcommand { + if let Some((name, subcommand_args)) = subcommand { match builtins().into_iter().find(|x| name == x.get_name()) { Some(cmd) => cmd.execute(env, subcommand_args), None => Err(DfxError::UnknownCommand(format!( diff --git a/src/dfx/src/commands/identity/new.rs b/src/dfx/src/commands/identity/new.rs index e76225d15b..187f17ed05 100644 --- a/src/dfx/src/commands/identity/new.rs +++ b/src/dfx/src/commands/identity/new.rs @@ -1,23 +1,24 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use slog::info; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("new") - .about(UserMessage::NewIdentity.to_str()) - .arg( - Arg::with_name("identity") - .help("The identity to create.") - .required(true) - .takes_value(true), - ) +/// Creates a new identity. +#[derive(Clap)] +#[clap(name("new"))] +pub struct NewIdentityOpts { + /// The identity to create. + identity: String, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let name = args.value_of("identity").unwrap(); +pub fn construct() -> App<'static> { + NewIdentityOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: NewIdentityOpts = NewIdentityOpts::from_arg_matches(args); + let name = opts.identity.as_str(); let log = env.get_logger(); info!(log, r#"Creating identity: "{}"."#, name); diff --git a/src/dfx/src/commands/identity/principal.rs b/src/dfx/src/commands/identity/principal.rs index 80dd3fed09..8737953cab 100644 --- a/src/dfx/src/commands/identity/principal.rs +++ b/src/dfx/src/commands/identity/principal.rs @@ -1,15 +1,19 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; use ic_agent::Identity; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("get-principal").about(UserMessage::GetPrincipalId.to_str()) +/// Shows the textual representation of the Principal associated with the current identity. +#[derive(Clap)] +#[clap(name("get-principal"))] +pub struct GetPrincipalOpts {} + +pub fn construct() -> App<'static> { + GetPrincipalOpts::into_app() } -pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, _args: &ArgMatches) -> DfxResult { let identity = IdentityManager::new(env)?.instantiate_selected_identity()?; let principal_id = identity.as_ref().sender()?; println!("{}", principal_id.to_text()); diff --git a/src/dfx/src/commands/identity/remove.rs b/src/dfx/src/commands/identity/remove.rs index e6856c287e..95c209709d 100644 --- a/src/dfx/src/commands/identity/remove.rs +++ b/src/dfx/src/commands/identity/remove.rs @@ -1,23 +1,24 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use slog::info; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("remove") - .about(UserMessage::RemoveIdentity.to_str()) - .arg( - Arg::with_name("identity") - .help("The identity to remove.") - .required(true) - .takes_value(true), - ) +/// Removes an existing identity. +#[derive(Clap)] +#[clap(name("remove"))] +pub struct RemoveOpts { + /// The identity to remove. + identity: String, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let name = args.value_of("identity").unwrap(); +pub fn construct() -> App<'static> { + RemoveOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: RemoveOpts = RemoveOpts::from_arg_matches(args); + let name = opts.identity.as_str(); let log = env.get_logger(); info!(log, r#"Removing identity "{}"."#, name); diff --git a/src/dfx/src/commands/identity/rename.rs b/src/dfx/src/commands/identity/rename.rs index 23d6638fbc..a17c68b613 100644 --- a/src/dfx/src/commands/identity/rename.rs +++ b/src/dfx/src/commands/identity/rename.rs @@ -1,30 +1,28 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use slog::info; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("rename") - .about(UserMessage::RenameIdentity.to_str()) - .arg( - Arg::with_name("from") - .help("The current name of the identity.") - .required(true) - .takes_value(true), - ) - .arg( - Arg::with_name("to") - .help("The new name of the identity.") - .required(true) - .takes_value(true), - ) +/// Renames an existing identity. +#[derive(Clap)] +#[clap(name("rename"))] +pub struct RenameOpts { + /// The current name of the identity. + from: String, + + /// The new name of the identity. + to: String, +} + +pub fn construct() -> App<'static> { + RenameOpts::into_app() } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let from = args.value_of("from").unwrap(); - let to = args.value_of("to").unwrap(); +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: RenameOpts = RenameOpts::from_arg_matches(args); + let from = opts.from.as_str(); + let to = opts.to.as_str(); let log = env.get_logger(); info!(log, r#"Renaming identity "{}" to "{}"."#, from, to); diff --git a/src/dfx/src/commands/identity/use.rs b/src/dfx/src/commands/identity/use.rs index db3023d98b..89f2368713 100644 --- a/src/dfx/src/commands/identity/use.rs +++ b/src/dfx/src/commands/identity/use.rs @@ -1,23 +1,24 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use slog::info; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("use") - .about(UserMessage::UseIdentity.to_str()) - .arg( - Arg::with_name("identity") - .help("The identity to use.") - .required(true) - .takes_value(true), - ) +/// Specifies the identity to use. +#[derive(Clap)] +#[clap(name("use"))] +pub struct UseOpts { + /// The identity to use. + identity: String, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let identity = args.value_of("identity").unwrap(); +pub fn construct() -> App<'static> { + UseOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: UseOpts = UseOpts::from_arg_matches(args); + let identity = opts.identity.as_str(); let log = env.get_logger(); info!(log, r#"Using identity: "{}"."#, identity); diff --git a/src/dfx/src/commands/identity/whoami.rs b/src/dfx/src/commands/identity/whoami.rs index d88e72276f..52638106a7 100644 --- a/src/dfx/src/commands/identity/whoami.rs +++ b/src/dfx/src/commands/identity/whoami.rs @@ -1,14 +1,18 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::IdentityManager; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("whoami").about(UserMessage::ShowIdentity.to_str()) +/// Shows the name of the current identity. +#[derive(Clap)] +#[clap(name("whoami"))] +pub struct WhoAmIOpts {} + +pub fn construct() -> App<'static> { + WhoAmIOpts::into_app() } -pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, _args: &ArgMatches) -> DfxResult { let mgr = IdentityManager::new(env)?; let identity = mgr.get_selected_identity_name(); println!("{}", identity); diff --git a/src/dfx/src/commands/language_service.rs b/src/dfx/src/commands/language_service.rs index 1a5276c01a..31b2230797 100644 --- a/src/dfx/src/commands/language_service.rs +++ b/src/dfx/src/commands/language_service.rs @@ -1,36 +1,41 @@ use crate::config::dfinity::{ConfigCanistersCanister, ConfigInterface, CONFIG_FILE_NAME}; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::package_arguments::{self, PackageArguments}; -use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; +use clap::{App, AppSettings, ArgMatches, Clap, FromArgMatches, IntoApp}; use std::process::Stdio; const CANISTER_ARG: &str = "canister"; -const FORCE_TTY: &str = "force-tty"; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("_language-service") - .setting(AppSettings::Hidden) // Hide it from help menus as it shouldn't be used by users. - .about(UserMessage::StartLanguageService.to_str()) - .arg(Arg::with_name(CANISTER_ARG).help(UserMessage::CanisterName.to_str())) - .arg( - Arg::with_name(FORCE_TTY) - .help(UserMessage::ForceTTY.to_str()) - .long(FORCE_TTY) - .takes_value(false), - ) +/// Starts the Motoko IDE Language Server. This is meant to be run by editor plugins not the +/// end-user. +#[derive(Clap)] +#[clap(name("_language-service"))] +#[clap(setting = AppSettings::Hidden)] +pub struct LanguageServiceOpts { + /// Specifies the canister name. If you don't specify this argument, all canisters are + /// processed. + canister: Option, + + /// Forces the language server to start even when run from a terminal. + #[clap(long)] + force_tty: bool, +} + +pub fn construct() -> App<'static> { + LanguageServiceOpts::into_app() } // Don't read anything from stdin or output anything to stdout while this function is being // executed or LSP will become very unhappy -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { - let force_tty = args.is_present(FORCE_TTY); +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: LanguageServiceOpts = LanguageServiceOpts::from_arg_matches(args); + let force_tty = opts.force_tty; // Are we being run from a terminal? That's most likely not what we want if atty::is(atty::Stream::Stdout) && !force_tty { Err(DfxError::LanguageServerFromATerminal) } else if let Some(config) = env.get_config() { - let main_path = get_main_path(config.get_config(), args)?; + let main_path = get_main_path(config.get_config(), opts.canister)?; let packtool = &config .get_config() .get_defaults() @@ -43,12 +48,13 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { } } -fn get_main_path(config: &ConfigInterface, args: &ArgMatches<'_>) -> Result { +fn get_main_path( + config: &ConfigInterface, + canister_name: Option, +) -> Result { // TODO try and point at the actual dfx.json path let dfx_json = CONFIG_FILE_NAME; - let canister_name: Option<&str> = args.value_of(CANISTER_ARG); - let (canister_name, canister): (String, ConfigCanistersCanister) = match (config.canisters.as_ref(), canister_name) { (None, _) => Err(DfxError::InvalidData(format!( @@ -57,7 +63,7 @@ fn get_main_path(config: &ConfigInterface, args: &ArgMatches<'_>) -> Result { - let c = canisters.get(cn).ok_or_else(|| { + let c = canisters.get(cn.as_str()).ok_or_else(|| { DfxError::InvalidArgument(format!( "Canister {0} cannot not be found in {1}", cn, dfx_json diff --git a/src/dfx/src/commands/mod.rs b/src/dfx/src/commands/mod.rs index 199bd0eac8..fb6c88d254 100644 --- a/src/dfx/src/commands/mod.rs +++ b/src/dfx/src/commands/mod.rs @@ -17,26 +17,26 @@ mod start; mod stop; mod upgrade; -pub type CliExecFn = fn(&dyn Environment, &ArgMatches<'_>) -> DfxResult; +pub type CliExecFn = fn(&dyn Environment, &ArgMatches) -> DfxResult; pub struct CliCommand { - subcommand: clap::App<'static, 'static>, + subcommand: clap::App<'static>, executor: CliExecFn, } impl CliCommand { - pub fn new(subcommand: clap::App<'static, 'static>, executor: CliExecFn) -> CliCommand { + pub fn new(subcommand: clap::App<'static>, executor: CliExecFn) -> CliCommand { CliCommand { subcommand, executor, } } - pub fn get_subcommand(&self) -> &clap::App<'static, 'static> { + pub fn get_subcommand(&self) -> &clap::App<'static> { &self.subcommand } pub fn get_name(&self) -> &str { self.subcommand.get_name() } - pub fn execute(self: &CliCommand, env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { + pub fn execute(self: &CliCommand, env: &dyn Environment, args: &ArgMatches) -> DfxResult { (self.executor)(env, args) } } diff --git a/src/dfx/src/commands/new.rs b/src/dfx/src/commands/new.rs index e24083aad4..58b8bcc305 100644 --- a/src/dfx/src/commands/new.rs +++ b/src/dfx/src/commands/new.rs @@ -3,9 +3,9 @@ use crate::config::dfinity::CONFIG_FILE_NAME; use crate::config::dfx_version_str; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::util::assets; -use clap::{App, Arg, ArgMatches, SubCommand}; +use crate::util::clap::validators::project_name_validator; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use console::{style, Style}; use indicatif::HumanBytes; use lazy_static::lazy_static; @@ -19,8 +19,8 @@ use std::process::Stdio; use std::time::Duration; use tar::Archive; -const DRY_RUN: &str = "dry_run"; -const PROJECT_NAME: &str = "project_name"; +// const DRY_RUN: &str = "dry_run"; +// const PROJECT_NAME: &str = "project_name"; const RELEASE_ROOT: &str = "https://sdk.dfinity.org"; lazy_static! { // Tested on a phone tethering connection. This should be fine with @@ -31,67 +31,28 @@ lazy_static! { static ref CHECK_VERSION_TIMEOUT: Duration = Duration::from_secs(2); } -/// Validate a String can be a valid project name. -/// A project name is valid if it starts with a letter, and is alphanumeric (with hyphens). -/// It cannot end with a dash. -pub fn project_name_validator(name: String) -> Result<(), String> { - let mut chars = name.chars(); - // Check first character first. If there's no first character it's empty. - if let Some(first) = chars.next() { - if first.is_ascii_alphabetic() { - // Then check all other characters. - // Reverses the search here; if there is a character that is not compatible - // it is found and an error is returned. - let m: Vec<&str> = name - .matches(|x: char| !x.is_ascii_alphanumeric() && x != '_') - .collect(); - - if m.is_empty() { - Ok(()) - } else { - Err(format!( - r#"Invalid character(s): "{}""#, - m.iter() - .fold(String::new(), |acc, &num| acc + &num.to_string()) - )) - } - } else { - Err("Must start with a letter.".to_owned()) - } - } else { - Err("Cannot be empty.".to_owned()) - } +/// Creates a new project. +#[derive(Clap)] +#[clap(name("new"))] +pub struct NewOpts { + /// Specifies the name of the project to create. + #[clap(validator(project_name_validator))] + project_name: String, + + /// Provides a preview the directories and files to be created without adding them to the file system. + #[clap(long)] + dry_run: bool, + + /// Installs the frontend code example for the default canister. This defaults to true if Node is installed, or false if it isn't. + #[clap(long)] + frontend: bool, + + #[clap(long, conflicts_with = "frontend")] + no_frontend: bool, } -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("new") - .about(UserMessage::CreateProject.to_str()) - .arg( - Arg::with_name(PROJECT_NAME) - .help(UserMessage::ProjectName.to_str()) - .validator(project_name_validator) - .required(true), - ) - .arg( - Arg::with_name(DRY_RUN) - .help(UserMessage::DryRun.to_str()) - .long("dry-run") - .takes_value(false), - ) - .arg( - Arg::with_name("frontend") - .long("--frontend") - .help(UserMessage::NewFrontend.to_str()) - .takes_value(false) - .conflicts_with("no-frontend"), - ) - .arg( - Arg::with_name("no-frontend") - .long("--no-frontend") - .hidden(true) - .takes_value(false) - .conflicts_with("frontend"), - ) +pub fn construct() -> App<'static> { + NewOpts::into_app() } enum Status<'a> { @@ -323,13 +284,11 @@ fn scaffold_frontend_code( Ok(()) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: NewOpts = NewOpts::from_arg_matches(args); let log = env.get_logger(); - let dry_run = args.is_present(DRY_RUN); - let project_name_path = args - .value_of(PROJECT_NAME) - .ok_or_else(|| DfxError::InvalidArgument("project_path".to_string()))?; - let project_name = Path::new(project_name_path); + let dry_run = opts.dry_run; + let project_name = Path::new(opts.project_name.as_str()); if project_name.exists() { return Err(DfxError::ProjectExists); @@ -410,8 +369,8 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { env, dry_run, project_name, - args.is_present("no-frontend"), - args.is_present("frontend"), + opts.no_frontend, + opts.frontend, &variables, )?; @@ -490,28 +449,28 @@ mod tests { #[test] fn project_name_is_valid() { - assert!(project_name_validator("a".to_owned()).is_ok()); - assert!(project_name_validator("a_".to_owned()).is_ok()); - assert!(project_name_validator("a_1".to_owned()).is_ok()); - assert!(project_name_validator("A".to_owned()).is_ok()); - assert!(project_name_validator("A1".to_owned()).is_ok()); - assert!(project_name_validator("a_good_name_".to_owned()).is_ok()); - assert!(project_name_validator("a_good_name".to_owned()).is_ok()); + assert!(project_name_validator("a").is_ok()); + assert!(project_name_validator("a_").is_ok()); + assert!(project_name_validator("a_1").is_ok()); + assert!(project_name_validator("A").is_ok()); + assert!(project_name_validator("A1").is_ok()); + assert!(project_name_validator("a_good_name_").is_ok()); + assert!(project_name_validator("a_good_name").is_ok()); } #[test] fn project_name_is_invalid() { - assert!(project_name_validator("_a_good_name_".to_owned()).is_err()); - assert!(project_name_validator("__also_good".to_owned()).is_err()); - assert!(project_name_validator("_1".to_owned()).is_err()); - assert!(project_name_validator("_a".to_owned()).is_err()); - assert!(project_name_validator("1".to_owned()).is_err()); - assert!(project_name_validator("1_".to_owned()).is_err()); - assert!(project_name_validator("-".to_owned()).is_err()); - assert!(project_name_validator("_".to_owned()).is_err()); - assert!(project_name_validator("a-b-c".to_owned()).is_err()); - assert!(project_name_validator("🕹".to_owned()).is_err()); - assert!(project_name_validator("不好".to_owned()).is_err()); - assert!(project_name_validator("a:b".to_owned()).is_err()); + assert!(project_name_validator("_a_good_name_").is_err()); + assert!(project_name_validator("__also_good").is_err()); + assert!(project_name_validator("_1").is_err()); + assert!(project_name_validator("_a").is_err()); + assert!(project_name_validator("1").is_err()); + assert!(project_name_validator("1_").is_err()); + assert!(project_name_validator("-").is_err()); + assert!(project_name_validator("_").is_err()); + assert!(project_name_validator("a-b-c").is_err()); + assert!(project_name_validator("🕹").is_err()); + assert!(project_name_validator("不好").is_err()); + assert!(project_name_validator("a:b").is_err()); } } diff --git a/src/dfx/src/commands/ping.rs b/src/dfx/src/commands/ping.rs index d76f38cee8..0cf3bcb99f 100644 --- a/src/dfx/src/commands/ping.rs +++ b/src/dfx/src/commands/ping.rs @@ -1,31 +1,33 @@ use crate::config::dfinity::NetworkType; use crate::lib::environment::{AgentEnvironment, Environment}; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::network::network_descriptor::NetworkDescriptor; use crate::lib::provider::{command_line_provider_to_url, get_network_descriptor}; use crate::util::expiry_duration; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use tokio::runtime::Runtime; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("ping") - .about(UserMessage::Ping.to_str()) - .arg( - Arg::with_name("network") - .help("The provider to use.") - .takes_value(true), - ) +/// Pings an Internet Computer network and returns its status. +#[derive(Clap)] +#[clap(name("ping"))] +pub struct PingOpts { + /// The provider to use. + network: Option, } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn construct() -> App<'static> { + PingOpts::into_app() +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: PingOpts = PingOpts::from_arg_matches(args); env.get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; // For ping, "provider" could either be a URL or a network name. // If not passed, we default to the "local" network. let network_descriptor = - get_network_descriptor(env, args).or_else::(|err| match err { + get_network_descriptor(env, opts.network).or_else::(|err| match err { DfxError::ComputeNetworkNotFound(network_name) => { let url = command_line_provider_to_url(&network_name)?; let network_descriptor = NetworkDescriptor { diff --git a/src/dfx/src/commands/replica.rs b/src/dfx/src/commands/replica.rs index 4bcd3d7919..c8b7f28a26 100644 --- a/src/dfx/src/commands/replica.rs +++ b/src/dfx/src/commands/replica.rs @@ -1,46 +1,39 @@ use crate::actors; +use crate::actors::shutdown_controller; +use crate::actors::shutdown_controller::ShutdownController; use crate::config::dfinity::ConfigDefaultsReplica; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::replica_config::{HttpHandlerConfig, ReplicaConfig, SchedulerConfig}; - -use crate::actors::shutdown_controller; -use crate::actors::shutdown_controller::ShutdownController; use actix::Actor; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use std::default::Default; -/// Constructs a sub-command to run the Internet Computer replica. -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("replica") - .about(UserMessage::Replica.to_str()) - .arg( - Arg::with_name("message-gas-limit") - .help(UserMessage::ReplicaMessageGasLimit.to_str()) - .hidden(true) - .long("message-gas-limit") - .takes_value(true), - ) - .arg( - Arg::with_name("port") - .help(UserMessage::ReplicaPort.to_str()) - .long("port") - .takes_value(true), - ) - .arg( - Arg::with_name("round-gas-limit") - .help(UserMessage::ReplicaRoundGasLimit.to_str()) - .hidden(true) - .long("round-gas-limit") - .takes_value(true), - ) +/// Starts a local Internet Computer replica. +#[derive(Clap)] +#[clap(name("replica"))] +pub struct ReplicaOpts { + /// Specifies the maximum number of cycles a single message can consume. + #[clap(long, hidden = true)] + message_gas_limit: Option, + + /// Specifies the port the local replica should listen to. + #[clap(long)] + port: Option, + + /// Specifies the maximum number of cycles a single round can consume. + #[clap(long, hidden = true)] + round_gas_limit: Option, +} + +pub fn construct() -> App<'static> { + ReplicaOpts::into_app() } /// Gets the configuration options for the Internet Computer replica. -fn get_config(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +fn get_config(env: &dyn Environment, opts: ReplicaOpts) -> DfxResult { let config = get_config_from_file(env); - let port = get_port(&config, args)?; + let port = get_port(&config, opts.port)?; let mut http_handler: HttpHandlerConfig = Default::default(); if port == 0 { let config_dir = env.get_temp_dir().join("config"); @@ -50,8 +43,8 @@ fn get_config(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult ConfigDefaultsReplica { /// Gets the port number that the Internet Computer replica listens on. First checks if the port /// number was specified on the command-line using --port, otherwise checks if the port number was /// specified in the dfx configuration file, otherise defaults to 8080. -fn get_port(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> DfxResult { - args.value_of("port") - .map(|port| port.parse()) +fn get_port(config: &ConfigDefaultsReplica, port: Option) -> DfxResult { + port.map(|port| port.parse()) .unwrap_or_else(|| { let default = 8080; Ok(config.port.unwrap_or(default)) @@ -92,8 +84,11 @@ fn get_port(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> DfxResult< /// Gets the maximum amount of gas a single message can consume. First checks if the gas limit was /// specified on the command-line using --message-gas-limit, otherwise checks if the gas limit was /// specified in the dfx configuration file, otherise defaults to 5368709120. -fn get_message_gas_limit(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> DfxResult { - args.value_of("message-gas-limit") +fn get_message_gas_limit( + config: &ConfigDefaultsReplica, + message_gas_limit: Option, +) -> DfxResult { + message_gas_limit .map(|limit| limit.parse()) .unwrap_or_else(|| { let default = 5_368_709_120; @@ -105,8 +100,11 @@ fn get_message_gas_limit(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) /// Gets the maximum amount of gas a single round can consume. First checks if the gas limit was /// specified on the command-line using --round-gas-limit, otherwise checks if the gas limit was /// specified in the dfx configuration file, otherise defaults to 26843545600. -fn get_round_gas_limit(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> DfxResult { - args.value_of("round-gas-limit") +fn get_round_gas_limit( + config: &ConfigDefaultsReplica, + round_gas_limit: Option, +) -> DfxResult { + round_gas_limit .map(|limit| limit.parse()) .unwrap_or_else(|| { let default = 26_843_545_600; @@ -118,12 +116,13 @@ fn get_round_gas_limit(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> /// Start the Internet Computer locally. Spawns a proxy to forward and /// manage browser requests. Responsible for running the network (one /// replica at the moment) and the proxy. -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: ReplicaOpts = ReplicaOpts::from_arg_matches(args); let replica_pathbuf = env.get_cache().get_binary_command_path("replica")?; let ic_starter_pathbuf = env.get_cache().get_binary_command_path("ic-starter")?; let system = actix::System::new("dfx-replica"); - let config = get_config(env, args)?; + let config = get_config(env, opts)?; let shutdown_controller = ShutdownController::new(shutdown_controller::Config { logger: Some(env.get_logger().clone()), diff --git a/src/dfx/src/commands/start.rs b/src/dfx/src/commands/start.rs index 8c11969dca..ae60dd4ea7 100644 --- a/src/dfx/src/commands/start.rs +++ b/src/dfx/src/commands/start.rs @@ -6,14 +6,13 @@ use crate::actors::shutdown_controller::ShutdownController; use crate::config::dfinity::Config; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::message::UserMessage; use crate::lib::network::network_descriptor::NetworkDescriptor; use crate::lib::provider::get_network_descriptor; use crate::lib::replica_config::ReplicaConfig; use crate::util::get_reusable_socket_addr; use actix::{Actor, Addr}; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use delay::{Delay, Waiter}; use ic_agent::Agent; use std::fs; @@ -24,29 +23,25 @@ use std::process::Command; use sysinfo::{System, SystemExt}; use tokio::runtime::Runtime; -/// Provide necessary arguments to start the Internet Computer -/// locally. See `exec` for further information. -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("start") - .about(UserMessage::StartNode.to_str()) - .arg( - Arg::with_name("host") - .help(UserMessage::NodeAddress.to_str()) - .long("host") - .takes_value(true), - ) - .arg( - Arg::with_name("background") - .help(UserMessage::StartBackground.to_str()) - .long("background") - .takes_value(false), - ) - .arg( - Arg::with_name("clean") - .help(UserMessage::CleanState.to_str()) - .long("clean") - .takes_value(false), - ) +/// Starts the local replica and a web server for the current project. +#[derive(Clap)] +#[clap(name("start"))] +pub struct StartOpts { + /// Specifies the host name and port number to bind the frontend to. + #[clap(long)] + host: Option, + + /// Exits the dfx leaving the replica running. Will wait until the replica replies before exiting. + #[clap(long)] + background: bool, + + /// Cleans the state of the current project. + #[clap(long)] + clean: bool, +} + +pub fn construct() -> App<'static> { + StartOpts::into_app() } fn ping_and_wait(frontend_url: &str) -> DfxResult { @@ -112,12 +107,13 @@ fn fg_ping_and_wait(webserver_port_path: PathBuf, frontend_url: String) -> DfxRe /// Start the Internet Computer locally. Spawns a proxy to forward and /// manage browser requests. Responsible for running the network (one /// replica at the moment) and the proxy. -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: StartOpts = StartOpts::from_arg_matches(args); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; - let network_descriptor = get_network_descriptor(env, args)?; + let network_descriptor = get_network_descriptor(env, None)?; let temp_dir = env.get_temp_dir(); let build_output_root = temp_dir.join(&network_descriptor.name).join("canisters"); @@ -129,16 +125,17 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { // As we know no start process is running in this project, we can // clean up the state if it is necessary. - if args.is_present("clean") { + if opts.clean { clean_state(temp_dir, &state_root)?; } std::fs::write(&pid_file_path, "")?; // make sure we can write to this file std::fs::write(&webserver_port_path, "")?; - let (frontend_url, address_and_port) = frontend_address(args, &config)?; + let background = opts.background; + let (frontend_url, address_and_port) = frontend_address(opts.host, &config, background)?; - if args.is_present("background") { + if background { send_background()?; return fg_ping_and_wait(webserver_port_path, frontend_url); } @@ -258,9 +255,12 @@ fn send_background() -> DfxResult<()> { Ok(()) } -fn frontend_address(args: &ArgMatches<'_>, config: &Config) -> DfxResult<(String, SocketAddr)> { - let mut address_and_port = args - .value_of("host") +fn frontend_address( + host: Option, + config: &Config, + background: bool, +) -> DfxResult<(String, SocketAddr)> { + let mut address_and_port = host .and_then(|host| Option::from(host.parse())) .unwrap_or_else(|| { Ok(config @@ -270,7 +270,7 @@ fn frontend_address(args: &ArgMatches<'_>, config: &Config) -> DfxResult<(String }) .map_err(|e| DfxError::InvalidArgument(format!("Invalid host: {}", e)))?; - if !args.is_present("background") { + if !background { // Since the user may have provided port "0", we need to grab a dynamically // allocated port and construct a resuable SocketAddr which the actix // HttpServer will bind to diff --git a/src/dfx/src/commands/stop.rs b/src/dfx/src/commands/stop.rs index a9f3c3f48c..4a28e75c6a 100644 --- a/src/dfx/src/commands/stop.rs +++ b/src/dfx/src/commands/stop.rs @@ -1,11 +1,15 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::message::UserMessage; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, IntoApp}; use sysinfo::{Pid, Process, ProcessExt, Signal, System, SystemExt}; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("stop").about(UserMessage::StopNode.to_str()) +/// Stops the local network replica. +#[derive(Clap)] +#[clap(name("stop"))] +pub struct StopOpts {} + +pub fn construct() -> App<'static> { + StopOpts::into_app() } fn list_all_descendants(pid: Pid) -> Vec { @@ -35,7 +39,7 @@ fn kill_all(pid: Pid) -> DfxResult { Ok(()) } -pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, _args: &ArgMatches) -> DfxResult { let pid_file_path = env.get_temp_dir().join("pid"); if pid_file_path.exists() { // Read and verify it's not running. If it is just return. diff --git a/src/dfx/src/commands/upgrade.rs b/src/dfx/src/commands/upgrade.rs index 5774d7699f..a726c24386 100644 --- a/src/dfx/src/commands/upgrade.rs +++ b/src/dfx/src/commands/upgrade.rs @@ -1,6 +1,6 @@ use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, Clap, FromArgMatches, IntoApp}; use indicatif::{ProgressBar, ProgressDrawTarget}; use libflate::gzip::Decoder; use semver::Version; @@ -8,27 +8,20 @@ use serde::{Deserialize, Deserializer}; use std::{collections::BTreeMap, env, fs, os::unix::fs::PermissionsExt}; use tar::Archive; -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("upgrade") - .about("Upgrade DFX.") - .arg( - Arg::with_name("current-version") - .hidden(true) - .long("current-version") - .takes_value(true), - ) - .arg( - Arg::with_name("release-root") - .default_value("https://sdk.dfinity.org") - .hidden(true) - .long("release-root") - .takes_value(true), - ) - .arg( - Arg::with_name("verbose") - .help("Verbose output.") - .long("verbose"), - ) +/// Upgrade DFX. +#[derive(Clap)] +#[clap(name("upgrade"))] +pub struct UpgradeOpts { + /// Current Version. + #[clap(long)] + current_version: Option, + + #[clap(long, default_value = "https://sdk.dfinity.org", hidden = true)] + release_root: String, +} + +pub fn construct() -> App<'static> { + UpgradeOpts::into_app() } fn parse_semver<'de, D>(version: &str) -> Result @@ -154,21 +147,23 @@ fn get_latest_release(release_root: &str, version: &Version, arch: &str) -> DfxR Ok(()) } -pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { +pub fn exec(env: &dyn Environment, args: &ArgMatches) -> DfxResult { + let opts: UpgradeOpts = UpgradeOpts::from_arg_matches(args); // Find OS architecture. let os_arch = match std::env::consts::OS { "linux" => "x86_64-linux", "macos" => "x86_64-darwin", _ => panic!("Not supported architecture"), }; - let current_version = if let Some(version) = args.value_of("current-version") { + let curr_ver_str = opts.current_version.unwrap(); + let current_version = if let Some(version) = Some(curr_ver_str.as_str()) { Version::parse(version)? } else { env.get_version().clone() }; println!("Current version: {}", current_version); - let release_root = args.value_of("release-root").unwrap(); + let release_root = opts.release_root.as_str(); let latest_version = get_latest_version(release_root, None)?; if latest_version > current_version { diff --git a/src/dfx/src/lib/message.rs b/src/dfx/src/lib/message.rs deleted file mode 100644 index 30f976b4fe..0000000000 --- a/src/dfx/src/lib/message.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::fmt; - -macro_rules! user_message { - ( $($name: ident => $msg: literal,)+ ) => { - #[derive(Debug, Copy, Clone)] - pub enum UserMessage { - $($name), + - } - - impl UserMessage { - pub fn to_str(&self) -> &str { - match &self { - $(UserMessage::$name => $msg,)+ - } - } - } - }; -} - -user_message!( - // dfx bootstrap - BootstrapCommand => "Starts the bootstrap server.", - BootstrapIP => "Specifies the IP address that the bootstrap server listens on. Defaults to 127.0.0.1.", - BootstrapPort => "Specifies the port number that the bootstrap server listens on. Defaults to 8081.", - BootstrapRoot => "Specifies the directory containing static assets served by the bootstrap server. Defaults to $HOME/.cache/dfinity/versions/$DFX_VERSION/js-user-library/dist/bootstrap.", - BootstrapTimeout => "Specifies the maximum number of seconds that the bootstrap server will wait for upstream requests to complete. Defaults to 30.", - - // dfx cache - ManageCache => "Manages the dfx version cache.", - CacheDelete => "Deletes a specific versioned cache of dfx.", - CacheUnpack => "Forces unpacking the cache from this dfx version.", - CacheList => "Lists installed and used version.", - CacheShow => "Shows the path of the cache used by this version.", - - // dfx canister id - IdCanister => "Prints the identifier of a canister.", - - // dfx canister call - CallCanister => "Calls a method on a deployed canister.", - MethodName => "Specifies the method name to call on the canister.", - AsyncResult => "Specifies not to wait for the result of the call to be returned by polling the replica. Instead return a response ID.", - ArgumentType => "Specifies the data type for the argument when making the call using an argument.", - OutputType => "Specifies the format for displaying the method's return result.", - ArgumentValue => "Specifies the argument to pass to the method.", - - - // dfx canister create - CreateCanister => "Creates an empty canister on the Internet Computer and associates the Internet Computer assigned Canister ID to the canister name.", - CreateCanisterName => "Specifies the canister name. Either this or the --all flag are required.", - CreateAll => "Creates all canisters configured in dfx.json.", - - // dfx canister install - InstallCanister => "Deploys compiled code as a canister on the Internet Computer.", - InstallAll => "Deploys all canisters configured in the project dfx.json files.", - InstallCanisterName => "Specifies the canister name to deploy. You must specify either canister name or the --all option.", - InstallComputeAllocation => "Specifies the canister's compute allocation. This should be a percent in the range [0..100]", - InstallMemoryAllocation => "Specifies how much memory the canister is allowed to use in total. This should be a value in the range [0..256 TB]", - InstallMode => "Specifies the type of deployment. You can set the canister deployment modes to install, reinstall, or upgrade.", - - // dfx canister mod - ManageCanister => "Manages canisters deployed on a network replica.", - - // dfx canister delete - DeleteCanister => "Deletes a canister on the Internet Computer network.", - DeleteCanisterName => "Specifies the name of the canister to delete. You must specify either a canister name or the --all flag.", - DeleteAll => "Deletes all of the canisters configured in the dfx.json file.", - - // dfx canister set-controller - SetController => "Sets the provided identity's name or its principal as the new controller of a canister on the Internet Computer network.", - SetControllerCanister => "Specifies the canister name or the canister identifier for the canister to be controlled.", - NewController => "Specifies the identity name or the principal of the new controller.", - - // dfx canister status - CanisterStatus => "Returns the current status of the canister on the Internet Computer network: Running, Stopping, or Stopped.", - StatusCanisterName => "Specifies the name of the canister to return information for. You must specify either a canister name or the --all flag.", - StatusAll => "Returns status information for all of the canisters configured in the dfx.json file.", - - // dfx canister start - StartCanister => "Starts a canister on the Internet Computer network.", - StartCanisterName => "Specifies the name of the canister to start. You must specify either a canister name or the --all flag.", - StartAll => "Starts all of the canisters configured in the dfx.json file.", - - // dfx canister stop - StopCanister => "Stops a canister that is currently running on the Internet Computer network.", - StopCanisterName => "Specifies the name of the canister to stop. You must specify either a canister name or the --all option.", - StopAll => "Stops all of the canisters configured in the dfx.json file.", - - // dfx canister query - QueryCanister => "Sends a query request to a canister.", - UpdateCanisterArg => "Sends an update request to a canister. This is the default if the method is not a query method.", - - // dfx canister request_status - RequestCallStatus => "Requests the status of a specified call from a canister.", - RequestId => "Specifies the request identifier. The request identifier is an hexadecimal string starting with 0x.", - - // dfx build - BuildAll => "Builds all canisters configured in the dfx.json file.", - BuildCanisterName => "Specifies the name of the canister to build. You must specify either a canister name or the --all option.", - BuildCanister => "Builds all or specific canisters from the code in your project. By default, all canisters are built.", - BuildCheck => "Build canisters without creating them. This can be used to check that canisters build ok.", - CanisterComputeNetwork => "Override the compute network to connect to. By default, the local network is used.", - - // dfx config - ConfigureOptions => "Configures project options for your currently-selected project.", - OptionName => "Specifies the name of the configuration option to set or read. Use the period delineated path to specify the option to set or read. If this is not mentioned, outputs the whole configuration.", - OptionValue => "Specifies the new value to set. If you don't specify a value, the command displays the current value of the option from the configuration file.", - OptionFormat => "Specifies the format of the output. By default, the output format is JSON.", - - // dfx deploy - DeployCanister => "Deploys all or a specific canister from the code in your project. By default, all canisters are deployed.", - DeployCanisterName => "Specifies the name of the canister you want to deploy. If you don’t specify a canister name, all canisters defined in the dfx.json file are deployed.", - - // dfx identity mod - ManageIdentity => "Manages identities used to communicate with the Internet Computer network. Setting an identity enables you to test user-based access controls.", - - // dfx identity new - NewIdentity => "Creates a new identity.", - - // dfx identity list - ListIdentities => "Lists existing identities.", - - // dfx identity remove - RemoveIdentity => "Removes an existing identity.", - - // dfx identity rename - RenameIdentity => "Renames an existing identity.", - - // dfx identity use - UseIdentity => "Specifies the identity to use.", - - // dfx identity whoami - ShowIdentity => "Shows the name of the current identity.", - - // dfx identity get-principal - GetPrincipalId => "Shows the textual representation of the Principal associated with the current identity.", - - // dfx new - CreateProject => "Creates a new project.", - ProjectName => "Specifies the name of the project to create.", - DryRun => "Provides a preview the directories and files to be created without adding them to the file system.", - NewFrontend => "Installs the frontend code example for the default canister. This defaults to true if Node is installed, or false if it isn't.", - - // dfx ping - Ping => "Pings an Internet Computer network and returns its status.", - - // dfx replica - Replica => "Starts a local Internet Computer replica.", - ReplicaMessageGasLimit => "Specifies the maximum number of cycles a single message can consume.", - ReplicaPort => "Specifies the port the local replica should listen to.", - ReplicaRoundGasLimit => "Specifies the maximum number of cycles a single round can consume.", - - // dfx start - CleanState => "Cleans the state of the current project.", - StartNode => "Starts the local replica and a web server for the current project.", - NodeAddress => "Specifies the host name and port number to bind the frontend to.", - StartBackground => "Exits the dfx leaving the replica running. Will wait until the replica replies before exiting.", - - // misc - CanisterName => "Specifies the canister name. If you don't specify this argument, all canisters are processed.", - - // dfx stop - StopNode => "Stops the local network replica.", - // dfx ide - StartLanguageService => "Starts the Motoko IDE Language Server. This is meant to be run by editor plugins not the end-user.", - ForceTTY => "Forces the language server to start even when run from a terminal.", -); - -impl fmt::Display for UserMessage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", &self.to_str()) - } -} diff --git a/src/dfx/src/lib/mod.rs b/src/dfx/src/lib/mod.rs index 6c663a9ea6..bfbe4ed842 100644 --- a/src/dfx/src/lib/mod.rs +++ b/src/dfx/src/lib/mod.rs @@ -7,7 +7,6 @@ pub mod identity; pub mod installers; pub mod locations; pub mod logger; -pub mod message; pub mod models; pub mod network; pub mod operations; diff --git a/src/dfx/src/lib/provider.rs b/src/dfx/src/lib/provider.rs index 9e59a322e9..609071c518 100644 --- a/src/dfx/src/lib/provider.rs +++ b/src/dfx/src/lib/provider.rs @@ -3,7 +3,6 @@ use crate::lib::environment::{AgentEnvironment, Environment}; use crate::lib::error::{DfxError, DfxResult}; use crate::lib::network::network_descriptor::NetworkDescriptor; use crate::util::expiry_duration; -use clap::ArgMatches; use lazy_static::lazy_static; use std::sync::{Arc, RwLock}; use url::Url; @@ -12,8 +11,8 @@ lazy_static! { static ref NETWORK_CONTEXT: Arc>> = Arc::new(RwLock::new(None)); } -fn set_network_context(args: &ArgMatches<'_>) { - let name = args.value_of("network").unwrap_or("local").to_string(); +fn set_network_context(network: Option) { + let name = network.unwrap_or_else(|| "local".to_string()); let mut n = NETWORK_CONTEXT.write().unwrap(); *n = Some(name); @@ -30,9 +29,9 @@ pub fn get_network_context() -> DfxResult { // always returns at least one url pub fn get_network_descriptor<'a>( env: &'a (dyn Environment + 'a), - args: &ArgMatches<'_>, + network: Option, ) -> DfxResult { - set_network_context(args); + set_network_context(network); let config = env .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; @@ -78,9 +77,9 @@ pub fn get_network_descriptor<'a>( pub fn create_agent_environment<'a>( env: &'a (dyn Environment + 'a), - args: &ArgMatches<'_>, + network: Option, ) -> DfxResult> { - let network_descriptor = get_network_descriptor(env, args)?; + let network_descriptor = get_network_descriptor(env, network)?; let timeout = expiry_duration(); AgentEnvironment::new(env, network_descriptor, timeout) } diff --git a/src/dfx/src/main.rs b/src/dfx/src/main.rs index 7ba703f1a0..79a0c384e4 100644 --- a/src/dfx/src/main.rs +++ b/src/dfx/src/main.rs @@ -1,9 +1,8 @@ -use crate::commands::CliCommand; use crate::config::{dfx_version, dfx_version_str}; use crate::lib::environment::{Environment, EnvironmentImpl}; use crate::lib::error::*; use crate::lib::logger::{create_root_logger, LoggingMode}; -use clap::{App, AppSettings, Arg, ArgMatches}; +use clap::{App, AppSettings, ArgMatches, Clap, FromArgMatches, IntoApp}; use std::path::PathBuf; mod actors; @@ -12,51 +11,38 @@ mod config; mod lib; mod util; -fn cli(_: &impl Environment) -> App<'_, '_> { - App::new("dfx") - .about("The DFINITY Executor.") - .version(dfx_version_str()) - .global_setting(AppSettings::ColoredHelp) - .arg( - Arg::with_name("verbose") - .long("verbose") - .short("v") - .multiple(true), - ) - .arg( - Arg::with_name("quiet") - .long("quiet") - .short("q") - .multiple(true), - ) - .arg( - Arg::with_name("logmode") - .long("log") - .takes_value(true) - .possible_values(&["stderr", "tee", "file"]) - .default_value("stderr"), - ) - .arg( - Arg::with_name("logfile") - .long("log-file") - .long("logfile") - .takes_value(true), - ) - .arg( - Arg::with_name("identity") - .long("identity") - .takes_value(true), - ) - .subcommands( - commands::builtin() - .into_iter() - .map(|x: CliCommand| x.get_subcommand().clone()), - ) +/// The DFINITY Executor. +#[derive(Clap)] +#[clap(name("dfx"))] +#[clap(version = dfx_version_str(), global_setting = AppSettings::ColoredHelp)] +pub struct CliOpts { + #[clap(long, short('v'), parse(from_occurrences))] + verbose: u64, + + #[clap(long, short('q'), parse(from_occurrences))] + quiet: u64, + + #[clap(long("log"), default_value("stderr"), possible_values(&["stderr", "tee", "file"]))] + logmode: String, + + #[clap(long)] + logfile: Option, + + #[clap(long)] + identity: Option, } -fn exec(env: &impl Environment, args: &clap::ArgMatches<'_>, cli: &App<'_, '_>) -> DfxResult { +fn cli(_: &impl Environment) -> App<'static> { + CliOpts::into_app().subcommands( + commands::builtin() + .into_iter() + .map(|x| x.get_subcommand().clone()), + ) +} + +fn exec(env: &impl Environment, args: &ArgMatches, cli: &mut App<'static>) -> DfxResult { let (name, subcommand_args) = match args.subcommand() { - (name, Some(args)) => (name, args), + Some((name, args)) => (name, args), _ => { cli.write_help(&mut std::io::stderr())?; eprintln!(); @@ -129,17 +115,13 @@ fn maybe_redirect_dfx(env: &impl Environment) -> Option<()> { /// Setup a logger with the proper configuration, based on arguments. /// Returns a topple of whether or not to have a progress bar, and a logger. -fn setup_logging(matches: &ArgMatches<'_>) -> (bool, slog::Logger) { +fn setup_logging(opts: &CliOpts) -> (bool, slog::Logger) { // Create a logger with our argument matches. - let level = matches.occurrences_of("verbose") as i64 - matches.occurrences_of("quiet") as i64; - - let mode = match matches.value_of("logmode") { - Some("tee") => LoggingMode::Tee(PathBuf::from( - matches.value_of("logfile").unwrap_or("log.txt"), - )), - Some("file") => LoggingMode::File(PathBuf::from( - matches.value_of("logfile").unwrap_or("log.txt"), - )), + let level = opts.verbose as i64 - opts.quiet as i64; + + let mode = match opts.logmode.as_str() { + "tee" => LoggingMode::Tee(PathBuf::from(opts.logfile.as_deref().unwrap_or("log.txt"))), + "file" => LoggingMode::File(PathBuf::from(opts.logfile.as_deref().unwrap_or("log.txt"))), _ => LoggingMode::Stderr, }; @@ -155,24 +137,23 @@ fn main() { } let matches = cli(&env).get_matches(); + let opts: CliOpts = CliOpts::from_arg_matches(&matches); - let (progress_bar, log) = setup_logging(&matches); - - let identity_name = matches.value_of("identity").map(String::from); + let (progress_bar, log) = setup_logging(&opts); // Need to recreate the environment because we use it to get matches. // TODO(hansl): resolve this double-create problem. match EnvironmentImpl::new().map(|x| { x.with_logger(log) .with_progress_bar(progress_bar) - .with_identity_override(identity_name) + .with_identity_override(opts.identity) }) { Ok(env) => { slog::trace!( env.get_logger(), "Trace mode enabled. Lots of logs coming up." ); - exec(&env, &matches, &(cli(&env))) + exec(&env, &matches, &mut (cli(&env))) } Err(e) => Err(e), } diff --git a/src/dfx/src/util/clap/validators.rs b/src/dfx/src/util/clap/validators.rs index 713cd3a91d..163a9ff6d4 100644 --- a/src/dfx/src/util/clap/validators.rs +++ b/src/dfx/src/util/clap/validators.rs @@ -1,4 +1,6 @@ -pub fn is_request_id(v: String) -> Result<(), String> { +use humanize_rs::bytes::{Bytes, Unit}; + +pub fn is_request_id(v: &str) -> Result<(), String> { // A valid Request Id starts with `0x` and is a series of 64 hexadecimals. if !v.starts_with("0x") { Err(String::from("A Request ID needs to start with 0x.")) @@ -6,7 +8,7 @@ pub fn is_request_id(v: String) -> Result<(), String> { Err(String::from( "A Request ID is 64 hexadecimal prefixed with 0x.", )) - } else if v.as_str()[2..].contains(|c: char| !c.is_ascii_hexdigit()) { + } else if v[2..].contains(|c: char| !c.is_ascii_hexdigit()) { Err(String::from( "A Request ID is 64 hexadecimal prefixed with 0x. An invalid character was found.", )) @@ -14,3 +16,54 @@ pub fn is_request_id(v: String) -> Result<(), String> { Ok(()) } } + +pub fn compute_allocation_validator(compute_allocation: &str) -> Result<(), String> { + if let Ok(num) = compute_allocation.parse::() { + if num <= 100 { + return Ok(()); + } + } + Err("Must be a percent between 0 and 100".to_string()) +} + +pub fn memory_allocation_validator(memory_allocation: &str) -> Result<(), String> { + let limit = Bytes::new(256, Unit::TByte).map_err(|_| "Parse Overflow.")?; + if let Ok(bytes) = memory_allocation.parse::() { + if bytes.size() <= limit.size() { + return Ok(()); + } + } + Err("Must be a value between 0..256 TB inclusive.".to_string()) +} + +/// Validate a String can be a valid project name. +/// A project name is valid if it starts with a letter, and is alphanumeric (with hyphens). +/// It cannot end with a dash. +pub fn project_name_validator(name: &str) -> Result<(), String> { + let mut chars = name.chars(); + // Check first character first. If there's no first character it's empty. + if let Some(first) = chars.next() { + if first.is_ascii_alphabetic() { + // Then check all other characters. + // Reverses the search here; if there is a character that is not compatible + // it is found and an error is returned. + let m: Vec<&str> = name + .matches(|x: char| !x.is_ascii_alphanumeric() && x != '_') + .collect(); + + if m.is_empty() { + Ok(()) + } else { + Err(format!( + r#"Invalid character(s): "{}""#, + m.iter() + .fold(String::new(), |acc, &num| acc + &num.to_string()) + )) + } + } else { + Err("Must start with a letter.".to_owned()) + } + } else { + Err("Cannot be empty.".to_owned()) + } +}