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,
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(())
}

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::{
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!(),

View File

@@ -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<VarFlags> {
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::<ShellParam>() {