diff --git a/Cargo.lock b/Cargo.lock index 2498213..eac18e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,110 +4,178 @@ name = "bitflags" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" [[package]] name = "cassowary" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cfg-if" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "either" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" [[package]] name = "itertools" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d47946d458e94a1b7bcabbf6521ea7c037062c81f534615abcad76e84d4970d" dependencies = [ - "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "either", ] [[package]] name = "libc" version = "0.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047" [[package]] name = "log" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +dependencies = [ + "cfg-if 0.1.6", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +dependencies = [ + "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" [[package]] name = "redox_termios" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" dependencies = [ - "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall", +] + +[[package]] +name = "rstest" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + +[[package]] +name = "syn" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] name = "termion" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" dependencies = [ - "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "redox_syscall", + "redox_termios", ] [[package]] name = "tome" version = "0.1.0" dependencies = [ - "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tui 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rstest", + "termion", + "tui", ] [[package]] name = "tui" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89923340858fdc4bf6a4655edb6b27dbc3f69f21eac312379f46047e46432770" dependencies = [ - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "cassowary 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cassowary", + "either", + "itertools", + "log", + "termion", + "unicode-segmentation", + "unicode-width", ] [[package]] name = "unicode-segmentation" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1" [[package]] name = "unicode-width" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" -[metadata] -"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" -"checksum cassowary 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" -"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" -"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" -"checksum itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0d47946d458e94a1b7bcabbf6521ea7c037062c81f534615abcad76e84d4970d" -"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047" -"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" -"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" -"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" -"checksum tui 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89923340858fdc4bf6a4655edb6b27dbc3f69f21eac312379f46047e46432770" -"checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1" -"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" diff --git a/Cargo.toml b/Cargo.toml index d41c3d4..62bb012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ termion = "*" [dev-dependencies] +rstest = "0.12.0" diff --git a/src/commands/init.rs b/src/commands/init.rs index 71ded07..34a87b1 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,7 +1,7 @@ +use super::super::shell_type::{get_shell_type, ShellType}; use std::{ env, iter::{Iterator, Peekable}, - path::Path, slice::Iter, }; @@ -195,32 +195,13 @@ pub fn init(tome_executable: &str, mut args: Peekable>) -> Result match p.to_str() { - Some(inner_p) => inner_p, - None => { - return Err(format!( - init_help_body!(), - format!( - "could not convert shell type to string for '{}'", - shell_type_or_path - ) - )); - } - }, - None => { - return Err(format!( - init_help_body!(), - format!("could not find shell type for '{}'", shell_type_or_path) - )); - } - }; + + let shell_type = get_shell_type(shell_type_or_path)?; // Bootstrapping the sc section requires two parts: // 1. creating the function in question // 2. wiring up tab completion for the function. @@ -230,18 +211,21 @@ pub fn init(tome_executable: &str, mut args: Peekable>) -> Result Ok(format!( + ShellType::FISH => Ok(format!( fish_init_body!(), tome_executable = tome_executable, script_root = script_root, function_name = function_name )), - "bash" | "zsh" => Ok(format!( + ShellType::BASH | ShellType::ZSH => Ok(format!( bash_zsh_init_body!(), tome_executable = tome_executable, script_root = script_root, function_name = function_name )), - _ => Err(format!("Unknown shell {}. Unable to init.", shell_type)), + ShellType::UNKNOWN => Err(format!( + "could not determine shell from {}. Unable to init.", + shell_type_or_path + )), } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ac8e180..b63e509 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,8 +1,6 @@ mod help; mod init; mod script; -#[cfg(test)] -mod tests; pub use self::help::help; pub use self::init::init; pub use self::script::Script; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c90926b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,136 @@ +use std::{fs, io, path::PathBuf}; + +pub mod commands; +pub mod directory; +pub mod script; +pub mod shell_type; + +pub enum CommandType { + Execute, + Completion, +} + +enum TargetType { + File, + Directory, +} + +pub fn execute(raw_args: Vec) -> Result { + let mut arguments = raw_args.iter().peekable(); + // the first argument should be location of the tome binary. + let tome_executable = match arguments.next() { + Some(arg) => arg, + None => return Err(String::from("0th argument should be the tome binary")), + }; + let first_arg = match arguments.next() { + Some(arg) => arg, + None => return Err(String::from("at least one argument expected")), + }; + // if the first command is init, then we should print the + // the contents of init, since a user is trying to instantiate. + if first_arg == "init" { + return commands::init(tome_executable, arguments); + } + + let mut target = PathBuf::from(first_arg); + // next, we determine if we have a file or a directory, + // recursing down arguments until we've exhausted arguments + // that match a directory or file. + let mut target_type = TargetType::Directory; + let mut first_arg = true; + let mut command_type = CommandType::Execute; + // if no argument is passed, return help. + if arguments.peek().is_none() { + match commands::help(target.to_str().unwrap_or_default(), arguments) { + Ok(message) => return Ok(message), + Err(io_error) => return Err(format!("{}", io_error)), + } + } + while let Some(arg) = arguments.peek() { + // match against builtin commands + if first_arg { + match arg.as_ref() { + "--help" => { + arguments.next(); + match commands::help(target.to_str().unwrap_or_default(), arguments) { + Ok(message) => return Ok(message), + Err(io_error) => return Err(format!("{}", io_error)), + } + } + "--complete" => { + arguments.next(); + command_type = CommandType::Completion; + continue; + } + _ => {} + } + } + first_arg = false; + target.push(arg); + if target.is_file() { + target_type = TargetType::File; + arguments.next(); + break; + } else if target.is_dir() { + target_type = TargetType::Directory; + arguments.next(); + } else { + // the current argument does not match + // a directory or a file, so we've landed + // on the strictest match. + target.pop(); + break; + } + } + let remaining_args: Vec<_> = arguments.collect(); + let output: String = match target_type { + TargetType::Directory => match command_type { + CommandType::Completion => { + let mut result = vec![]; + let paths_raw: io::Result<_> = fs::read_dir(target.to_str().unwrap_or("")); + // TODO(zph) deftly fix panics when this code path is triggered with empty string: ie sc dir_example bar + // current implementation avoids the panic but is crude. + let mut paths: Vec<_> = match paths_raw { + Err(_a) => return Err("Invalid argument to completion".to_string()), + Ok(a) => a, + } + .map(|r| r.unwrap()) + .collect(); + paths.sort_by_key(|f| f.path()); + for path_buf in paths { + let path = path_buf.path(); + if path.is_dir() && !directory::is_tome_script_directory(&path) { + continue; + } + if path.is_file() + && !script::is_tome_script( + path_buf.file_name().to_str().unwrap_or_default(), + ) + { + continue; + } + result.push(path.file_name().unwrap().to_str().unwrap_or("").to_owned()); + } + result.join(" ") + } + CommandType::Execute => { + return match remaining_args.len() { + 0 => Err(format!( + "{} is a directory. tab-complete to choose subcommands", + target.to_str().unwrap_or("") + )), + _ => Err(format!( + "command {} not found in directory {}", + remaining_args[0], + target.to_str().unwrap_or("") + )), + }; + } + }, + TargetType::File => match commands::Script::load(&target.to_str().unwrap_or_default()) { + Ok(script) => script.get_execution_body(command_type, &remaining_args)?, + Err(error) => return Err(format!("IOError loading file: {:?}", error)), + }, + }; + Ok(output) +} diff --git a/src/main.rs b/src/main.rs index cdb4d3a..12a08d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,5 @@ -use std::{env::args, fs, io, path::PathBuf}; - -mod commands; -mod directory; -mod script; -#[cfg(test)] -mod tests; +use std::env::args; +use tome::execute; pub fn main() { let args: Vec = args().peekable().collect(); @@ -13,133 +8,3 @@ pub fn main() { Err(error_message) => print!("echo {}", error_message), }; } - -pub enum CommandType { - Execute, - Completion, -} - -enum TargetType { - File, - Directory, -} - -pub fn execute(raw_args: Vec) -> Result { - let mut arguments = raw_args.iter().peekable(); - // the first argument should be location of the tome binary. - let tome_executable = match arguments.next() { - Some(arg) => arg, - None => return Err(String::from("0th argument should be the tome binary")), - }; - let first_arg = match arguments.next() { - Some(arg) => arg, - None => return Err(String::from("at least one argument expected")), - }; - // if the first command is init, then we should print the - // the contents of init, since a user is trying to instantiate. - if first_arg == "init" { - return commands::init(tome_executable, arguments); - } - - let mut target = PathBuf::from(first_arg); - // next, we determine if we have a file or a directory, - // recursing down arguments until we've exhausted arguments - // that match a directory or file. - let mut target_type = TargetType::Directory; - let mut first_arg = true; - let mut command_type = CommandType::Execute; - // if no argument is passed, return help. - if arguments.peek().is_none() { - match commands::help(target.to_str().unwrap_or_default(), arguments) { - Ok(message) => return Ok(message), - Err(io_error) => return Err(format!("{}", io_error)), - } - } - while let Some(arg) = arguments.peek() { - // match against builtin commands - if first_arg { - match arg.as_ref() { - "--help" => { - arguments.next(); - match commands::help(target.to_str().unwrap_or_default(), arguments) { - Ok(message) => return Ok(message), - Err(io_error) => return Err(format!("{}", io_error)), - } - } - "--complete" => { - arguments.next(); - command_type = CommandType::Completion; - continue; - } - _ => {} - } - } - first_arg = false; - target.push(arg); - if target.is_file() { - target_type = TargetType::File; - arguments.next(); - break; - } else if target.is_dir() { - target_type = TargetType::Directory; - arguments.next(); - } else { - // the current argument does not match - // a directory or a file, so we've landed - // on the strictest match. - target.pop(); - break; - } - } - let remaining_args: Vec<_> = arguments.collect(); - let output: String = match target_type { - TargetType::Directory => match command_type { - CommandType::Completion => { - let mut result = vec![]; - let paths_raw: io::Result<_> = fs::read_dir(target.to_str().unwrap_or("")); - // TODO(zph) deftly fix panics when this code path is triggered with empty string: ie sc dir_example bar - // current implementation avoids the panic but is crude. - let mut paths: Vec<_> = match paths_raw { - Err(_a) => return Err("Invalid argument to completion".to_string()), - Ok(a) => a, - } - .map(|r| r.unwrap()) - .collect(); - paths.sort_by_key(|f| f.path()); - for path_buf in paths { - let path = path_buf.path(); - if path.is_dir() && !directory::is_tome_script_directory(&path) { - continue; - } - if path.is_file() - && !script::is_tome_script( - path_buf.file_name().to_str().unwrap_or_default(), - ) - { - continue; - } - result.push(path.file_name().unwrap().to_str().unwrap_or("").to_owned()); - } - result.join(" ") - } - CommandType::Execute => { - return match remaining_args.len() { - 0 => Err(format!( - "{} is a directory. tab-complete to choose subcommands", - target.to_str().unwrap_or("") - )), - _ => Err(format!( - "command {} not found in directory {}", - remaining_args[0], - target.to_str().unwrap_or("") - )), - }; - } - }, - TargetType::File => match commands::Script::load(&target.to_str().unwrap_or_default()) { - Ok(script) => script.get_execution_body(command_type, &remaining_args)?, - Err(error) => return Err(format!("IOError loading file: {:?}", error)), - }, - }; - Ok(output) -} diff --git a/src/shell_type.rs b/src/shell_type.rs new file mode 100644 index 0000000..800e989 --- /dev/null +++ b/src/shell_type.rs @@ -0,0 +1,52 @@ +use std::path::Path; + +#[derive(Debug, PartialEq)] +pub enum ShellType { + BASH, + FISH, + UNKNOWN, + ZSH, +} + +/// Determine the shell type from the shell_executable_path +/// passed. +/// +/// If no string is passed, attempt to infer the information from the +/// "SHELL" environment variable. +/// +/// This function is designed to infer the shell from a +/// variety of architectures and operating systems. +pub fn get_shell_type(shell_executable_path: &str) -> Result { + // strip any leading directories. The root should be the shell. + let shell_type = match Path::new(shell_executable_path).file_stem() { + Some(p) => match p.to_str() { + Some(inner_p) => inner_p, + None => { + return Err(format!( + "could not covert shell type to string for '{}'", + shell_executable_path + )); + } + }, + None => { + return Err(format!( + "could not extract file stem to determine shell type from '{}'", + shell_executable_path + )); + } + }; + + if shell_type.ends_with("fish") { + return Ok(ShellType::FISH); + } + + if shell_type.ends_with("bash") { + return Ok(ShellType::BASH); + } + + if shell_type.ends_with("zsh") { + return Ok(ShellType::ZSH); + } + + return Ok(ShellType::UNKNOWN); +} diff --git a/src/commands/tests.rs b/tests/commands.rs similarity index 98% rename from src/commands/tests.rs rename to tests/commands.rs index eca69e7..c8418f1 100644 --- a/src/commands/tests.rs +++ b/tests/commands.rs @@ -1,5 +1,5 @@ -use super::*; use std::io::{Cursor, Read}; +use tome::commands::*; /// if a script has "SOURCE" /// at the top, it should be sourced in. #[test] diff --git a/src/tests.rs b/tests/lib.rs similarity index 99% rename from src/tests.rs rename to tests/lib.rs index 0c10a6d..903975a 100644 --- a/src/tests.rs +++ b/tests/lib.rs @@ -1,5 +1,5 @@ -use super::execute; use std::env; +use tome::execute; const EXAMPLE_DIR: &'static str = "./example"; diff --git a/tests/shell_type.rs b/tests/shell_type.rs new file mode 100644 index 0000000..2e9d69a --- /dev/null +++ b/tests/shell_type.rs @@ -0,0 +1,22 @@ +use rstest::*; + +use tome::shell_type::{get_shell_type, ShellType}; + +#[rstest] +#[case::bash("bash", Ok(ShellType::BASH))] +#[case::bin_bash("/bin/bash", Ok(ShellType::BASH))] +// on OSX, "-zsh" is now the default. +#[case::zsh("-zsh", Ok(ShellType::ZSH))] +#[case::bin_zsh("/bin/zsh", Ok(ShellType::ZSH))] +#[case::fish("fish", Ok(ShellType::FISH))] +fn test_shell_type(#[case] input: &str, #[case] expected: Result) { + let result = get_shell_type(input); + match expected { + Ok(expected_output) => { + assert_eq!(expected_output, result.unwrap()); + } + Err(_) => { + assert!(result.is_err()) + } + } +}