forked from extern/nushell
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:
parent
558cd58d09
commit
a35a71fd82
@ -92,6 +92,7 @@ impl Command for If {
|
|||||||
call.redirect_stdout,
|
call.redirect_stdout,
|
||||||
call.redirect_stderr,
|
call.redirect_stderr,
|
||||||
)
|
)
|
||||||
|
.map(|res| res.0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eval_expression_with_input(
|
eval_expression_with_input(
|
||||||
@ -102,6 +103,7 @@ impl Command for If {
|
|||||||
call.redirect_stdout,
|
call.redirect_stdout,
|
||||||
call.redirect_stderr,
|
call.redirect_stderr,
|
||||||
)
|
)
|
||||||
|
.map(|res| res.0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(PipelineData::new(call.head))
|
Ok(PipelineData::new(call.head))
|
||||||
|
@ -61,7 +61,8 @@ impl Command for Let {
|
|||||||
input,
|
input,
|
||||||
call.redirect_stdout,
|
call.redirect_stdout,
|
||||||
call.redirect_stderr,
|
call.redirect_stderr,
|
||||||
)?;
|
)?
|
||||||
|
.0;
|
||||||
|
|
||||||
//println!("Adding: {:?} to {}", rhs, var_id);
|
//println!("Adding: {:?} to {}", rhs, var_id);
|
||||||
|
|
||||||
|
1
crates/nu-command/src/env/let_env.rs
vendored
1
crates/nu-command/src/env/let_env.rs
vendored
@ -43,6 +43,7 @@ impl Command for LetEnv {
|
|||||||
|
|
||||||
let rhs =
|
let rhs =
|
||||||
eval_expression_with_input(engine_state, stack, keyword_expr, input, false, true)?
|
eval_expression_with_input(engine_state, stack, keyword_expr, input, false, true)?
|
||||||
|
.0
|
||||||
.into_value(call.head);
|
.into_value(call.head);
|
||||||
|
|
||||||
if env_var == "PWD" {
|
if env_var == "PWD" {
|
||||||
|
@ -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)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_glob_windows() {
|
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)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
// This test case might fail based on the running shell on Windows - CMD vs PowerShell, the reason is
|
// This test case might fail based on the running shell on Windows - CMD vs PowerShell, the reason is
|
||||||
|
@ -3,8 +3,8 @@ use nu_path::expand_path_with;
|
|||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Block, Call, Expr, Expression, Operator},
|
ast::{Block, Call, Expr, Expression, Operator},
|
||||||
engine::{EngineState, Stack, Visibility},
|
engine::{EngineState, Stack, Visibility},
|
||||||
HistoryFileFormat, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Range,
|
HistoryFileFormat, IntoInterruptiblePipelineData, IntoPipelineData, ListStream, PipelineData,
|
||||||
ShellError, Span, Spanned, SyntaxShape, Unit, Value, VarId, ENV_VARIABLE_ID,
|
Range, ShellError, Span, Spanned, SyntaxShape, Unit, Value, VarId, ENV_VARIABLE_ID,
|
||||||
};
|
};
|
||||||
use nu_utils::stdout_write_all_and_flush;
|
use nu_utils::stdout_write_all_and_flush;
|
||||||
use std::cmp::Ordering;
|
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(
|
fn eval_external(
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
@ -191,7 +194,7 @@ fn eval_external(
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
redirect_stdout: bool,
|
redirect_stdout: bool,
|
||||||
redirect_stderr: bool,
|
redirect_stderr: bool,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<(PipelineData, bool), ShellError> {
|
||||||
let decl_id = engine_state
|
let decl_id = engine_state
|
||||||
.find_decl("run-external".as_bytes(), &[])
|
.find_decl("run-external".as_bytes(), &[])
|
||||||
.ok_or(ShellError::ExternalNotSupported(head.span))?;
|
.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(
|
pub fn eval_expression(
|
||||||
@ -317,6 +367,7 @@ pub fn eval_expression(
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
)?
|
)?
|
||||||
|
.0
|
||||||
.into_value(span))
|
.into_value(span))
|
||||||
}
|
}
|
||||||
Expr::DateTime(dt) => Ok(Value::Date {
|
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
|
/// 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
|
/// into the call and gets out the result
|
||||||
/// Otherwise, invokes the expression
|
/// 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(
|
pub fn eval_expression_with_input(
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
@ -611,7 +665,8 @@ pub fn eval_expression_with_input(
|
|||||||
mut input: PipelineData,
|
mut input: PipelineData,
|
||||||
redirect_stdout: bool,
|
redirect_stdout: bool,
|
||||||
redirect_stderr: bool,
|
redirect_stderr: bool,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<(PipelineData, bool), ShellError> {
|
||||||
|
let mut external_failed = false;
|
||||||
match expr {
|
match expr {
|
||||||
Expression {
|
Expression {
|
||||||
expr: Expr::Call(call),
|
expr: Expr::Call(call),
|
||||||
@ -631,7 +686,7 @@ pub fn eval_expression_with_input(
|
|||||||
expr: Expr::ExternalCall(head, args),
|
expr: Expr::ExternalCall(head, args),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
input = eval_external(
|
let external_result = eval_external(
|
||||||
engine_state,
|
engine_state,
|
||||||
stack,
|
stack,
|
||||||
head,
|
head,
|
||||||
@ -640,6 +695,8 @@ pub fn eval_expression_with_input(
|
|||||||
redirect_stdout,
|
redirect_stdout,
|
||||||
redirect_stderr,
|
redirect_stderr,
|
||||||
)?;
|
)?;
|
||||||
|
input = external_result.0;
|
||||||
|
external_failed = external_result.1
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression {
|
Expression {
|
||||||
@ -657,7 +714,7 @@ pub fn eval_expression_with_input(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(input)
|
Ok((input, external_failed))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eval_block(
|
pub fn eval_block(
|
||||||
@ -671,14 +728,22 @@ pub fn eval_block(
|
|||||||
let num_pipelines = block.len();
|
let num_pipelines = block.len();
|
||||||
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
|
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
|
||||||
for (i, elem) in pipeline.expressions.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,
|
engine_state,
|
||||||
stack,
|
stack,
|
||||||
elem,
|
elem,
|
||||||
input,
|
input,
|
||||||
redirect_stdout || (i != pipeline.expressions.len() - 1),
|
redirect_stdout || (i != pipeline.expressions.len() - 1),
|
||||||
redirect_stderr,
|
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 {
|
if pipeline_idx < (num_pipelines) - 1 {
|
||||||
@ -789,7 +854,7 @@ pub fn eval_subexpression(
|
|||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
for pipeline in block.pipelines.iter() {
|
for pipeline in block.pipelines.iter() {
|
||||||
for expr in pipeline.expressions.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,6 +301,7 @@ mod nu_commands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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() {
|
fn failed_with_proper_exit_code() {
|
||||||
Playground::setup("external failed", |dirs, _sandbox| {
|
Playground::setup("external failed", |dirs, _sandbox| {
|
||||||
let actual = nu!(cwd: dirs.test(), r#"
|
let actual = nu!(cwd: dirs.test(), r#"
|
||||||
|
Loading…
Reference in New Issue
Block a user