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/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 50b503be..f2392474 100644 --- a/config/src/args.rs +++ b/config/src/args.rs @@ -1,10 +1,25 @@ +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, - pub no_cache: bool, // don't use the cache and force a new discovery, low priority + /// 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, + /// Whether to use the cache or not + pub no_cache: bool, } +#[derive(Debug, PartialEq, Eq)] pub enum SubCommand { + None, List, Get, Set, @@ -13,18 +28,193 @@ pub enum SubCommand { } impl Args { - pub fn new() -> Self { - // TODO: parse args and populate this struct + /// 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() { + eprintln!("No subcommand provided"); + show_help_and_exit(); + } + + let subcommand = match command_args[0].to_lowercase().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::None + } + }; + + 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) otherwise stdin is used for keyboard input + 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.to_lowercase().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; + } + } + } + } + } + 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..e2fceecf 100644 --- a/config/src/main.rs +++ b/config/src/main.rs @@ -1,31 +1,41 @@ +use args::*; +use atty::Stream; +use std::process::exit; +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"); } + _ => { + eprintln!("Invalid subcommand"); + exit(EXIT_INVALID_ARGS); + } } }