Pipeline operators: && and || (#7448)

# Description

We got some feedback from folks used to other shells that `try/catch`
isn't quite as convenient as things like `||`. This PR adds `&&` as a
synonym for `;` and `||` as equivalent to what `try/catch` would do.

# User-Facing Changes

Adds `&&` and `||` pipeline operators.

# Tests + Formatting

Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
JT
2022-12-13 09:53:46 +13:00
committed by GitHub
parent 7917cf9f00
commit 35bea5e044
12 changed files with 320 additions and 179 deletions

View File

@ -789,14 +789,16 @@ pub fn eval_element_with_input(
redirect_stderr: bool,
) -> Result<(PipelineData, bool), ShellError> {
match element {
PipelineElement::Expression(_, expr) => eval_expression_with_input(
engine_state,
stack,
expr,
input,
redirect_stdout,
redirect_stderr,
),
PipelineElement::Expression(_, expr) | PipelineElement::Or(_, expr) => {
eval_expression_with_input(
engine_state,
stack,
expr,
input,
redirect_stdout,
redirect_stderr,
)
}
PipelineElement::Redirection(span, redirection, expr) => match &expr.expr {
Expr::String(_) => {
let input = match (redirection, input) {
@ -903,22 +905,6 @@ pub fn eval_element_with_input(
}
_ => Err(ShellError::CommandNotFound(*span)),
},
PipelineElement::And(_, expr) => eval_expression_with_input(
engine_state,
stack,
expr,
input,
redirect_stdout,
redirect_stderr,
),
PipelineElement::Or(_, expr) => eval_expression_with_input(
engine_state,
stack,
expr,
input,
redirect_stdout,
redirect_stderr,
),
}
}
@ -952,10 +938,16 @@ pub fn eval_block(
redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
let num_pipelines = block.len();
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
let mut pipeline_idx = 0;
while pipeline_idx < block.pipelines.len() {
let pipeline = &block.pipelines[pipeline_idx];
let mut i = 0;
while i < pipeline.elements.len() {
let mut last_element_is_pipeline_or = false;
while i < pipeline.elements.len() && !last_element_is_pipeline_or {
let redirect_stderr = redirect_stderr
|| ((i < pipeline.elements.len() - 1)
&& (matches!(
@ -964,6 +956,8 @@ pub fn eval_block(
| PipelineElement::Redirection(_, Redirection::StdoutAndStderr, _)
)));
last_element_is_pipeline_or = matches!(&pipeline.elements[i], PipelineElement::Or(..));
// if eval internal command failed, it can just make early return with `Err(ShellError)`.
let eval_result = eval_element_with_input(
engine_state,
@ -990,10 +984,16 @@ pub fn eval_block(
// don't return `Err(ShellError)`, so nushell wouldn't show extra error message.
}
(Err(error), true) => input = PipelineData::Value(Value::Error { error }, None),
(output, false) if last_element_is_pipeline_or => match output {
Ok(output) => {
input = output.0;
}
Err(error) => input = PipelineData::Value(Value::Error { error }, None),
},
(output, false) => {
let output = output?;
input = output.0;
// external command may runs to failed
// external command may have 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 output.1 {
@ -1006,83 +1006,109 @@ pub fn eval_block(
}
if pipeline_idx < (num_pipelines) - 1 {
match input {
PipelineData::Value(Value::Nothing { .. }, ..) => {}
PipelineData::ExternalStream {
ref mut exit_code, ..
} => {
let exit_code = exit_code.take();
if last_element_is_pipeline_or {
let input_is_error = matches!(input, PipelineData::Value(Value::Error { .. }, ..));
// Drain the input to the screen via tabular output
let config = engine_state.get_config();
let result =
drain_and_print(engine_state, stack, input, redirect_stdout, redirect_stderr);
match engine_state.find_decl("table".as_bytes(), &[]) {
Some(decl_id) => {
let table = engine_state.get_decl(decl_id).run(
engine_state,
stack,
&Call::new(Span::new(0, 0)),
input,
)?;
let last_exit_code = stack.last_exit_code(engine_state).unwrap_or(0);
print_or_return(table, config)?;
}
None => {
print_or_return(input, config)?;
}
};
if let Some(exit_code) = exit_code {
let mut v: Vec<_> = exit_code.collect();
if let Some(v) = v.pop() {
stack.add_env_var("LAST_EXIT_CODE".into(), v);
}
}
if last_exit_code == 0 && result.is_ok() && !input_is_error {
// Skip the next pipeline ot run because this pipeline was successful and the
// user used the `a || b` connector
pipeline_idx += 1;
}
_ => {
// Drain the input to the screen via tabular output
let config = engine_state.get_config();
input = PipelineData::empty()
} else {
drain_and_print(engine_state, stack, input, redirect_stdout, redirect_stderr)?;
match engine_state.find_decl("table".as_bytes(), &[]) {
Some(decl_id) => {
let table = engine_state.get_decl(decl_id);
if let Some(block_id) = table.get_block_id() {
let block = engine_state.get_block(block_id);
eval_block(
engine_state,
stack,
block,
input,
redirect_stdout,
redirect_stderr,
)?;
} else {
let table = table.run(
engine_state,
stack,
&Call::new(Span::new(0, 0)),
input,
)?;
print_or_return(table, config)?;
}
}
None => {
print_or_return(input, config)?;
}
};
}
input = PipelineData::empty()
}
input = PipelineData::empty()
}
pipeline_idx += 1;
}
Ok(input)
}
fn drain_and_print(
engine_state: &EngineState,
stack: &mut Stack,
mut input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<(), ShellError> {
match input {
PipelineData::Value(Value::Nothing { .. }, ..) => {}
PipelineData::ExternalStream {
ref mut exit_code, ..
} => {
let exit_code = exit_code.take();
// Drain the input to the screen via tabular output
let config = engine_state.get_config();
match engine_state.find_decl("table".as_bytes(), &[]) {
Some(decl_id) => {
let table = engine_state.get_decl(decl_id).run(
engine_state,
stack,
&Call::new(Span::new(0, 0)),
input,
)?;
print_or_return(table, config)?;
}
None => {
print_or_return(input, config)?;
}
};
if let Some(exit_code) = exit_code {
let mut v: Vec<_> = exit_code.collect();
if let Some(v) = v.pop() {
stack.add_env_var("LAST_EXIT_CODE".into(), v);
}
}
}
_ => {
// Drain the input to the screen via tabular output
let config = engine_state.get_config();
match engine_state.find_decl("table".as_bytes(), &[]) {
Some(decl_id) => {
let table = engine_state.get_decl(decl_id);
if let Some(block_id) = table.get_block_id() {
let block = engine_state.get_block(block_id);
eval_block(
engine_state,
stack,
block,
input,
redirect_stdout,
redirect_stderr,
)?;
} else {
let table =
table.run(engine_state, stack, &Call::new(Span::new(0, 0)), input)?;
print_or_return(table, config)?;
}
}
None => {
print_or_return(input, config)?;
}
};
}
}
Ok(())
}
fn print_or_return(pipeline_data: PipelineData, config: &Config) -> Result<(), ShellError> {
for item in pipeline_data {
if let Value::Error { error } = item {