Skip to content

Commit ed75bcf

Browse files
committed
Clean up the CLI.
1 parent 50f8b01 commit ed75bcf

File tree

1 file changed

+104
-101
lines changed

1 file changed

+104
-101
lines changed

examples/cli.rs

Lines changed: 104 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
extern crate keyring;
22

3-
use clap::Parser;
3+
use clap::{Args, Parser};
44
use std::collections::HashMap;
55

66
use keyring::{Entry, Error, Result};
@@ -22,57 +22,52 @@ fn main() {
2222
};
2323
match &args.command {
2424
Command::Set { .. } => {
25-
let (secret, password, attributes) = args.get_password_and_attributes();
26-
if secret.is_none() && password.is_none() && attributes.is_none() {
27-
eprintln!("You must provide either a password or attributes to the set command");
28-
std::process::exit(1);
29-
}
30-
if let Some(secret) = secret {
31-
match entry.set_secret(&secret) {
32-
Ok(()) => args.success_message_for(Some(&secret), None, None),
33-
Err(err) => args.error_message_for(err),
34-
}
35-
}
36-
if let Some(password) = password {
37-
match entry.set_password(&password) {
38-
Ok(()) => args.success_message_for(None, Some(&password), None),
25+
let value = args.get_password_and_attributes();
26+
match &value {
27+
Value::Secret(secret) => match entry.set_secret(secret) {
28+
Ok(()) => args.success_message_for(&value),
3929
Err(err) => args.error_message_for(err),
40-
}
41-
}
42-
if let Some(attributes) = attributes {
43-
let attrs: HashMap<&str, &str> = attributes
44-
.iter()
45-
.map(|(key, value)| (key.as_str(), value.as_str()))
46-
.collect();
47-
match entry.update_attributes(&attrs) {
48-
Ok(()) => args.success_message_for(None, None, Some(attributes)),
30+
},
31+
Value::Password(password) => match entry.set_password(password) {
32+
Ok(()) => args.success_message_for(&value),
4933
Err(err) => args.error_message_for(err),
34+
},
35+
Value::Attributes(attributes) => {
36+
let attrs: HashMap<&str, &str> = attributes
37+
.iter()
38+
.map(|(k, v)| (k.as_str(), v.as_str()))
39+
.collect();
40+
match entry.update_attributes(&attrs) {
41+
Ok(()) => args.success_message_for(&value),
42+
Err(err) => args.error_message_for(err),
43+
}
5044
}
45+
_ => panic!("Can't set without a value"),
5146
}
5247
}
5348
Command::Password => match entry.get_password() {
5449
Ok(password) => {
5550
println!("{password}");
56-
args.success_message_for(None, Some(&password), None);
51+
args.success_message_for(&Value::Password(password));
5752
}
5853
Err(err) => args.error_message_for(err),
5954
},
6055
Command::Secret => match entry.get_secret() {
6156
Ok(secret) => {
6257
println!("{}", secret_string(&secret));
63-
args.success_message_for(Some(&secret), None, None);
58+
args.success_message_for(&Value::Secret(secret));
6459
}
6560
Err(err) => args.error_message_for(err),
6661
},
6762
Command::Attributes => match entry.get_attributes() {
6863
Ok(attributes) => {
6964
println!("{}", attributes_string(&attributes));
70-
args.success_message_for(None, None, Some(attributes));
65+
args.success_message_for(&Value::Attributes(attributes));
7166
}
7267
Err(err) => args.error_message_for(err),
7368
},
7469
Command::Delete => match entry.delete_credential() {
75-
Ok(()) => args.success_message_for(None, None, None),
70+
Ok(()) => args.success_message_for(&Value::None),
7671
Err(err) => args.error_message_for(err),
7772
},
7873
}
@@ -105,24 +100,15 @@ pub struct Cli {
105100

106101
#[derive(Debug, Parser)]
107102
pub enum Command {
108-
/// Set the password and, optionally, attributes in the secure store
103+
/// Set the password or update the attributes in the secure store
109104
Set {
110-
#[clap(short, long, action)]
111-
/// The password is base64-encoded binary
112-
binary: bool,
113-
114-
#[clap(short, long, value_parser, default_value = "")]
115-
attributes: String,
105+
#[command(flatten)]
106+
what: What,
116107

117108
#[clap(value_parser)]
118-
/// The password to set into the secure store.
119-
/// If it's a valid base64 encoding (with padding),
120-
/// it will be decoded and used to set the binary secret.
121-
/// Otherwise, it will be interpreted as a string password.
122-
/// If no password is specified, it will be
123-
/// collected interactively (without echo)
124-
/// from the terminal.
125-
password: Option<String>,
109+
/// The input to parse. If not specified, it will be
110+
/// read interactively (without echo) from the terminal.
111+
input: Option<String>,
126112
},
127113
/// Retrieve the (string) password from the secure store
128114
/// and write it to the standard output.
@@ -132,9 +118,33 @@ pub enum Command {
132118
Secret,
133119
/// Retrieve attributes available in the secure store.
134120
Attributes,
121+
/// Delete the credential from the secure store.
135122
Delete,
136123
}
137124

125+
#[derive(Debug, Args)]
126+
#[group(multiple = false)]
127+
pub struct What {
128+
#[clap(short, long, action)]
129+
// The input is a password.
130+
password: bool,
131+
132+
#[clap(short, long, action)]
133+
// The input is a base64-encoded secret.
134+
secret: bool,
135+
136+
#[clap(short, long, action)]
137+
// The input is comma-separated, key=val attribute pairs.
138+
attributes: bool,
139+
}
140+
141+
enum Value {
142+
Secret(Vec<u8>),
143+
Password(String),
144+
Attributes(HashMap<String, String>),
145+
None,
146+
}
147+
138148
impl Cli {
139149
fn description(&self) -> String {
140150
if let Some(target) = &self.target {
@@ -184,78 +194,67 @@ impl Cli {
184194
std::process::exit(1)
185195
}
186196

187-
fn success_message_for(
188-
&self,
189-
secret: Option<&[u8]>,
190-
password: Option<&str>,
191-
attributes: Option<HashMap<String, String>>,
192-
) {
197+
fn success_message_for(&self, value: &Value) {
193198
if !self.verbose {
194199
return;
195200
}
196201
let description = self.description();
197202
match self.command {
198-
Command::Set { .. } => {
199-
if let Some(pw) = password {
200-
eprintln!("Set password for '{description}' to '{pw}'");
201-
}
202-
if let Some(secret) = secret {
203+
Command::Set { .. } => match value {
204+
Value::Secret(secret) => {
203205
let secret = secret_string(secret);
204206
eprintln!("Set secret for '{description}' to decode of '{secret}'");
205207
}
206-
if let Some(attributes) = attributes {
208+
Value::Password(password) => {
209+
eprintln!("Set password for '{description}' to '{password}'");
210+
}
211+
Value::Attributes(attributes) => {
207212
eprintln!("Set attributes for '{description}' to:");
208213
eprint_attributes(attributes);
209214
}
210-
}
215+
_ => panic!("Can't set without a value"),
216+
},
211217
Command::Password => {
212-
let pw = password.unwrap();
213-
eprintln!("Password for '{description}' is '{pw}'");
214-
}
215-
Command::Secret => {
216-
let secret = secret_string(secret.unwrap());
217-
eprintln!("Secret for '{description}' encodes as {secret}");
218+
match value {
219+
Value::Password(password) => {
220+
eprintln!("Password for '{description}' is '{password}'");
221+
}
222+
_ => panic!("Wrong value type for command"),
223+
};
218224
}
219-
Command::Attributes => {
220-
let attributes = attributes.unwrap();
221-
if attributes.is_empty() {
222-
eprintln!("No attributes found for '{description}'");
223-
} else {
224-
eprintln!("Attributes for '{description}' are:");
225-
eprint_attributes(attributes);
225+
Command::Secret => match value {
226+
Value::Secret(secret) => {
227+
let encoded = secret_string(secret);
228+
eprintln!("Secret for '{description}' encodes as {encoded}");
226229
}
227-
}
230+
_ => panic!("Wrong value type for command"),
231+
},
232+
Command::Attributes => match value {
233+
Value::Attributes(attributes) => {
234+
if attributes.is_empty() {
235+
eprintln!("No attributes found for '{description}'");
236+
} else {
237+
eprintln!("Attributes for '{description}' are:");
238+
eprint_attributes(attributes);
239+
}
240+
}
241+
_ => panic!("Wrong value type for command"),
242+
},
228243
Command::Delete => {
229244
eprintln!("Successfully deleted credential for '{description}'");
230245
}
231246
}
232247
}
233248

234-
fn get_password_and_attributes(
235-
&self,
236-
) -> (
237-
Option<Vec<u8>>,
238-
Option<String>,
239-
Option<HashMap<String, String>>,
240-
) {
241-
if let Command::Set {
242-
binary,
243-
attributes,
244-
password,
245-
} = &self.command
246-
{
247-
let secret = if *binary {
248-
Some(decode_secret(password))
249-
} else {
250-
None
251-
};
252-
let password = if !*binary {
253-
Some(read_password(password))
249+
fn get_password_and_attributes(&self) -> Value {
250+
if let Command::Set { what, input } = &self.command {
251+
if what.password || (!what.secret && !what.attributes) {
252+
Value::Password(read_password(input))
253+
} else if what.secret {
254+
Value::Secret(decode_secret(input))
254255
} else {
255-
None
256-
};
257-
let attributes = parse_attributes(attributes);
258-
(secret, password, attributes)
256+
Value::Attributes(parse_attributes(input))
257+
}
259258
} else {
260259
panic!("Can't happen: asking for password and attributes on non-set command")
261260
}
@@ -268,7 +267,7 @@ fn secret_string(secret: &[u8]) -> String {
268267
BASE64_STANDARD.encode(secret)
269268
}
270269

271-
fn eprint_attributes(attributes: HashMap<String, String>) {
270+
fn eprint_attributes(attributes: &HashMap<String, String>) {
272271
for (key, value) in attributes {
273272
println!(" {key}: {value}");
274273
}
@@ -295,12 +294,11 @@ fn decode_secret(input: &Option<String>) -> Vec<u8> {
295294
}
296295

297296
fn read_password(input: &Option<String>) -> String {
298-
let password = if let Some(input) = input {
297+
if let Some(input) = input {
299298
input.clone()
300299
} else {
301300
rpassword::prompt_password("Password: ").unwrap_or_else(|_| String::new())
302-
};
303-
password
301+
}
304302
}
305303

306304
fn attributes_string(attributes: &HashMap<String, String>) -> String {
@@ -311,9 +309,14 @@ fn attributes_string(attributes: &HashMap<String, String>) -> String {
311309
strings.join(",")
312310
}
313311

314-
fn parse_attributes(input: &String) -> Option<HashMap<String, String>> {
312+
fn parse_attributes(input: &Option<String>) -> HashMap<String, String> {
313+
let input = if let Some(input) = input {
314+
input.clone()
315+
} else {
316+
rpassword::prompt_password("Attributes: ").unwrap_or_else(|_| String::new())
317+
};
315318
if input.is_empty() {
316-
return None;
319+
eprintln!("You must specify at least one key=value attribute pair to set")
317320
}
318321
let mut attributes = HashMap::new();
319322
let parts = input.split(',');
@@ -325,5 +328,5 @@ fn parse_attributes(input: &String) -> Option<HashMap<String, String>> {
325328
}
326329
attributes.insert(parts[0].to_string(), parts[1].to_string());
327330
}
328-
Some(attributes)
331+
attributes
329332
}

0 commit comments

Comments
 (0)