From 707f0bac2141ef7120c0493de3a5d331a511c40c Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Fri, 22 May 2020 00:59:34 -0400 Subject: use structopt instead of clap --- src/bin/rbw/commands.rs | 13 +- src/bin/rbw/main.rs | 591 +++++++++++++++++++----------------------------- 2 files changed, 244 insertions(+), 360 deletions(-) (limited to 'src') diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index ff6f51e..f7cd6c2 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -279,11 +279,11 @@ enum ListField { Folder, } -impl std::convert::TryFrom<&str> for ListField { +impl std::convert::TryFrom<&String> for ListField { type Error = anyhow::Error; - fn try_from(s: &str) -> anyhow::Result { - Ok(match s { + fn try_from(s: &String) -> anyhow::Result { + Ok(match s.as_str() { "name" => Self::Name, "id" => Self::Id, "user" => Self::User, @@ -368,10 +368,9 @@ pub fn sync() -> anyhow::Result<()> { Ok(()) } -pub fn list(fields: &[&str]) -> anyhow::Result<()> { +pub fn list(fields: &[String]) -> anyhow::Result<()> { let fields: Vec = fields .iter() - .copied() .map(std::convert::TryFrom::try_from) .collect::>()?; @@ -438,7 +437,7 @@ pub fn get(name: &str, user: Option<&str>, full: bool) -> anyhow::Result<()> { pub fn add( name: &str, username: Option<&str>, - uris: Vec<&str>, + uris: Vec, folder: Option<&str>, ) -> anyhow::Result<()> { unlock()?; @@ -529,7 +528,7 @@ pub fn add( pub fn generate( name: Option<&str>, username: Option<&str>, - uris: Vec<&str>, + uris: Vec, folder: Option<&str>, len: usize, ty: rbw::pwgen::Type, diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 9c65cb4..3b6e9b6 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -6,377 +6,262 @@ mod actions; mod commands; mod sock; -fn main() { +#[derive(Debug, structopt::StructOpt)] +#[structopt(about = "Unofficial Bitwarden CLI")] +enum Opt { + #[structopt(about = "Get or set configuration options")] + Config { + #[structopt(subcommand)] + config: Config, + }, + + #[structopt(about = "Log in to the Bitwarden server")] + Login, + + #[structopt(about = "Unlock the local Bitwarden database")] + Unlock, + + #[structopt(about = "Update the local copy of the Bitwarden database")] + Sync, + + #[structopt( + about = "List all entries in the local Bitwarden database", + visible_alias = "ls" + )] + List { + #[structopt( + long, + help = "Fields to display. \ + Available options are id, name, user, folder. \ + Multiple fields will be separated by tabs.", + default_value = "name" + )] + fields: Vec, + }, + + #[structopt(about = "Display the password for a given entry")] + Get { + #[structopt(help = "Name of the entry to display")] + name: String, + #[structopt(help = "Username of the entry to display")] + user: Option, + #[structopt( + long, + help = "Display the notes in addition to the password" + )] + full: bool, + }, + + #[structopt( + about = "Add a new password to the database", + long_about = "Add a new password to the database\n\n\ + This command will open a text editor to enter \ + the password and notes. The editor to use is determined \ + by the value of the $EDITOR environment variable. The \ + first line will be saved as the password and the \ + remainder will be saved as a note." + )] + Add { + #[structopt(help = "Name of the password entry")] + name: String, + #[structopt(help = "Username for the password entry")] + user: Option, + #[structopt(long, help = "URI for the password entry")] + uri: Vec, + #[structopt(long, help = "Folder for the password entry")] + folder: Option, + }, + + #[structopt( + about = "Generate a new password", + long_about = "Generate a new password\n\n\ + If given a password entry name, also save the generated \ + password to the database.", + visible_alias = "gen", + group = clap::ArgGroup::with_name("password-type").args(&[ + "no-symbols", + "only-numbers", + "nonconfusables", + "diceware", + ]) + )] + Generate { + #[structopt(help = "Length of the password to generate")] + len: usize, + #[structopt(help = "Name of the password entry")] + name: Option, + #[structopt(help = "Username for the password entry")] + user: Option, + #[structopt(long, help = "URI for the password entry")] + uri: Vec, + #[structopt(long, help = "Folder for the password entry")] + folder: Option, + #[structopt( + long = "no-symbols", + help = "Generate a password with no special characters" + )] + no_symbols: bool, + #[structopt( + long = "only-numbers", + help = "Generate a password consisting of only numbers" + )] + only_numbers: bool, + #[structopt( + long, + help = "Generate a password without visually similar \ + characters (useful for passwords intended to be \ + written down)" + )] + nonconfusables: bool, + #[structopt( + long, + help = "Generate a password of multiple dictionary \ + words chosen from the EFF word list. The len \ + parameter for this option will set the number \ + of words to generate, rather than characters." + )] + diceware: bool, + }, + + #[structopt( + about = "Modify an existing password", + long_about = "Modify an existing password\n\n\ + This command will open a text editor with the existing \ + password and notes of the given entry for editing. \ + The editor to use is determined by the value of the \ + $EDITOR environment variable. The first line will be \ + saved as the password and the remainder will be saved \ + as a note." + )] + Edit { + #[structopt(help = "Name of the password entry")] + name: String, + #[structopt(help = "Username for the password entry")] + user: Option, + }, + + #[structopt(about = "Remove a given entry", visible_alias = "rm")] + Remove { + #[structopt(help = "Name of the password entry")] + name: String, + #[structopt(help = "Username for the password entry")] + user: Option, + }, + + #[structopt(about = "View the password history for a given entry")] + History { + #[structopt(help = "Name of the password entry")] + name: String, + #[structopt(help = "Username for the password entry")] + user: Option, + }, + + #[structopt(about = "Lock the password database")] + Lock, + + #[structopt(about = "Remove the local copy of the password database")] + Purge, + + #[structopt( + name = "stop-agent", + about = "Terminate the background agent" + )] + StopAgent, +} + +#[derive(Debug, structopt::StructOpt)] +enum Config { + #[structopt(about = "Show the values of all configuration settings")] + Show, + #[structopt(about = "Set a configuration option")] + Set { + #[structopt(help = "Configuration key to set")] + key: String, + #[structopt(help = "Value to set the configuration option to")] + value: String, + }, + #[structopt(about = "Reset a configuration option to its default")] + Unset { + #[structopt(help = "Configuration key to unset")] + key: String, + }, +} + +#[paw::main] +fn main(opt: Opt) { env_logger::from_env( env_logger::Env::default().default_filter_or("info"), ) .init(); - let matches = clap::App::new("rbw") - .about("Unofficial Bitwarden CLI") - .author(clap::crate_authors!()) - .version(clap::crate_version!()) - .subcommand( - clap::SubCommand::with_name("config") - .about("Get or set configuration options") - .subcommand( - clap::SubCommand::with_name("show").about( - "Show the values of all configuration settings", - ), - ) - .subcommand( - clap::SubCommand::with_name("set") - .about("Set a configuration option") - .arg( - clap::Arg::with_name("key") - .required(true) - .help("Configuration key to set"), - ) - .arg( - clap::Arg::with_name("value") - .required(true) - .help( - "Value to set the configuration option to", - ), - ), - ) - .subcommand( - clap::SubCommand::with_name("unset") - .about("Reset a configuration option to its default") - .arg( - clap::Arg::with_name("key") - .required(true) - .help("Configuration key to unset"), - ), - ), - ) - .subcommand( - clap::SubCommand::with_name("login") - .about("Log in to the Bitwarden server"), - ) - .subcommand( - clap::SubCommand::with_name("unlock") - .about("Unlock the local Bitwarden database"), - ) - .subcommand( - clap::SubCommand::with_name("sync") - .about("Update the local copy of the Bitwarden database"), - ) - .subcommand( - clap::SubCommand::with_name("list") - .about("List all entries in the local Bitwarden database") - .arg( - clap::Arg::with_name("fields") - .long("fields") - .takes_value(true) - .use_delimiter(true) - .multiple(true) - .help( - "Fields to display. \ - Available options are id, name, user, folder. \ - Multiple fields will be separated by tabs.", - ), - ) - .visible_alias("ls"), - ) - .subcommand( - clap::SubCommand::with_name("get") - .about("Display the password for a given entry") - .arg( - clap::Arg::with_name("name") - .required(true) - .help("Name of the entry to display"), - ) - .arg( - clap::Arg::with_name("user") - .help("Username of the entry to display"), - ) - .arg( - clap::Arg::with_name("full").long("full").help( - "Display the notes in addition to the password", - ), - ), - ) - .subcommand( - clap::SubCommand::with_name("add") - .about("Add a new password to the database") - .long_about( - "Add a new password to the database\n\n\ - This command will open a text editor to enter \ - the password and notes. The editor to use is determined \ - by the value of the $EDITOR environment variable. The \ - first line will be saved as the password and the \ - remainder will be saved as a note.", - ) - .arg( - clap::Arg::with_name("name") - .required(true) - .help("Name of the password entry"), - ) - .arg( - clap::Arg::with_name("user") - .help("Username for the password entry"), - ) - .arg( - clap::Arg::with_name("uri") - .long("uri") - .takes_value(true) - .multiple(true) - .number_of_values(1) - .use_delimiter(false) - .help("URI for the password entry"), - ) - .arg( - clap::Arg::with_name("folder") - .long("folder") - .takes_value(true) - .help("Folder for the password entry"), - ), - ) - .subcommand( - clap::SubCommand::with_name("generate") - .about("Generate a new password") - .long_about( - "Generate a new password\n\n\ - If given a password entry name, also save the generated \ - password to the database.", - ) - .arg( - clap::Arg::with_name("len") - .required(true) - .help("Length of the password to generate"), - ) - .arg( - clap::Arg::with_name("name") - .help("Name of the password entry"), - ) - .arg( - clap::Arg::with_name("user") - .help("Username for the password entry"), - ) - .arg( - clap::Arg::with_name("uri") - .long("uri") - .takes_value(true) - .multiple(true) - .number_of_values(1) - .use_delimiter(false) - .help("URI for the password entry"), - ) - .arg( - clap::Arg::with_name("folder") - .long("folder") - .takes_value(true) - .help("Folder for the password entry"), - ) - .arg( - clap::Arg::with_name("no-symbols") - .long("no-symbols") - .help( - "Generate a password with no special characters", - ), - ) - .arg( - clap::Arg::with_name("only-numbers") - .long("only-numbers") - .help( - "Generate a password consisting of only numbers", - ), - ) - .arg( - clap::Arg::with_name("nonconfusables") - .long("nonconfusables") - .help( - "Generate a password without visually similar \ - characters (useful for passwords intended to be \ - written down)", - ), - ) - .arg(clap::Arg::with_name("diceware").long("diceware").help( - "Generate a password of multiple dictionary \ - words chosen from the EFF word list. The len \ - parameter for this option will set the number \ - of words to generate, rather than characters.", - )) - .group(clap::ArgGroup::with_name("password-type").args(&[ - "no-symbols", - "only-numbers", - "nonconfusables", - "diceware", - ])) - .visible_alias("gen"), - ) - .subcommand( - clap::SubCommand::with_name("edit") - .about("Modify an existing password") - .long_about( - "Modify an existing password\n\n\ - This command will open a text editor with the existing \ - password and notes of the given entry for editing. \ - The editor to use is determined by the value of the \ - $EDITOR environment variable. The first line will be \ - saved as the password and the remainder will be saved \ - as a note.", - ) - .arg( - clap::Arg::with_name("name") - .required(true) - .help("Name of the password entry"), - ) - .arg( - clap::Arg::with_name("user") - .help("Username for the password entry"), - ), - ) - .subcommand( - clap::SubCommand::with_name("remove") - .about("Remove a given entry") - .arg( - clap::Arg::with_name("name") - .required(true) - .help("Name of the password entry"), - ) - .arg( - clap::Arg::with_name("user") - .help("Username for the password entry"), - ) - .visible_alias("rm"), - ) - .subcommand( - clap::SubCommand::with_name("history") - .about("View the password history for a given entry") - .arg( - clap::Arg::with_name("name") - .required(true) - .help("Name of the password entry"), - ) - .arg( - clap::Arg::with_name("user") - .help("Username for the password entry"), - ), - ) - .subcommand( - clap::SubCommand::with_name("lock") - .about("Lock the password database"), - ) - .subcommand( - clap::SubCommand::with_name("purge") - .about("Remove the local copy of the password database"), - ) - .subcommand( - clap::SubCommand::with_name("stop-agent") - .about("Terminate the background agent") - .visible_alias("logout"), - ) - .get_matches(); - - let res = match matches.subcommand() { - ("config", Some(smatches)) => match smatches.subcommand() { - ("show", Some(_)) => { - commands::config_show().context("config show") + let res = match opt { + Opt::Config { config } => match config { + Config::Show => commands::config_show().context("config show"), + Config::Set { key, value } => { + commands::config_set(&key, &value).context("config set") } - // these unwraps are fine because key and value are both marked - // .required(true) - ("set", Some(ssmatches)) => commands::config_set( - ssmatches.value_of("key").unwrap(), - ssmatches.value_of("value").unwrap(), - ) - .context("config set"), - // this unwrap is fine because key is marked .required(true) - ("unset", Some(ssmatches)) => { - commands::config_unset(ssmatches.value_of("key").unwrap()) - .context("config unset") - } - _ => { - eprintln!("{}", smatches.usage()); - std::process::exit(1); + Config::Unset { key } => { + commands::config_unset(&key).context("config unset") } }, - ("login", Some(_)) => commands::login().context("login"), - ("unlock", Some(_)) => commands::unlock().context("unlock"), - ("sync", Some(_)) => commands::sync().context("sync"), - ("list", Some(smatches)) => commands::list( - &smatches - .values_of("fields") - .map(|it| it.collect()) - .unwrap_or_else(|| vec!["name"]), - ) - .context("list"), - // this unwrap is safe because name is marked .required(true) - ("get", Some(smatches)) => commands::get( - smatches.value_of("name").unwrap(), - smatches.value_of("user"), - smatches.is_present("full"), - ) - .context("get"), - // this unwrap is safe because name is marked .required(true) - ("add", Some(smatches)) => commands::add( - smatches.value_of("name").unwrap(), - smatches.value_of("user"), - smatches - .values_of("uri") - .map(|it| it.collect()) - .unwrap_or_else(|| vec![]), - smatches.value_of("folder"), - ) - .context("add"), - ("generate", Some(smatches)) => { - let ty = if smatches.is_present("no-symbols") { + Opt::Login => commands::login().context("login"), + Opt::Unlock => commands::unlock().context("unlock"), + Opt::Sync => commands::sync().context("sync"), + Opt::List { fields } => commands::list(&fields).context("list"), + Opt::Get { name, user, full } => { + commands::get(&name, user.as_deref(), full).context("get") + } + Opt::Add { + name, + user, + uri, + folder, + } => commands::add(&name, user.as_deref(), uri, folder.as_deref()) + .context("add"), + Opt::Generate { + len, + name, + user, + uri, + folder, + no_symbols, + only_numbers, + nonconfusables, + diceware, + } => { + let ty = if no_symbols { rbw::pwgen::Type::NoSymbols - } else if smatches.is_present("only-numbers") { + } else if only_numbers { rbw::pwgen::Type::Numbers - } else if smatches.is_present("nonconfusables") { + } else if nonconfusables { rbw::pwgen::Type::NonConfusables - } else if smatches.is_present("diceware") { + } else if diceware { rbw::pwgen::Type::Diceware } else { rbw::pwgen::Type::AllChars }; - // this unwrap is fine because len is marked as .required(true) - let len = smatches.value_of("len").unwrap(); - match len.parse() { - Ok(len) => commands::generate( - smatches.value_of("name"), - smatches.value_of("user"), - smatches - .values_of("uri") - .map(|it| it.collect()) - .unwrap_or_else(|| vec![]), - smatches.value_of("folder"), - len, - ty, - ) - .context("generate"), - Err(e) => Err(e.into()), - } + commands::generate( + name.as_deref(), + user.as_deref(), + uri, + folder.as_deref(), + len, + ty, + ) + .context("generate") + } + Opt::Edit { name, user } => { + commands::edit(&name, user.as_deref()).context("edit") } - // this unwrap is safe because name is marked .required(true) - ("edit", Some(smatches)) => commands::edit( - smatches.value_of("name").unwrap(), - smatches.value_of("user"), - ) - .context("edit"), - // this unwrap is safe because name is marked .required(true) - ("remove", Some(smatches)) => commands::remove( - smatches.value_of("name").unwrap(), - smatches.value_of("user"), - ) - .context("remove"), - // this unwrap is safe because name is marked .required(true) - ("history", Some(smatches)) => commands::history( - smatches.value_of("name").unwrap(), - smatches.value_of("user"), - ) - .context("history"), - ("lock", Some(_)) => commands::lock().context("lock"), - ("purge", Some(_)) => commands::purge().context("purge"), - ("stop-agent", Some(_)) => { - commands::stop_agent().context("stop-agent") + Opt::Remove { name, user } => { + commands::remove(&name, user.as_deref()).context("remove") } - _ => { - eprintln!("{}", matches.usage()); - std::process::exit(1); + Opt::History { name, user } => { + commands::history(&name, user.as_deref()).context("history") } + Opt::Lock => commands::lock().context("lock"), + Opt::Purge => commands::purge().context("purge"), + Opt::StopAgent => commands::stop_agent().context("stop-agent"), } .context("rbw"); -- cgit v1.2.3-54-g00ecf