Add array support for local/export/readonly builtins

Add array length syntax ${arr[#]}

Map read path now expands variables before splitting on ., fixing map "$node" with dotted paths

Map assignment path uses quote-aware token splitting, enabling quoted keys like "--type="

Completion errors now display above prompt instead of being overwritten

Fix nested if/fi parser bug when closing keywords appear on separate lines

Add QuoteState enum, replacing ad-hoc quote tracking booleans across lexer, highlighter, and expansion

Add split_tk_at/split_tk for quote-aware token splitting with span preservation

Refactor setup_builtin to accept optional argv for deferred expansion

Add ariadne dependency (not yet wired up)
This commit is contained in:
2026-02-28 15:51:09 -05:00
parent ab5f42b281
commit 1b63eff783
26 changed files with 375 additions and 281 deletions

View File

@@ -1,7 +1,7 @@
use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
parse::{NdRule, Node, lex::split_tk_at},
prelude::*,
procio::{IoStack, borrow_fd},
state::{self, VarFlags, VarKind, read_vars, write_vars},
@@ -18,7 +18,10 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
// Remove "readonly" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] };
if argv.is_empty() {
// Display the local variables
@@ -38,10 +41,17 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, vars_output.as_bytes())?; // Write it
} else {
for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::READONLY))?;
for tk in argv {
if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
let var = var_tk.expand()?.get_words().join(" ");
let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') {
VarKind::arr_from_tk(val_tk.clone())?
} else {
VarKind::Str(val_tk.expand()?.get_words().join(" "))
};
write_vars(|v| v.set_var(&var, val, VarFlags::READONLY))?;
} else {
let arg = tk.clone().expand()?.get_words().join(" ");
write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::READONLY))?;
}
}
@@ -61,7 +71,8 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?;
let argv = argv.unwrap();
if argv.is_empty() {
return Err(ShErr::full(
@@ -95,7 +106,10 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
// Remove "export" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] };
if argv.is_empty() {
// Display the environment variables
@@ -109,12 +123,18 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, env_output.as_bytes())?; // Write it
} else {
for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::EXPORT))?;
for tk in argv {
if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
let var = var_tk.expand()?.get_words().join(" ");
let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') {
VarKind::arr_from_tk(val_tk.clone())?
} else {
VarKind::Str(val_tk.expand()?.get_words().join(" "))
};
write_vars(|v| v.set_var(&var, val, VarFlags::EXPORT))?;
} else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any
let arg = tk.clone().expand()?.get_words().join(" ");
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any
}
}
}
@@ -131,7 +151,10 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
// Remove "local" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] };
if argv.is_empty() {
// Display the local variables
@@ -150,10 +173,17 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, vars_output.as_bytes())?; // Write it
} else {
for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::LOCAL))?;
for tk in argv {
if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
let var = var_tk.expand()?.get_words().join(" ");
let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') {
VarKind::arr_from_tk(val_tk.clone())?
} else {
VarKind::Str(val_tk.expand()?.get_words().join(" "))
};
write_vars(|v| v.set_var(&var, val, VarFlags::LOCAL))?;
} else {
let arg = tk.clone().expand()?.get_words().join(" ");
write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::LOCAL))?;
}
}