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 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,
"<commandline>",
input,
true,

View File

@ -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<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,
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,
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<Option<i32>, 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::<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()))
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)]

View File

@ -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,
};

View File

@ -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::<f64>()
.is_ok()
.parse::<f64>()
.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<u8>, Option<ParseError>
}
// 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;

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