diff --git a/nur-tests/nurfile b/nur-tests/nurfile index d6c61e8..3a6de5a 100644 --- a/nur-tests/nurfile +++ b/nur-tests/nurfile @@ -9,7 +9,11 @@ def "nur test-env" [] { } def "nur test-config" [] { - std assert ($env.config | is-not-empty) + try { + $env.config + } catch { + error make {"msg": "Config does not exist"} + } } def "nur test-nu" [] { diff --git a/nurfile b/nurfile index 6ddf7a6..b09afb8 100644 --- a/nurfile +++ b/nurfile @@ -41,7 +41,7 @@ def "nur test" [ --coverage ] { if $coverage { - cargo tarpaulin + cargo tarpaulin --exclude-files _* } else { cargo test } diff --git a/src/args.rs b/src/args.rs index ca402ca..79afb33 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,8 +1,8 @@ use crate::commands::Nur; use crate::names::NUR_NAME; use nu_engine::{get_full_help, CallExt}; +use nu_parser::escape_for_script_arg; use nu_parser::parse; -use nu_parser::{escape_for_script_arg, escape_quote_string}; use nu_protocol::report_error; use nu_protocol::{ ast::Expr, @@ -11,32 +11,33 @@ use nu_protocol::{ }; use nu_utils::stdout_write_all_and_flush; -pub(crate) fn gather_commandline_args() -> (Vec, String, Vec) { - let mut args_to_nur = Vec::from([NUR_NAME.into()]); +pub(crate) fn gather_commandline_args(args: Vec) -> (Vec, String, Vec) { + let mut args_to_nur = Vec::from([String::from(NUR_NAME)]); let mut task_name = String::new(); - let mut args = std::env::args(); + let mut args_iter = args.iter(); - args.next(); // Ignore own name - while let Some(arg) = args.next() { + args_iter.next(); // Ignore own name + #[allow(clippy::while_let_on_iterator)] + while let Some(arg) = args_iter.next() { if !arg.starts_with('-') { - task_name = arg; + task_name = arg.clone(); break; } - let flag_value = match arg.as_ref() { - "--config" => args.next().map(|a| escape_quote_string(&a)), - _ => None, - }; + // let flag_value = match arg.as_ref() { + // // "--some-file" => args.next().map(|a| escape_quote_string(&a)), + // _ => None, + // }; - args_to_nur.push(arg); + args_to_nur.push(arg.clone()); - if let Some(flag_value) = flag_value { - args_to_nur.push(flag_value); - } + // if let Some(flag_value) = flag_value { + // args_to_nur.push(flag_value); + // } } let args_to_task = if !task_name.is_empty() { - args.map(|arg| escape_for_script_arg(&arg)).collect() + args_iter.map(|arg| escape_for_script_arg(arg)).collect() } else { Vec::default() }; @@ -46,10 +47,9 @@ pub(crate) fn gather_commandline_args() -> (Vec, String, Vec) { pub(crate) fn parse_commandline_args( commandline_args: &str, engine_state: &mut EngineState, -) -> Result { +) -> Result { let (block, delta) = { let mut working_set = StateWorkingSet::new(engine_state); - working_set.add_decl(Box::new(Nur)); let output = parse(&mut working_set, None, commandline_args.as_bytes(), false); if let Some(err) = working_set.parse_errors.first() { @@ -58,7 +58,6 @@ pub(crate) fn parse_commandline_args( std::process::exit(1); } - working_set.hide_decl(NUR_NAME.as_bytes()); (output, working_set.render()) }; @@ -77,29 +76,6 @@ pub(crate) fn parse_commandline_args( #[cfg(feature = "debug")] let debug_output = call.has_flag(engine_state, &mut stack, "debug")?; - // fn extract_contents( - // expression: Option<&Expression>, - // ) -> Result>, ShellError> { - // if let Some(expr) = expression { - // let str = expr.as_string(); - // if let Some(str) = str { - // Ok(Some(Spanned { - // item: str, - // span: expr.span, - // })) - // } else { - // Err(ShellError::TypeMismatch { - // err_message: "string".into(), - // span: expr.span, - // }) - // } - // } else { - // Ok(None) - // } - // } - // - // let config_file = extract_contents(config_file)?; - if call.has_flag(engine_state, &mut stack, "version")? { let version = env!("CARGO_PKG_VERSION").to_string(); let _ = std::panic::catch_unwind(move || { @@ -109,8 +85,7 @@ pub(crate) fn parse_commandline_args( std::process::exit(0); } - return Ok(NurCliArgs { - // config_file, + return Ok(NurArgs { list_tasks, quiet_execution, attach_stdin, @@ -134,8 +109,7 @@ pub(crate) fn parse_commandline_args( } #[derive(Debug, Clone)] -pub(crate) struct NurCliArgs { - // pub(crate) config_file: Option>, +pub(crate) struct NurArgs { pub(crate) list_tasks: bool, pub(crate) quiet_execution: bool, pub(crate) attach_stdin: bool, @@ -143,3 +117,139 @@ pub(crate) struct NurCliArgs { #[cfg(feature = "debug")] pub(crate) debug_output: bool, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::init_engine_state; + use tempfile::tempdir; + + #[test] + fn test_gather_commandline_args_splits_on_task_name() { + let args = vec![ + String::from("nur"), + String::from("--quiet"), + String::from("some_task_name"), + String::from("--task-option"), + String::from("task-value"), + ]; + let (nur_args, task_name, task_args) = gather_commandline_args(args); + assert_eq!(nur_args, vec![String::from("nur"), String::from("--quiet")]); + assert_eq!(task_name, "some_task_name"); + assert_eq!( + task_args, + vec![String::from("--task-option"), String::from("task-value")] + ); + } + + #[test] + fn test_gather_commandline_args_handles_missing_nur_args() { + let args = vec![ + String::from("nur"), + String::from("some_task_name"), + String::from("--task-option"), + String::from("task-value"), + ]; + let (nur_args, task_name, task_args) = gather_commandline_args(args); + assert_eq!(nur_args, vec![String::from("nur")]); + assert_eq!(task_name, "some_task_name"); + assert_eq!( + task_args, + vec![String::from("--task-option"), String::from("task-value")] + ); + } + + #[test] + fn test_gather_commandline_args_handles_missing_task_name() { + let args = vec![String::from("nur"), String::from("--help")]; + let (nur_args, task_name, task_args) = gather_commandline_args(args); + assert_eq!(nur_args, vec![String::from("nur"), String::from("--help")]); + assert_eq!(task_name, ""); + assert_eq!(task_args, vec![] as Vec); + } + + #[test] + fn test_gather_commandline_args_handles_missing_task_args() { + let args = vec![ + String::from("nur"), + String::from("--quiet"), + String::from("some_task_name"), + ]; + let (nur_args, task_name, task_args) = gather_commandline_args(args); + assert_eq!(nur_args, vec![String::from("nur"), String::from("--quiet")]); + assert_eq!(task_name, "some_task_name"); + assert_eq!(task_args, vec![] as Vec); + } + + #[test] + fn test_gather_commandline_args_handles_no_args_at_all() { + let args = vec![String::from("nur")]; + let (nur_args, task_name, task_args) = gather_commandline_args(args); + assert_eq!(nur_args, vec![String::from("nur")]); + assert_eq!(task_name, ""); + assert_eq!(task_args, vec![] as Vec); + } + + fn _create_minimal_engine_for_erg_parsing() -> EngineState { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_path_buf(); + let engine_state = init_engine_state(&temp_dir_path).unwrap(); + + engine_state + } + + #[test] + fn test_parse_commandline_args_without_args() { + let mut engine_state = _create_minimal_engine_for_erg_parsing(); + + let nur_args = parse_commandline_args("nur", &mut engine_state).unwrap(); + assert_eq!(nur_args.list_tasks, false); + assert_eq!(nur_args.quiet_execution, false); + assert_eq!(nur_args.attach_stdin, false); + assert_eq!(nur_args.show_help, false); + } + + #[test] + fn test_parse_commandline_args_list() { + let mut engine_state = _create_minimal_engine_for_erg_parsing(); + + let nur_args = parse_commandline_args("nur --list", &mut engine_state).unwrap(); + assert_eq!(nur_args.list_tasks, true); + assert_eq!(nur_args.quiet_execution, false); + assert_eq!(nur_args.attach_stdin, false); + assert_eq!(nur_args.show_help, false); + } + + #[test] + fn test_parse_commandline_args_quiet() { + let mut engine_state = _create_minimal_engine_for_erg_parsing(); + + let nur_args = parse_commandline_args("nur --quiet", &mut engine_state).unwrap(); + assert_eq!(nur_args.list_tasks, false); + assert_eq!(nur_args.quiet_execution, true); + assert_eq!(nur_args.attach_stdin, false); + assert_eq!(nur_args.show_help, false); + } + + #[test] + fn test_parse_commandline_args_stdin() { + let mut engine_state = _create_minimal_engine_for_erg_parsing(); + + let nur_args = parse_commandline_args("nur --stdin", &mut engine_state).unwrap(); + assert_eq!(nur_args.list_tasks, false); + assert_eq!(nur_args.quiet_execution, false); + assert_eq!(nur_args.attach_stdin, true); + assert_eq!(nur_args.show_help, false); + } + + #[test] + fn test_parse_commandline_args_help() { + let mut engine_state = _create_minimal_engine_for_erg_parsing(); + + let nur_args = parse_commandline_args("nur --help", &mut engine_state).unwrap(); + assert_eq!(nur_args.list_tasks, false); + assert_eq!(nur_args.quiet_execution, false); + assert_eq!(nur_args.attach_stdin, false); + assert_eq!(nur_args.show_help, true); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f1c235c..89d768c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,35 @@ mod nur; +use nu_protocol::engine::{EngineState, StateWorkingSet}; pub(crate) use nur::Nur; + +pub(crate) fn create_nu_context(mut engine_state: EngineState) -> EngineState { + // Custom additions only used in cli, normally registered in nu main() as "custom additions" + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + working_set.add_decl(Box::new(nu_cli::NuHighlight)); + working_set.add_decl(Box::new(nu_cli::Print)); + working_set.render() + }; + + if let Err(err) = engine_state.merge_delta(delta) { + eprintln!("Error creating nu command context: {err:?}"); + } + + engine_state +} + +pub(crate) fn create_nur_context(mut engine_state: EngineState) -> EngineState { + // Add nur own commands + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + working_set.add_decl(Box::new(nur::Nur)); + working_set.render() + }; + + if let Err(err) = engine_state.merge_delta(delta) { + eprintln!("Error creating nur command context: {err:?}"); + } + + engine_state +} diff --git a/src/commands/nur.rs b/src/commands/nur.rs index 9290012..48000b6 100644 --- a/src/commands/nur.rs +++ b/src/commands/nur.rs @@ -18,12 +18,6 @@ impl Command for Nur { signature = signature .usage("nur - a taskrunner based on nu shell.") - // .named( - // "config", - // SyntaxShape::String, - // "path to config", - // None, - // ) .switch("version", "output version number and exit", None) .switch("list", "list available tasks and then just exit", None) .switch( @@ -53,7 +47,7 @@ impl Command for Nur { } fn usage(&self) -> &str { - "nu run - simple task runner." + "nur - a taskrunner based on nu shell." } fn run( diff --git a/src/compat.rs b/src/compat.rs index d4b18e9..44a9038 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -1,9 +1,9 @@ use nu_ansi_term::Color; use std::path::Path; -pub(crate) fn show_nurscripts_hint(project_path: &Path, use_color: bool) { +pub(crate) fn show_nurscripts_hint>(project_path: P, use_color: bool) { // Give some hints if old ".nurscripts" exists - let old_nur_lib_path = project_path.join(".nurscripts"); + let old_nur_lib_path = project_path.as_ref().join(".nurscripts"); if old_nur_lib_path.exists() && old_nur_lib_path.is_dir() { eprintln!( "{}WARNING: .nurscripts/ has moved to .nur/scripts/ -> please update your project{}", diff --git a/src/engine.rs b/src/engine.rs index b4e2dd4..5d1411b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,17 +1,28 @@ +use crate::args::{parse_commandline_args, NurArgs}; use crate::errors::{NurError, NurResult}; +use crate::names::{ + NUR_ENV_NU_LIB_DIRS, NUR_NAME, NUR_VAR_CONFIG_DIR, NUR_VAR_DEFAULT_LIB_DIR, + NUR_VAR_PROJECT_PATH, NUR_VAR_RUN_PATH, NUR_VAR_TASK_NAME, +}; use crate::nu_version::NU_VERSION; +use crate::scripts::{get_default_nur_config, get_default_nur_env}; +use crate::state::NurState; use nu_cli::gather_parent_env_vars; use nu_engine::get_full_help; use nu_protocol::ast::Block; use nu_protocol::engine::{Command, Stack, StateWorkingSet}; -use nu_protocol::{engine::EngineState, report_error, report_error_new, PipelineData, Span, Value}; +use nu_protocol::eval_const::create_nu_constant; +use nu_protocol::{ + engine::EngineState, report_error, report_error_new, PipelineData, Record, Span, Type, Value, + NU_VARIABLE_ID, +}; use nu_std::load_standard_library; use nu_utils::stdout_write_all_and_flush; use std::fs; use std::path::Path; use std::sync::Arc; -pub(crate) fn init_engine_state(project_path: &Path) -> NurResult { +pub(crate) fn init_engine_state>(project_path: P) -> NurResult { let engine_state = nu_cmd_lang::create_default_context(); let engine_state = nu_command::add_shell_command_context(engine_state); let engine_state = nu_cmd_extra::add_extra_command_context(engine_state); @@ -19,27 +30,14 @@ pub(crate) fn init_engine_state(project_path: &Path) -> NurResult { let engine_state = nu_cmd_dataframe::add_dataframe_context(engine_state); let engine_state = nu_cli::add_cli_context(engine_state); let engine_state = nu_explore::add_explore_context(engine_state); + let engine_state = crate::commands::create_nu_context(engine_state); + let engine_state = crate::commands::create_nur_context(engine_state); // Prepare engine state to be changed let mut engine_state = engine_state; - // Custom additions only used in cli - let delta = { - let mut working_set = StateWorkingSet::new(&engine_state); - working_set.add_decl(Box::new(nu_cli::NuHighlight)); - working_set.add_decl(Box::new(nu_cli::Print)); - working_set.render() - }; - - if let Err(err) = engine_state.merge_delta(delta) { - report_error_new(&engine_state, &err); - return Err(NurError::InitError(String::from( - "Could not load CLI functions", - ))); - } - // First, set up env vars as strings only - gather_parent_env_vars(&mut engine_state, project_path); + gather_parent_env_vars(&mut engine_state, project_path.as_ref()); engine_state.add_env_var( "NU_VERSION".to_string(), Value::string(NU_VERSION, Span::unknown()), @@ -62,16 +60,129 @@ pub(crate) fn init_engine_state(project_path: &Path) -> NurResult { #[derive(Clone)] pub(crate) struct NurEngine { - engine_state: EngineState, - stack: Stack, + pub(crate) engine_state: EngineState, + pub(crate) stack: Stack, + + pub(crate) state: NurState, } impl NurEngine { - pub(crate) fn new(engine_state: EngineState, stack: Stack) -> NurEngine { - NurEngine { + pub(crate) fn new(engine_state: EngineState, nur_state: NurState) -> NurResult { + let mut nur_engine = NurEngine { engine_state, - stack, + stack: Stack::new(), + + state: nur_state, + }; + + nur_engine._apply_nur_state()?; + + Ok(nur_engine) + } + + fn _apply_nur_state(&mut self) -> NurResult<()> { + // Set default scripts path + self.engine_state.add_env_var( + NUR_ENV_NU_LIB_DIRS.to_string(), + Value::test_string(self.state.lib_dir_path.to_string_lossy()), + ); + + // Set config and env paths to .nur versions + self.engine_state + .set_config_path("env-path", self.state.env_path.clone()); + self.engine_state + .set_config_path("config-path", self.state.config_path.clone()); + + // Set up the $nu constant before evaluating any files (need to have $nu available in them) + let nu_const = create_nu_constant( + &self.engine_state, + PipelineData::empty().span().unwrap_or_else(Span::unknown), + )?; + self.engine_state + .set_variable_const_val(NU_VARIABLE_ID, nu_const); + + // Set up the $nur constant record (like $nu) + let mut nur_record = Record::new(); + nur_record.push( + NUR_VAR_RUN_PATH, + Value::string( + String::from(self.state.run_path.to_str().unwrap()), + Span::unknown(), + ), + ); + nur_record.push( + NUR_VAR_PROJECT_PATH, + Value::string( + String::from(self.state.project_path.to_str().unwrap()), + Span::unknown(), + ), + ); + nur_record.push( + NUR_VAR_TASK_NAME, + Value::string(&self.state.task_name, Span::unknown()), + ); + nur_record.push( + NUR_VAR_CONFIG_DIR, + Value::string( + String::from(self.state.config_dir.to_str().unwrap()), + Span::unknown(), + ), + ); + nur_record.push( + NUR_VAR_DEFAULT_LIB_DIR, + Value::string( + String::from(self.state.lib_dir_path.to_str().unwrap()), + Span::unknown(), + ), + ); + let mut working_set = StateWorkingSet::new(&self.engine_state); + let nur_var_id = working_set.add_variable( + NUR_NAME.as_bytes().into(), + Span::unknown(), + Type::Any, + false, + ); + self.stack + .add_var(nur_var_id, Value::record(nur_record, Span::unknown())); + self.engine_state.merge_delta(working_set.render())?; + + Ok(()) + } + + pub(crate) fn parse_args(&mut self) -> NurArgs { + parse_commandline_args(&self.state.args_to_nur.join(" "), &mut self.engine_state) + .unwrap_or_else(|_| std::process::exit(1)) + } + + pub(crate) fn load_env(&mut self) -> NurResult<()> { + if self.state.env_path.exists() { + self.source_and_merge_env(self.state.env_path.clone(), PipelineData::empty())?; + } else { + self.eval_and_merge_env(get_default_nur_env(), PipelineData::empty())?; } + + Ok(()) + } + + pub(crate) fn load_config(&mut self) -> NurResult<()> { + if self.state.config_path.exists() { + self.source_and_merge_env(self.state.config_path.clone(), PipelineData::empty())?; + } else { + self.eval_and_merge_env(get_default_nur_config(), PipelineData::empty())?; + } + + Ok(()) + } + + pub(crate) fn load_nurfiles(&mut self) -> NurResult<()> { + if self.state.nurfile_path.exists() { + self.source(self.state.nurfile_path.clone(), PipelineData::empty())?; + } + if self.state.local_nurfile_path.exists() { + self.source(self.state.local_nurfile_path.clone(), PipelineData::empty())?; + } + + Ok(()) } fn _parse_nu_script( @@ -170,9 +281,11 @@ impl NurEngine { } } - // pub fn eval(&mut self, contents: S, input: PipelineData) -> NurResult { - // self._eval(None, contents, input, false, false) - // } + // This is used in tests only currently + #[allow(dead_code)] + pub fn eval(&mut self, contents: S, input: PipelineData) -> NurResult { + self._eval(None, contents, input, false, false) + } pub(crate) fn eval_and_print( &mut self, @@ -237,11 +350,201 @@ impl NurEngine { } } -impl From for NurEngine { - fn from(engine_state: EngineState) -> NurEngine { - NurEngine { - engine_state, - stack: Stack::new(), +#[cfg(test)] +mod tests { + use super::*; + use crate::names::{ + NUR_CONFIG_CONFIG_FILENAME, NUR_CONFIG_DIR, NUR_CONFIG_ENV_FILENAME, NUR_CONFIG_LIB_PATH, + NUR_FILE, NUR_LOCAL_FILE, + }; + use std::fs::File; + use std::io::Write; + use tempfile::{tempdir, TempDir}; + + fn _has_decl>(engine_state: &mut EngineState, name: S) -> bool { + engine_state + .find_decl(name.as_ref().as_bytes(), &[]) + .is_some() + } + + #[test] + fn test_init_engine_state_will_add_commands() { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_path_buf(); + let mut engine_state = init_engine_state(&temp_dir_path).unwrap(); + + assert!(_has_decl(&mut engine_state, "alias")); + assert!(_has_decl(&mut engine_state, "do")); + assert!(_has_decl(&mut engine_state, "uniq")); + assert!(_has_decl(&mut engine_state, "help")); + assert!(_has_decl(&mut engine_state, "str")); + assert!(_has_decl(&mut engine_state, "format pattern")); + assert!(_has_decl(&mut engine_state, "history")); + assert!(_has_decl(&mut engine_state, "explore")); + assert!(_has_decl(&mut engine_state, "print")); + assert!(_has_decl(&mut engine_state, "nu-highlight")); + assert!(_has_decl(&mut engine_state, "nur")); + } + + #[test] + fn test_init_engine_state_will_set_nu_version() { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_path_buf(); + let engine_state = init_engine_state(&temp_dir_path).unwrap(); + + assert!(engine_state.get_env_var("NU_VERSION").is_some()); + } + + #[test] + fn test_init_engine_state_will_add_std_lib() { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_path_buf(); + let engine_state = init_engine_state(&temp_dir_path).unwrap(); + + assert!(engine_state + .find_module("std".as_bytes(), &[vec![]],) + .is_some()); + } + + #[test] + fn test_init_engine_state_will_set_flags() { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_path_buf(); + let engine_state = init_engine_state(&temp_dir_path).unwrap(); + + assert_eq!(engine_state.is_interactive, false); + assert_eq!(engine_state.is_login, false); + assert_eq!(engine_state.history_enabled, false); + } + + fn _prepare_nur_engine(temp_dir: &TempDir) -> NurEngine { + let temp_dir_path = temp_dir.path().to_path_buf(); + let nurfile_path = temp_dir.path().join(NUR_FILE); + File::create(&nurfile_path).unwrap(); + + let args = vec![String::from("nur"), String::from("some_task")]; + let nur_state = NurState::new(temp_dir_path.clone(), args); + let engine_state = init_engine_state(temp_dir_path).unwrap(); + + NurEngine::new(engine_state, nur_state).unwrap() + } + + fn _cleanup_nur_engine(temp_dir: &TempDir) { + let nurfile_path = temp_dir.path().join(NUR_FILE); + let nurfile_local_path = temp_dir.path().join(NUR_FILE); + let config_dir = temp_dir.path().join(NUR_CONFIG_DIR); + + fs::remove_file(nurfile_path).unwrap(); + if nurfile_local_path.exists() { + fs::remove_file(nurfile_local_path).unwrap(); } + if config_dir.exists() { + fs::remove_dir_all(config_dir).unwrap(); + } + } + + fn _has_var>(nur_engine: &mut NurEngine, name: S) -> bool { + let name = name.as_ref(); + let dollar_name = format!("${name}"); + let var_id = nur_engine + .engine_state + .active_overlays(&vec![]) + .find_map(|o| { + o.vars + .get(dollar_name.as_bytes()) + .or(o.vars.get(name.as_bytes())) + }) + .unwrap(); + + nur_engine.stack.get_var(*var_id, Span::unknown()).is_ok() + } + + #[test] + fn test_nur_engine_will_set_nur_variable() { + let temp_dir = tempdir().unwrap(); + let mut nur_engine = _prepare_nur_engine(&temp_dir); + + assert!(_has_var(&mut nur_engine, "nur")); + + _cleanup_nur_engine(&temp_dir); + } + + #[test] + fn test_nur_engine_will_load_nurfiles() { + let temp_dir = tempdir().unwrap(); + let mut nur_engine = _prepare_nur_engine(&temp_dir); + + let nurfile_path = temp_dir.path().join(NUR_FILE); + let mut nurfile = File::create(&nurfile_path).unwrap(); + nurfile.write_all(b"def nurfile-command [] {}").unwrap(); + let nurfile_local_path = temp_dir.path().join(NUR_LOCAL_FILE); + let mut nurfile_local = File::create(&nurfile_local_path).unwrap(); + nurfile_local + .write_all(b"def nurfile-local-command [] {}") + .unwrap(); + + nur_engine.load_env().unwrap(); + nur_engine.load_config().unwrap(); + nur_engine.load_nurfiles().unwrap(); + + assert!(_has_decl(&mut nur_engine.engine_state, "nurfile-command")); + assert!(_has_decl( + &mut nur_engine.engine_state, + "nurfile-local-command" + )); + + _cleanup_nur_engine(&temp_dir); + } + + #[test] + fn test_nur_engine_will_load_env_and_config() { + let temp_dir = tempdir().unwrap(); + let mut nur_engine = _prepare_nur_engine(&temp_dir); + + let config_dir = temp_dir.path().join(NUR_CONFIG_DIR); + fs::create_dir(config_dir.clone()).unwrap(); + let env_path = config_dir.join(NUR_CONFIG_ENV_FILENAME); + let mut env_file = File::create(&env_path).unwrap(); + env_file.write_all(b"def env-command [] {}").unwrap(); + let config_path = config_dir.join(NUR_CONFIG_CONFIG_FILENAME); + let mut config_file = File::create(&config_path).unwrap(); + config_file.write_all(b"def config-command [] {}").unwrap(); + + nur_engine.load_env().unwrap(); + nur_engine.load_config().unwrap(); + nur_engine.load_nurfiles().unwrap(); + + assert!(_has_decl(&mut nur_engine.engine_state, "env-command")); + assert!(_has_decl(&mut nur_engine.engine_state, "config-command")); + + _cleanup_nur_engine(&temp_dir); + } + + #[test] + fn test_nur_engine_will_allow_scripts() { + let temp_dir = tempdir().unwrap(); + let mut nur_engine = _prepare_nur_engine(&temp_dir); + + let config_dir = temp_dir.path().join(NUR_CONFIG_DIR); + fs::create_dir(config_dir.clone()).unwrap(); + let scripts_dir = config_dir.join(NUR_CONFIG_LIB_PATH); + fs::create_dir(scripts_dir.clone()).unwrap(); + let module_path = scripts_dir.join("test-module.nu"); + let mut module_file = File::create(&module_path).unwrap(); + module_file + .write_all(b"export def module-command [] {}") + .unwrap(); + + nur_engine.load_env().unwrap(); + nur_engine.load_config().unwrap(); + nur_engine.load_nurfiles().unwrap(); + + nur_engine + .eval("use test-module.nu *", PipelineData::empty()) + .unwrap(); + + assert!(_has_decl(&mut nur_engine.engine_state, "module-command")); + + _cleanup_nur_engine(&temp_dir); } } diff --git a/src/main.rs b/src/main.rs index 78213b5..be10453 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,73 +1,79 @@ mod args; mod commands; mod compat; -mod defaults; mod engine; mod errors; mod names; mod nu_version; mod path; +mod scripts; +mod state; -use crate::args::{gather_commandline_args, parse_commandline_args}; use crate::commands::Nur; use crate::compat::show_nurscripts_hint; -use crate::defaults::{get_default_nur_config, get_default_nur_env}; use crate::engine::init_engine_state; +use crate::engine::NurEngine; use crate::errors::NurError; -use crate::names::{ - NUR_CONFIG_CONFIG_FILENAME, NUR_CONFIG_ENV_FILENAME, NUR_CONFIG_LIB_PATH, NUR_CONFIG_PATH, - NUR_ENV_NU_LIB_DIRS, NUR_FILE, NUR_LOCAL_FILE, NUR_NAME, NUR_VAR_CONFIG_DIR, - NUR_VAR_DEFAULT_LIB_DIR, NUR_VAR_PROJECT_PATH, NUR_VAR_RUN_PATH, NUR_VAR_TASK_NAME, -}; -use crate::path::find_project_path; -use engine::NurEngine; +use crate::names::NUR_NAME; +use crate::state::NurState; use miette::Result; use nu_ansi_term::Color; use nu_cmd_base::util::get_init_cwd; -use nu_protocol::engine::{Stack, StateWorkingSet}; -use nu_protocol::{ - eval_const::create_nu_constant, BufferedReader, PipelineData, RawStream, Record, Span, Type, - Value, NU_VARIABLE_ID, -}; +use nu_protocol::{BufferedReader, PipelineData, RawStream, Span}; use std::env; use std::io::BufReader; use std::process::ExitCode; fn main() -> Result { - // Get initial directory details + // Initialise nur state let run_path = get_init_cwd(); - let found_project_path = find_project_path(&run_path); - let has_project_path = found_project_path.is_some(); - let project_path = found_project_path.unwrap_or(&run_path); + let nur_state = NurState::new(run_path, env::args().collect()); - // Initialize nu engine state and stack - let mut engine_state = init_engine_state(project_path)?; - let mut stack = Stack::new(); - let use_color = engine_state.get_config().use_ansi_coloring; + // Create raw nu engine state + let engine_state = init_engine_state(&nur_state.project_path)?; + + // Setup nur engine from engine state + let mut nur_engine = NurEngine::new(engine_state, nur_state)?; + let use_color = nur_engine.engine_state.get_config().use_ansi_coloring; // Parse args - let (args_to_nur, task_name, args_to_task) = gather_commandline_args(); - let parsed_nur_args = parse_commandline_args(&args_to_nur.join(" "), &mut engine_state) - .unwrap_or_else(|_| std::process::exit(1)); + let parsed_nur_args = nur_engine.parse_args(); #[cfg(feature = "debug")] if parsed_nur_args.debug_output { + eprintln!("run path: {:?}", nur_engine.state.run_path); + eprintln!("project path: {:?}", nur_engine.state.project_path); + eprintln!(); eprintln!("nur args: {:?}", parsed_nur_args); - eprintln!("task name: {:?}", task_name); - eprintln!("task args: {:?}", args_to_task); - eprintln!("run path: {:?}", run_path); - eprintln!("project path: {:?}", project_path); + eprintln!("task name: {:?}", nur_engine.state.task_name); + eprintln!("task args: {:?}", nur_engine.state.args_to_task); + eprintln!(); + eprintln!("nur config dir: {:?}", nur_engine.state.config_dir); + eprintln!( + "nur lib path (scripts/): {:?}", + nur_engine.state.lib_dir_path + ); + eprintln!("nur env path (env.nu): {:?}", nur_engine.state.env_path); + eprintln!( + "nur config path (config.nu): {:?}", + nur_engine.state.config_path + ); + eprintln!(); + eprintln!("nurfile path: {:?}", nur_engine.state.nurfile_path); + eprintln!( + "nurfile local path: {:?}", + nur_engine.state.local_nurfile_path + ); } // Show hints for compatibility issues - if has_project_path { - show_nurscripts_hint(project_path, use_color); + if nur_engine.state.has_project_path { + show_nurscripts_hint(nur_engine.state.project_path.clone(), use_color); } // Handle execution without project path, only allow to show help, abort otherwise - if !has_project_path { + if !nur_engine.state.has_project_path { if parsed_nur_args.show_help { - let mut nur_engine = NurEngine::new(engine_state, stack); nur_engine.print_help(&Nur); std::process::exit(0); @@ -76,110 +82,12 @@ fn main() -> Result { } } - // Base path for nur config/env - let nur_config_dir = project_path.join(NUR_CONFIG_PATH); - #[cfg(feature = "debug")] - if parsed_nur_args.debug_output { - eprintln!("nur config path: {:?}", nur_config_dir); - } - - // Set default scripts path - let mut nur_lib_dir_path = nur_config_dir.clone(); - nur_lib_dir_path.push(NUR_CONFIG_LIB_PATH); - engine_state.add_env_var( - NUR_ENV_NU_LIB_DIRS.to_string(), - Value::test_string(nur_lib_dir_path.to_string_lossy()), - ); - #[cfg(feature = "debug")] - if parsed_nur_args.debug_output { - eprintln!("nur scripts path: {:?}", nur_lib_dir_path); - } - - // Set config and env paths to .nur versions - let mut nur_env_path = nur_config_dir.clone(); - nur_env_path.push(NUR_CONFIG_ENV_FILENAME); - engine_state.set_config_path("env-path", nur_env_path.clone()); - let mut nur_config_path = nur_config_dir.clone(); - nur_config_path.push(NUR_CONFIG_CONFIG_FILENAME); - engine_state.set_config_path("config-path", nur_config_path.clone()); - - // Set up the $nu constant before evaluating any files (need to have $nu available in them) - let nu_const = create_nu_constant( - &engine_state, - PipelineData::empty().span().unwrap_or_else(Span::unknown), - )?; - engine_state.set_variable_const_val(NU_VARIABLE_ID, nu_const); - - // Set up the $nur constant record (like $nu) - let mut nur_record = Record::new(); - nur_record.push( - NUR_VAR_RUN_PATH, - Value::string(String::from(run_path.to_str().unwrap()), Span::unknown()), - ); - nur_record.push( - NUR_VAR_PROJECT_PATH, - Value::string( - String::from(project_path.to_str().unwrap()), - Span::unknown(), - ), - ); - nur_record.push( - NUR_VAR_TASK_NAME, - Value::string(&task_name, Span::unknown()), - ); - nur_record.push( - NUR_VAR_CONFIG_DIR, - Value::string( - String::from(nur_config_dir.to_str().unwrap()), - Span::unknown(), - ), - ); - nur_record.push( - NUR_VAR_DEFAULT_LIB_DIR, - Value::string( - String::from(nur_lib_dir_path.to_str().unwrap()), - Span::unknown(), - ), - ); - let mut working_set = StateWorkingSet::new(&engine_state); - let nur_var_id = working_set.add_variable( - NUR_NAME.as_bytes().into(), - Span::unknown(), - Type::Any, - false, - ); - stack.add_var(nur_var_id, Value::record(nur_record, Span::unknown())); - engine_state.merge_delta(working_set.render())?; - - // Switch to using nur engine using the already setup engine state and stack - let mut nur_engine = NurEngine::new(engine_state, stack); - // Load env and config - if nur_env_path.exists() { - nur_engine.source_and_merge_env(&nur_env_path, PipelineData::empty())?; - } else { - nur_engine.eval_and_merge_env(get_default_nur_env(), PipelineData::empty())?; - } - if nur_config_path.exists() { - nur_engine.source_and_merge_env(&nur_config_path, PipelineData::empty())?; - } else { - nur_engine.eval_and_merge_env(get_default_nur_config(), PipelineData::empty())?; - } + nur_engine.load_env()?; + nur_engine.load_config()?; // Load task files - let nurfile_path = project_path.join(NUR_FILE); - let local_nurfile_path = project_path.join(NUR_LOCAL_FILE); - #[cfg(feature = "debug")] - if parsed_nur_args.debug_output { - eprintln!("nurfile path: {:?}", nurfile_path); - eprintln!("nurfile local path: {:?}", local_nurfile_path); - } - if nurfile_path.exists() { - nur_engine.source(nurfile_path, PipelineData::empty())?; - } - if local_nurfile_path.exists() { - nur_engine.source(local_nurfile_path, PipelineData::empty())?; - } + nur_engine.load_nurfiles()?; // Handle list tasks if parsed_nur_args.list_tasks { @@ -193,20 +101,22 @@ fn main() -> Result { } // Initialize internal data - let task_def_name = format!("{} {}", NUR_NAME, task_name); + let task_def_name = format!("{} {}", NUR_NAME, nur_engine.state.task_name); #[cfg(feature = "debug")] if parsed_nur_args.debug_output { eprintln!("task def name: {}", task_def_name); } // Handle help - if parsed_nur_args.show_help || task_name.is_empty() { - if task_name.is_empty() { + if parsed_nur_args.show_help || nur_engine.state.task_name.is_empty() { + if nur_engine.state.task_name.is_empty() { nur_engine.print_help(&Nur); } else if let Some(command) = nur_engine.get_def(task_def_name) { nur_engine.clone().print_help(command); } else { - return Err(miette::ErrReport::from(NurError::TaskNotFound(task_name))); + return Err(miette::ErrReport::from(NurError::TaskNotFound( + nur_engine.state.task_name, + ))); } std::process::exit(0); @@ -214,7 +124,9 @@ fn main() -> Result { // Check if requested task exists if !nur_engine.has_def(&task_def_name) { - return Err(miette::ErrReport::from(NurError::TaskNotFound(task_name))); + return Err(miette::ErrReport::from(NurError::TaskNotFound( + nur_engine.state.task_name, + ))); } // Prepare input data - if requested @@ -241,7 +153,11 @@ fn main() -> Result { // Execute the task let exit_code: i64; - let full_task_call = format!("{} {}", task_def_name, args_to_task.join(" ")); + let full_task_call = format!( + "{} {}", + task_def_name, + nur_engine.state.args_to_task.join(" ") + ); #[cfg(feature = "debug")] if parsed_nur_args.debug_output { eprintln!("full task call: {}", full_task_call); @@ -255,8 +171,8 @@ fn main() -> Result { } } else { println!("nur version {}", env!("CARGO_PKG_VERSION")); - println!("Project path {:?}", project_path); - println!("Executing task {}", task_name); + println!("Project path {:?}", nur_engine.state.project_path); + println!("Executing task {}", nur_engine.state.task_name); println!(); exit_code = nur_engine.eval_and_print(full_task_call, input)?; #[cfg(feature = "debug")] diff --git a/src/names.rs b/src/names.rs index 6f6a73a..f454ca4 100644 --- a/src/names.rs +++ b/src/names.rs @@ -1,7 +1,7 @@ pub(crate) const NUR_NAME: &str = "nur"; // Config paths/files -pub(crate) const NUR_CONFIG_PATH: &str = ".nur"; +pub(crate) const NUR_CONFIG_DIR: &str = ".nur"; pub(crate) const NUR_CONFIG_LIB_PATH: &str = "scripts"; pub(crate) const NUR_CONFIG_CONFIG_FILENAME: &str = "config.nu"; pub(crate) const NUR_CONFIG_ENV_FILENAME: &str = "env.nu"; diff --git a/src/nu-scripts/default_nur_config.nu b/src/nu-scripts/default_nur_config.nu new file mode 100644 index 0000000..7a2052d --- /dev/null +++ b/src/nu-scripts/default_nur_config.nu @@ -0,0 +1,4 @@ +# nur Config File + +# We don't set anything special here +$env.config = {} diff --git a/src/nu-scripts/default_nur_env.nu b/src/nu-scripts/default_nur_env.nu index 680ee8e..49bcd32 100644 --- a/src/nu-scripts/default_nur_env.nu +++ b/src/nu-scripts/default_nur_env.nu @@ -1,4 +1,4 @@ -# nur Environment Config File +# nur Environment File # Specifies how environment variables are: # - converted from a string to a value on Nushell startup (from_string) diff --git a/src/path.rs b/src/path.rs index 193e92b..76ad52c 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,13 +1,13 @@ use crate::names::NUR_FILE; -use std::path::Path; +use std::path::{Path, PathBuf}; -pub(crate) fn find_project_path(cwd: &Path) -> Option<&Path> { - let mut path = cwd; +pub(crate) fn find_project_path>(cwd: P) -> Option { + let mut path = cwd.as_ref(); loop { let taskfile_path = path.join(NUR_FILE); if taskfile_path.exists() { - return Some(path); + return Some(path.to_path_buf()); } if let Some(parent) = path.parent() { diff --git a/src/defaults.rs b/src/scripts.rs similarity index 56% rename from src/defaults.rs rename to src/scripts.rs index 9fd1f34..08dcd5d 100644 --- a/src/defaults.rs +++ b/src/scripts.rs @@ -1,10 +1,7 @@ -use nu_utils::get_default_config as nu_get_default_config; - -pub(crate) fn get_default_nur_config() -> &'static str { - // Just use nu default config for now - nu_get_default_config() -} - pub(crate) fn get_default_nur_env() -> &'static str { include_str!("nu-scripts/default_nur_env.nu") } + +pub(crate) fn get_default_nur_config() -> &'static str { + include_str!("nu-scripts/default_nur_config.nu") +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..6150318 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,154 @@ +use crate::args::gather_commandline_args; +use crate::names::{ + NUR_CONFIG_CONFIG_FILENAME, NUR_CONFIG_DIR, NUR_CONFIG_ENV_FILENAME, NUR_CONFIG_LIB_PATH, + NUR_FILE, NUR_LOCAL_FILE, +}; +use crate::path::find_project_path; +use std::path::PathBuf; + +#[derive(Clone)] +pub(crate) struct NurState { + pub(crate) run_path: PathBuf, + pub(crate) has_project_path: bool, + pub(crate) project_path: PathBuf, + + pub(crate) config_dir: PathBuf, + pub(crate) lib_dir_path: PathBuf, + pub(crate) env_path: PathBuf, + pub(crate) config_path: PathBuf, + + pub(crate) nurfile_path: PathBuf, + pub(crate) local_nurfile_path: PathBuf, + + pub(crate) args_to_nur: Vec, + pub(crate) task_name: String, + pub(crate) args_to_task: Vec, +} + +impl NurState { + pub(crate) fn new(run_path: PathBuf, args: Vec) -> Self { + // Get initial directory details + let found_project_path = find_project_path(&run_path); + let has_project_path = found_project_path.is_some(); + let project_path = found_project_path.unwrap_or(run_path.clone()); + + // Set all paths + let config_dir = project_path.join(NUR_CONFIG_DIR); + let lib_dir_path = config_dir.join(NUR_CONFIG_LIB_PATH); + let env_path = config_dir.join(NUR_CONFIG_ENV_FILENAME); + let config_path = config_dir.join(NUR_CONFIG_CONFIG_FILENAME); + + // Set nurfiles + let nurfile_path = project_path.join(NUR_FILE); + let local_nurfile_path = project_path.join(NUR_LOCAL_FILE); + + // Parse args into bits + let (args_to_nur, task_name, args_to_task) = gather_commandline_args(args); + + NurState { + run_path, + has_project_path, + project_path, + + config_dir, + lib_dir_path, + env_path, + config_path, + + nurfile_path, + local_nurfile_path, + + args_to_nur, + task_name, + args_to_task, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_nur_state_with_project_path() { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_path_buf(); + let nurfile_path = temp_dir.path().join(NUR_FILE); + File::create(&nurfile_path).unwrap(); + + // Setup test + let args = vec![ + String::from("nur"), + String::from("--quiet"), + String::from("some_task"), + String::from("task_arg"), + ]; + let state = NurState::new(temp_dir_path.clone(), args); + + // Check everything works out + assert_eq!(state.run_path, temp_dir_path); + assert_eq!(state.project_path, temp_dir_path); + assert_eq!(state.has_project_path, true); + + assert_eq!(state.config_dir, temp_dir_path.join(".nur")); + assert_eq!(state.lib_dir_path, temp_dir_path.join(".nur/scripts")); + assert_eq!(state.env_path, temp_dir_path.join(".nur/env.nu")); + assert_eq!(state.config_path, temp_dir_path.join(".nur/config.nu")); + + assert_eq!(state.nurfile_path, temp_dir_path.join("nurfile")); + assert_eq!( + state.local_nurfile_path, + temp_dir_path.join("nurfile.local") + ); + + assert_eq!( + state.args_to_nur, + vec![String::from("nur"), String::from("--quiet"),] + ); + assert_eq!(state.task_name, "some_task"); + assert_eq!(state.args_to_task, vec![String::from("task_arg"),]); + + // Clean up + std::fs::remove_file(nurfile_path).unwrap(); + } + + #[test] + fn test_nur_state_without_project_path() { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_path_buf(); + + // Setup test + let args = vec![ + String::from("nur"), + String::from("--quiet"), + String::from("some_task"), + String::from("task_arg"), + ]; + let state = NurState::new(temp_dir_path.clone(), args); + + // Check everything works out + assert_eq!(state.run_path, temp_dir_path); + assert_eq!(state.project_path, temp_dir_path); // same as run_path, as this is the fallback + assert_eq!(state.has_project_path, false); + + assert_eq!(state.config_dir, temp_dir_path.join(".nur")); + assert_eq!(state.lib_dir_path, temp_dir_path.join(".nur/scripts")); + assert_eq!(state.env_path, temp_dir_path.join(".nur/env.nu")); + assert_eq!(state.config_path, temp_dir_path.join(".nur/config.nu")); + + assert_eq!(state.nurfile_path, temp_dir_path.join("nurfile")); + assert_eq!( + state.local_nurfile_path, + temp_dir_path.join("nurfile.local") + ); + + assert_eq!( + state.args_to_nur, + vec![String::from("nur"), String::from("--quiet"),] + ); + assert_eq!(state.task_name, "some_task"); + assert_eq!(state.args_to_task, vec![String::from("task_arg"),]); + } +}