Skip to content

Commit 8e05412

Browse files
authored
new command: unlet (#17270)
Presenting a new command `unlet` that will delete variables from the nushell memory. I'm not sure how acutally useful this is but I thought it would be fun to play around and see if it's useful. I don't really like the name but it says what it does so that counts for something. I also kind of see this as a tiny first step toward some type of garbage collection. <img width="1907" height="681" alt="image" src="https://github.com/user-attachments/assets/a4caffab-4ef8-447e-a3ed-8c2e7256aaaa" /> nushell's internal variables like `$in`, `$env`, `$nu` are protected from deletion. Feel free to take it for a spin and let me know if it's trash. ## Release notes summary - What our users need to know The new command `unlet` will delete variables from memory. ## Tasks after submitting N/A
1 parent ec9b7a6 commit 8e05412

File tree

7 files changed

+283
-4
lines changed

7 files changed

+283
-4
lines changed

crates/nu-command/src/default_context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
9797

9898
// Misc
9999
bind_command! {
100+
DeleteVar,
100101
Panic,
101102
Source,
102103
Tutor,

crates/nu-command/src/misc/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
mod panic;
22
mod source;
33
mod tutor;
4+
mod unlet;
45

56
pub use panic::Panic;
67
pub use source::Source;
78
pub use tutor::Tutor;
9+
pub use unlet::DeleteVar;
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use nu_engine::command_prelude::*;
2+
use nu_protocol::engine::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID};
3+
4+
#[derive(Clone)]
5+
pub struct DeleteVar;
6+
7+
impl Command for DeleteVar {
8+
fn name(&self) -> &str {
9+
"unlet"
10+
}
11+
12+
fn description(&self) -> &str {
13+
"Delete variables from nushell memory, making them unrecoverable."
14+
}
15+
16+
fn signature(&self) -> nu_protocol::Signature {
17+
Signature::build("unlet")
18+
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
19+
.rest(
20+
"rest",
21+
SyntaxShape::Any,
22+
"The variables to delete (pass as $variable_name).",
23+
)
24+
.category(Category::Experimental)
25+
}
26+
27+
fn run(
28+
&self,
29+
_engine_state: &EngineState,
30+
stack: &mut Stack,
31+
call: &Call,
32+
_input: PipelineData,
33+
) -> Result<PipelineData, ShellError> {
34+
// Collect all positional arguments passed to the command
35+
let expressions: Vec<_> = (0..).map_while(|i| call.positional_nth(stack, i)).collect();
36+
37+
// Ensure at least one argument is provided
38+
if expressions.is_empty() {
39+
return Err(ShellError::GenericError {
40+
error: "Wrong number of arguments".into(),
41+
msg: "unlet takes at least one argument".into(),
42+
span: Some(call.head),
43+
help: None,
44+
inner: vec![],
45+
});
46+
}
47+
48+
// Validate each argument and collect valid variable IDs
49+
let mut var_ids = Vec::with_capacity(expressions.len());
50+
for expr in expressions {
51+
match &expr.expr {
52+
nu_protocol::ast::Expr::Var(var_id) => {
53+
// Prevent deletion of built-in variables that are essential for nushell operation
54+
if var_id == &NU_VARIABLE_ID
55+
|| var_id == &ENV_VARIABLE_ID
56+
|| var_id == &IN_VARIABLE_ID
57+
{
58+
// Determine the variable name for the error message
59+
let var_name = match *var_id {
60+
NU_VARIABLE_ID => "nu",
61+
ENV_VARIABLE_ID => "env",
62+
IN_VARIABLE_ID => "in",
63+
_ => "unknown", // This should never happen due to the check above
64+
};
65+
66+
return Err(ShellError::GenericError {
67+
error: "Cannot delete built-in variable".into(),
68+
msg: format!(
69+
"'${}' is a built-in variable and cannot be deleted",
70+
var_name
71+
),
72+
span: Some(expr.span),
73+
help: None,
74+
inner: vec![],
75+
});
76+
}
77+
var_ids.push(*var_id);
78+
}
79+
_ => {
80+
// Argument is not a variable reference
81+
return Err(ShellError::GenericError {
82+
error: "Not a variable".into(),
83+
msg: "Argument must be a variable reference like $x".into(),
84+
span: Some(expr.span),
85+
help: Some("Use $variable_name to refer to the variable".into()),
86+
inner: vec![],
87+
});
88+
}
89+
}
90+
}
91+
92+
// Remove all valid variables from the stack
93+
for var_id in var_ids {
94+
stack.remove_var(var_id);
95+
}
96+
97+
Ok(PipelineData::empty())
98+
}
99+
100+
fn requires_ast_for_arguments(&self) -> bool {
101+
true
102+
}
103+
104+
fn examples(&self) -> Vec<Example<'_>> {
105+
vec![
106+
Example {
107+
example: "let x = 42; unlet $x",
108+
description: "Delete a variable from memory",
109+
result: None,
110+
},
111+
Example {
112+
example: "let x = 1; let y = 2; unlet $x $y",
113+
description: "Delete multiple variables from memory",
114+
result: None,
115+
},
116+
Example {
117+
example: "unlet $nu",
118+
description: "Attempting to delete a built-in variable fails",
119+
result: None,
120+
},
121+
Example {
122+
example: "unlet 42",
123+
description: "Attempting to delete a non-variable fails",
124+
result: None,
125+
},
126+
]
127+
}
128+
}

crates/nu-command/tests/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ mod try_;
119119
mod ucp;
120120
#[cfg(unix)]
121121
mod ulimit;
122+
mod unlet;
122123
mod window;
123124

124125
mod debug;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use nu_test_support::nu;
2+
3+
#[test]
4+
fn unlet_basic() {
5+
let actual = nu!("let x = 42; unlet $x; $x");
6+
7+
assert!(actual.err.contains("Variable not found"));
8+
}
9+
10+
#[test]
11+
fn unlet_builtin_nu() {
12+
let actual = nu!("unlet $nu");
13+
14+
assert!(actual.err.contains("cannot be deleted"));
15+
}
16+
17+
#[test]
18+
fn unlet_builtin_env() {
19+
let actual = nu!("unlet $env");
20+
21+
assert!(actual.err.contains("cannot be deleted"));
22+
}
23+
24+
#[test]
25+
fn unlet_not_variable() {
26+
let actual = nu!("unlet 42");
27+
28+
assert!(
29+
actual
30+
.err
31+
.contains("Argument must be a variable reference like $x")
32+
);
33+
}
34+
35+
#[test]
36+
fn unlet_wrong_number_args() {
37+
let actual = nu!("unlet");
38+
39+
assert!(actual.err.contains("unlet takes at least one argument"));
40+
}
41+
42+
#[test]
43+
fn unlet_multiple_args() {
44+
let actual = nu!("let x = 1; let y = 2; unlet $x $y; $x");
45+
46+
assert!(actual.err.contains("Variable not found"));
47+
}
48+
49+
#[test]
50+
fn unlet_multiple_deletes_both() {
51+
let actual = nu!("let x = 1; let y = 2; unlet $x $y; $y");
52+
53+
assert!(actual.err.contains("Variable not found"));
54+
}

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

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::Arc;
33
use nu_protocol::{
44
IntoSpanned, RegId, Span, Spanned,
55
ast::{Argument, Call, Expression, ExternalArgument},
6-
engine::StateWorkingSet,
6+
engine::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, StateWorkingSet},
77
ir::{Instruction, IrAstRef, Literal},
88
};
99

@@ -77,6 +77,11 @@ pub(crate) fn compile_call(
7777
}
7878
}
7979

80+
// Special handling for builtin commands that have direct IR equivalents
81+
if decl.name() == "unlet" {
82+
return compile_unlet(working_set, builder, call, io_reg);
83+
}
84+
8085
// Keep AST if the decl needs it.
8186
let requires_ast = decl.requires_ast_for_arguments();
8287

@@ -270,3 +275,85 @@ pub(crate) fn compile_external_call(
270275

271276
compile_call(working_set, builder, &call, redirect_modes, io_reg)
272277
}
278+
279+
pub(crate) fn compile_unlet(
280+
_working_set: &StateWorkingSet,
281+
builder: &mut BlockBuilder,
282+
call: &Call,
283+
io_reg: RegId,
284+
) -> Result<(), CompileError> {
285+
// unlet takes one or more positional arguments which should be variable references
286+
if call.positional_len() == 0 {
287+
return Err(CompileError::InvalidLiteral {
288+
msg: "unlet takes at least one argument".into(),
289+
span: call.head,
290+
});
291+
}
292+
293+
// Process each positional argument
294+
for i in 0..call.positional_len() {
295+
let Some(arg) = call.positional_nth(i) else {
296+
return Err(CompileError::InvalidLiteral {
297+
msg: "Expected positional argument".into(),
298+
span: call.head,
299+
});
300+
};
301+
302+
// Extract variable ID from the expression
303+
// Handle both direct variable references (Expr::Var) and full cell paths (Expr::FullCellPath)
304+
// that represent simple variables (e.g., $var parsed as FullCellPath with empty tail).
305+
// This allows unlet to work with variables parsed in different contexts.
306+
let var_id = match &arg.expr {
307+
nu_protocol::ast::Expr::Var(var_id) => Some(*var_id),
308+
nu_protocol::ast::Expr::FullCellPath(cell_path) => {
309+
if cell_path.tail.is_empty() {
310+
match &cell_path.head.expr {
311+
nu_protocol::ast::Expr::Var(var_id) => Some(*var_id),
312+
_ => None,
313+
}
314+
} else {
315+
None
316+
}
317+
}
318+
_ => None,
319+
};
320+
321+
match var_id {
322+
Some(var_id) => {
323+
// Prevent deletion of built-in variables that are essential for nushell operation
324+
if var_id == NU_VARIABLE_ID || var_id == ENV_VARIABLE_ID || var_id == IN_VARIABLE_ID
325+
{
326+
// Determine the variable name for the error message
327+
let var_name = match var_id {
328+
NU_VARIABLE_ID => "nu",
329+
ENV_VARIABLE_ID => "env",
330+
IN_VARIABLE_ID => "in",
331+
_ => "unknown", // This should never happen due to the check above
332+
};
333+
334+
return Err(CompileError::InvalidLiteral {
335+
msg: format!(
336+
"'${}' is a built-in variable and cannot be deleted",
337+
var_name
338+
),
339+
span: arg.span,
340+
});
341+
}
342+
343+
// Emit instruction to drop the variable
344+
builder.push(Instruction::DropVariable { var_id }.into_spanned(call.head))?;
345+
}
346+
None => {
347+
// Argument is not a valid variable reference
348+
return Err(CompileError::InvalidLiteral {
349+
msg: "Argument must be a variable reference like $x".into(),
350+
span: arg.span,
351+
});
352+
}
353+
}
354+
}
355+
356+
// Load empty value as the result
357+
builder.load_empty(io_reg)?;
358+
Ok(())
359+
}

crates/nu-engine/src/scope.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,15 @@ impl<'e, 's> ScopeData<'e, 's> {
5757
let var_type = Value::string(var.ty.to_string(), span);
5858
let is_const = Value::bool(var.const_val.is_some(), span);
5959

60-
let var_value = self
61-
.stack
62-
.get_var(**var_id, span)
60+
let var_value_result = self.stack.get_var(**var_id, span);
61+
62+
// Skip variables that have no value in the stack and are not constants.
63+
// This ensures that variables deleted with unlet disappear from scope variables.
64+
if var_value_result.is_err() && var.const_val.is_none() {
65+
continue;
66+
}
67+
68+
let var_value = var_value_result
6369
.ok()
6470
.or(var.const_val.clone())
6571
.unwrap_or(Value::nothing(span));

0 commit comments

Comments
 (0)