From 2184b9b3617e151162cc260a2cd14d5a3b353509 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Fri, 20 Feb 2026 01:29:40 -0500 Subject: [PATCH] implemented the pushd, popd, and dirs builtins --- flake.nix | 4 +- src/builtin/dirstack.rs | 397 ++++++++++++++++++++++++++++++++++++++++ src/builtin/mod.rs | 4 +- src/parse/execute.rs | 18 +- src/state.rs | 31 ++++ 5 files changed, 437 insertions(+), 17 deletions(-) create mode 100644 src/builtin/dirstack.rs diff --git a/flake.nix b/flake.nix index fd96f92..e6bad81 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "A very basic flake"; + description = "A Linux shell written in Rust"; inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; @@ -38,7 +38,7 @@ homeModules.fern = import ./nix/hm-module.nix; overlays.default = final: prev: { - fern = self.packages.${final.system}.default; + fern = self.packages.${final.stdenv.hostPlatform.system}.default; }; }; } diff --git a/src/builtin/dirstack.rs b/src/builtin/dirstack.rs new file mode 100644 index 0000000..e9f3e77 --- /dev/null +++ b/src/builtin/dirstack.rs @@ -0,0 +1,397 @@ +use std::{env, path::PathBuf}; + +use nix::{libc::STDOUT_FILENO, unistd::write}; + +use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::Span}, procio::{IoStack, borrow_fd}, state::{self, read_meta, write_meta}}; + +enum StackIdx { + FromTop(usize), + FromBottom(usize), +} + +fn print_dirs() -> ShResult<()> { + let current_dir = env::current_dir()?; + let dirs_iter = read_meta(|m| { + m.dirs() + .clone() + .into_iter() + }); + let all_dirs = [current_dir].into_iter().chain(dirs_iter) + .map(|d| d.to_string_lossy().to_string()) + .map(|d| { + let Ok(home) = env::var("HOME") else { + return d; + }; + + if d.starts_with(&home) { + let new = d.strip_prefix(&home).unwrap(); + format!("~{new}") + } else { + d + } + }).collect::>() + .join(" "); + + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, all_dirs.as_bytes())?; + write(stdout, b"\n")?; + + Ok(()) +} + +fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> { + if !target.is_dir() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("not a directory: {}", target.display()), + blame, + )); + } + + if let Err(e) = env::set_current_dir(target) { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("Failed to change directory: {}", e), + blame, + )); + } + let new_dir = env::current_dir().map_err(|e| { + ShErr::full( + ShErrKind::ExecFail, + format!("Failed to get current directory: {}", e), + blame, + ) + })?; + unsafe { env::set_var("PWD", new_dir) }; + Ok(()) +} + +fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult { + let (from_top, digits) = if let Some(rest) = arg.strip_prefix('+') { + (true, rest) + } else if let Some(rest) = arg.strip_prefix('-') { + (false, rest) + } else { + unreachable!() + }; + + if digits.is_empty() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("{cmd}: missing index after '{}'", if from_top { "+" } else { "-" }), + blame, + )); + } + + for ch in digits.chars() { + if !ch.is_ascii_digit() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("{cmd}: invalid argument: {arg}"), + blame, + )); + } + } + + let n = digits.parse::().map_err(|e| { + ShErr::full( + ShErrKind::ExecFail, + format!("{cmd}: invalid index: {e}"), + blame, + ) + })?; + + if from_top { + Ok(StackIdx::FromTop(n)) + } else { + Ok(StackIdx::FromBottom(n)) + } +} + +pub fn pushd(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)))?; + + let mut dir = None; + let mut rotate_idx = None; + let mut no_cd = false; + + for (arg, _) in argv { + if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) { + rotate_idx = Some(parse_stack_idx(&arg, blame.clone(), "pushd")?); + } else if arg == "-n" { + no_cd = true; + } else if arg.starts_with('-') { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("pushd: invalid option: {arg}"), + blame.clone(), + )); + } else { + if dir.is_some() { + return Err(ShErr::full( + ShErrKind::ExecFail, + "pushd: too many arguments".to_string(), + blame.clone(), + )); + } + let target = PathBuf::from(&arg); + if !target.is_dir() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("pushd: not a directory: {arg}"), + blame.clone(), + )); + } + dir = Some(target); + } + } + + if let Some(idx) = rotate_idx { + let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); + let new_cwd = write_meta(|m| { + let dirs = m.dirs_mut(); + dirs.push_front(cwd); + match idx { + StackIdx::FromTop(n) => dirs.rotate_left(n + 1), + StackIdx::FromBottom(n) => dirs.rotate_right(n + 1), + } + dirs.pop_front() + }); + + if let Some(dir) = new_cwd + && !no_cd { + change_directory(&dir, blame)?; + print_dirs()?; + } + } else if let Some(dir) = dir { + let old_dir = env::current_dir()?; + if old_dir != dir { + write_meta(|m| m.push_dir(old_dir)); + } + + if no_cd { + state::set_status(0); + return Ok(()); + } + + change_directory(&dir, blame)?; + print_dirs()?; + } + + state::set_status(0); + Ok(()) +} + +pub fn popd(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)))?; + + let mut remove_idx = None; + let mut no_cd = false; + + for (arg, _) in argv { + if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) { + remove_idx = Some(parse_stack_idx(&arg, blame.clone(), "popd")?); + } else if arg == "-n" { + no_cd = true; + } else if arg.starts_with('-') { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("popd: invalid option: {arg}"), + blame.clone(), + )); + } + } + + if let Some(idx) = remove_idx { + match idx { + StackIdx::FromTop(0) => { + // +0 is same as plain popd: pop top, cd to it + let dir = write_meta(|m| m.pop_dir()); + if !no_cd { + if let Some(dir) = dir { + change_directory(&dir, blame.clone())?; + } else { + return Err(ShErr::full( + ShErrKind::ExecFail, + "popd: directory stack empty".to_string(), + blame.clone(), + )); + } + } + } + StackIdx::FromTop(n) => { + // +N (N>0): remove (N-1)th stored entry, no cd + write_meta(|m| { + let dirs = m.dirs_mut(); + let idx = n - 1; + if idx >= dirs.len() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("popd: directory index out of range: +{n}"), + blame.clone(), + )); + } + dirs.remove(idx); + Ok(()) + })?; + } + StackIdx::FromBottom(n) => { + write_meta(|m| -> ShResult<()> { + let dirs = m.dirs_mut(); + let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| { + ShErr::full( + ShErrKind::ExecFail, + format!("popd: directory index out of range: -{n}"), + blame.clone(), + ) + })?; + dirs.remove(actual); + Ok(()) + })?; + } + } + print_dirs()?; + } else { + let dir = write_meta(|m| m.pop_dir()); + + if no_cd { + state::set_status(0); + return Ok(()); + } + + if let Some(dir) = dir { + change_directory(&dir, blame.clone())?; + print_dirs()?; + } else { + return Err(ShErr::full( + ShErrKind::ExecFail, + "popd: directory stack empty".to_string(), + blame.clone(), + )); + } + } + + Ok(()) +} + +pub fn dirs(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)))?; + + let mut abbreviate_home = true; + let mut one_per_line = false; + let mut one_per_line_indexed = false; + let mut clear_stack = false; + let mut target_idx: Option = None; + + for (arg,_) in argv { + match arg.as_str() { + "-p" => one_per_line = true, + "-v" => one_per_line_indexed = true, + "-c" => clear_stack = true, + "-l" => abbreviate_home = false, + _ if (arg.starts_with('+') || arg.starts_with('-')) && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit() => { + target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?); + } + _ if arg.starts_with('-') => { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("dirs: invalid option: {arg}"), + blame.clone(), + )); + } + _ => { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("dirs: unexpected argument: {arg}"), + blame.clone(), + )); + } + } + } + + if clear_stack { + write_meta(|m| m.dirs_mut().clear()); + return Ok(()) + } + + + let mut dirs: Vec = read_meta(|m| { + let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); + let stack = [current_dir].into_iter() + .chain(m.dirs().clone()) + .map(|d| d.to_string_lossy().to_string()); + + if abbreviate_home { + let Ok(home) = env::var("HOME") else { + return stack.collect(); + }; + stack.map(|d| { + if d.starts_with(&home) { + let new = d.strip_prefix(&home).unwrap(); + format!("~{new}") + } else { + d + } + }).collect() + } else { + stack.collect() + } + }); + + if let Some(idx) = target_idx { + let target = match idx { + StackIdx::FromTop(n) => dirs.get(n), + StackIdx::FromBottom(n) => dirs.get(dirs.len().saturating_sub(n + 1)), + }; + + if let Some(dir) = target { + dirs = vec![dir.clone()]; + } else { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("dirs: directory index out of range: {}", match idx { + StackIdx::FromTop(n) => format!("+{n}"), + StackIdx::FromBottom(n) => format!("-{n}"), + }), + blame.clone(), + )); + } + } + + let mut output = String::new(); + + if one_per_line { + output = dirs.join("\n"); + } else if one_per_line_indexed { + for (i, dir) in dirs.iter_mut().enumerate() { + *dir = format!("{i}\t{dir}"); + } + output = dirs.join("\n"); + output.push('\n'); + } else { + print_dirs()?; + } + + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, output.as_bytes())?; + + Ok(()) +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 8dde960..5b8a6fa 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -25,10 +25,12 @@ pub mod source; pub mod test; // [[ ]] thing pub mod trap; pub mod zoltraak; +pub mod dirstack; -pub const BUILTINS: [&str; 21] = [ +pub const BUILTINS: [&str; 24] = [ "echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", + "pushd", "popd", "dirs" ]; /// Sets up a builtin command diff --git a/src/parse/execute.rs b/src/parse/execute.rs index af71201..4ac28da 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -2,20 +2,7 @@ use std::collections::{HashSet, VecDeque}; use crate::{ builtin::{ - alias::{alias, unalias}, - cd::cd, - echo::echo, - export::export, - flowctl::flowctl, - jobctl::{JobBehavior, continue_job, 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, export::export, flowctl::flowctl, jobctl::{JobBehavior, continue_job, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak }, expand::expand_aliases, jobs::{ChildProc, JobStack, dispatch_job}, @@ -613,6 +600,9 @@ impl Dispatcher { "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), "trap" => trap(cmd, io_stack_mut, curr_job_mut), + "pushd" => pushd(cmd, io_stack_mut, curr_job_mut), + "popd" => popd(cmd, io_stack_mut, curr_job_mut), + "dirs" => dirs(cmd, io_stack_mut, curr_job_mut), _ => unimplemented!( "Have not yet added support for builtin '{}'", cmd_raw.span.as_str() diff --git a/src/state.rs b/src/state.rs index a29b63d..979c063 100644 --- a/src/state.rs +++ b/src/state.rs @@ -705,6 +705,8 @@ pub struct MetaTab { // pending system messages system_msg: Vec, + + dir_stack: VecDeque } impl MetaTab { @@ -733,6 +735,35 @@ impl MetaTab { pub fn system_msg_pending(&self) -> bool { !self.system_msg.is_empty() } + pub fn dir_stack_top(&self) -> Option<&PathBuf> { + self.dir_stack.front() + } + pub fn push_dir(&mut self, path: PathBuf) { + self.dir_stack.push_front(path); + } + pub fn pop_dir(&mut self) -> Option { + self.dir_stack.pop_front() + } + pub fn remove_dir(&mut self, idx: i32) -> Option { + if idx < 0 { + let neg_idx = (self.dir_stack.len() - 1).saturating_sub((-idx) as usize); + self.dir_stack.remove(neg_idx) + } else { + self.dir_stack.remove((idx - 1) as usize) + } + } + pub fn rotate_dirs_fwd(&mut self, steps: usize) { + self.dir_stack.rotate_left(steps); + } + pub fn rotate_dirs_bkwd(&mut self, steps: usize) { + self.dir_stack.rotate_right(steps); + } + pub fn dirs(&self) -> &VecDeque { + &self.dir_stack + } + pub fn dirs_mut(&mut self) -> &mut VecDeque { + &mut self.dir_stack + } } /// Read from the job table