From 2f47b0e4c6d24d48b0517dfd7fdb35e2d818e14d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 6 Dec 2022 18:44:27 -0800 Subject: [PATCH 1/3] implement args parsing and add unittests --- config/Cargo.toml | 3 +- config/src/args.rs | 201 +++++++++++++++++++++++++++++++++++++++++++-- config/src/main.rs | 17 ++-- 3 files changed, 206 insertions(+), 15 deletions(-) diff --git a/config/Cargo.toml b/config/Cargo.toml index 675c731d..58906628 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -thiserror = "1.0" +atty = { version = "0.2" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } +thiserror = "1.0" diff --git a/config/src/args.rs b/config/src/args.rs index 50b503be..1804b40d 100644 --- a/config/src/args.rs +++ b/config/src/args.rs @@ -1,9 +1,16 @@ +use std::io; +use std::process::exit; +use super::*; + pub struct Args { pub subcommand: SubCommand, pub resource: String, + pub filter: String, + pub stdin: String, pub no_cache: bool, // don't use the cache and force a new discovery, low priority } +#[derive(Debug, PartialEq, Eq)] pub enum SubCommand { List, Get, @@ -13,18 +20,196 @@ pub enum SubCommand { } impl Args { - pub fn new() -> Self { - // TODO: parse args and populate this struct + pub fn new(args: &mut dyn Iterator, input: &mut dyn io::Read, atty: bool) -> Self { + let mut command_args: Vec = args.skip(1).collect(); // skip the first arg which is the program name + if command_args.is_empty() || command_args[0].starts_with('-') { + eprintln!("No subcommand provided"); + show_help_and_exit(); + } + + let subcommand = match command_args[0].as_str() { + "list" => SubCommand::List, + "get" => SubCommand::Get, + "set" => SubCommand::Set, + "test" => SubCommand::Test, + "flushcache" => SubCommand::FlushCache, + _ => { + eprintln!("Invalid subcommand provided"); + show_help_and_exit(); + SubCommand::List + } + }; + + command_args.remove(0); // remove the subcommand + + let mut no_cache = false; + let mut resource = String::new(); + let mut filter = String::new(); + let mut stdin = Vec::new(); + + if !atty { + // only read if input is piped in and not a tty (terminal) + input.read_to_end(&mut stdin).unwrap(); + } + + let stdin = match String::from_utf8(stdin) { + Ok(v) => v, + Err(e) => { + eprintln!("Invalid UTF-8 sequence: {}", e); + exit(EXIT_INVALID_INPUT); + } + }; + + // go through reset of provided argsĀ  + for arg in command_args { + match arg.as_str() { + "-h" | "--help" => show_help_and_exit(), + "-n" | "--nocache" => no_cache = true, + _ => { + if subcommand == SubCommand::FlushCache { + eprintln!("No arguments allowed for `flushcache`"); + show_help_and_exit(); + } + + match subcommand { + SubCommand::List => { + if !filter.is_empty() { + eprintln!("Only one filter allowed"); + show_help_and_exit(); + } + filter = arg; + } + _ => { + if !resource.is_empty() { + eprintln!("Only one resource allowed"); + show_help_and_exit(); + } + resource = arg; + } + } + } + } + } + + match subcommand { + SubCommand::Set | SubCommand::Test => { + if stdin.is_empty() { + eprintln!("Desired state input via stdin is required for `set` and `test`"); + show_help_and_exit(); + } + } + _ => {} + } + Self { - subcommand: SubCommand::List, - resource: String::new(), - no_cache: false, + subcommand, + resource, + filter, + stdin, + no_cache, } } } -impl Default for Args { - fn default() -> Self { - Args::new() +fn show_help_and_exit() { + eprintln!(); + eprintln!("Usage: config [subcommand] [options]"); + eprintln!("Subcommands:"); + eprintln!(" list [filter] - list all resources, optional filter"); + eprintln!(" get - invoke `get` on a resource"); + eprintln!(" set - invoke `set` on a resource"); + eprintln!(" test - invoke `test` on a resource"); + eprintln!(" flushcache - flush the resource cache"); + eprintln!("Options:"); + eprintln!(" -h, --help"); + eprintln!(" -n, --nocache - don't use the cache and force a new discovery"); + exit(EXIT_INVALID_ARGS); +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Stdin { + input: String, + } + + impl Stdin { + fn new(input: &str) -> Self { + Self { + input: input.to_string(), + } + } + } + + impl io::Read for Stdin { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let bytes = self.input.as_bytes(); + let len = bytes.len(); + buf.clone_from_slice(bytes); + Ok(len) + } + + fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { + let bytes = self.input.as_bytes(); + let len = bytes.len(); + buf.extend_from_slice(bytes); + Ok(len) + } + } + + #[test] + fn test_args_list() { + let mut args = ["config", "list", "myfilter"].iter().map(|s| s.to_string()); + let args = Args::new(&mut args, &mut Stdin::new(""), false); + assert_eq!(args.subcommand, SubCommand::List); + assert_eq!(args.resource, ""); + assert_eq!(args.filter, "myfilter"); + assert_eq!(args.stdin, ""); + assert_eq!(args.no_cache, false); + } + + #[test] + fn test_args_get() { + let mut args = ["config", "get", "myresource"].iter().map(|s| s.to_string()); + let args = Args::new(&mut args, &mut Stdin::new("abc"), false); + assert_eq!(args.subcommand, SubCommand::Get); + assert_eq!(args.resource, "myresource"); + assert_eq!(args.filter, ""); + assert_eq!(args.stdin, "abc"); + assert_eq!(args.no_cache, false); + } + + #[test] + fn test_args_set() { + let mut args = ["config", "set", "myresource"].iter().map(|s| s.to_string()); + let args = Args::new(&mut args, &mut Stdin::new("abc"), false); + assert_eq!(args.subcommand, SubCommand::Set); + assert_eq!(args.resource, "myresource"); + assert_eq!(args.filter, ""); + assert_eq!(args.stdin, "abc"); + assert_eq!(args.no_cache, false); + } + + #[test] + fn test_args_test() { + let mut args = ["config", "test", "myresource"].iter().map(|s| s.to_string()); + let args = Args::new(&mut args, &mut Stdin::new("abc"), false); + assert_eq!(args.subcommand, SubCommand::Test); + assert_eq!(args.resource, "myresource"); + assert_eq!(args.filter, ""); + assert_eq!(args.stdin, "abc"); + assert_eq!(args.no_cache, false); + } + + #[test] + fn test_args_flushcache() { + let mut args = ["config", "flushcache"].iter().map(|s| s.to_string()); + let args = Args::new(&mut args, &mut Stdin::new(""), false); + assert_eq!(args.subcommand, SubCommand::FlushCache); + assert_eq!(args.resource, ""); + assert_eq!(args.filter, ""); + assert_eq!(args.stdin, ""); + assert_eq!(args.no_cache, false); } } diff --git a/config/src/main.rs b/config/src/main.rs index dac577cd..08cc2e41 100644 --- a/config/src/main.rs +++ b/config/src/main.rs @@ -1,28 +1,33 @@ +use args::*; +use atty::Stream; +use std::{env, io}; + pub mod args; pub mod discovery; pub mod dscresources; pub mod dscerror; -use args::*; +pub const EXIT_INVALID_ARGS: i32 = 1; +pub const EXIT_INVALID_INPUT: i32 = 2; fn main() { - let args = Args::new(); + let args = Args::new(&mut env::args(), &mut io::stdin(), atty::is(Stream::Stdin)); match args.subcommand { SubCommand::List => { // perform discovery - println!("List"); + println!("List {}", args.filter); } SubCommand::Get => { // perform discovery - println!("Get"); + println!("Get {}: {}", args.resource, args.stdin); } SubCommand::Set => { // perform discovery - println!("Set"); + println!("Set {}: {}", args.resource, args.stdin); } SubCommand::Test => { // perform discovery - println!("Test"); + println!("Test {}: {}", args.resource, args.stdin); } SubCommand::FlushCache => { println!("FlushCache"); From 131b026fb241342d86a7c7ce6b85563f2f05734b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 6 Dec 2022 18:47:56 -0800 Subject: [PATCH 2/3] add readme --- config/README.md | 18 ++++++++++++++++++ config/src/args.rs | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 config/README.md diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..f291af89 --- /dev/null +++ b/config/README.md @@ -0,0 +1,18 @@ +# `config` command for using DSC resources + +## DESCRIPTION + +The `config` command is used to discover and invoke DSC resources. + +## Usage + +Usage: config [subcommand] [options] +Subcommands: + list [filter] - list all resources, optional filter + get - invoke `get` on a resource + set - invoke `set` on a resource + test - invoke `test` on a resource + flushcache - flush the resource cache +Options: + -h, --help + -n, --nocache - don't use the cache and force a new discovery diff --git a/config/src/args.rs b/config/src/args.rs index 1804b40d..cd2e9d50 100644 --- a/config/src/args.rs +++ b/config/src/args.rs @@ -22,7 +22,7 @@ pub enum SubCommand { impl Args { pub fn new(args: &mut dyn Iterator, input: &mut dyn io::Read, atty: bool) -> Self { let mut command_args: Vec = args.skip(1).collect(); // skip the first arg which is the program name - if command_args.is_empty() || command_args[0].starts_with('-') { + if command_args.is_empty() { eprintln!("No subcommand provided"); show_help_and_exit(); } From 3853a9c40997480bdf897230dab535b8cd98a846 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 7 Dec 2022 13:34:49 -0800 Subject: [PATCH 3/3] address Andrew's feedback --- config/src/args.rs | 35 ++++++++++++++++++++--------------- config/src/main.rs | 5 +++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/config/src/args.rs b/config/src/args.rs index cd2e9d50..f2392474 100644 --- a/config/src/args.rs +++ b/config/src/args.rs @@ -2,16 +2,24 @@ use std::io; use std::process::exit; use super::*; +/// Struct containing the parsed command line arguments +#[derive(Debug)] pub struct Args { + /// The subcommand to run pub subcommand: SubCommand, + /// The name of the resource to run the subcommand on pub resource: String, + /// The filter to use when listing resources pub filter: String, + /// String representing any input piped in via stdin, this is typically expected to be JSON pub stdin: String, - pub no_cache: bool, // don't use the cache and force a new discovery, low priority + /// Whether to use the cache or not + pub no_cache: bool, } #[derive(Debug, PartialEq, Eq)] pub enum SubCommand { + None, List, Get, Set, @@ -20,6 +28,13 @@ pub enum SubCommand { } impl Args { + /// Parse the command line arguments and return an Args struct + /// + /// # Arguments + /// + /// * `args` - Iterator of command line arguments + /// * `input` - Input stream to read from + /// * `atty` - Whether the input stream is a tty (terminal) or not pub fn new(args: &mut dyn Iterator, input: &mut dyn io::Read, atty: bool) -> Self { let mut command_args: Vec = args.skip(1).collect(); // skip the first arg which is the program name if command_args.is_empty() { @@ -27,7 +42,7 @@ impl Args { show_help_and_exit(); } - let subcommand = match command_args[0].as_str() { + let subcommand = match command_args[0].to_lowercase().as_str() { "list" => SubCommand::List, "get" => SubCommand::Get, "set" => SubCommand::Set, @@ -36,7 +51,7 @@ impl Args { _ => { eprintln!("Invalid subcommand provided"); show_help_and_exit(); - SubCommand::List + SubCommand::None } }; @@ -48,7 +63,7 @@ impl Args { let mut stdin = Vec::new(); if !atty { - // only read if input is piped in and not a tty (terminal) + // only read if input is piped in and not a tty (terminal) otherwise stdin is used for keyboard input input.read_to_end(&mut stdin).unwrap(); } @@ -62,7 +77,7 @@ impl Args { // go through reset of provided argsĀ  for arg in command_args { - match arg.as_str() { + match arg.to_lowercase().as_str() { "-h" | "--help" => show_help_and_exit(), "-n" | "--nocache" => no_cache = true, _ => { @@ -91,16 +106,6 @@ impl Args { } } - match subcommand { - SubCommand::Set | SubCommand::Test => { - if stdin.is_empty() { - eprintln!("Desired state input via stdin is required for `set` and `test`"); - show_help_and_exit(); - } - } - _ => {} - } - Self { subcommand, resource, diff --git a/config/src/main.rs b/config/src/main.rs index 08cc2e41..e2fceecf 100644 --- a/config/src/main.rs +++ b/config/src/main.rs @@ -1,5 +1,6 @@ use args::*; use atty::Stream; +use std::process::exit; use std::{env, io}; pub mod args; @@ -32,5 +33,9 @@ fn main() { SubCommand::FlushCache => { println!("FlushCache"); } + _ => { + eprintln!("Invalid subcommand"); + exit(EXIT_INVALID_ARGS); + } } }