diff --git a/crates/nu-cli/src/eval_file.rs b/crates/nu-cli/src/eval_file.rs index bac4378d1b..b368113689 100644 --- a/crates/nu-cli/src/eval_file.rs +++ b/crates/nu-cli/src/eval_file.rs @@ -1,4 +1,4 @@ -use crate::util::eval_source; +use crate::util::{eval_source, evaluate_block_with_exit_code, make_main_call}; use log::{info, trace}; use nu_engine::{convert_env_values, eval_block}; use nu_parser::parse; @@ -126,11 +126,19 @@ pub fn evaluate_file( // Invoke the main command with arguments. // Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace. - let args = format!("main {}", args.join(" ")); - eval_source( + let mut working_set = StateWorkingSet::new(engine_state); + let main_call_block = make_main_call( + &mut working_set, + source_filename.to_string_lossy().to_string(), + args, + true, + )?; + engine_state.merge_delta(working_set.delta)?; + + evaluate_block_with_exit_code( engine_state, stack, - args.as_bytes(), + main_call_block, "", input, true, diff --git a/crates/nu-cli/src/util.rs b/crates/nu-cli/src/util.rs index e912bceb5f..d3f924eb75 100644 --- a/crates/nu-cli/src/util.rs +++ b/crates/nu-cli/src/util.rs @@ -1,15 +1,20 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::{eval_block, eval_block_with_early_return}; -use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents}; +use nu_parser::{ + escape_quote_string, find_longest_command, lex, lite_parse, parse, parse_internal_call, + unescape_unquote_string, Token, TokenContents, +}; +use nu_protocol::ast::{Block, Expr, Expression, Pipeline}; use nu_protocol::{ debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet}, - report_error, report_error_new, PipelineData, ShellError, Span, Value, + report_error, report_error_new, PipelineData, ShellError, Signature, Span, Value, }; #[cfg(windows)] use nu_utils::enable_vt_processing; use nu_utils::perf; use std::path::Path; +use std::sync::Arc; // This will collect environment variables from std::env and adds them to a stack. // @@ -199,17 +204,123 @@ fn gather_env_vars( } } -pub fn eval_source( +pub fn make_main_call( + working_set: &mut StateWorkingSet, + fname: String, + args: &[String], + redirect_env: bool, +) -> Result, ShellError> { + let source = format!("{} {}", fname, args.join(" ")); + let source = source.as_bytes(); + let file_id = working_set.add_file("".to_string(), source); + let file_span = working_set.get_span_for_file(file_id); + let (tokens, err) = lex(source, file_span.start, &[], &[], false); + if let Some(err) = err { + working_set.error(err) + } + let (lite_block, err) = lite_parse(tokens.as_slice()); + if let Some(err) = err { + working_set.error(err); + } + let lite_command = &lite_block.block[0].commands[0]; + + let (decl_id, command_len) = + find_longest_command(working_set, b"main", &lite_command.parts[1..]) + .expect("make_main_call() called, but 'main' definition not found in state"); + + let parsed_call = parse_internal_call( + working_set, + Span::concat(&lite_command.parts[..command_len + 1]), + &lite_command.parts[(command_len + 1)..], + decl_id, + ); + + let expression = Expression::new( + working_set, + Expr::Call(parsed_call.call), + Span::concat(lite_command.parts.as_slice()), + parsed_call.output, + ); + + if let Some(warning) = working_set.parse_warnings.first() { + report_error(working_set, warning); + } + + // If any parse errors were found, report the first error and exit. + if let Some(err) = working_set.parse_errors.first() { + report_error(working_set, err); + std::process::exit(1); + } + + if let Some(err) = working_set.compile_errors.first() { + report_error(working_set, err); + // Not a fatal error, for now + } + + let mut block = Block { + signature: Box::new(Signature::new("")), + pipelines: vec![Pipeline::from_vec(vec![expression])], + captures: vec![], + redirect_env, + ir_block: None, + span: Some(file_span), + }; + + match nu_engine::compile(working_set, &block) { + Ok(ir_block) => { + block.ir_block = Some(ir_block); + } + Err(err) => working_set.compile_errors.push(err), + } + + Ok(Arc::new(block)) +} + +pub fn evaluate_block( engine_state: &mut EngineState, stack: &mut Stack, - source: &[u8], + block: Arc, + input: PipelineData, + allow_return: bool, +) -> Result, ShellError> { + let pipeline = if allow_return { + eval_block_with_early_return::(engine_state, stack, &block, input) + } else { + eval_block::(engine_state, stack, &block, input) + }?; + + let status = if let PipelineData::ByteStream(..) = pipeline { + pipeline.print(engine_state, stack, false, false)? + } else { + if let Some(hook) = engine_state.get_config().hooks.display_output.clone() { + let pipeline = eval_hook( + engine_state, + stack, + Some(pipeline), + vec![], + &hook, + "display_output", + )?; + pipeline.print(engine_state, stack, false, false) + } else { + pipeline.print(engine_state, stack, true, false) + }? + }; + + Ok(status.map(|status| status.code())) +} + +pub fn evaluate_block_with_exit_code( + engine_state: &mut EngineState, + stack: &mut Stack, + block: Arc, fname: &str, input: PipelineData, allow_return: bool, ) -> i32 { let start_time = std::time::Instant::now(); - let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) { + let exit_code = match evaluate_block(engine_state, stack, block, input, allow_return) { Ok(code) => code.unwrap_or(0), Err(err) => { report_error_new(engine_state, &err); @@ -237,14 +348,16 @@ pub fn eval_source( exit_code } -fn evaluate_source( +pub fn eval_source( engine_state: &mut EngineState, stack: &mut Stack, source: &[u8], fname: &str, input: PipelineData, allow_return: bool, -) -> Result, ShellError> { +) -> i32 { + let start_time = std::time::Instant::now(); + let (block, delta) = { let mut working_set = StateWorkingSet::new(engine_state); let output = parse( @@ -259,7 +372,7 @@ fn evaluate_source( if let Some(err) = working_set.parse_errors.first() { report_error(&working_set, err); - return Ok(Some(1)); + return 1; } if let Some(err) = working_set.compile_errors.first() { @@ -270,33 +383,19 @@ fn evaluate_source( (output, working_set.render()) }; - engine_state.merge_delta(delta)?; + if let Err(err) = engine_state.merge_delta(delta) { + report_error_new(engine_state, &err); + return 1; + } - let pipeline = if allow_return { - eval_block_with_early_return::(engine_state, stack, &block, input) - } else { - eval_block::(engine_state, stack, &block, input) - }?; - - let status = if let PipelineData::ByteStream(..) = pipeline { - pipeline.print(engine_state, stack, false, false)? - } else { - if let Some(hook) = engine_state.get_config().hooks.display_output.clone() { - let pipeline = eval_hook( - engine_state, - stack, - Some(pipeline), - vec![], - &hook, - "display_output", - )?; - pipeline.print(engine_state, stack, false, false) - } else { - pipeline.print(engine_state, stack, true, false) - }? - }; - - Ok(status.map(|status| status.code())) + let exit_code = + evaluate_block_with_exit_code(engine_state, stack, block, fname, input, allow_return); + perf!( + &format!("eval_source {}", &fname), + start_time, + engine_state.get_config().use_ansi_coloring + ); + exit_code } #[cfg(test)] diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index 8f871aa815..79974d4cc1 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -22,6 +22,7 @@ pub use nu_protocol::parser_path::*; pub use parse_keywords::*; pub use parser::{ - is_math_expression_like, parse, parse_block, parse_expression, parse_external_call, - parse_unit_value, trim_quotes, trim_quotes_str, unescape_unquote_string, DURATION_UNIT_GROUPS, + find_longest_command, is_math_expression_like, parse, parse_block, parse_expression, + parse_external_call, parse_internal_call, parse_unit_value, trim_quotes, trim_quotes_str, + unescape_unquote_string, DURATION_UNIT_GROUPS, }; diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 8332d2f383..98b764578f 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -11,7 +11,7 @@ use itertools::Itertools; use log::trace; use nu_engine::DIR_VAR_PARSER_INFO; use nu_protocol::{ - ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DidYouMean, Flag, + ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DeclId, DidYouMean, Flag, ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, }; @@ -690,8 +690,8 @@ fn parse_short_flags( }) ) && String::from_utf8_lossy(working_set.get_span_contents(arg_span)) - .parse::() - .is_ok() + .parse::() + .is_ok() { return None; } else if let Some(first) = unmatched_short_flags.first() { @@ -1250,6 +1250,46 @@ pub fn parse_internal_call( } } +/// finds the longest command matching a command name and a list of arguments +/// if a command is found, it returns its decl id and the number of arguments that were consumed as +/// part of the command name +pub fn find_longest_command( + working_set: &StateWorkingSet, + name: &[u8], + args: &[Span], +) -> Option<(DeclId, usize)> { + let mut pos = 0; + let mut name_spans = vec![]; + let mut name = name.to_owned(); + + // combine all the args together, separated by spaces + for arg in args { + name_spans.push(*arg); + let name_part = working_set.get_span_contents(*arg); + name.push(b' '); + name.extend(name_part); + pos += 1; + } + + let mut maybe_decl_id = working_set.find_decl(&name); + + while maybe_decl_id.is_none() && !name_spans.is_empty() { + // Find the longest command match + + // name_spans length is checked in the loop condition so we can unwrap + let popped_span = name_spans + .pop() + .expect("internal error: already checked for non-empty"); + pos -= 1; + + let popped_len = popped_span.end - popped_span.start; + name.truncate(name.len() - popped_len - 1); // remove the length of the removed span and the whitespace + maybe_decl_id = working_set.find_decl(&name); + } + + maybe_decl_id.map(|id| (id, pos)) +} + pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span) -> Expression { trace!("parsing: call"); @@ -1261,57 +1301,15 @@ pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span) return garbage(working_set, head); } - let mut pos = 0; - let cmd_start = pos; - let mut name_spans = vec![]; - let mut name = vec![]; + let cmd_name = working_set.get_span_contents(spans[0]); - for word_span in spans[cmd_start..].iter() { - // Find the longest group of words that could form a command - - name_spans.push(*word_span); - - let name_part = working_set.get_span_contents(*word_span); - if name.is_empty() { - name.extend(name_part); - } else { - name.push(b' '); - name.extend(name_part); - } - - pos += 1; - } - - let mut maybe_decl_id = working_set.find_decl(&name); - - while maybe_decl_id.is_none() { - // Find the longest command match - if name_spans.len() <= 1 { - // Keep the first word even if it does not match -- could be external command - break; - } - - name_spans.pop(); - pos -= 1; - - let mut name = vec![]; - for name_span in &name_spans { - let name_part = working_set.get_span_contents(*name_span); - if name.is_empty() { - name.extend(name_part); - } else { - name.push(b' '); - name.extend(name_part); - } - } - maybe_decl_id = working_set.find_decl(&name); - } - - if let Some(decl_id) = maybe_decl_id { + if let Some((decl_id, len)) = find_longest_command(working_set, cmd_name, &spans[1..]) { + let cmd_spans = &spans[..len + 1]; + let arg_spans = &spans[len + 1..]; // Before the internal parsing we check if there is no let or alias declarations // that are missing their name, e.g.: let = 1 or alias = 2 - if spans.len() > 1 { - let test_equal = working_set.get_span_contents(spans[1]); + if !arg_spans.is_empty() { + let test_equal = working_set.get_span_contents(arg_spans[0]); if test_equal == [b'='] { trace!("incomplete statement"); @@ -1358,21 +1356,11 @@ pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span) return expression; } else { trace!("parsing: alias of internal call"); - parse_internal_call( - working_set, - Span::concat(&spans[cmd_start..pos]), - &spans[pos..], - decl_id, - ) + parse_internal_call(working_set, Span::concat(cmd_spans), arg_spans, decl_id) } } else { trace!("parsing: internal call"); - parse_internal_call( - working_set, - Span::concat(&spans[cmd_start..pos]), - &spans[pos..], - decl_id, - ) + parse_internal_call(working_set, Span::concat(cmd_spans), arg_spans, decl_id) }; Expression::new( @@ -2795,9 +2783,9 @@ pub fn unescape_string(bytes: &[u8], span: Span) -> (Vec, Option } // fall through -- escape not accepted above, must be error. error = error.or(Some(ParseError::InvalidLiteral( - "invalid unicode escape '\\u{X...}', must be 1-6 hex digits, max value 10FFFF".into(), - "string".into(), - Span::new(span.start + idx, span.end), + "invalid unicode escape '\\u{X...}', must be 1-6 hex digits, max value 10FFFF".into(), + "string".into(), + Span::new(span.start + idx, span.end), ))); break 'us_loop; } @@ -3808,8 +3796,8 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) -> ParseError::AssignmentMismatch( "Default value wrong type".into(), format!( - "expected default value to be `{var_type}`" - ), + "expected default value to be `{var_type}`" + ), expression.span, ), ) @@ -3884,8 +3872,8 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) -> "Default value is the wrong type" .into(), format!( - "expected default value to be `{t}`" - ), + "expected default value to be `{t}`" + ), expression_span, ), ) @@ -4433,7 +4421,7 @@ pub fn parse_match_block_expression(working_set: &mut StateWorkingSet, span: Spa guard: None, span: Span::new(start, end), } - // A match guard + // A match guard } else if connector == b"if" { let if_end = { let end = output[position].span.end; diff --git a/tests/shell/mod.rs b/tests/shell/mod.rs index 62f79a59c1..eeba650ca3 100644 --- a/tests/shell/mod.rs +++ b/tests/shell/mod.rs @@ -324,6 +324,43 @@ fn main_script_can_have_subcommands2() { }) } +#[test] +fn main_script_use_filename_in_error() { + Playground::setup("main_filename", |dirs, sandbox| { + sandbox.mkdir("main_filename"); + sandbox.with_files(&[FileWithContent( + "script.nu", + r#"def main [--foo: string] { + print foo + }"#, + )]); + + let actual = nu!(cwd: dirs.test(), pipeline("nu script.nu --foo")); + + assert!(actual.err.contains("script.nu --foo")); + assert!(!actual.err.contains("main --foo")); + }) +} + +#[test] +fn main_script_subcommand_use_filename_in_error() { + Playground::setup("main_filename", |dirs, sandbox| { + sandbox.mkdir("main_filename"); + sandbox.with_files(&[FileWithContent( + "script.nu", + r#"def main [] {} + def 'main asdf' [--foo: string] { + print foo + }"#, + )]); + + let actual = nu!(cwd: dirs.test(), pipeline("nu script.nu asdf --foo")); + + assert!(actual.err.contains("script.nu asdf --foo")); + assert!(!actual.err.contains("main asdf --foo")); + }) +} + #[test] fn source_empty_file() { Playground::setup("source_empty_file", |dirs, sandbox| {