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

@@ -18,7 +18,8 @@ pub fn alias(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() {
// Display the environment variables
@@ -67,7 +68,8 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul
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() {
// Display the environment variables

View File

@@ -1,3 +1,5 @@
use std::iter::Peekable;
use crate::{
getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state::{self, VarFlags, VarKind, write_vars}
};
@@ -51,7 +53,8 @@ fn arr_pop_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?;
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();
let stdout = borrow_fd(STDOUT_FILENO);
let mut status = 0;
@@ -91,7 +94,8 @@ fn arr_push_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: En
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let _arr_op_opts = get_arr_op_opts(opts)?;
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();
let mut argv = argv.into_iter();
let Some((name, _)) = argv.next() else {
@@ -140,7 +144,8 @@ pub fn arr_rotate(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShRe
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?;
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();
for (arg, _) in argv {
write_vars(|v| -> ShResult<()> {

View File

@@ -18,7 +18,8 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!()
};
let (argv, _) = setup_builtin(argv, job, None)?;
let (argv, _) = setup_builtin(Some(argv), job, None)?;
let argv = argv.unwrap();
let new_dir = if let Some((arg, _)) = argv.into_iter().next() {
PathBuf::from(arg)

View File

@@ -168,7 +168,8 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -
let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?;
let comp_opts = get_comp_opts(opts)?;
let (argv, _) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (argv, _) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?;
let argv = argv.unwrap();
if comp_opts.flags.contains(CompFlags::PRINT) {
if argv.is_empty() {
@@ -242,7 +243,7 @@ pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) ->
let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?;
let prefix = argv.clone().into_iter().nth(1).unwrap_or_default();
let comp_opts = get_comp_opts(opts)?;
let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (_, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?;
let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);

View File

@@ -127,7 +127,8 @@ pub fn pushd(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();
let mut dir = None;
let mut rotate_idx = None;
@@ -213,7 +214,8 @@ pub fn popd(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();
let mut remove_idx = None;
let mut no_cd = false;
@@ -316,7 +318,8 @@ pub fn dirs(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();
let mut abbreviate_home = true;
let mut one_per_line = false;

View File

@@ -51,7 +51,8 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
assert!(!argv.is_empty());
let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?;
let flags = get_echo_flags(opts).blame(blame)?;
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();
let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
borrow_fd(STDERR_FILENO)

View File

@@ -16,7 +16,8 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!()
};
let (expanded_argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (expanded_argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?;
let expanded_argv = expanded_argv.unwrap();
if expanded_argv.is_empty() {
state::set_status(0);

View File

@@ -18,7 +18,8 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh
unreachable!()
};
let (expanded_argv, guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (expanded_argv, guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?;
let expanded_argv = expanded_argv.unwrap();
if let Some(g) = guard {
// Persist redirections so they affect the entire shell,
// not just this command call

View File

@@ -28,7 +28,8 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR
unreachable!()
};
let (argv, _) = setup_builtin(argv, job, None)?;
let (argv, _) = setup_builtin(Some(argv), job, None)?;
let argv = argv.unwrap();
let mut argv = argv.into_iter();
if read_jobs(|j| j.get_fg().is_some()) {
@@ -143,7 +144,8 @@ pub fn jobs(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();
let mut flags = JobCmdFlags::empty();
for (arg, span) in argv {
@@ -190,7 +192,8 @@ pub fn disown(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();
let mut argv = argv.into_iter();
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {

View File

@@ -5,7 +5,7 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
use serde_json::{Map, Value};
use crate::{
expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_all_unescaped, split_at_unescaped}}, procio::{IoStack, borrow_fd}, state::{self, read_vars, write_vars}
expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_tk, split_tk_at}}, procio::{IoStack, borrow_fd}, state::{self, read_vars, write_vars}
};
#[derive(Debug, Clone)]
@@ -252,17 +252,23 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()
unreachable!()
};
let (argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?;
let (mut argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?;
let map_opts = get_map_opts(opts);
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
if !argv.is_empty() {
argv.remove(0); // remove "map" command from argv
}
for (arg,_) in argv {
if let Some((lhs,rhs)) = split_at_unescaped(&arg, "=") {
let path = split_all_unescaped(&lhs, ".");
for arg in argv {
if let Some((lhs,rhs)) = split_tk_at(&arg, "=") {
let path = split_tk(&lhs, ".")
.into_iter()
.map(|s| s.expand().map(|exp| exp.get_words().join(" ")))
.collect::<ShResult<Vec<String>>>()?;
let Some(name) = path.first() else {
return Err(ShErr::simple(
ShErrKind::InternalErr,
format!("invalid map path: {}", lhs)
format!("invalid map path: {}", lhs.as_str())
));
};
@@ -271,39 +277,42 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()
let make_leaf = |s: String| {
if is_func { MapNode::DynamicLeaf(s) } else { MapNode::StaticLeaf(s) }
};
let found = write_vars(|v| {
let expanded = rhs.expand()?.get_words().join(" ");
let found = write_vars(|v| -> ShResult<bool> {
if let Some(map) = v.get_map_mut(name) {
if is_json {
if let Ok(parsed) = serde_json::from_str::<Value>(&rhs) {
if let Ok(parsed) = serde_json::from_str::<Value>(expanded.as_str()) {
map.set(&path[1..], parsed.into());
} else {
map.set(&path[1..], make_leaf(rhs.clone()));
map.set(&path[1..], make_leaf(expanded.clone()));
}
} else {
map.set(&path[1..], make_leaf(rhs.clone()));
map.set(&path[1..], make_leaf(expanded.clone()));
}
true
Ok(true)
} else {
false
Ok(false)
}
});
if !found {
if !found? {
let mut new = MapNode::default();
if is_json && let Ok(parsed) = serde_json::from_str::<Value>(&rhs) {
if is_json /*&& let Ok(parsed) = serde_json::from_str::<Value>(rhs.as_str()) */{
let parsed = serde_json::from_str::<Value>(expanded.as_str()).unwrap();
let node: MapNode = parsed.into();
new.set(&path[1..], node);
} else {
new.set(&path[1..], make_leaf(rhs));
new.set(&path[1..], make_leaf(expanded));
}
write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL)));
}
} else {
let path = split_all_unescaped(&arg, ".");
let expanded = arg.expand()?.get_words().join(" ");
let path: Vec<String> = expanded.split('.').map(|s| s.to_string()).collect();
let Some(name) = path.first() else {
return Err(ShErr::simple(
ShErrKind::InternalErr,
format!("invalid map path: {}", &arg)
format!("invalid map path: {}", expanded)
));
};

View File

@@ -67,13 +67,13 @@ pub const BUILTINS: [&str; 41] = [
/// * If redirections are given, the second field of the resulting tuple will
/// *always* be `Some()`
/// * If no redirections are given, the second field will *always* be `None`
type SetupReturns = ShResult<(Vec<(String, Span)>, Option<RedirGuard>)>;
type SetupReturns = ShResult<(Option<Vec<(String, Span)>>, Option<RedirGuard>)>;
pub fn setup_builtin(
argv: Vec<Tk>,
argv: Option<Vec<Tk>>,
job: &mut JobBldr,
io_mode: Option<(&mut IoStack, Vec<Redir>)>,
) -> SetupReturns {
let mut argv: Vec<(String, Span)> = prepare_argv(argv)?;
let mut argv = argv.map(|argv| prepare_argv(argv)).transpose()?;
let child_pgid = if let Some(pgid) = job.pgid() {
pgid
@@ -81,18 +81,22 @@ pub fn setup_builtin(
job.set_pgid(Pid::this());
Pid::this()
};
let cmd_name = argv.remove(0).0;
let cmd_name = argv
.as_mut()
.and_then(|argv| {
if argv.is_empty() {
None
} else {
Some(argv.remove(0).0)
}
}).unwrap_or_else(|| String::new());
let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?;
job.push_child(child);
let guard = if let Some((io_stack, redirs)) = io_mode {
io_stack.append_to_frame(redirs);
let io_frame = io_stack.pop_frame();
let guard = io_frame.redirect()?;
Some(guard)
} else {
None
};
let guard = io_mode.map(|(io,rdrs)| {
io.append_to_frame(rdrs);
io.pop_frame().redirect()
}).transpose()?;
// We return the io_frame because the caller needs to also call
// io_frame.restore()

View File

@@ -18,7 +18,7 @@ pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()
unreachable!()
};
let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let (_, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?;
let stdout = borrow_fd(STDOUT_FILENO);

View File

@@ -75,7 +75,8 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?;
let read_opts = get_read_flags(opts).blame(blame.clone())?;
let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?;
let (argv, _) = setup_builtin(Some(argv), job, None).blame(blame.clone())?;
let argv = argv.unwrap();
if let Some(prompt) = read_opts.prompt {
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;

View File

@@ -16,7 +16,8 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!()
};
let (argv, _) = setup_builtin(argv, job, None)?;
let (argv, _) = setup_builtin(Some(argv), job, None)?;
let argv = argv.unwrap();
let mut argv = argv.into_iter();
if let Some((arg, span)) = argv.next() {

View File

@@ -18,7 +18,8 @@ pub fn shopt(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() {
let mut output = write_shopts(|s| s.display_opts())?;

View File

@@ -17,7 +17,8 @@ pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!()
};
let (argv, _) = setup_builtin(argv, job, None)?;
let (argv, _) = setup_builtin(Some(argv), job, None)?;
let argv = argv.unwrap();
for (arg, span) in argv {
let path = PathBuf::from(arg);

View File

@@ -123,7 +123,8 @@ pub fn trap(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() {
let stdout = borrow_fd(STDOUT_FILENO);

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))?;
}
}

View File

@@ -106,7 +106,8 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
}
}
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();
for (arg, span) in argv {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {