diff --git a/Cargo.lock b/Cargo.lock index 1a1f4ca52c..40e472e65b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1626,9 +1626,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ "log 0.4.14", "regex 1.5.4", @@ -3226,13 +3226,11 @@ dependencies = [ name = "nu" version = "0.32.1" dependencies = [ - "clap", "ctrlc", "dunce", "futures 0.3.15", "hamcrest2", "itertools", - "log 0.4.14", "nu-cli", "nu-command", "nu-data", @@ -3263,7 +3261,6 @@ dependencies = [ "nu_plugin_to_sqlite", "nu_plugin_tree", "nu_plugin_xpath", - "pretty_env_logger", "serial_test", ] @@ -3340,6 +3337,7 @@ dependencies = [ "num-traits 0.2.14", "parking_lot 0.11.1", "pin-utils", + "pretty_env_logger", "ptree", "query_interface", "quick-xml 0.21.0", @@ -4860,7 +4858,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger 0.8.3", + "env_logger 0.8.4", "log 0.4.14", "rand 0.8.3", ] diff --git a/Cargo.toml b/Cargo.toml index 71d29530b1..5dde0ebe4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,12 +50,9 @@ nu_plugin_tree = { version = "0.32.1", path = "./crates/nu_plugin_tree", optiona nu_plugin_xpath = { version = "0.32.1", path = "./crates/nu_plugin_xpath", optional = true } # Required to bootstrap the main binary -clap = "2.33.3" ctrlc = { version = "3.1.7", optional = true } futures = { version = "0.3.12", features = ["compat", "io-compat"] } itertools = "0.10.0" -log = "0.4.14" -pretty_env_logger = "0.4.0" [dev-dependencies] nu-test-support = { version = "0.32.1", path = "./crates/nu-test-support" } diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 42a5547a8c..9ccff71612 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -62,6 +62,7 @@ indexmap = { version = "1.6.1", features = ["serde-1"] } itertools = "0.10.0" lazy_static = "1.*" log = "0.4.14" +pretty_env_logger = "0.4.0" meval = "0.2.0" num-bigint = { version = "0.3.1", features = ["serde"] } num-format = { version = "0.4.0", features = ["with-num-bigint"] } diff --git a/crates/nu-cli/src/app.rs b/crates/nu-cli/src/app.rs new file mode 100644 index 0000000000..8d0cf9f28e --- /dev/null +++ b/crates/nu-cli/src/app.rs @@ -0,0 +1,465 @@ +mod logger; +mod options; +mod options_parser; + +pub use options::{CliOptions, NuScript, Options}; +use options_parser::{NuParser, OptionsParser}; + +use nu_command::{commands::nu::Nu, utils::test_bins as binaries}; +use nu_engine::get_full_help; +use nu_errors::ShellError; +use nu_protocol::hir::{Call, Expression, SpannedExpression}; +use nu_protocol::{Primitive, UntaggedValue}; +use nu_source::{Span, Tag}; + +pub struct App { + parser: Box, + pub options: Options, +} + +impl App { + pub fn new(parser: Box, options: Options) -> Self { + Self { parser, options } + } + + pub fn run(args: &[String]) -> Result<(), ShellError> { + let nu = Box::new(NuParser::new()); + let options = Options::default(); + let ui = App::new(nu, options); + + ui.main(args) + } + + pub fn main(&self, argv: &[String]) -> Result<(), ShellError> { + let argv = quote_positionals(argv).join(" "); + + if let Err(cause) = self.parse(&argv) { + self.parser + .context() + .host() + .lock() + .print_err(cause, &nu_source::Text::from(argv)); + std::process::exit(1); + } + + if self.help() { + let ctx = self.parser.context(); + let autoview_cmd = ctx + .get_command("autoview") + .expect("could not find autoview command"); + + if let Ok(output_stream) = ctx.run_command( + autoview_cmd, + Tag::unknown(), + Call::new( + Box::new(SpannedExpression::new( + Expression::string("autoview".to_string()), + Span::unknown(), + )), + Span::unknown(), + ), + nu_stream::OutputStream::one( + UntaggedValue::string(get_full_help(&Nu, &ctx.scope)) + .into_value(nu_source::Tag::unknown()), + ), + ) { + for _ in output_stream {} + } + + std::process::exit(0); + } + + if let Some(bin) = self.testbin() { + match bin.as_deref() { + Ok("echo_env") => binaries::echo_env(), + Ok("cococo") => binaries::cococo(), + Ok("meow") => binaries::meow(), + Ok("iecho") => binaries::iecho(), + Ok("fail") => binaries::fail(), + Ok("nonu") => binaries::nonu(), + Ok("chop") => binaries::chop(), + Ok("repeater") => binaries::repeater(), + _ => unreachable!(), + } + + return Ok(()); + } + + let mut opts = CliOptions::new(); + + opts.config = self.config().map(std::ffi::OsString::from); + opts.stdin = self.takes_stdin(); + opts.save_history = self.save_history(); + + use logger::{configure, debug_filters, logger, trace_filters}; + + logger(|builder| { + configure(&self, builder)?; + trace_filters(&self, builder)?; + debug_filters(&self, builder)?; + + Ok(()) + })?; + + if let Some(commands) = self.commands() { + let commands = commands?; + let script = NuScript::code(&commands)?; + opts.scripts = vec![script]; + let context = crate::create_default_context(false)?; + return crate::run_script_file(context, opts); + } + + if let Some(scripts) = self.scripts() { + opts.scripts = scripts + .into_iter() + .filter_map(Result::ok) + .map(|path| { + let path = std::ffi::OsString::from(path); + + NuScript::source_file(path.as_os_str()) + }) + .filter_map(Result::ok) + .collect(); + + let context = crate::create_default_context(false)?; + return crate::run_script_file(context, opts); + } + + let context = crate::create_default_context(true)?; + + if !self.skip_plugins() { + let _ = crate::register_plugins(&context); + } + + #[cfg(feature = "rustyline-support")] + { + crate::cli(context, opts)?; + } + + #[cfg(not(feature = "rustyline-support"))] + { + println!("Nushell needs the 'rustyline-support' feature for CLI support"); + } + + Ok(()) + } + + pub fn commands(&self) -> Option> { + self.options.get("commands").map(|v| match v.value { + UntaggedValue::Error(err) => Err(err), + UntaggedValue::Primitive(Primitive::String(name)) => Ok(name), + _ => Err(ShellError::untagged_runtime_error("Unsupported option")), + }) + } + + pub fn help(&self) -> bool { + let help_asked = self + .options + .get("args") + .map(|v| { + v.table_entries().next().map(|v| { + if let Ok(value) = v.as_string() { + value == "help" + } else { + false + } + }) + }) + .flatten() + .unwrap_or(false); + + if help_asked { + self.options.shift(); + return true; + } + + false + } + + pub fn scripts(&self) -> Option>> { + self.options.get("args").map(|v| { + v.table_entries() + .map(|v| match &v.value { + UntaggedValue::Error(err) => Err(err.clone()), + UntaggedValue::Primitive(Primitive::FilePath(path)) => { + Ok(path.display().to_string()) + } + UntaggedValue::Primitive(Primitive::String(name)) => Ok(name.clone()), + _ => Err(ShellError::untagged_runtime_error("Unsupported option")), + }) + .collect() + }) + } + + pub fn takes_stdin(&self) -> bool { + self.options + .get("stdin") + .map(|v| matches!(v.as_bool(), Ok(true))) + .unwrap_or(false) + } + + pub fn config(&self) -> Option { + self.options + .get("config-file") + .map(|v| v.as_string().expect("not a string")) + } + + pub fn develop(&self) -> Option>> { + self.options.get("develop").map(|v| { + let mut values = vec![]; + + match v.value { + UntaggedValue::Error(err) => values.push(Err(err)), + UntaggedValue::Primitive(Primitive::String(filters)) => { + values.extend(filters.split(',').map(|filter| Ok(filter.to_string()))); + } + _ => values.push(Err(ShellError::untagged_runtime_error( + "Unsupported option", + ))), + }; + + values + }) + } + + pub fn debug(&self) -> Option>> { + self.options.get("debug").map(|v| { + let mut values = vec![]; + + match v.value { + UntaggedValue::Error(err) => values.push(Err(err)), + UntaggedValue::Primitive(Primitive::String(filters)) => { + values.extend(filters.split(',').map(|filter| Ok(filter.to_string()))); + } + _ => values.push(Err(ShellError::untagged_runtime_error( + "Unsupported option", + ))), + }; + + values + }) + } + + pub fn loglevel(&self) -> Option> { + self.options.get("loglevel").map(|v| match v.value { + UntaggedValue::Error(err) => Err(err), + UntaggedValue::Primitive(Primitive::String(name)) => Ok(name), + _ => Err(ShellError::untagged_runtime_error("Unsupported option")), + }) + } + + pub fn testbin(&self) -> Option> { + self.options.get("testbin").map(|v| match v.value { + UntaggedValue::Error(err) => Err(err), + UntaggedValue::Primitive(Primitive::String(name)) => Ok(name), + _ => Err(ShellError::untagged_runtime_error("Unsupported option")), + }) + } + + pub fn skip_plugins(&self) -> bool { + self.options + .get("skip-plugins") + .map(|v| matches!(v.as_bool(), Ok(true))) + .unwrap_or(false) + } + + pub fn save_history(&self) -> bool { + self.options + .get("no-history") + .map(|v| !matches!(v.as_bool(), Ok(true))) + .unwrap_or(true) + } + + pub fn parse(&self, args: &str) -> Result<(), ShellError> { + self.parser.parse(&args).map(|options| { + self.options.swap(&options); + }) + } +} + +fn quote_positionals(parameters: &[String]) -> Vec { + parameters + .iter() + .cloned() + .map(|arg| { + if arg.contains(' ') { + format!("\"{}\"", arg) + } else { + arg + } + }) + .collect::>() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn cli_app() -> App { + let parser = Box::new(NuParser::new()); + let options = Options::default(); + + App::new(parser, options) + } + + #[test] + fn default_options() -> Result<(), ShellError> { + let ui = cli_app(); + + ui.parse("nu")?; + assert_eq!(ui.help(), false); + assert_eq!(ui.takes_stdin(), false); + assert_eq!(ui.save_history(), true); + assert_eq!(ui.skip_plugins(), false); + assert_eq!(ui.config(), None); + assert_eq!(ui.loglevel(), None); + assert_eq!(ui.debug(), None); + assert_eq!(ui.develop(), None); + assert_eq!(ui.testbin(), None); + assert_eq!(ui.commands(), None); + assert_eq!(ui.scripts(), None); + Ok(()) + } + + #[test] + fn reports_errors_on_unsupported_flags() -> Result<(), ShellError> { + let ui = cli_app(); + + assert!(ui.parse("nu --coonfig-file /path/to/config.toml").is_err()); + assert!(ui.config().is_none()); + Ok(()) + } + + #[test] + fn configures_debug_trace_level_with_filters() -> Result<(), ShellError> { + let ui = cli_app(); + ui.parse("nu --develop=cli,parser")?; + assert_eq!(ui.develop().unwrap()[0], Ok("cli".to_string())); + assert_eq!(ui.develop().unwrap()[1], Ok("parser".to_string())); + Ok(()) + } + + #[test] + fn configures_debug_level_with_filters() -> Result<(), ShellError> { + let ui = cli_app(); + ui.parse("nu --debug=cli,run")?; + assert_eq!(ui.debug().unwrap()[0], Ok("cli".to_string())); + assert_eq!(ui.debug().unwrap()[1], Ok("run".to_string())); + Ok(()) + } + + #[test] + fn can_use_loglevels() -> Result<(), ShellError> { + for level in &["error", "warn", "info", "debug", "trace"] { + let ui = cli_app(); + let args = format!("nu --loglevel={}", *level); + ui.parse(&args)?; + assert_eq!(ui.loglevel().unwrap(), Ok(level.to_string())); + + let ui = cli_app(); + let args = format!("nu -l {}", *level); + ui.parse(&args)?; + assert_eq!(ui.loglevel().unwrap(), Ok(level.to_string())); + } + + let ui = cli_app(); + ui.parse("nu --loglevel=nada")?; + assert_eq!( + ui.loglevel().unwrap(), + Err(ShellError::untagged_runtime_error("nada is not supported.")) + ); + + Ok(()) + } + + #[test] + fn can_be_passed_nu_scripts() -> Result<(), ShellError> { + let ui = cli_app(); + ui.parse("nu code.nu bootstrap.nu")?; + assert_eq!(ui.scripts().unwrap()[0], Ok("code.nu".into())); + assert_eq!(ui.scripts().unwrap()[1], Ok("bootstrap.nu".into())); + Ok(()) + } + + #[test] + fn can_use_test_binaries() -> Result<(), ShellError> { + for binarie_name in &[ + "echo_env", "cococo", "iecho", "fail", "nonu", "chop", "repeater", "meow", + ] { + let ui = cli_app(); + let args = format!("nu --testbin={}", *binarie_name); + ui.parse(&args)?; + assert_eq!(ui.testbin().unwrap(), Ok(binarie_name.to_string())); + } + + let ui = cli_app(); + ui.parse("nu --testbin=andres")?; + assert_eq!( + ui.testbin().unwrap(), + Err(ShellError::untagged_runtime_error( + "andres is not supported." + )) + ); + + Ok(()) + } + + #[test] + fn has_help() -> Result<(), ShellError> { + let ui = cli_app(); + + ui.parse("nu help")?; + assert_eq!(ui.help(), true); + Ok(()) + } + + #[test] + fn can_take_stdin() -> Result<(), ShellError> { + let ui = cli_app(); + + ui.parse("nu --stdin")?; + assert_eq!(ui.takes_stdin(), true); + Ok(()) + } + + #[test] + fn can_opt_to_avoid_saving_history() -> Result<(), ShellError> { + let ui = cli_app(); + + ui.parse("nu --no-history")?; + assert_eq!(ui.save_history(), false); + Ok(()) + } + + #[test] + fn can_opt_to_skip_plugins() -> Result<(), ShellError> { + let ui = cli_app(); + + ui.parse("nu --skip-plugins")?; + assert_eq!(ui.skip_plugins(), true); + Ok(()) + } + + #[test] + fn understands_commands_need_to_be_run() -> Result<(), ShellError> { + let ui = cli_app(); + + ui.parse("nu -c \"ls | get name\"")?; + assert_eq!(ui.commands().unwrap(), Ok(String::from("ls | get name"))); + + let ui = cli_app(); + + ui.parse("nu -c \"echo 'hola'\"")?; + assert_eq!(ui.commands().unwrap(), Ok(String::from("echo 'hola'"))); + Ok(()) + } + + #[test] + fn knows_custom_configurations() -> Result<(), ShellError> { + let ui = cli_app(); + + ui.parse("nu --config-file /path/to/config.toml")?; + assert_eq!(ui.config().unwrap(), String::from("/path/to/config.toml")); + Ok(()) + } +} diff --git a/crates/nu-cli/src/app/logger.rs b/crates/nu-cli/src/app/logger.rs new file mode 100644 index 0000000000..02644a856b --- /dev/null +++ b/crates/nu-cli/src/app/logger.rs @@ -0,0 +1,52 @@ +use super::App; +use log::LevelFilter; +use nu_errors::ShellError; +use pretty_env_logger::env_logger::Builder; + +pub fn logger(f: impl FnOnce(&mut Builder) -> Result<(), ShellError>) -> Result<(), ShellError> { + let mut builder = pretty_env_logger::formatted_builder(); + f(&mut builder)?; + let _ = builder.try_init(); + Ok(()) +} + +pub fn configure(app: &App, logger: &mut Builder) -> Result<(), ShellError> { + if let Some(level) = app.loglevel() { + let level = match level.as_deref() { + Ok("error") => LevelFilter::Error, + Ok("warn") => LevelFilter::Warn, + Ok("info") => LevelFilter::Info, + Ok("debug") => LevelFilter::Debug, + Ok("trace") => LevelFilter::Trace, + Ok(_) | Err(_) => LevelFilter::Warn, + }; + + logger.filter_module("nu", level); + }; + + if let Ok(s) = std::env::var("RUST_LOG") { + logger.parse_filters(&s); + } + + Ok(()) +} + +pub fn trace_filters(app: &App, logger: &mut Builder) -> Result<(), ShellError> { + if let Some(filters) = app.develop() { + filters.into_iter().filter_map(Result::ok).for_each(|name| { + logger.filter_module(&name, LevelFilter::Trace); + }) + } + + Ok(()) +} + +pub fn debug_filters(app: &App, logger: &mut Builder) -> Result<(), ShellError> { + if let Some(filters) = app.debug() { + filters.into_iter().filter_map(Result::ok).for_each(|name| { + logger.filter_module(&name, LevelFilter::Debug); + }) + } + + Ok(()) +} diff --git a/crates/nu-cli/src/app/options.rs b/crates/nu-cli/src/app/options.rs new file mode 100644 index 0000000000..c14d87b7d7 --- /dev/null +++ b/crates/nu-cli/src/app/options.rs @@ -0,0 +1,100 @@ +use indexmap::IndexMap; +use nu_errors::ShellError; +use nu_protocol::{UntaggedValue, Value}; +use std::cell::RefCell; +use std::ffi::{OsStr, OsString}; + +#[derive(Debug)] +pub struct CliOptions { + pub config: Option, + pub stdin: bool, + pub scripts: Vec, + pub save_history: bool, +} + +impl Default for CliOptions { + fn default() -> Self { + Self::new() + } +} + +impl CliOptions { + pub fn new() -> Self { + Self { + config: None, + stdin: false, + scripts: vec![], + save_history: true, + } + } +} + +#[derive(Debug)] +pub struct Options { + inner: RefCell>, +} + +impl Options { + pub fn default() -> Self { + Self { + inner: RefCell::new(IndexMap::default()), + } + } + + pub fn get(&self, key: &str) -> Option { + self.inner.borrow().get(key).map(Clone::clone) + } + + pub fn put(&self, key: &str, value: Value) { + self.inner.borrow_mut().insert(key.into(), value); + } + + pub fn shift(&self) { + if let Some(Value { + value: UntaggedValue::Table(ref mut args), + .. + }) = self.inner.borrow_mut().get_mut("args") + { + args.remove(0); + } + } + + pub fn swap(&self, other: &Options) { + self.inner.swap(&other.inner); + } +} + +#[derive(Debug)] +pub struct NuScript { + pub filepath: Option, + pub contents: String, +} + +impl NuScript { + pub fn code(content: &str) -> Result { + Ok(Self { + filepath: None, + contents: content.to_string(), + }) + } + + pub fn get_code(&self) -> &str { + &self.contents + } + + pub fn source_file(path: &OsStr) -> Result { + use std::fs::File; + use std::io::Read; + + let path = path.to_os_string(); + let mut file = File::open(&path)?; + let mut buffer = String::new(); + + file.read_to_string(&mut buffer)?; + + Ok(Self { + filepath: Some(path), + contents: buffer, + }) + } +} diff --git a/crates/nu-cli/src/app/options_parser.rs b/crates/nu-cli/src/app/options_parser.rs new file mode 100644 index 0000000000..38274e952f --- /dev/null +++ b/crates/nu-cli/src/app/options_parser.rs @@ -0,0 +1,140 @@ +use super::Options; + +use nu_command::commands::nu::{self, Nu}; +use nu_command::commands::Autoview; +use nu_engine::{whole_stream_command, EvaluationContext}; +use nu_errors::ShellError; +use nu_protocol::hir::{ClassifiedCommand, InternalCommand, NamedValue}; +use nu_protocol::UntaggedValue; +use nu_source::Tag; + +pub struct NuParser { + context: EvaluationContext, +} + +pub trait OptionsParser { + fn parse(&self, input: &str) -> Result; + fn context(&self) -> &EvaluationContext; +} + +impl NuParser { + pub fn new() -> Self { + let context = EvaluationContext::basic(); + context.add_commands(vec![ + whole_stream_command(Nu {}), + whole_stream_command(Autoview {}), + ]); + + Self { context } + } +} + +impl OptionsParser for NuParser { + fn context(&self) -> &EvaluationContext { + &self.context + } + + fn parse(&self, input: &str) -> Result { + let options = Options::default(); + let (lite_result, _err) = nu_parser::lex(input, 0); + let (lite_result, _err) = nu_parser::parse_block(lite_result); + + let (parsed, err) = nu_parser::classify_block(&lite_result, &self.context.scope); + + if let Some(reason) = err { + return Err(reason.into()); + } + + match parsed.block[0].pipelines[0].list[0] { + ClassifiedCommand::Internal(InternalCommand { ref args, .. }) => { + if let Some(ref params) = args.named { + params.iter().for_each(|(k, v)| { + let value = match v { + NamedValue::AbsentSwitch => { + Some(UntaggedValue::from(false).into_untagged_value()) + } + NamedValue::PresentSwitch(span) => { + Some(UntaggedValue::from(true).into_value(Tag::from(span))) + } + NamedValue::AbsentValue => None, + NamedValue::Value(span, exprs) => { + let value = nu_engine::evaluate_baseline_expr(exprs, &self.context) + .expect("value"); + Some(value.value.into_value(Tag::from(span))) + } + }; + + let value = + value + .map(|v| match k.as_ref() { + "testbin" => { + if let Ok(name) = v.as_string() { + if nu::testbins().iter().any(|n| name == *n) { + Some(v) + } else { + Some( + UntaggedValue::Error( + ShellError::untagged_runtime_error( + format!("{} is not supported.", name), + ), + ) + .into_value(v.tag), + ) + } + } else { + Some(v) + } + } + "loglevel" => { + if let Ok(name) = v.as_string() { + if nu::loglevels().iter().any(|n| name == *n) { + Some(v) + } else { + Some( + UntaggedValue::Error( + ShellError::untagged_runtime_error( + format!("{} is not supported.", name), + ), + ) + .into_value(v.tag), + ) + } + } else { + Some(v) + } + } + _ => Some(v), + }) + .flatten(); + + if let Some(value) = value { + options.put(&k, value); + } + }); + } + + let mut positional_args = vec![]; + + if let Some(positional) = &args.positional { + for pos in positional { + let result = nu_engine::evaluate_baseline_expr(pos, &self.context)?; + positional_args.push(result); + } + } + + if !positional_args.is_empty() { + options.put( + "args", + UntaggedValue::Table(positional_args).into_untagged_value(), + ); + } + } + ClassifiedCommand::Error(ref reason) => { + return Err(reason.clone().into()); + } + _ => return Err(ShellError::untagged_runtime_error("unrecognized command")), + } + + Ok(options) + } +} diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 3550cce351..574759f797 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -15,7 +15,6 @@ use crate::line_editor::{ use nu_data::config; use nu_source::{Tag, Text}; use nu_stream::InputStream; -use std::ffi::{OsStr, OsString}; #[allow(unused_imports)] use std::sync::atomic::Ordering; @@ -31,69 +30,6 @@ use std::error::Error; use std::iter::Iterator; use std::path::PathBuf; -pub struct Options { - pub config: Option, - pub stdin: bool, - pub scripts: Vec, - pub save_history: bool, -} - -impl Default for Options { - fn default() -> Self { - Self::new() - } -} - -impl Options { - pub fn new() -> Self { - Self { - config: None, - stdin: false, - scripts: vec![], - save_history: true, - } - } -} - -pub struct NuScript { - pub filepath: Option, - pub contents: String, -} - -impl NuScript { - pub fn code<'a>(content: impl Iterator) -> Result { - let text = content - .map(|x| x.to_string()) - .collect::>() - .join("\n"); - - Ok(Self { - filepath: None, - contents: text, - }) - } - - pub fn get_code(&self) -> &str { - &self.contents - } - - pub fn source_file(path: &OsStr) -> Result { - use std::fs::File; - use std::io::Read; - - let path = path.to_os_string(); - let mut file = File::open(&path)?; - let mut buffer = String::new(); - - file.read_to_string(&mut buffer)?; - - Ok(Self { - filepath: Some(path), - contents: buffer, - }) - } -} - pub fn search_paths() -> Vec { use std::env; @@ -123,7 +59,10 @@ pub fn search_paths() -> Vec { search_paths } -pub fn run_script_file(context: EvaluationContext, options: Options) -> Result<(), Box> { +pub fn run_script_file( + context: EvaluationContext, + options: super::app::CliOptions, +) -> Result<(), ShellError> { if let Some(cfg) = options.config { load_cfg_as_global_cfg(&context, PathBuf::from(cfg)); } else { @@ -144,7 +83,10 @@ pub fn run_script_file(context: EvaluationContext, options: Options) -> Result<( } #[cfg(feature = "rustyline-support")] -pub fn cli(context: EvaluationContext, options: Options) -> Result<(), Box> { +pub fn cli( + context: EvaluationContext, + options: super::app::CliOptions, +) -> Result<(), Box> { let _ = configure_ctrl_c(&context); // start time for running startup scripts (this metric includes loading of the cfg, but w/e) diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 58ad1d2a8d..91305e9e73 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -9,6 +9,7 @@ extern crate quickcheck; #[macro_use(quickcheck)] extern crate quickcheck_macros; +mod app; mod cli; #[cfg(feature = "rustyline-support")] mod completion; @@ -22,8 +23,8 @@ pub mod types; #[cfg(feature = "rustyline-support")] pub use crate::cli::cli; +pub use crate::app::App; pub use crate::cli::{parse_and_eval, register_plugins, run_script_file}; -pub use crate::cli::{NuScript, Options}; pub use nu_command::commands::default_context::create_default_context; pub use nu_data::config; diff --git a/crates/nu-command/src/commands.rs b/crates/nu-command/src/commands.rs index de52c5df5f..2480174f34 100644 --- a/crates/nu-command/src/commands.rs +++ b/crates/nu-command/src/commands.rs @@ -86,7 +86,7 @@ pub(crate) mod mkdir; pub(crate) mod move_; pub(crate) mod next; pub(crate) mod nth; -pub(crate) mod nu; +pub mod nu; pub(crate) mod open; pub(crate) mod parse; pub(crate) mod path; @@ -140,7 +140,7 @@ pub(crate) mod which_; pub(crate) mod with_env; pub(crate) mod wrap; -pub(crate) use autoview::Autoview; +pub use autoview::Autoview; pub(crate) use cd::Cd; pub(crate) use alias::Alias; diff --git a/crates/nu-command/src/commands/nu/command.rs b/crates/nu-command/src/commands/nu/command.rs new file mode 100644 index 0000000000..668657cc6b --- /dev/null +++ b/crates/nu-command/src/commands/nu/command.rs @@ -0,0 +1,60 @@ +use nu_engine::WholeStreamCommand; +use nu_protocol::{Signature, SyntaxShape}; + +pub struct Command; + +impl WholeStreamCommand for Command { + fn name(&self) -> &str { + "nu" + } + + fn signature(&self) -> Signature { + Signature::build("nu") + .switch("stdin", "stdin", None) + .switch("skip-plugins", "do not load plugins", None) + .switch("no-history", "don't save history", None) + .named("commands", SyntaxShape::String, "Nu commands", Some('c')) + .named( + "testbin", + SyntaxShape::String, + "BIN: echo_env, cococo, iecho, fail, nonu, chop, repeater, meow", + None, + ) + .named("develop", SyntaxShape::String, "trace mode", None) + .named("debug", SyntaxShape::String, "debug mode", None) + .named( + "loglevel", + SyntaxShape::String, + "LEVEL: error, warn, info, debug, trace", + Some('l'), + ) + .named( + "config-file", + SyntaxShape::FilePath, + "custom configuration source file", + None, + ) + .optional("script", SyntaxShape::FilePath, "The Nu script to run") + .rest(SyntaxShape::String, "Left overs...") + } + + fn usage(&self) -> &str { + "Nu" + } +} + +pub fn testbins() -> Vec { + vec![ + "echo_env", "cococo", "iecho", "fail", "nonu", "chop", "repeater", "meow", + ] + .into_iter() + .map(String::from) + .collect() +} + +pub fn loglevels() -> Vec { + vec!["error", "warn", "info", "debug", "trace"] + .into_iter() + .map(String::from) + .collect() +} diff --git a/crates/nu-command/src/commands/nu/mod.rs b/crates/nu-command/src/commands/nu/mod.rs index bec8cebfe6..5d3e099844 100644 --- a/crates/nu-command/src/commands/nu/mod.rs +++ b/crates/nu-command/src/commands/nu/mod.rs @@ -1,3 +1,6 @@ +pub mod command; mod plugin; +pub use command::Command as Nu; +pub use command::{loglevels, testbins}; pub use plugin::SubCommand as NuPlugin; diff --git a/crates/nu-command/src/commands/source.rs b/crates/nu-command/src/commands/source.rs index ca2e67f6fc..653019658b 100644 --- a/crates/nu-command/src/commands/source.rs +++ b/crates/nu-command/src/commands/source.rs @@ -52,7 +52,7 @@ pub fn source(args: CommandArgs) -> Result { let result = script::run_script_standalone(contents, true, &ctx, false); if let Err(err) = result { - ctx.error(err.into()); + ctx.error(err); } Ok(ActionStream::empty()) } diff --git a/crates/nu-engine/src/evaluation_context.rs b/crates/nu-engine/src/evaluation_context.rs index 9df9a4c02e..8594a3bcdf 100644 --- a/crates/nu-engine/src/evaluation_context.rs +++ b/crates/nu-engine/src/evaluation_context.rs @@ -153,8 +153,7 @@ impl EvaluationContext { } } - #[allow(unused)] - pub(crate) fn get_command(&self, name: &str) -> Option { + pub fn get_command(&self, name: &str) -> Option { self.scope.get_command(name) } @@ -162,7 +161,7 @@ impl EvaluationContext { self.scope.has_command(name) } - pub(crate) fn run_command( + pub fn run_command( &self, command: Command, name_tag: Tag, diff --git a/crates/nu-engine/src/script.rs b/crates/nu-engine/src/script.rs index 5f1cdf9713..8aebc36916 100644 --- a/crates/nu-engine/src/script.rs +++ b/crates/nu-engine/src/script.rs @@ -265,11 +265,11 @@ pub fn run_script_standalone( redirect_stdin: bool, context: &EvaluationContext, exit_on_error: bool, -) -> Result<(), Box> { +) -> Result<(), ShellError> { context .shell_manager() .enter_script_mode() - .map_err(Box::new)?; + .map_err(ShellError::from)?; let line = process_script(&script_text, context, redirect_stdin, 0, false); match line { diff --git a/src/main.rs b/src/main.rs index cc97bfa731..97a5369272 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,191 +1,13 @@ -use clap::{App, Arg}; -use log::LevelFilter; -use nu_cli::{create_default_context, NuScript, Options}; -use nu_command::utils::test_bins as binaries; -use std::error::Error; +use nu_cli::App as CliApp; +use nu_errors::ShellError; -fn main() -> Result<(), Box> { - let mut options = Options::new(); +fn main() -> Result<(), ShellError> { + let mut argv = vec![String::from("nu")]; + argv.extend(positionals()); - let matches = App::new("nushell") - .version(clap::crate_version!()) - .arg( - Arg::with_name("config-file") - .long("config-file") - .help("custom configuration source file") - .hidden(true) - .takes_value(true), - ) - .arg( - Arg::with_name("no-history") - .hidden(true) - .long("no-history") - .multiple(false) - .takes_value(false), - ) - .arg( - Arg::with_name("loglevel") - .short("l") - .long("loglevel") - .value_name("LEVEL") - .possible_values(&["error", "warn", "info", "debug", "trace"]) - .takes_value(true), - ) - .arg( - Arg::with_name("skip-plugins") - .hidden(true) - .long("skip-plugins") - .multiple(false) - .takes_value(false), - ) - .arg( - Arg::with_name("testbin") - .hidden(true) - .long("testbin") - .value_name("TESTBIN") - .possible_values(&[ - "echo_env", "cococo", "iecho", "fail", "nonu", "chop", "repeater", "meow", - ]) - .takes_value(true), - ) - .arg( - Arg::with_name("commands") - .short("c") - .long("commands") - .multiple(false) - .takes_value(true), - ) - .arg( - Arg::with_name("develop") - .long("develop") - .multiple(true) - .takes_value(true), - ) - .arg( - Arg::with_name("debug") - .long("debug") - .multiple(true) - .takes_value(true), - ) - .arg( - Arg::with_name("stdin") - .long("stdin") - .multiple(false) - .takes_value(false), - ) - .arg( - Arg::with_name("script") - .help("the nu script to run") - .index(1), - ) - .arg( - Arg::with_name("args") - .help("positional args (used by --testbin)") - .index(2) - .multiple(true), - ) - .get_matches(); - - if let Some(bin) = matches.value_of("testbin") { - match bin { - "echo_env" => binaries::echo_env(), - "cococo" => binaries::cococo(), - "meow" => binaries::meow(), - "iecho" => binaries::iecho(), - "fail" => binaries::fail(), - "nonu" => binaries::nonu(), - "chop" => binaries::chop(), - "repeater" => binaries::repeater(), - _ => unreachable!(), - } - - return Ok(()); - } - - options.config = matches - .value_of("config-file") - .map(std::ffi::OsString::from); - options.stdin = matches.is_present("stdin"); - options.save_history = !matches.is_present("no-history"); - - let loglevel = match matches.value_of("loglevel") { - None => LevelFilter::Warn, - Some("error") => LevelFilter::Error, - Some("warn") => LevelFilter::Warn, - Some("info") => LevelFilter::Info, - Some("debug") => LevelFilter::Debug, - Some("trace") => LevelFilter::Trace, - _ => unreachable!(), - }; - - let mut builder = pretty_env_logger::formatted_builder(); - - if let Ok(s) = std::env::var("RUST_LOG") { - builder.parse_filters(&s); - } - - builder.filter_module("nu", loglevel); - - match matches.values_of("develop") { - None => {} - Some(values) => { - for item in values { - builder.filter_module(&format!("nu::{}", item), LevelFilter::Trace); - } - } - } - - match matches.values_of("debug") { - None => {} - Some(values) => { - for item in values { - builder.filter_module(&format!("nu::{}", item), LevelFilter::Debug); - } - } - } - - builder.try_init()?; - - match matches.values_of("commands") { - None => {} - Some(values) => { - options.scripts = vec![NuScript::code(values)?]; - - let context = create_default_context(false)?; - nu_cli::run_script_file(context, options)?; - return Ok(()); - } - } - - match matches.value_of("script") { - Some(filepath) => { - let filepath = std::ffi::OsString::from(filepath); - - options.scripts = vec![NuScript::source_file(filepath.as_os_str())?]; - - let context = create_default_context(false)?; - nu_cli::run_script_file(context, options)?; - return Ok(()); - } - - None => { - let context = create_default_context(true)?; - - if !matches.is_present("skip-plugins") { - let _ = nu_cli::register_plugins(&context); - } - - #[cfg(feature = "rustyline-support")] - { - nu_cli::cli(context, options)?; - } - - #[cfg(not(feature = "rustyline-support"))] - { - println!("Nushell needs the 'rustyline-support' feature for CLI support"); - } - } - } - - Ok(()) + CliApp::run(&argv) +} + +fn positionals() -> Vec { + std::env::args().skip(1).collect::>() }