diff --git a/.gitignore b/.gitignore index d986b3f..c0eaade 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ shell.nix *~ TODO.md rust-toolchain.toml -*src_old # cachix tmp file store-path-pre-build diff --git a/Cargo.lock b/Cargo.lock index 73118f6..becdcff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,12 +59,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "errno" version = "0.3.10" @@ -97,19 +91,18 @@ name = "fern" version = "0.1.0" dependencies = [ "bitflags", + "glob", "insta", "nix", "pretty_assertions", "rustyline", - "serde", - "serde_yaml", ] [[package]] -name = "hashbrown" -version = "0.15.2" +name = "glob" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "home" @@ -120,16 +113,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "indexmap" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "insta" version = "1.42.2" @@ -143,12 +126,6 @@ dependencies = [ "similar", ] -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "libc" version = "0.2.169" @@ -311,45 +288,6 @@ dependencies = [ "syn", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "similar" version = "2.7.0" @@ -391,12 +329,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 8859129..f93761d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,11 @@ debug = true [dependencies] bitflags = "2.8.0" +glob = "0.3.2" insta = "1.42.2" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] } pretty_assertions = "1.4.1" rustyline = { version = "15.0.0", features = [ "derive" ] } -serde = { version = "1.0.219", features = [ "derive" ] } -serde_yaml = "0.9.34" [[bin]] name = "fern" diff --git a/src/expand.rs b/src/expand.rs index e981979..809b9d7 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -8,6 +8,8 @@ pub const VAR_SUB: char = '\u{fdd0}'; pub const DUB_QUOTE: char = '\u{fdd1}'; /// Single quote '\\'' marker pub const SNG_QUOTE: char = '\u{fdd2}'; +/// Tilde sub marker +pub const TILDE_SUB: char = '\u{fdd3}'; impl Tk { /// Create a new expanded token @@ -21,6 +23,7 @@ impl Tk { let class = TkRule::Expanded { exp }; Ok(Self { class, span, flags, }) } + /// Perform word splitting pub fn get_words(&self) -> Vec { match &self.class { TkRule::Expanded { exp } => exp.clone(), @@ -40,6 +43,11 @@ impl Expander { } pub fn expand(&mut self) -> ShResult> { self.raw = self.expand_raw()?; + if let Ok(glob_exp) = expand_glob(&self.raw) { + if !glob_exp.is_empty() { + self.raw = glob_exp; + } + } Ok(self.split_words()) } pub fn split_words(&mut self) -> Vec { @@ -76,6 +84,10 @@ impl Expander { while let Some(ch) = chars.next() { match ch { + TILDE_SUB => { + let home = env::var("HOME").unwrap_or_default(); + result.push_str(&home); + } VAR_SUB => { while let Some(ch) = chars.next() { match ch { @@ -130,6 +142,19 @@ impl Expander { } } +pub fn expand_glob(raw: &str) -> ShResult { + let mut words = vec![]; + + for entry in glob::glob(raw) + .map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? { + let entry = entry + .map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; + + words.push(entry.to_str().unwrap().to_string()) + } + Ok(words.join(" ")) +} + /// Get the command output of a given command input as a String pub fn expand_cmd_sub(raw: &str) -> ShResult { flog!(DEBUG, "in expand_cmd_sub"); @@ -173,9 +198,14 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { pub fn unescape_str(raw: &str) -> String { let mut chars = raw.chars(); let mut result = String::new(); + let mut first_char = true; + while let Some(ch) = chars.next() { match ch { + '~' if first_char => { + result.push(TILDE_SUB) + } '\\' => { if let Some(next_ch) = chars.next() { result.push(next_ch) @@ -215,6 +245,7 @@ pub fn unescape_str(raw: &str) -> String { '$' => result.push(VAR_SUB), _ => result.push(ch) } + first_char = false; } result } @@ -602,6 +633,8 @@ pub fn expand_prompt(raw: &str) -> ShResult { } /// Expand aliases in the given input string +/// +/// Recursively calls itself until all aliases are expanded pub fn expand_aliases(input: String, mut already_expanded: HashSet, log_tab: &LogTab) -> String { let mut result = input.clone(); let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect(); diff --git a/src/fern.rs b/src/fern.rs index cce9d58..01e906b 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -13,80 +13,13 @@ pub mod shopt; #[cfg(test)] pub mod tests; -use std::collections::HashSet; - -use crate::expand::expand_aliases; -use libsh::error::ShResult; -use parse::{execute::Dispatcher, ParsedSrc}; -use signal::sig_setup; -use state::{read_logic, source_rc, write_meta}; -use termios::{LocalFlags, Termios}; +use crate::libsh::sys::{save_termios, set_termios}; +use crate::parse::execute::exec_input; +use crate::signal::sig_setup; +use crate::state::source_rc; use crate::prelude::*; -/// The previous state of the terminal options. -/// -/// This variable stores the terminal settings at the start of the program and restores them when the program exits. -/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program. -/// It will not be mutated or accessed under any other circumstances. -/// -/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky. -/// -/// The possible states of this variable are: -/// - `None`: The terminal options have not been set yet (before initialization). -/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected). -/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved. -/// -/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because: -/// - It is set once during program startup and accessed once during program exit. -/// - It is not mutated or accessed after the initial setup and final read. -/// -/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior. -pub(crate) static mut SAVED_TERMIOS: Option> = None; -pub fn save_termios() { - unsafe { - SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { - let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); - termios.local_flags &= !LocalFlags::ECHOCTL; - termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); - Some(termios) - } else { - None - }); - } -} -#[allow(static_mut_refs)] -pub unsafe fn get_saved_termios() -> Option { - // SAVED_TERMIOS should *only ever* be set once and accessed once - // Set at the start of the program, and accessed during the exit of the program to reset the termios. - // Do not use this variable anywhere else - SAVED_TERMIOS.clone().flatten() -} - -/// Set termios to not echo control characters, like ^Z for instance -fn set_termios() { - if isatty(std::io::stdin().as_raw_fd()).unwrap() { - let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); - termios.local_flags &= !LocalFlags::ECHOCTL; - termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); - } -} - -pub fn exec_input(input: String) -> ShResult<()> { - write_meta(|m| m.start_timer()); - let log_tab = read_logic(|l| l.clone()); - let input = expand_aliases(input, HashSet::new(), &log_tab); - let mut parser = ParsedSrc::new(Arc::new(input)); - if let Err(errors) = parser.parse_src() { - for error in errors { - eprintln!("{error}"); - } - return Ok(()) - } - - let mut dispatcher = Dispatcher::new(parser.extract_nodes()); - dispatcher.begin_dispatch() -} fn main() { save_termios(); @@ -97,7 +30,6 @@ fn main() { eprintln!("{e}"); } - const MAX_READLINE_ERRORS: u32 = 5; let mut readline_err_count: u32 = 0; loop { // Main loop @@ -109,7 +41,7 @@ fn main() { Err(e) => { eprintln!("{e}"); readline_err_count += 1; - if readline_err_count == MAX_READLINE_ERRORS { + if readline_err_count == 5 { eprintln!("reached maximum readline error count, exiting"); break } else { diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 669a421..6a262fb 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -45,7 +45,11 @@ pub struct Note { impl Note { pub fn new(main: impl Into) -> Self { - Self { main: main.into(), sub_notes: vec![], depth: 0 } + Self { + main: main.into(), + sub_notes: vec![], + depth: 0 + } } pub fn with_sub_notes(self, new_sub_notes: Vec>) -> Self { @@ -192,7 +196,7 @@ impl ShErr { let mut indicator_lines = vec![]; for line in lines { - let indicator_line = "^".repeat(line.len()).styled(Style::Red | Style::Bold); + let indicator_line = "^".repeat(line.trim().len()).styled(Style::Red | Style::Bold); indicator_lines.push(indicator_line); } diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs index ae0fe06..f504e5d 100644 --- a/src/libsh/sys.rs +++ b/src/libsh/sys.rs @@ -1,4 +1,55 @@ +use termios::{LocalFlags, Termios}; + use crate::{prelude::*, state::write_jobs}; +/// +/// The previous state of the terminal options. +/// +/// This variable stores the terminal settings at the start of the program and restores them when the program exits. +/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program. +/// It will not be mutated or accessed under any other circumstances. +/// +/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky. +/// +/// The possible states of this variable are: +/// - `None`: The terminal options have not been set yet (before initialization). +/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected). +/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved. +/// +/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because: +/// - It is set once during program startup and accessed once during program exit. +/// - It is not mutated or accessed after the initial setup and final read. +/// +/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior. +pub(crate) static mut SAVED_TERMIOS: Option> = None; + +pub fn save_termios() { + unsafe { + SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { + let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); + termios.local_flags &= !LocalFlags::ECHOCTL; + termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); + Some(termios) + } else { + None + }); + } +} +#[allow(static_mut_refs)] +pub unsafe fn get_saved_termios() -> Option { + // SAVED_TERMIOS should *only ever* be set once and accessed once + // Set at the start of the program, and accessed during the exit of the program to reset the termios. + // Do not use this variable anywhere else + SAVED_TERMIOS.clone().flatten() +} + +/// Set termios to not echo control characters, like ^Z for instance +pub fn set_termios() { + if isatty(std::io::stdin().as_raw_fd()).unwrap() { + let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); + termios.local_flags &= !LocalFlags::ECHOCTL; + termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); + } +} pub fn sh_quit(code: i32) -> ! { write_jobs(|j| { @@ -6,7 +57,7 @@ pub fn sh_quit(code: i32) -> ! { job.killpg(Signal::SIGTERM).ok(); } }); - if let Some(termios) = unsafe { crate::get_saved_termios() } { + if let Some(termios) = unsafe { get_saved_termios() } { termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap(); } if code == 0 { diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 4757042..9f4bab5 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,7 +1,7 @@ -use std::collections::VecDeque; +use std::collections::{HashSet, VecDeque}; -use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, read_logic, read_vars, write_logic, write_vars, ShFunc, VarTab}}; +use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, expand::expand_aliases, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, read_logic, read_vars, write_logic, write_meta, write_vars, ShFunc, VarTab, LOGIC_TABLE}}; use super::{lex::{Span, Tk, TkFlags, KEYWORDS}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType}; @@ -27,7 +27,9 @@ impl ExecArgs { Ok(Self { cmd, argv, envp }) } pub fn get_cmd(argv: &[(String,Span)]) -> (CString,Span) { - (CString::new(argv[0].0.as_str()).unwrap(),argv[0].1.clone()) + let cmd = argv[0].0.as_str(); + let span = argv[0].1.clone(); + (CString::new(cmd).unwrap(),span) } pub fn get_argv(argv: Vec<(String,Span)>) -> Vec { argv.into_iter().map(|s| CString::new(s.0).unwrap()).collect() @@ -37,6 +39,23 @@ impl ExecArgs { } } +pub fn exec_input(input: String) -> ShResult<()> { + write_meta(|m| m.start_timer()); + let log_tab = LOGIC_TABLE.read().unwrap(); + let input = expand_aliases(input, HashSet::new(), &log_tab); + mem::drop(log_tab); // Release lock ASAP + let mut parser = ParsedSrc::new(Arc::new(input)); + if let Err(errors) = parser.parse_src() { + for error in errors { + eprintln!("{error}"); + } + return Ok(()) + } + + let mut dispatcher = Dispatcher::new(parser.extract_nodes()); + dispatcher.begin_dispatch() +} + pub struct Dispatcher { nodes: VecDeque, pub io_stack: IoStack, @@ -430,7 +449,7 @@ impl Dispatcher { let var = var.span.as_str(); let val = val.span.as_str(); match kind { - AssignKind::Eq => std::env::set_var(var, val), + AssignKind::Eq => write_vars(|v| v.set_var(var, val, true)), AssignKind::PlusEq => todo!(), AssignKind::MinusEq => todo!(), AssignKind::MultEq => todo!(), diff --git a/src/parse/lex.rs b/src/parse/lex.rs index b166eb6..aa0572a 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -108,6 +108,9 @@ impl Tk { _ => self.span.as_str().to_string() } } + pub fn as_str(&self) -> &str { + self.span.as_str() + } pub fn source(&self) -> Arc { self.span.source.clone() } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index c7d7358..8b6ee9f 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -596,10 +596,7 @@ impl ParseStream { } node_tks.push(self.next_tk().unwrap()); - let Some(pat_tk) = self.next_tk() else { - self.panic_mode(&mut node_tks); - return Err( - parse_err_full( + let pat_err = parse_err_full( "Expected a pattern after 'case' keyword", &node_tks.get_span().unwrap() ) .with_note( @@ -607,10 +604,17 @@ impl ParseStream { .with_sub_notes(vec![ "This includes variables like '$foo' or command substitutions like '$(echo foo)'" ]) - ) - ); + ); + + let Some(pat_tk) = self.next_tk() else { + self.panic_mode(&mut node_tks); + return Err(pat_err); }; + if pat_tk.span.as_str() == "in" { + return Err(pat_err) + } + pattern = pat_tk; node_tks.push(pattern.clone());