Make Semicolon stop on error (#6079)

* introduce external command runs to failed error, and implement semicolon relative logic

* ignore test due to semicolon works

* not raise ShellError for external commands

* update comment

* add relative test in for windows

* fix type-o

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
WindSoilder 2022-07-20 20:44:42 +08:00 committed by GitHub
parent 558cd58d09
commit a35a71fd82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 111 additions and 11 deletions

View File

@ -92,6 +92,7 @@ impl Command for If {
call.redirect_stdout,
call.redirect_stderr,
)
.map(|res| res.0)
}
} else {
eval_expression_with_input(
@ -102,6 +103,7 @@ impl Command for If {
call.redirect_stdout,
call.redirect_stderr,
)
.map(|res| res.0)
}
} else {
Ok(PipelineData::new(call.head))

View File

@ -61,7 +61,8 @@ impl Command for Let {
input,
call.redirect_stdout,
call.redirect_stderr,
)?;
)?
.0;
//println!("Adding: {:?} to {}", rhs, var_id);

View File

@ -43,6 +43,7 @@ impl Command for LetEnv {
let rhs =
eval_expression_with_input(engine_state, stack, keyword_expr, input, false, true)?
.0
.into_value(call.head);
if env_var == "PWD" {

View File

@ -123,6 +123,21 @@ fn double_quote_does_not_expand_path_glob() {
})
}
#[cfg(not(windows))]
#[test]
fn failed_command_with_semicolon_will_not_execute_following_cmds() {
Playground::setup("external failed command with semicolon", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls *.abc; echo done
"#
));
assert!(!actual.out.contains("done"));
})
}
#[cfg(windows)]
#[test]
fn explicit_glob_windows() {
@ -166,6 +181,21 @@ fn bare_word_expand_path_glob_windows() {
})
}
#[cfg(windows)]
#[test]
fn failed_command_with_semicolon_will_not_execute_following_cmds_windows() {
Playground::setup("external failed command with semicolon", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^cargo asdf; echo done
"#
));
assert!(!actual.out.contains("done"));
})
}
#[cfg(windows)]
#[test]
// This test case might fail based on the running shell on Windows - CMD vs PowerShell, the reason is

View File

@ -3,8 +3,8 @@ use nu_path::expand_path_with;
use nu_protocol::{
ast::{Block, Call, Expr, Expression, Operator},
engine::{EngineState, Stack, Visibility},
HistoryFileFormat, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Range,
ShellError, Span, Spanned, SyntaxShape, Unit, Value, VarId, ENV_VARIABLE_ID,
HistoryFileFormat, IntoInterruptiblePipelineData, IntoPipelineData, ListStream, PipelineData,
Range, ShellError, Span, Spanned, SyntaxShape, Unit, Value, VarId, ENV_VARIABLE_ID,
};
use nu_utils::stdout_write_all_and_flush;
use std::cmp::Ordering;
@ -183,6 +183,9 @@ pub fn eval_call(
}
}
/// Eval extarnal expression
///
/// It returns PipelineData with a boolean flag, indicate that if the external runs to failed.
fn eval_external(
engine_state: &EngineState,
stack: &mut Stack,
@ -191,7 +194,7 @@ fn eval_external(
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
) -> Result<(PipelineData, bool), ShellError> {
let decl_id = engine_state
.find_decl("run-external".as_bytes(), &[])
.ok_or(ShellError::ExternalNotSupported(head.span))?;
@ -228,7 +231,54 @@ fn eval_external(
))
}
command.run(engine_state, stack, &call, input)
// when the external command doesn't redirect output, we eagerly check the result
// and find if the command runs to failed.
let mut runs_to_failed = false;
let result = command.run(engine_state, stack, &call, input)?;
if let PipelineData::ExternalStream {
stdout: None,
stderr,
mut exit_code,
span,
metadata,
} = result
{
let exit_code = exit_code.take();
match exit_code {
Some(exit_code_stream) => {
let ctrlc = exit_code_stream.ctrlc.clone();
let exit_code: Vec<Value> = exit_code_stream.into_iter().collect();
if let Some(Value::Int { val: code, .. }) = exit_code.last() {
// if exit_code is not 0, it indicates error occured, return back Err.
if *code != 0 {
runs_to_failed = true;
}
}
Ok((
PipelineData::ExternalStream {
stdout: None,
stderr,
exit_code: Some(ListStream::from_stream(exit_code.into_iter(), ctrlc)),
span,
metadata,
},
runs_to_failed,
))
}
None => Ok((
PipelineData::ExternalStream {
stdout: None,
stderr,
exit_code: None,
span,
metadata,
},
runs_to_failed,
)),
}
} else {
Ok((result, runs_to_failed))
}
}
pub fn eval_expression(
@ -317,6 +367,7 @@ pub fn eval_expression(
false,
false,
)?
.0
.into_value(span))
}
Expr::DateTime(dt) => Ok(Value::Date {
@ -604,6 +655,9 @@ pub fn eval_expression(
/// Checks the expression to see if it's a internal or external call. If so, passes the input
/// into the call and gets out the result
/// Otherwise, invokes the expression
///
/// It returns PipelineData with a boolean flag, indicate that if the external runs to failed.
/// The boolean flag **may only be true** for external calls, for internal calls, it always to be false.
pub fn eval_expression_with_input(
engine_state: &EngineState,
stack: &mut Stack,
@ -611,7 +665,8 @@ pub fn eval_expression_with_input(
mut input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
) -> Result<(PipelineData, bool), ShellError> {
let mut external_failed = false;
match expr {
Expression {
expr: Expr::Call(call),
@ -631,7 +686,7 @@ pub fn eval_expression_with_input(
expr: Expr::ExternalCall(head, args),
..
} => {
input = eval_external(
let external_result = eval_external(
engine_state,
stack,
head,
@ -640,6 +695,8 @@ pub fn eval_expression_with_input(
redirect_stdout,
redirect_stderr,
)?;
input = external_result.0;
external_failed = external_result.1
}
Expression {
@ -657,7 +714,7 @@ pub fn eval_expression_with_input(
}
}
Ok(input)
Ok((input, external_failed))
}
pub fn eval_block(
@ -671,14 +728,22 @@ pub fn eval_block(
let num_pipelines = block.len();
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
for (i, elem) in pipeline.expressions.iter().enumerate() {
input = eval_expression_with_input(
// if eval internal command failed, it can just make early return with `Err(ShellError)`.
let eval_result = eval_expression_with_input(
engine_state,
stack,
elem,
input,
redirect_stdout || (i != pipeline.expressions.len() - 1),
redirect_stderr,
)?
)?;
input = eval_result.0;
// external command may runs to failed
// make early return so remaining commands will not be executed.
// don't return `Err(ShellError)`, so nushell wouldn't show extra error message.
if eval_result.1 {
return Ok(input);
}
}
if pipeline_idx < (num_pipelines) - 1 {
@ -789,7 +854,7 @@ pub fn eval_subexpression(
) -> Result<PipelineData, ShellError> {
for pipeline in block.pipelines.iter() {
for expr in pipeline.expressions.iter() {
input = eval_expression_with_input(engine_state, stack, expr, input, true, false)?
input = eval_expression_with_input(engine_state, stack, expr, input, true, false)?.0
}
}

View File

@ -301,6 +301,7 @@ mod nu_commands {
}
#[test]
#[ignore = "For now we have no way to check LAST_EXIT_CODE in tests, ignore it for now"]
fn failed_with_proper_exit_code() {
Playground::setup("external failed", |dirs, _sandbox| {
let actual = nu!(cwd: dirs.test(), r#"