From 2f0cb931d6570212d8d5b552b313e3f4e20ee7d8 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Tue, 24 Feb 2026 17:57:09 -0500 Subject: [PATCH] implemented builtins: readonly, unset, true, false, and : (no-op) --- src/builtin/export.rs | 87 ---------------------- src/builtin/mod.rs | 23 +++++- src/builtin/varcmds.rs | 163 +++++++++++++++++++++++++++++++++++++++++ src/parse/execute.rs | 20 +++-- src/state.rs | 49 ++++++++++--- 5 files changed, 234 insertions(+), 108 deletions(-) delete mode 100644 src/builtin/export.rs create mode 100644 src/builtin/varcmds.rs diff --git a/src/builtin/export.rs b/src/builtin/export.rs deleted file mode 100644 index ce78b8f..0000000 --- a/src/builtin/export.rs +++ /dev/null @@ -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::>(); - 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::>(); - 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(()) -} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 563c7db..89b68f2 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -8,13 +8,13 @@ use crate::{ execute::prepare_argv, lex::{Span, Tk}, }, - procio::{IoFrame, IoStack, RedirGuard}, + procio::{IoFrame, IoStack, RedirGuard}, state, }; pub mod alias; pub mod cd; pub mod echo; -pub mod export; +pub mod varcmds; pub mod flowctl; pub mod jobctl; pub mod pwd; @@ -29,10 +29,10 @@ pub mod dirstack; pub mod exec; 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", "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 @@ -93,3 +93,18 @@ pub fn setup_builtin( // io_frame.restore() 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(()) +} diff --git a/src/builtin/varcmds.rs b/src/builtin/varcmds.rs new file mode 100644 index 0000000..14d12c4 --- /dev/null +++ b/src/builtin/varcmds.rs @@ -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::>(); + 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::>(); + 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::>(); + 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(()) +} diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 0315d29..0dd3053 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -2,7 +2,7 @@ use std::{collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt}; use crate::{ 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}, jobs::{ChildProc, JobStack, dispatch_job}, @@ -90,7 +90,7 @@ impl Drop for VarCtxGuard { fn drop(&mut self) { write_vars(|v| { 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))); 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()); } @@ -769,6 +769,16 @@ impl Dispatcher { "dirs" => dirs(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), + "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!( "Have not yet added support for builtin '{}'", cmd_raw @@ -885,7 +895,7 @@ impl Dispatcher { let var = var.span.as_str(); let val = val.expand()?.get_words().join(" "); 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::MinusEq => todo!(), AssignKind::MultEq => todo!(), @@ -902,7 +912,7 @@ impl Dispatcher { let var = var.span.as_str(); let val = val.expand()?.get_words().join(" "); 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::MinusEq => todo!(), AssignKind::MultEq => todo!(), diff --git a/src/state.rs b/src/state.rs index 3059d3a..44e62f2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -151,13 +151,16 @@ impl ScopeStack { pub fn cur_scope_mut(&mut self) -> &mut VarTab { 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() { if scope.var_exists(var_name) { - scope.unset_var(var_name); - return; + return scope.unset_var(var_name); } } + Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' not found", var_name) + )) } pub fn export_var(&mut self, var_name: &str) { for scope in self.scopes.iter_mut().rev() { @@ -187,22 +190,26 @@ impl ScopeStack { } 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); if flags.contains(VarFlags::LOCAL) || is_local { - self.set_var_local(var_name, val, flags); + self.set_var_local(var_name, val, flags) } 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() { - 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() { - 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 { @@ -477,6 +484,9 @@ impl Var { pub fn mark_for_export(&mut self) { self.flags.set(VarFlags::EXPORT, true); } + pub fn flags(&self) -> VarFlags { + self.flags + } } impl Display for Var { @@ -654,13 +664,27 @@ impl VarTab { pub fn get_var_flags(&self, var_name: &str) -> Option { 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); 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 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.flags |= flags; if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) { if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) { var.mark_for_export(); @@ -675,6 +699,7 @@ impl VarTab { } self.vars.insert(var_name.to_string(), var); } + Ok(()) } pub fn var_exists(&self, var_name: &str) -> bool { if let Ok(param) = var_name.parse::() {