Skip to main content

nu_cmd_lang/core_commands/
let_.rs

1use nu_engine::command_prelude::*;
2use nu_engine::eval_expression;
3use nu_protocol::ast::Expr;
4use nu_protocol::debugger::WithoutDebug;
5use nu_protocol::engine::CommandType;
6
7#[derive(Clone)]
8pub struct Let;
9
10impl Command for Let {
11    fn name(&self) -> &str {
12        "let"
13    }
14
15    fn description(&self) -> &str {
16        "Create a variable and give it a value."
17    }
18
19    fn signature(&self) -> nu_protocol::Signature {
20        Signature::build("let")
21            .input_output_types(vec![(Type::Any, Type::Any)])
22            .allow_variants_without_examples(true)
23            .required(
24                "var_name",
25                SyntaxShape::VarWithOptType,
26                "The variable name to create.",
27            )
28            .optional(
29                "initial_value",
30                SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)),
31                "Equals sign followed by value.",
32            )
33            .category(Category::Core)
34    }
35
36    fn extra_description(&self) -> &str {
37        r#"This command is a parser keyword. For details, check:
38  https://www.nushell.sh/book/thinking_in_nu.html"#
39    }
40
41    fn command_type(&self) -> CommandType {
42        CommandType::Keyword
43    }
44
45    fn search_terms(&self) -> Vec<&str> {
46        vec!["set", "const"]
47    }
48
49    fn run(
50        &self,
51        engine_state: &EngineState,
52        stack: &mut Stack,
53        call: &Call,
54        input: PipelineData,
55    ) -> Result<PipelineData, ShellError> {
56        let expr = call
57            .positional_nth(stack, 0)
58            .ok_or(ShellError::NushellFailed {
59                msg: "Missing variable name".to_string(),
60            })?;
61        let var_id = expr.as_var().ok_or(ShellError::NushellFailed {
62            msg: "Expected variable".to_string(),
63        })?;
64
65        let initial_value = call.get_parser_info(stack, "initial_value").cloned();
66
67        // Evaluate the right-hand side value:
68        // - If there's an initial_value (let x = expr), evaluate the expression
69        // - Otherwise (let x), use the pipeline input
70        let rhs = if let Some(ref initial_value_expr) = initial_value {
71            // Validate that blocks/subexpressions don't have multiple pipeline elements
72            if let Expr::Block(block_id) | Expr::Subexpression(block_id) = &initial_value_expr.expr
73            {
74                let block = engine_state.get_block(*block_id);
75                if block
76                    .pipelines
77                    .iter()
78                    .any(|pipeline| pipeline.elements.len() > 1)
79                {
80                    return Err(ShellError::NushellFailed {
81                        msg: "invalid `let` keyword call".to_string(),
82                    });
83                }
84            }
85            // Discard input when using = syntax
86            let _ = input.into_value(call.head)?;
87            eval_expression::<WithoutDebug>(engine_state, stack, initial_value_expr)?
88        } else {
89            // Use pipeline input directly when no = is provided
90            input.into_value(call.head)?
91        };
92
93        // If the variable is declared `: glob` and the RHS is a string,
94        // coerce it to an *expandable* `Value::Glob(..., no_expand = false)` so
95        // runtime `let` behavior matches the compiled `GlobFrom { no_expand: false }`.
96        // This ensures `let g: glob = "*.toml"; ls $g` expands like a glob
97        // literal and keeps parity with `into glob`.
98        let value_to_store = {
99            let variable = engine_state.get_var(var_id);
100            if variable.ty == Type::Glob {
101                match &rhs {
102                    Value::String { val, .. } => Value::glob(val.clone(), false, rhs.span()),
103                    _ => rhs.clone(),
104                }
105            } else {
106                rhs.clone()
107            }
108        };
109        stack.add_var(var_id, value_to_store);
110
111        if initial_value.is_some() {
112            // `let var = expr`: suppress output (traditional assignment, no display)
113            Ok(PipelineData::Empty)
114        } else {
115            // `input | let var`: pass through the assigned value
116            Ok(PipelineData::Value(rhs, None))
117        }
118    }
119
120    fn examples(&self) -> Vec<Example<'_>> {
121        vec![
122            Example {
123                description: "Set a variable to a value (no output).",
124                example: "let x = 10",
125                result: None,
126            },
127            Example {
128                description: "Set a variable to the result of an expression (no output).",
129                example: "let x = 10 + 100",
130                result: None,
131            },
132            Example {
133                description: "Set a variable based on the condition (no output).",
134                example: "let x = if false { -1 } else { 1 }",
135                result: None,
136            },
137            Example {
138                description: "Set a variable to the output of a pipeline.",
139                example: "ls | let files",
140                result: None,
141            },
142            Example {
143                description: "Use let in the middle of a pipeline to assign and pass the value.",
144                example: "10 | let x | $x + 5",
145                result: Some(Value::test_int(15)),
146            },
147            Example {
148                description: "Use let in the middle of a pipeline, then consume value with $in.",
149                example: "10 | let x | $in + 5",
150                result: Some(Value::test_int(15)),
151            },
152        ]
153    }
154}
155
156#[cfg(test)]
157mod test {
158    use super::*;
159
160    #[test]
161    fn test_examples() {
162        use crate::test_examples;
163
164        test_examples(Let {})
165    }
166
167    #[test]
168    fn test_command_type() {
169        assert!(matches!(Let.command_type(), CommandType::Keyword));
170    }
171}