From 7c7e5112ea79e436374d626935c37c2a1f0b2962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Tue, 15 Jun 2021 17:43:25 -0500 Subject: [PATCH] Make Nu bootstrap itself from main. (#3619) We've relied on `clap` for building our cli app bootstrapping that figures out the positionals, flags, and other convenient facilities. Nu has been capable of solving this problem for quite some time. Given this and much more reasons (including the build time caused by `clap`) we start here working with our own. --- Cargo.lock | 10 +- Cargo.toml | 3 - crates/nu-cli/Cargo.toml | 1 + crates/nu-cli/src/app.rs | 465 +++++++++++++++++++ crates/nu-cli/src/app/logger.rs | 52 +++ crates/nu-cli/src/app/options.rs | 100 ++++ crates/nu-cli/src/app/options_parser.rs | 140 ++++++ crates/nu-cli/src/cli.rs | 74 +-- crates/nu-cli/src/lib.rs | 3 +- crates/nu-command/src/commands.rs | 4 +- crates/nu-command/src/commands/nu/command.rs | 60 +++ crates/nu-command/src/commands/nu/mod.rs | 3 + crates/nu-command/src/commands/source.rs | 2 +- crates/nu-engine/src/evaluation_context.rs | 5 +- crates/nu-engine/src/script.rs | 4 +- src/main.rs | 198 +------- 16 files changed, 852 insertions(+), 272 deletions(-) create mode 100644 crates/nu-cli/src/app.rs create mode 100644 crates/nu-cli/src/app/logger.rs create mode 100644 crates/nu-cli/src/app/options.rs create mode 100644 crates/nu-cli/src/app/options_parser.rs create mode 100644 crates/nu-command/src/commands/nu/command.rs 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::>() }