implemented builtins: readonly, unset, true, false, and : (no-op)

This commit is contained in:
2026-02-24 17:57:09 -05:00
parent fee45145c8
commit 2f0cb931d6
5 changed files with 234 additions and 108 deletions

View File

@@ -1,87 +0,0 @@
use crate::{
jobs::JobBldr,
libsh::error::ShResult,
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state::{self, read_vars, write_vars, VarFlags},
};
use super::setup_builtin;
pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the environment variables
let mut env_output = env::vars()
.map(|var| format!("{}={}", var.0, var.1)) // Get all of them, zip them into one string
.collect::<Vec<_>>();
env_output.sort(); // Sort them alphabetically
let mut env_output = env_output.join("\n"); // Join them with newlines
env_output.push('\n'); // Push a final newline
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, val, VarFlags::EXPORT)); // Export an assignment like
// 'foo=bar'
} else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any
}
}
}
state::set_status(0);
Ok(())
}
pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the local variables
let vars_output = read_vars(|v| {
let mut vars = v
.flatten_vars()
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>();
vars.sort();
let mut vars_joined = vars.join("\n");
vars_joined.push('\n');
vars_joined
});
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, val, VarFlags::LOCAL));
} else {
write_vars(|v| v.set_var(&arg, "", VarFlags::LOCAL)); // Create an uninitialized local variable
}
}
}
state::set_status(0);
Ok(())
}

View File

@@ -8,13 +8,13 @@ use crate::{
execute::prepare_argv, execute::prepare_argv,
lex::{Span, Tk}, lex::{Span, Tk},
}, },
procio::{IoFrame, IoStack, RedirGuard}, procio::{IoFrame, IoStack, RedirGuard}, state,
}; };
pub mod alias; pub mod alias;
pub mod cd; pub mod cd;
pub mod echo; pub mod echo;
pub mod export; pub mod varcmds;
pub mod flowctl; pub mod flowctl;
pub mod jobctl; pub mod jobctl;
pub mod pwd; pub mod pwd;
@@ -29,10 +29,10 @@ pub mod dirstack;
pub mod exec; pub mod exec;
pub mod eval; pub mod eval;
pub const BUILTINS: [&str; 28] = [ pub const BUILTINS: [&str; 33] = [
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias",
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
"pushd", "popd", "dirs", "exec", "eval" "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset"
]; ];
/// Sets up a builtin command /// Sets up a builtin command
@@ -93,3 +93,18 @@ pub fn setup_builtin(
// io_frame.restore() // io_frame.restore()
Ok((argv, guard)) Ok((argv, guard))
} }
pub fn true_builtin() -> ShResult<()> {
state::set_status(0);
Ok(())
}
pub fn false_builtin() -> ShResult<()> {
state::set_status(1);
Ok(())
}
pub fn noop_builtin() -> ShResult<()> {
state::set_status(0);
Ok(())
}

163
src/builtin/varcmds.rs Normal file
View File

@@ -0,0 +1,163 @@
use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
prelude::*,
procio::{IoStack, borrow_fd},
state::{self, VarFlags, read_vars, write_vars},
};
use super::setup_builtin;
pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the local variables
let vars_output = read_vars(|v| {
let mut vars = v
.flatten_vars()
.into_iter()
.filter(|(_, v)| v.flags().contains(VarFlags::READONLY))
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>();
vars.sort();
let mut vars_joined = vars.join("\n");
vars_joined.push('\n');
vars_joined
});
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, val, VarFlags::READONLY))?;
} else {
write_vars(|v| v.set_var(&arg, "", VarFlags::READONLY))?;
}
}
}
state::set_status(0);
Ok(())
}
pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
return Err(ShErr::full(
ShErrKind::SyntaxErr,
"unset: Expected at least one argument",
blame
));
}
for (arg,span) in argv {
if !read_vars(|v| v.var_exists(&arg)) {
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("unset: No such variable '{arg}'"),
span
));
}
write_vars(|v| v.unset_var(&arg))?;
}
state::set_status(0);
Ok(())
}
pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the environment variables
let mut env_output = env::vars()
.map(|var| format!("{}={}", var.0, var.1)) // Get all of them, zip them into one string
.collect::<Vec<_>>();
env_output.sort(); // Sort them alphabetically
let mut env_output = env_output.join("\n"); // Join them with newlines
env_output.push('\n'); // Push a final newline
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, val, VarFlags::EXPORT))?;
} else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any
}
}
}
state::set_status(0);
Ok(())
}
pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the local variables
let vars_output = read_vars(|v| {
let mut vars = v
.flatten_vars()
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>();
vars.sort();
let mut vars_joined = vars.join("\n");
vars_joined.push('\n');
vars_joined
});
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, val, VarFlags::LOCAL))?;
} else {
write_vars(|v| v.set_var(&arg, "", VarFlags::LOCAL))?;
}
}
}
state::set_status(0);
Ok(())
}

View File

@@ -2,7 +2,7 @@ use std::{collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt};
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, cd::cd, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, export::{export, local}, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak alias::{alias, unalias}, cd::cd, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, true_builtin, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak
}, },
expand::{expand_aliases, glob_to_regex}, expand::{expand_aliases, glob_to_regex},
jobs::{ChildProc, JobStack, dispatch_job}, jobs::{ChildProc, JobStack, dispatch_job},
@@ -90,7 +90,7 @@ impl Drop for VarCtxGuard {
fn drop(&mut self) { fn drop(&mut self) {
write_vars(|v| { write_vars(|v| {
for var in &self.vars { for var in &self.vars {
v.unset_var(var); v.unset_var(var).ok();
} }
}); });
} }
@@ -569,7 +569,7 @@ impl Dispatcher {
.zip(chunk.iter().chain(std::iter::repeat(&empty))); .zip(chunk.iter().chain(std::iter::repeat(&empty)));
for (var, val) in chunk_iter { for (var, val) in chunk_iter {
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE)); write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE))?;
for_guard.vars.insert(var.to_string()); for_guard.vars.insert(var.to_string());
} }
@@ -769,6 +769,16 @@ impl Dispatcher {
"dirs" => dirs(cmd, io_stack_mut, curr_job_mut), "dirs" => dirs(cmd, io_stack_mut, curr_job_mut),
"exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut), "exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut),
"eval" => eval::eval(cmd, io_stack_mut, curr_job_mut), "eval" => eval::eval(cmd, io_stack_mut, curr_job_mut),
"readonly" => readonly(cmd, io_stack_mut, curr_job_mut),
"unset" => unset(cmd, io_stack_mut, curr_job_mut),
"true" | ":" => {
state::set_status(0);
Ok(())
},
"false" => {
state::set_status(1);
Ok(())
},
_ => unimplemented!( _ => unimplemented!(
"Have not yet added support for builtin '{}'", "Have not yet added support for builtin '{}'",
cmd_raw cmd_raw
@@ -885,7 +895,7 @@ impl Dispatcher {
let var = var.span.as_str(); let var = var.span.as_str();
let val = val.expand()?.get_words().join(" "); let val = val.expand()?.get_words().join(" ");
match kind { match kind {
AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::EXPORT)), AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::EXPORT))?,
AssignKind::PlusEq => todo!(), AssignKind::PlusEq => todo!(),
AssignKind::MinusEq => todo!(), AssignKind::MinusEq => todo!(),
AssignKind::MultEq => todo!(), AssignKind::MultEq => todo!(),
@@ -902,7 +912,7 @@ impl Dispatcher {
let var = var.span.as_str(); let var = var.span.as_str();
let val = val.expand()?.get_words().join(" "); let val = val.expand()?.get_words().join(" ");
match kind { match kind {
AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::NONE)), AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::NONE))?,
AssignKind::PlusEq => todo!(), AssignKind::PlusEq => todo!(),
AssignKind::MinusEq => todo!(), AssignKind::MinusEq => todo!(),
AssignKind::MultEq => todo!(), AssignKind::MultEq => todo!(),

View File

@@ -151,13 +151,16 @@ impl ScopeStack {
pub fn cur_scope_mut(&mut self) -> &mut VarTab { pub fn cur_scope_mut(&mut self) -> &mut VarTab {
self.scopes.last_mut().unwrap() self.scopes.last_mut().unwrap()
} }
pub fn unset_var(&mut self, var_name: &str) { pub fn unset_var(&mut self, var_name: &str) -> ShResult<()> {
for scope in self.scopes.iter_mut().rev() { for scope in self.scopes.iter_mut().rev() {
if scope.var_exists(var_name) { if scope.var_exists(var_name) {
scope.unset_var(var_name); return scope.unset_var(var_name);
return;
} }
} }
Err(ShErr::simple(
ShErrKind::ExecFail,
format!("Variable '{}' not found", var_name)
))
} }
pub fn export_var(&mut self, var_name: &str) { pub fn export_var(&mut self, var_name: &str) {
for scope in self.scopes.iter_mut().rev() { for scope in self.scopes.iter_mut().rev() {
@@ -187,22 +190,26 @@ impl ScopeStack {
} }
flat_vars flat_vars
} }
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) -> ShResult<()> {
let is_local = self.is_local_var(var_name); let is_local = self.is_local_var(var_name);
if flags.contains(VarFlags::LOCAL) || is_local { if flags.contains(VarFlags::LOCAL) || is_local {
self.set_var_local(var_name, val, flags); self.set_var_local(var_name, val, flags)
} else { } else {
self.set_var_global(var_name, val, flags); self.set_var_global(var_name, val, flags)
} }
} }
fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) { fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) -> ShResult<()> {
if let Some(scope) = self.scopes.first_mut() { if let Some(scope) = self.scopes.first_mut() {
scope.set_var(var_name, val, flags); scope.set_var(var_name, val, flags)
} else {
Ok(())
} }
} }
fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) { fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) -> ShResult<()> {
if let Some(scope) = self.scopes.last_mut() { if let Some(scope) = self.scopes.last_mut() {
scope.set_var(var_name, val, flags); scope.set_var(var_name, val, flags)
} else {
Ok(())
} }
} }
pub fn get_var(&self, var_name: &str) -> String { pub fn get_var(&self, var_name: &str) -> String {
@@ -477,6 +484,9 @@ impl Var {
pub fn mark_for_export(&mut self) { pub fn mark_for_export(&mut self) {
self.flags.set(VarFlags::EXPORT, true); self.flags.set(VarFlags::EXPORT, true);
} }
pub fn flags(&self) -> VarFlags {
self.flags
}
} }
impl Display for Var { impl Display for Var {
@@ -654,13 +664,27 @@ impl VarTab {
pub fn get_var_flags(&self, var_name: &str) -> Option<VarFlags> { pub fn get_var_flags(&self, var_name: &str) -> Option<VarFlags> {
self.vars.get(var_name).map(|var| var.flags) self.vars.get(var_name).map(|var| var.flags)
} }
pub fn unset_var(&mut self, var_name: &str) { pub fn unset_var(&mut self, var_name: &str) -> ShResult<()> {
if let Some(var) = self.vars.get(var_name) && var.flags.contains(VarFlags::READONLY) {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("cannot unset readonly variable '{}'", var_name)
));
}
self.vars.remove(var_name); self.vars.remove(var_name);
unsafe { env::remove_var(var_name) }; unsafe { env::remove_var(var_name) };
Ok(())
} }
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) -> ShResult<()> {
if let Some(var) = self.vars.get_mut(var_name) { if let Some(var) = self.vars.get_mut(var_name) {
if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("Variable '{}' is readonly", var_name)
));
}
var.kind = VarKind::Str(val.to_string()); var.kind = VarKind::Str(val.to_string());
var.flags |= flags;
if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) { if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) { if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
var.mark_for_export(); var.mark_for_export();
@@ -675,6 +699,7 @@ impl VarTab {
} }
self.vars.insert(var_name.to_string(), var); self.vars.insert(var_name.to_string(), var);
} }
Ok(())
} }
pub fn var_exists(&self, var_name: &str) -> bool { pub fn var_exists(&self, var_name: &str) -> bool {
if let Ok(param) = var_name.parse::<ShellParam>() { if let Ok(param) = var_name.parse::<ShellParam>() {