Skip to content

Commit 3728728

Browse files
authored
Allow let to be able to be used at the end of the pipeline (#17247)
Fun change here. This PR allows the `let` command to be used at the end of a pipeline. <img width="2316" height="1538" alt="image" src="https://github.com/user-attachments/assets/8acf9a8e-79c1-4be8-8fb6-93966f671fc4" /> Disallow infinite ranges when used with `let` at the end of the pipeline <img width="1911" height="1203" alt="image" src="https://github.com/user-attachments/assets/dfed053c-8b5c-476c-9b09-c3df3ca7dc38" /> Related #16404, #11951 ## Release notes summary - What our users need to know The `let` command is now allowed to be at the end of the pipeline when a variable name is also provided. ```nushell ls | get name | let files ``` ## Tasks after submitting <!-- Remove any tasks which aren't relevant for your PR, or add your own --> - [ ] Update the [documentation](https://github.com/nushell/nushell.github.io)
1 parent 9989a11 commit 3728728

File tree

4 files changed

+57
-16
lines changed

4 files changed

+57
-16
lines changed

crates/nu-cmd-lang/src/core_commands/let_.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl Command for Let {
1818
.input_output_types(vec![(Type::Any, Type::Nothing)])
1919
.allow_variants_without_examples(true)
2020
.required("var_name", SyntaxShape::VarWithOptType, "Variable name.")
21-
.required(
21+
.optional(
2222
"initial_value",
2323
SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)),
2424
"Equals sign followed by value.",
@@ -71,6 +71,11 @@ impl Command for Let {
7171
example: "let x = if false { -1 } else { 1 }",
7272
result: None,
7373
},
74+
Example {
75+
description: "Set a variable to the output of a pipeline",
76+
example: "ls | let files",
77+
result: None,
78+
},
7479
]
7580
}
7681
}

crates/nu-engine/src/compile/keyword.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -297,30 +297,36 @@ pub(crate) fn compile_let(
297297
) -> Result<(), CompileError> {
298298
// Pseudocode:
299299
//
300-
// %io_reg <- ...<block>... <- %io_reg
300+
// %io_reg <- ...<block>... <- %io_reg (if block provided)
301301
// store-variable $var, %io_reg
302302
let invalid = || CompileError::InvalidKeywordCall {
303303
keyword: "let".into(),
304304
span: call.head,
305305
};
306306

307307
let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?;
308-
let block_arg = call.positional_nth(1).ok_or_else(invalid)?;
309-
310308
let var_id = var_decl_arg.as_var().ok_or_else(invalid)?;
311-
let block_id = block_arg.as_block().ok_or_else(invalid)?;
312-
let block = working_set.get_block(block_id);
313309

314-
let variable = working_set.get_variable(var_id);
310+
// Handle the optional initial_value (expression after =)
311+
// Two cases:
312+
// 1. `let var = expr`: compile the expr block and store its result
313+
// 2. `let var` (at end of pipeline): use the pipeline input directly
314+
if let Some(block_arg) = call.positional_nth(1) {
315+
let block_id = block_arg.as_block().ok_or_else(invalid)?;
316+
let block = working_set.get_block(block_id);
315317

316-
compile_block(
317-
working_set,
318-
builder,
319-
block,
320-
RedirectModes::value(call.head),
321-
Some(io_reg),
322-
io_reg,
323-
)?;
318+
compile_block(
319+
working_set,
320+
builder,
321+
block,
322+
RedirectModes::value(call.head),
323+
Some(io_reg),
324+
io_reg,
325+
)?;
326+
}
327+
// If no initial_value provided, io_reg already contains the pipeline input to assign
328+
329+
let variable = working_set.get_variable(var_id);
324330

325331
// If the variable is a glob type variable, we should cast it with GlobFrom
326332
if variable.ty == Type::Glob {

crates/nu-parser/src/parser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6051,7 +6051,7 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex
60516051

60526052
parse_call(working_set, &spans[pos..], spans[0])
60536053
}
6054-
b"let" | b"const" | b"mut" => {
6054+
b"const" | b"mut" => {
60556055
working_set.error(ParseError::AssignInPipeline(
60566056
String::from_utf8(bytes)
60576057
.expect("builtin commands bytes should be able to convert to string"),

crates/nu-parser/tests/test_parser.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3169,3 +3169,33 @@ mod record {
31693169
assert_ne!(op.span, rhs.span)
31703170
}
31713171
}
3172+
3173+
#[test]
3174+
fn parse_let_in_pipeline() {
3175+
let engine_state = EngineState::new();
3176+
let mut working_set = StateWorkingSet::new(&engine_state);
3177+
3178+
// This should parse without errors
3179+
let block = parse(&mut working_set, None, b"ls | let files", true);
3180+
3181+
assert!(
3182+
working_set.parse_errors.is_empty(),
3183+
"Parse errors: {:?}",
3184+
working_set.parse_errors
3185+
);
3186+
3187+
// Check that we have a pipeline with two elements
3188+
assert_eq!(block.pipelines.len(), 1);
3189+
let pipeline = &block.pipelines[0];
3190+
assert_eq!(pipeline.elements.len(), 2);
3191+
3192+
// Both elements should be external calls since ls and let are not declared
3193+
assert!(matches!(
3194+
pipeline.elements[0].expr.expr,
3195+
Expr::ExternalCall(_, _)
3196+
));
3197+
assert!(matches!(
3198+
pipeline.elements[1].expr.expr,
3199+
Expr::ExternalCall(_, _)
3200+
));
3201+
}

0 commit comments

Comments
 (0)