This commit is contained in:
Gwendolyn 2024-08-04 17:10:59 +02:00 committed by GitHub
commit b19c736d60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 244 additions and 111 deletions

View File

@ -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 log::{info, trace};
use nu_engine::{convert_env_values, eval_block}; use nu_engine::{convert_env_values, eval_block};
use nu_parser::parse; use nu_parser::parse;
@ -126,11 +126,19 @@ pub fn evaluate_file(
// Invoke the main command with arguments. // Invoke the main command with arguments.
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace. // Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
let args = format!("main {}", args.join(" ")); let mut working_set = StateWorkingSet::new(engine_state);
eval_source( 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, engine_state,
stack, stack,
args.as_bytes(), main_call_block,
"<commandline>", "<commandline>",
input, input,
true, true,

View File

@ -1,15 +1,20 @@
use nu_cmd_base::hook::eval_hook; use nu_cmd_base::hook::eval_hook;
use nu_engine::{eval_block, eval_block_with_early_return}; 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::{ use nu_protocol::{
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, 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)] #[cfg(windows)]
use nu_utils::enable_vt_processing; use nu_utils::enable_vt_processing;
use nu_utils::perf; use nu_utils::perf;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
// This will collect environment variables from std::env and adds them to a stack. // 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<Arc<Block>, ShellError> {
let source = format!("{} {}", fname, args.join(" "));
let source = source.as_bytes();
let file_id = working_set.add_file("<commandline>".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, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
source: &[u8], block: Arc<Block>,
input: PipelineData,
allow_return: bool,
) -> Result<Option<i32>, ShellError> {
let pipeline = if allow_return {
eval_block_with_early_return::<WithoutDebug>(engine_state, stack, &block, input)
} else {
eval_block::<WithoutDebug>(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<Block>,
fname: &str, fname: &str,
input: PipelineData, input: PipelineData,
allow_return: bool, allow_return: bool,
) -> i32 { ) -> i32 {
let start_time = std::time::Instant::now(); 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), Ok(code) => code.unwrap_or(0),
Err(err) => { Err(err) => {
report_error_new(engine_state, &err); report_error_new(engine_state, &err);
@ -237,14 +348,16 @@ pub fn eval_source(
exit_code exit_code
} }
fn evaluate_source( pub fn eval_source(
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
source: &[u8], source: &[u8],
fname: &str, fname: &str,
input: PipelineData, input: PipelineData,
allow_return: bool, allow_return: bool,
) -> Result<Option<i32>, ShellError> { ) -> i32 {
let start_time = std::time::Instant::now();
let (block, delta) = { let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
let output = parse( let output = parse(
@ -259,7 +372,7 @@ fn evaluate_source(
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_error(&working_set, err); report_error(&working_set, err);
return Ok(Some(1)); return 1;
} }
if let Some(err) = working_set.compile_errors.first() { if let Some(err) = working_set.compile_errors.first() {
@ -270,33 +383,19 @@ fn evaluate_source(
(output, working_set.render()) (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 { let exit_code =
eval_block_with_early_return::<WithoutDebug>(engine_state, stack, &block, input) evaluate_block_with_exit_code(engine_state, stack, block, fname, input, allow_return);
} else { perf!(
eval_block::<WithoutDebug>(engine_state, stack, &block, input) &format!("eval_source {}", &fname),
}?; start_time,
engine_state.get_config().use_ansi_coloring
let status = if let PipelineData::ByteStream(..) = pipeline { );
pipeline.print(engine_state, stack, false, false)? exit_code
} 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()))
} }
#[cfg(test)] #[cfg(test)]

View File

@ -22,6 +22,7 @@ pub use nu_protocol::parser_path::*;
pub use parse_keywords::*; pub use parse_keywords::*;
pub use parser::{ pub use parser::{
is_math_expression_like, parse, parse_block, parse_expression, parse_external_call, find_longest_command, is_math_expression_like, parse, parse_block, parse_expression,
parse_unit_value, trim_quotes, trim_quotes_str, unescape_unquote_string, DURATION_UNIT_GROUPS, parse_external_call, parse_internal_call, parse_unit_value, trim_quotes, trim_quotes_str,
unescape_unquote_string, DURATION_UNIT_GROUPS,
}; };

View File

@ -11,7 +11,7 @@ use itertools::Itertools;
use log::trace; use log::trace;
use nu_engine::DIR_VAR_PARSER_INFO; use nu_engine::DIR_VAR_PARSER_INFO;
use nu_protocol::{ 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, ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, VarId, ENV_VARIABLE_ID,
IN_VARIABLE_ID, IN_VARIABLE_ID,
}; };
@ -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 { pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span) -> Expression {
trace!("parsing: call"); trace!("parsing: call");
@ -1261,57 +1301,15 @@ pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span)
return garbage(working_set, head); return garbage(working_set, head);
} }
let mut pos = 0; let cmd_name = working_set.get_span_contents(spans[0]);
let cmd_start = pos;
let mut name_spans = vec![];
let mut name = vec![];
for word_span in spans[cmd_start..].iter() { if let Some((decl_id, len)) = find_longest_command(working_set, cmd_name, &spans[1..]) {
// Find the longest group of words that could form a command let cmd_spans = &spans[..len + 1];
let arg_spans = &spans[len + 1..];
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 {
// Before the internal parsing we check if there is no let or alias declarations // 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 // that are missing their name, e.g.: let = 1 or alias = 2
if spans.len() > 1 { if !arg_spans.is_empty() {
let test_equal = working_set.get_span_contents(spans[1]); let test_equal = working_set.get_span_contents(arg_spans[0]);
if test_equal == [b'='] { if test_equal == [b'='] {
trace!("incomplete statement"); trace!("incomplete statement");
@ -1358,21 +1356,11 @@ pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span)
return expression; return expression;
} else { } else {
trace!("parsing: alias of internal call"); trace!("parsing: alias of internal call");
parse_internal_call( parse_internal_call(working_set, Span::concat(cmd_spans), arg_spans, decl_id)
working_set,
Span::concat(&spans[cmd_start..pos]),
&spans[pos..],
decl_id,
)
} }
} else { } else {
trace!("parsing: internal call"); trace!("parsing: internal call");
parse_internal_call( parse_internal_call(working_set, Span::concat(cmd_spans), arg_spans, decl_id)
working_set,
Span::concat(&spans[cmd_start..pos]),
&spans[pos..],
decl_id,
)
}; };
Expression::new( Expression::new(

View File

@ -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] #[test]
fn source_empty_file() { fn source_empty_file() {
Playground::setup("source_empty_file", |dirs, sandbox| { Playground::setup("source_empty_file", |dirs, sandbox| {