From ad0e4277cb063daeed0a549b2dfdd921648e1be2 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Wed, 28 Jan 2026 19:30:48 -0500 Subject: [PATCH] Implemented proper variable scoping Extracted business logic out of signal handler functions Consolidated state variables into a single struct Implemented var types --- .gitignore | 1 + Cargo.lock | 241 ++++-- Cargo.toml | 2 +- src/builtin/cd.rs | 2 +- src/builtin/export.rs | 6 +- src/builtin/shift.rs | 2 +- src/expand.rs | 336 +++++++- src/fern.rs | 50 +- src/jobs.rs | 32 +- src/libsh/error.rs | 16 +- src/parse/execute.rs | 1304 ++++++++++++++++---------------- src/prompt/mod.rs | 12 +- src/prompt/readline/linebuf.rs | 437 +++++++---- src/prompt/readline/mod.rs | 14 +- src/prompt/readline/term.rs | 26 +- src/signal.rs | 177 ++++- src/state.rs | 623 +++++++++++---- 17 files changed, 2154 insertions(+), 1127 deletions(-) diff --git a/.gitignore b/.gitignore index 07e8e81..864891d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ shell.nix *~ TODO.md rust-toolchain.toml +/ref # cachix tmp file store-path-pre-build diff --git a/Cargo.lock b/Cargo.lock index e0a0477..bb8cd72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -28,50 +28,50 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "bitflags" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -81,9 +81,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.38" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" dependencies = [ "clap_builder", "clap_derive", @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" dependencies = [ "anstream", "anstyle", @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -115,15 +115,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "console" @@ -134,7 +134,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -149,6 +149,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fern" version = "0.1.0" @@ -165,10 +181,22 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.2" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "heck" @@ -178,40 +206,39 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "insta" -version = "1.42.2" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" dependencies = [ "console", - "linked-hash-map", "once_cell", - "pin-project", "similar", + "tempfile", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "nix" @@ -227,29 +254,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "pin-project" -version = "1.1.10" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pretty_assertions" @@ -263,27 +276,33 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] -name = "regex" -version = "1.11.1" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -293,9 +312,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -304,9 +323,22 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] [[package]] name = "similar" @@ -322,9 +354,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -332,10 +364,23 @@ dependencies = [ ] [[package]] -name = "unicode-ident" -version = "1.0.17" +name = "tempfile" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -345,9 +390,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "utf8parse" @@ -355,6 +400,21 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -364,6 +424,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -428,6 +497,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index dda9159..5948418 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ description = "A linux shell written in rust" publish = false version = "0.1.0" -edition = "2021" +edition = "2024" [profile.release] debug = true diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index 888819e..6c8c6a1 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -44,7 +44,7 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { env::set_current_dir(new_dir).unwrap(); let new_dir = env::current_dir().unwrap(); - env::set_var("PWD", new_dir); + unsafe { env::set_var("PWD", new_dir) }; state::set_status(0); Ok(()) diff --git a/src/builtin/export.rs b/src/builtin/export.rs index d64bde4..c2b398f 100644 --- a/src/builtin/export.rs +++ b/src/builtin/export.rs @@ -3,8 +3,8 @@ use crate::{ libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, - state::{self, write_vars}, + procio::{IoStack, borrow_fd}, + state::{self, VarFlags, write_vars}, }; use super::setup_builtin; @@ -34,7 +34,7 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult } else { for (arg, _) in argv { if let Some((var, val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, val, true)); // Export an assignment like + 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 diff --git a/src/builtin/shift.rs b/src/builtin/shift.rs index 931bbc1..40b65a5 100644 --- a/src/builtin/shift.rs +++ b/src/builtin/shift.rs @@ -28,7 +28,7 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { )); }; for _ in 0..count { - write_vars(|v| v.fpop_arg()); + write_vars(|v| v.cur_scope_mut().fpop_arg()); } } diff --git a/src/expand.rs b/src/expand.rs index 90964b8..16fb476 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; use std::iter::Peekable; +use std::mem::take; use std::str::{Chars, FromStr}; use glob::Pattern; @@ -11,7 +12,7 @@ use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFl use crate::parse::{Redir, RedirType}; use crate::prelude::*; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; -use crate::state::{read_jobs, read_vars, write_jobs, write_meta, write_vars, LogTab}; +use crate::state::{LogTab, VarFlags, read_jobs, read_vars, write_jobs, write_meta, write_vars}; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; @@ -40,7 +41,7 @@ impl Tk { pub fn expand(self) -> ShResult { let flags = self.flags; let span = self.span.clone(); - let exp = Expander::new(self).expand()?; + let exp = Expander::new(self)?.expand()?; let class = TkRule::Expanded { exp }; Ok(Self { class, span, flags }) } @@ -58,9 +59,11 @@ pub struct Expander { } impl Expander { - pub fn new(raw: Tk) -> Self { - let unescaped = unescape_str(raw.span.as_str()); - Self { raw: unescaped } + pub fn new(raw: Tk) -> ShResult { + let mut raw = raw.span.as_str().to_string(); + raw = expand_braces_full(&raw)?.join(" "); + let unescaped = unescape_str(&raw); + Ok(Self { raw: unescaped }) } pub fn expand(&mut self) -> ShResult> { let mut chars = self.raw.chars().peekable(); @@ -100,6 +103,323 @@ impl Expander { } } +/// Check if a string contains valid brace expansion patterns. +/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost level. +fn has_braces(s: &str) -> bool { + let mut chars = s.chars().peekable(); + let mut depth = 0; + let mut found_open = false; + let mut has_comma = false; + let mut has_range = false; + let mut cur_quote: Option = None; + + while let Some(ch) = chars.next() { + match ch { + '\\' => { chars.next(); } // skip escaped char + '\'' if cur_quote.is_none() => cur_quote = Some('\''), + '\'' if cur_quote == Some('\'') => cur_quote = None, + '"' if cur_quote.is_none() => cur_quote = Some('"'), + '"' if cur_quote == Some('"') => cur_quote = None, + '{' if cur_quote.is_none() => { + if depth == 0 { + found_open = true; + has_comma = false; + has_range = false; + } + depth += 1; + } + '}' if cur_quote.is_none() && depth > 0 => { + depth -= 1; + if depth == 0 && found_open && (has_comma || has_range) { + return true; + } + } + ',' if cur_quote.is_none() && depth == 1 => { + has_comma = true; + } + '.' if cur_quote.is_none() && depth == 1 => { + if chars.peek() == Some(&'.') { + chars.next(); + has_range = true; + } + } + _ => {} + } + } + false +} + +/// Expand braces in a string, zsh-style: one level per call, loop until done. +/// Returns a Vec of expanded strings. +fn expand_braces_full(input: &str) -> ShResult> { + let mut results = vec![input.to_string()]; + + // Keep expanding until no results contain braces + loop { + let mut any_expanded = false; + let mut new_results = Vec::new(); + + for word in results { + if has_braces(&word) { + any_expanded = true; + let expanded = expand_one_brace(&word)?; + new_results.extend(expanded); + } else { + new_results.push(word); + } + } + + results = new_results; + if !any_expanded { + break; + } + } + + Ok(results) +} + +/// Expand the first (outermost) brace expression in a word. +/// "pre{a,b}post" -> ["preapost", "prebpost"] +/// "pre{1..3}post" -> ["pre1post", "pre2post", "pre3post"] +fn expand_one_brace(word: &str) -> ShResult> { + let (prefix, inner, suffix) = match get_brace_parts(word) { + Some(parts) => parts, + None => return Ok(vec![word.to_string()]), // No valid braces + }; + + // Split the inner content on top-level commas, or expand as range + let parts = split_brace_inner(&inner); + + // If we got back a single part with no expansion, treat as literal + if parts.len() == 1 && parts[0] == inner { + // Check if it's a range + if let Some(range_parts) = try_expand_range(&inner) { + return Ok(range_parts + .into_iter() + .map(|p| format!("{}{}{}", prefix, p, suffix)) + .collect()); + } + // Not a valid brace expression, return as-is with literal braces + return Ok(vec![format!("{}{{{}}}{}", prefix, inner, suffix)]); + } + + Ok(parts + .into_iter() + .map(|p| format!("{}{}{}", prefix, p, suffix)) + .collect()) +} + +/// Extract prefix, inner, and suffix from a brace expression. +/// "pre{a,b}post" -> Some(("pre", "a,b", "post")) +fn get_brace_parts(word: &str) -> Option<(String, String, String)> { + let mut chars = word.chars().enumerate().peekable(); + let mut prefix = String::new(); + let mut cur_quote: Option = None; + let mut brace_start = None; + + // Find the opening brace + while let Some((i, ch)) = chars.next() { + match ch { + '\\' => { + prefix.push(ch); + if let Some((_, next)) = chars.next() { + prefix.push(next); + } + } + '\'' if cur_quote.is_none() => { cur_quote = Some('\''); + prefix.push(ch); } + '\'' if cur_quote == Some('\'') => { cur_quote = None; + prefix.push(ch); } + '"' if cur_quote.is_none() => { cur_quote = Some('"'); prefix.push(ch); } + '"' if cur_quote == Some('"') => { cur_quote = None; prefix.push(ch); } + '{' if cur_quote.is_none() => { + brace_start = Some(i); + break; + } + _ => prefix.push(ch), + } + } + + let brace_start = brace_start?; + + // Find matching closing brace + let mut depth = 1; + let mut inner = String::new(); + cur_quote = None; + + while let Some((_, ch)) = chars.next() { + match ch { + '\\' => { + inner.push(ch); + if let Some((_, next)) = chars.next() { + inner.push(next); + } + } + '\'' if cur_quote.is_none() => { cur_quote = Some('\''); inner.push(ch); } + '\'' if cur_quote == Some('\'') => { cur_quote = None; inner.push(ch); } + '"' if cur_quote.is_none() => { cur_quote = Some('"'); inner.push(ch); } + '"' if cur_quote == Some('"') => { cur_quote = None; inner.push(ch); } + '{' if cur_quote.is_none() => { + depth += 1; + inner.push(ch); + } + '}' if cur_quote.is_none() => { + depth -= 1; + if depth == 0 { + break; + } + inner.push(ch); + } + _ => inner.push(ch), + } + } + + if depth != 0 { + return None; // Unbalanced braces + } + + // Collect suffix + let suffix: String = chars.map(|(_, c)| c).collect(); + + Some((prefix, inner, suffix)) +} + +/// Split brace inner content on top-level commas. +/// "a,b,c" -> ["a", "b", "c"] +/// "a,{b,c},d" -> ["a", "{b,c}", "d"] +fn split_brace_inner(inner: &str) -> Vec { + let mut parts = Vec::new(); + let mut current = String::new(); + let mut chars = inner.chars().peekable(); + let mut depth = 0; + let mut cur_quote: Option = None; + + while let Some(ch) = chars.next() { + match ch { + '\\' => { + current.push(ch); + if let Some(next) = chars.next() { + current.push(next); + } + } + '\'' if cur_quote.is_none() => { cur_quote = Some('\''); current.push(ch); } + '\'' if cur_quote == Some('\'') => { cur_quote = None; current.push(ch); } + '"' if cur_quote.is_none() => { cur_quote = Some('"'); current.push(ch); } + '"' if cur_quote == Some('"') => { cur_quote = None; current.push(ch); } + '{' if cur_quote.is_none() => { + depth += 1; + current.push(ch); + } + '}' if cur_quote.is_none() => { + depth -= 1; + current.push(ch); + } + ',' if cur_quote.is_none() && depth == 0 => { + parts.push(std::mem::take(&mut current)); + } + _ => current.push(ch), + } + } + + parts.push(current); + parts +} + +/// Try to expand a range like "1..5" or "a..z" or "1..10..2" +fn try_expand_range(inner: &str) -> Option> { + // Look for ".." pattern + let parts: Vec<&str> = inner.split("..").collect(); + + match parts.len() { + 2 => { + let start = parts[0]; + let end = parts[1]; + expand_range(start, end, 1) + } + 3 => { + let start = parts[0]; + let end = parts[1]; + let step: i32 = parts[2].parse().ok()?; + if step == 0 { return None; } + expand_range(start, end, step.unsigned_abs() as usize) + } + _ => None, + } +} + +fn expand_range(start: &str, end: &str, step: usize) -> +Option> { + // Try character range first + if is_alpha_range_bound(start) && is_alpha_range_bound(end) { + let start_char = start.chars().next()? as u8; + let end_char = end.chars().next()? as u8; + let reverse = end_char < start_char; + + let (lo, hi) = if reverse { + (end_char, start_char) + } else { + (start_char, end_char) + }; + + let chars: Vec = (lo..=hi) + .step_by(step) + .map(|c| (c as char).to_string()) + .collect(); + + return Some(if reverse { + chars.into_iter().rev().collect() + } else { + chars + }); + } + + // Try numeric range + if is_numeric_range_bound(start) && is_numeric_range_bound(end) { + let start_num: i32 = start.parse().ok()?; + let end_num: i32 = end.parse().ok()?; + let reverse = end_num < start_num; + + // Handle zero-padding + let pad_width = start.len().max(end.len()); + let needs_padding = start.starts_with('0') || + end.starts_with('0'); + + let (lo, hi) = if reverse { + (end_num, start_num) + } else { + (start_num, end_num) + }; + + let nums: Vec = (lo..=hi) + .step_by(step) + .map(|n| { + if needs_padding { + format!("{:0>width$}", n, width = pad_width) + } else { + n.to_string() + } + }) + .collect(); + + return Some(if reverse { + nums.into_iter().rev().collect() + } else { + nums + }); + } + + None +} + + +fn is_alpha_range_bound(word: &str) -> bool { + word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic()) +} + +fn is_numeric_range_bound(word: &str) -> bool { + !word.is_empty() && word.chars().all(|c| c.is_ascii_digit()) +} + pub fn expand_raw(chars: &mut Peekable>) -> ShResult { let mut result = String::new(); @@ -897,7 +1217,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { } ParamExp::SetDefaultUnsetOrNull(default) => { if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { - write_vars(|v| v.set_var(&var_name, &default, false)); + write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE)); Ok(default) } else { Ok(vars.get_var(&var_name)) @@ -905,7 +1225,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { } ParamExp::SetDefaultUnset(default) => { if !vars.var_exists(&var_name) { - write_vars(|v| v.set_var(&var_name, &default, false)); + write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE)); Ok(default) } else { Ok(vars.get_var(&var_name)) @@ -1061,7 +1381,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { } ParamExp::VarNamesWithPrefix(prefix) => { let mut match_vars = vec![]; - for var in vars.vars().keys() { + for var in vars.flatten_vars().keys() { if var.starts_with(&prefix) { match_vars.push(var.clone()) } diff --git a/src/fern.rs b/src/fern.rs index 8c4fc88..900440f 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -18,10 +18,11 @@ pub mod state; #[cfg(test)] pub mod tests; +use crate::libsh::error::ShErrKind; use crate::libsh::sys::{save_termios, set_termios}; use crate::parse::execute::exec_input; use crate::prelude::*; -use crate::signal::sig_setup; +use crate::signal::{check_signals, sig_setup, signals_pending}; use crate::state::source_rc; use clap::Parser; use shopt::FernEditMode; @@ -78,9 +79,9 @@ fn run_script>(path: P, args: Vec) { exit(1); }; - write_vars(|v| v.bpush_arg(path.to_string_lossy().to_string())); + write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string())); for arg in args { - write_vars(|v| v.bpush_arg(arg)) + write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) } if let Err(e) = exec_input(input, None) { @@ -100,26 +101,49 @@ fn fern_interactive() { let mut readline_err_count: u32 = 0; - loop { + // Initialize a new string, we will use this to store + // partial line inputs when read() calls are interrupted by EINTR + let mut partial_input = String::new(); + + 'outer: loop { // Main loop let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) .unwrap() .map(|mode| mode.parse::().unwrap_or_default()) .unwrap(); - let input = match prompt::readline(edit_mode) { + let input = match prompt::readline(edit_mode, Some(&partial_input)) { Ok(line) => { readline_err_count = 0; + partial_input.clear(); line } Err(e) => { - eprintln!("{e}"); - readline_err_count += 1; - if readline_err_count == 20 { - eprintln!("reached maximum readline error count, exiting"); - break; - } else { - continue; - } + if let ShErrKind::ReadlineIntr(partial) = e.kind() { + // Did we get signaled? Check signal flags + // If nothing to worry about, retry the readline + while signals_pending() { + if let Err(e) = check_signals() { + if let ShErrKind::ClearReadline = e.kind() { + partial_input.clear(); + if !signals_pending() { + continue 'outer; + } + }; + eprintln!("{e}"); + } + } + partial_input = partial.to_string(); + continue; + } else { + eprintln!("{e}"); + readline_err_count += 1; + if readline_err_count == 20 { + eprintln!("reached maximum readline error count, exiting"); + break; + } else { + continue; + } + } } }; diff --git a/src/jobs.rs b/src/jobs.rs index 8ab4ca4..391f301 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -2,10 +2,7 @@ use crate::{ libsh::{ error::ShResult, term::{Style, Styled}, - }, - prelude::*, - procio::{borrow_fd, IoMode}, - state::{self, set_status, write_jobs}, + }, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, set_status, write_jobs} }; pub const SIG_EXIT_OFFSET: i32 = 128; @@ -643,29 +640,6 @@ pub fn take_term() -> ShResult<()> { Ok(()) } -pub fn disable_reaping() -> ShResult<()> { - flog!(TRACE, "Disabling reaping"); - unsafe { - signal( - Signal::SIGCHLD, - SigHandler::Handler(crate::signal::ignore_sigchld), - ) - }?; - Ok(()) -} - -pub fn enable_reaping() -> ShResult<()> { - flog!(TRACE, "Enabling reaping"); - unsafe { - signal( - Signal::SIGCHLD, - SigHandler::Handler(crate::signal::handle_sigchld), - ) - } - .unwrap(); - Ok(()) -} - /// Waits on the current foreground job and updates the shell's last status code pub fn wait_fg(job: Job) -> ShResult<()> { if job.children().is_empty() { @@ -674,7 +648,7 @@ pub fn wait_fg(job: Job) -> ShResult<()> { flog!(TRACE, "Waiting on foreground job"); let mut code = 0; attach_tty(job.pgid())?; - disable_reaping()?; + disable_reaping(); let statuses = write_jobs(|j| j.new_fg(job))?; for status in statuses { match status { @@ -697,7 +671,7 @@ pub fn wait_fg(job: Job) -> ShResult<()> { take_term()?; set_status(code); flog!(TRACE, "exit code: {}", code); - enable_reaping()?; + enable_reaping(); Ok(()) } diff --git a/src/libsh/error.rs b/src/libsh/error.rs index e130f03..dfff5b3 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -388,7 +388,7 @@ impl From for ShErr { impl From for ShErr { fn from(value: Errno) -> Self { - ShErr::simple(ShErrKind::Errno, value.to_string()) + ShErr::simple(ShErrKind::Errno(value), value.to_string()) } } @@ -402,14 +402,18 @@ pub enum ShErrKind { HistoryReadErr, ResourceLimitExceeded, BadPermission, - Errno, + Errno(Errno), FileNotFound(String), CmdNotFound(String), + ReadlineIntr(String), + ReadlineErr, + + // Not really errors, more like internal signals CleanExit(i32), FuncReturn(i32), LoopContinue(i32), LoopBreak(i32), - ReadlineErr, + ClearReadline, Null, } @@ -424,14 +428,16 @@ impl Display for ShErrKind { Self::ExecFail => "Execution Failed", Self::ResourceLimitExceeded => "Resource Limit Exceeded", Self::BadPermission => "Bad Permissions", - Self::Errno => "ERRNO", + Self::Errno(e) => &format!("Errno: {}", e.desc()), Self::FileNotFound(file) => &format!("File not found: {file}"), Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), Self::CleanExit(_) => "", Self::FuncReturn(_) => "", Self::LoopContinue(_) => "", Self::LoopBreak(_) => "", - Self::ReadlineErr => "Line Read Error", + Self::ReadlineIntr(_) => "", + Self::ReadlineErr => "Readline Error", + Self::ClearReadline => "", Self::Null => "", }; write!(f, "{output}") diff --git a/src/parse/execute.rs b/src/parse/execute.rs index eeceaa6..01d993d 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,711 +1,747 @@ use std::collections::{HashSet, VecDeque}; use crate::{ - builtin::{ - alias::{alias, unalias}, - cd::cd, - echo::echo, - export::export, - flowctl::flowctl, - jobctl::{continue_job, jobs, JobBehavior}, - pwd::pwd, - shift::shift, - shopt::shopt, - source::source, - test::double_bracket_test, - 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, get_snapshots, read_logic, restore_snapshot, write_logic, write_meta, write_vars, ShFunc, - VarTab, LOGIC_TABLE, - }, + builtin::{ + alias::{alias, unalias}, + cd::cd, + echo::echo, + export::export, + flowctl::flowctl, + jobctl::{JobBehavior, continue_job, jobs}, + pwd::pwd, + shift::shift, + shopt::shopt, + source::source, + test::double_bracket_test, + zoltraak::zoltraak, + }, + expand::expand_aliases, + jobs::{ChildProc, JobBldr, JobStack, dispatch_job}, + libsh::{ + error::{ShErr, ShErrKind, ShResult, ShResultExt}, + utils::RedirVecUtils, + }, + prelude::*, + procio::{IoFrame, IoMode, IoStack}, + state::{ + self, FERN, ShFunc, VarFlags, read_logic, write_logic, write_meta, write_vars + }, }; use super::{ - lex::{Span, Tk, TkFlags, KEYWORDS}, - AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, - ParsedSrc, Redir, RedirType, + lex::{Span, Tk, TkFlags, KEYWORDS}, + AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, + ParsedSrc, Redir, RedirType, }; +pub struct ScopeGuard; + + +impl ScopeGuard { + pub fn exclusive_scope(args: Option>) -> Self { + let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::>()); + write_vars(|v| v.descend(argv)); + Self + } + pub fn shared_scope() -> Self { + // used in environments that inherit from the parent, like subshells + write_vars(|v| v.descend(None)); + Self + } +} + +impl Drop for ScopeGuard { + fn drop(&mut self) { + write_vars(|v| v.ascend()); + } +} + +/// Used to throw away variables that exist in temporary contexts +/// such as 'VAR=value ' +/// or for-loop variables +struct VarCtxGuard { + vars: HashSet +} +impl VarCtxGuard { + fn new(vars: HashSet) -> Self { + Self { vars } + } +} +impl Drop for VarCtxGuard { + fn drop(&mut self) { + write_vars(|v| { + for var in &self.vars { + v.unset_var(var); + } + }); + } +} + pub enum AssignBehavior { - Export, - Set, + Export, + Set, } /// Arguments to the execvpe function pub struct ExecArgs { - pub cmd: (CString, Span), - pub argv: Vec, - pub envp: Vec, + pub cmd: (CString, Span), + pub argv: Vec, + pub envp: Vec, } impl ExecArgs { - pub fn new(argv: Vec) -> ShResult { - assert!(!argv.is_empty()); - let argv = prepare_argv(argv)?; - let cmd = Self::get_cmd(&argv); - let argv = Self::get_argv(argv); - let envp = Self::get_envp(); - Ok(Self { cmd, argv, envp }) - } - pub fn get_cmd(argv: &[(String, Span)]) -> (CString, Span) { - 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() - } - pub fn get_envp() -> Vec { - std::env::vars() - .map(|v| CString::new(format!("{}={}", v.0, v.1)).unwrap()) - .collect() - } + pub fn new(argv: Vec) -> ShResult { + assert!(!argv.is_empty()); + let argv = prepare_argv(argv)?; + let cmd = Self::get_cmd(&argv); + let argv = Self::get_argv(argv); + let envp = Self::get_envp(); + Ok(Self { cmd, argv, envp }) + } + pub fn get_cmd(argv: &[(String, Span)]) -> (CString, Span) { + 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() + } + pub fn get_envp() -> Vec { + std::env::vars() + .map(|v| CString::new(format!("{}={}", v.0, v.1)).unwrap()) + .collect() + } } pub fn exec_input(input: String, io_stack: Option) -> 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(()); - } + write_meta(|m| m.start_timer()); + let log_tab = { + let fern = FERN.read().unwrap(); + // TODO: Is there a better way to do this? + // The goal is mainly to not be holding a lock while executing input + fern.read_logic().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()); - if let Some(mut stack) = io_stack { - dispatcher.io_stack.extend(stack.drain(..)); - } - dispatcher.begin_dispatch() + let mut dispatcher = Dispatcher::new(parser.extract_nodes()); + if let Some(mut stack) = io_stack { + dispatcher.io_stack.extend(stack.drain(..)); + } + dispatcher.begin_dispatch() } pub struct Dispatcher { - nodes: VecDeque, - pub io_stack: IoStack, - pub job_stack: JobStack, + nodes: VecDeque, + pub io_stack: IoStack, + pub job_stack: JobStack, } impl Dispatcher { - pub fn new(nodes: Vec) -> Self { - let nodes = VecDeque::from(nodes); - Self { - nodes, - io_stack: IoStack::new(), - job_stack: JobStack::new(), - } - } - pub fn begin_dispatch(&mut self) -> ShResult<()> { - flog!(TRACE, "beginning dispatch"); - while let Some(node) = self.nodes.pop_front() { - let blame = node.get_span(); - self.dispatch_node(node).try_blame(blame)?; - } - Ok(()) - } - pub fn dispatch_node(&mut self, node: Node) -> ShResult<()> { - match node.class { - NdRule::Conjunction { .. } => self.exec_conjunction(node)?, - NdRule::Pipeline { .. } => self.exec_pipeline(node)?, - NdRule::IfNode { .. } => self.exec_if(node)?, - NdRule::LoopNode { .. } => self.exec_loop(node)?, - NdRule::ForNode { .. } => self.exec_for(node)?, - NdRule::CaseNode { .. } => self.exec_case(node)?, - NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?, - NdRule::FuncDef { .. } => self.exec_func_def(node)?, - NdRule::Command { .. } => self.dispatch_cmd(node)?, - NdRule::Test { .. } => self.exec_test(node)?, - _ => unreachable!(), - } - Ok(()) - } - pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { - let Some(cmd) = node.get_command() else { - return self.exec_cmd(node); // Argv is empty, probably an assignment - }; - if cmd.flags.contains(TkFlags::BUILTIN) { - self.exec_builtin(node) - } else if is_func(node.get_command().cloned()) { - self.exec_func(node) - } else if is_subsh(node.get_command().cloned()) { - self.exec_subsh(node) - } else { - self.exec_cmd(node) - } - } - pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> { - let NdRule::Conjunction { elements } = conjunction.class else { - unreachable!() - }; + pub fn new(nodes: Vec) -> Self { + let nodes = VecDeque::from(nodes); + Self { + nodes, + io_stack: IoStack::new(), + job_stack: JobStack::new(), + } + } + pub fn begin_dispatch(&mut self) -> ShResult<()> { + flog!(TRACE, "beginning dispatch"); + while let Some(node) = self.nodes.pop_front() { + let blame = node.get_span(); + self.dispatch_node(node).try_blame(blame)?; + } + Ok(()) + } + pub fn dispatch_node(&mut self, node: Node) -> ShResult<()> { + match node.class { + NdRule::Conjunction { .. } => self.exec_conjunction(node)?, + NdRule::Pipeline { .. } => self.exec_pipeline(node)?, + NdRule::IfNode { .. } => self.exec_if(node)?, + NdRule::LoopNode { .. } => self.exec_loop(node)?, + NdRule::ForNode { .. } => self.exec_for(node)?, + NdRule::CaseNode { .. } => self.exec_case(node)?, + NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?, + NdRule::FuncDef { .. } => self.exec_func_def(node)?, + NdRule::Command { .. } => self.dispatch_cmd(node)?, + NdRule::Test { .. } => self.exec_test(node)?, + _ => unreachable!(), + } + Ok(()) + } + pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { + let Some(cmd) = node.get_command() else { + return self.exec_cmd(node); // Argv is empty, probably an assignment + }; + if is_func(node.get_command().cloned()) { + self.exec_func(node) + } else if cmd.flags.contains(TkFlags::BUILTIN) { + self.exec_builtin(node) + } else if is_subsh(node.get_command().cloned()) { + self.exec_subsh(node) + } else { + self.exec_cmd(node) + } + } + pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> { + let NdRule::Conjunction { elements } = conjunction.class else { + unreachable!() + }; - let mut elem_iter = elements.into_iter(); - while let Some(element) = elem_iter.next() { - let ConjunctNode { cmd, operator } = element; - self.dispatch_node(*cmd)?; + let mut elem_iter = elements.into_iter(); + while let Some(element) = elem_iter.next() { + let ConjunctNode { cmd, operator } = element; + self.dispatch_node(*cmd)?; - let status = state::get_status(); - match operator { - ConjunctOp::And => { - if status != 0 { - break; - } - } - ConjunctOp::Or => { - if status == 0 { - break; - } - } - ConjunctOp::Null => break, - } - } - Ok(()) - } - pub fn exec_test(&mut self, node: Node) -> ShResult<()> { - let test_result = double_bracket_test(node)?; - match test_result { - true => state::set_status(0), - false => state::set_status(1), - } - Ok(()) - } - pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> { - let blame = func_def.get_span(); - let NdRule::FuncDef { name, body } = func_def.class else { - unreachable!() - }; - let body_span = body.get_span(); - let body = body_span.as_str().to_string(); - let name = name.span.as_str().strip_suffix("()").unwrap(); + let status = state::get_status(); + match operator { + ConjunctOp::And => { + if status != 0 { + break; + } + } + ConjunctOp::Or => { + if status == 0 { + break; + } + } + ConjunctOp::Null => break, + } + } + Ok(()) + } + pub fn exec_test(&mut self, node: Node) -> ShResult<()> { + let test_result = double_bracket_test(node)?; + match test_result { + true => state::set_status(0), + false => state::set_status(1), + } + Ok(()) + } + pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> { + let blame = func_def.get_span(); + let NdRule::FuncDef { name, body } = func_def.class else { + unreachable!() + }; + let body_span = body.get_span(); + let body = body_span.as_str().to_string(); + let name = name.span.as_str().strip_suffix("()").unwrap(); - if KEYWORDS.contains(&name) { - return Err(ShErr::full( - ShErrKind::SyntaxErr, - format!("function: Forbidden function name `{name}`"), - blame, - )); - } + if KEYWORDS.contains(&name) { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + format!("function: Forbidden function name `{name}`"), + blame, + )); + } - let mut func_parser = ParsedSrc::new(Arc::new(body)); - if let Err(errors) = func_parser.parse_src() { - for error in errors { - eprintln!("{error}"); - } - return Ok(()); - } + let mut func_parser = ParsedSrc::new(Arc::new(body)); + if let Err(errors) = func_parser.parse_src() { + for error in errors { + eprintln!("{error}"); + } + return Ok(()); + } - let func = ShFunc::new(func_parser); - write_logic(|l| l.insert_func(name, func)); // Store the AST - Ok(()) - } - fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { - let NdRule::Command { assignments, argv } = subsh.class else { - unreachable!() - }; + let func = ShFunc::new(func_parser); + write_logic(|l| l.insert_func(name, func)); // Store the AST + Ok(()) + } + fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { + let NdRule::Command { assignments, argv } = subsh.class else { + unreachable!() + }; - self.set_assignments(assignments, AssignBehavior::Export)?; - self.io_stack.append_to_frame(subsh.redirs); - let mut argv = prepare_argv(argv)?; + let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; + let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); + self.io_stack.append_to_frame(subsh.redirs); + let mut argv = prepare_argv(argv)?; - let subsh = argv.remove(0); - let subsh_body = subsh.0.to_string(); - let snapshot = get_snapshots(); + let subsh = argv.remove(0); + let subsh_body = subsh.0.to_string(); + let _guard = ScopeGuard::shared_scope(); - if let Err(e) = exec_input(subsh_body, None) { - restore_snapshot(snapshot); - return Err(e); - } + exec_input(subsh_body, None)?; - restore_snapshot(snapshot); - Ok(()) - } - fn exec_func(&mut self, func: Node) -> ShResult<()> { - let blame = func.get_span().clone(); - let NdRule::Command { - assignments, - mut argv, - } = func.class - else { - unreachable!() - }; + Ok(()) + } + fn exec_func(&mut self, func: Node) -> ShResult<()> { + let blame = func.get_span().clone(); + let NdRule::Command { + assignments, + mut argv, + } = func.class + else { + unreachable!() + }; - self.set_assignments(assignments, AssignBehavior::Export)?; + let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; + let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); - self.io_stack.append_to_frame(func.redirs); + self.io_stack.append_to_frame(func.redirs); - let func_name = argv.remove(0).span.as_str().to_string(); - let argv = prepare_argv(argv)?; - if let Some(func) = read_logic(|l| l.get_func(&func_name)) { - let snapshot = get_snapshots(); - // Set up the inner scope - write_vars(|v| { - **v = VarTab::new(); - v.clear_args(); - for (arg, _) in argv { - v.bpush_arg(arg.to_string()); - } - }); + let func_name = argv.remove(0).span.as_str().to_string(); + let argv = prepare_argv(argv)?; + if let Some(func) = read_logic(|l| l.get_func(&func_name)) { + let _guard = ScopeGuard::exclusive_scope(Some(argv)); - if let Err(e) = self.exec_brc_grp((*func).clone()) { - restore_snapshot(snapshot); - match e.kind() { - ShErrKind::FuncReturn(code) => { - state::set_status(*code); - return Ok(()); - } - _ => return { Err(e) }, - } - } + if let Err(e) = self.exec_brc_grp((*func).clone()) { + match e.kind() { + ShErrKind::FuncReturn(code) => { + state::set_status(*code); + return Ok(()); + } + _ => return Err(e), + } + } - // Return to the outer scope - restore_snapshot(snapshot); - Ok(()) - } else { - Err(ShErr::full( - ShErrKind::InternalErr, - format!("Failed to find function '{}'", func_name), - blame, - )) - } - } - fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { - let NdRule::BraceGrp { body } = brc_grp.class else { - unreachable!() - }; - let mut io_frame = self.io_stack.pop_frame(); - io_frame.extend(brc_grp.redirs); + Ok(()) + } else { + Err(ShErr::full( + ShErrKind::InternalErr, + format!("Failed to find function '{}'", func_name), + blame, + )) + } + } + fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { + let NdRule::BraceGrp { body } = brc_grp.class else { + unreachable!() + }; + let mut io_frame = self.io_stack.pop_frame(); + io_frame.extend(brc_grp.redirs); - for node in body { - let blame = node.get_span(); - self.io_stack.push_frame(io_frame.clone()); - self.dispatch_node(node).try_blame(blame)?; - } + for node in body { + let blame = node.get_span(); + self.io_stack.push_frame(io_frame.clone()); + self.dispatch_node(node).try_blame(blame)?; + } - Ok(()) - } - fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { - let NdRule::CaseNode { - pattern, - case_blocks, - } = case_stmt.class - else { - unreachable!() - }; + Ok(()) + } + fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { + let NdRule::CaseNode { + pattern, + case_blocks, + } = case_stmt.class + else { + unreachable!() + }; - self.io_stack.append_to_frame(case_stmt.redirs); + self.io_stack.append_to_frame(case_stmt.redirs); - let exp_pattern = pattern.clone().expand()?; - let pattern_raw = exp_pattern - .get_words() - .first() - .map(|s| s.to_string()) - .unwrap_or_default(); + let exp_pattern = pattern.clone().expand()?; + let pattern_raw = exp_pattern + .get_words() + .first() + .map(|s| s.to_string()) + .unwrap_or_default(); - 'outer: for block in case_blocks { - let CaseNode { pattern, body } = block; - let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); - // Split at '|' to allow for multiple patterns like `foo|bar)` - let block_patterns = block_pattern_raw.split('|'); + 'outer: for block in case_blocks { + let CaseNode { pattern, body } = block; + let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); + // Split at '|' to allow for multiple patterns like `foo|bar)` + let block_patterns = block_pattern_raw.split('|'); - for pattern in block_patterns { - if pattern_raw == pattern || pattern == "*" { - for node in &body { - self.dispatch_node(node.clone())?; - } - break 'outer; - } - } - } + for pattern in block_patterns { + if pattern_raw == pattern || pattern == "*" { + for node in &body { + self.dispatch_node(node.clone())?; + } + break 'outer; + } + } + } - Ok(()) - } - fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { - let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { - unreachable!(); - }; - let keep_going = |kind: LoopKind, status: i32| -> bool { - match kind { - LoopKind::While => status == 0, - LoopKind::Until => status != 0, - } - }; + Ok(()) + } + fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { + let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { + unreachable!(); + }; + let keep_going = |kind: LoopKind, status: i32| -> bool { + match kind { + LoopKind::While => status == 0, + LoopKind::Until => status != 0, + } + }; - let io_frame = self.io_stack.pop_frame(); - let (mut cond_frame, mut body_frame) = io_frame.split_frame(); - let (in_redirs, out_redirs) = loop_stmt.redirs.split_by_channel(); - cond_frame.extend(in_redirs); - body_frame.extend(out_redirs); + let io_frame = self.io_stack.pop_frame(); + let (mut cond_frame, mut body_frame) = io_frame.split_frame(); + let (in_redirs, out_redirs) = loop_stmt.redirs.split_by_channel(); + cond_frame.extend(in_redirs); + body_frame.extend(out_redirs); - let CondNode { cond, body } = cond_node; - 'outer: loop { - self.io_stack.push(cond_frame.clone()); + let CondNode { cond, body } = cond_node; + 'outer: loop { + self.io_stack.push(cond_frame.clone()); - if let Err(e) = self.dispatch_node(*cond.clone()) { - state::set_status(1); - return Err(e); - } + if let Err(e) = self.dispatch_node(*cond.clone()) { + state::set_status(1); + return Err(e); + } - let status = state::get_status(); - if keep_going(kind, status) { - self.io_stack.push(body_frame.clone()); - for node in &body { - if let Err(e) = self.dispatch_node(node.clone()) { - match e.kind() { - ShErrKind::LoopBreak(code) => { - state::set_status(*code); - break 'outer; - } - ShErrKind::LoopContinue(code) => { - state::set_status(*code); - continue 'outer; - } - _ => return Err(e), - } - } - } - } else { - break; - } - } + let status = state::get_status(); + if keep_going(kind, status) { + self.io_stack.push(body_frame.clone()); + for node in &body { + if let Err(e) = self.dispatch_node(node.clone()) { + match e.kind() { + ShErrKind::LoopBreak(code) => { + state::set_status(*code); + break 'outer; + } + ShErrKind::LoopContinue(code) => { + state::set_status(*code); + continue 'outer; + } + _ => return Err(e), + } + } + } + } else { + break; + } + } - Ok(()) - } - fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { - let NdRule::ForNode { vars, arr, body } = for_stmt.class else { - unreachable!(); - }; + Ok(()) + } + fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { - let io_frame = self.io_stack.pop_frame(); - let (_, mut body_frame) = io_frame.split_frame(); - let (_, out_redirs) = for_stmt.redirs.split_by_channel(); - body_frame.extend(out_redirs); + let NdRule::ForNode { vars, arr, body } = for_stmt.class else { + unreachable!(); + }; + let mut for_guard = VarCtxGuard::new( + vars.iter().map(|v| v.to_string()).collect() + ); - 'outer: for chunk in arr.chunks(vars.len()) { - let empty = Tk::default(); - let chunk_iter = vars.iter().zip( - chunk.iter().chain(std::iter::repeat(&empty)), // Or however you define an empty token - ); + let io_frame = self.io_stack.pop_frame(); + let (_, mut body_frame) = io_frame.split_frame(); + let (_, out_redirs) = for_stmt.redirs.split_by_channel(); + body_frame.extend(out_redirs); - for (var, val) in chunk_iter { - write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), false)); - } + 'outer: for chunk in arr.chunks(vars.len()) { + let empty = Tk::default(); + let chunk_iter = vars.iter().zip( + chunk.iter().chain(std::iter::repeat(&empty)), // Or however you define an empty token + ); - for node in body.clone() { - self.io_stack.push(body_frame.clone()); - if let Err(e) = self.dispatch_node(node) { - match e.kind() { - ShErrKind::LoopBreak(code) => { - state::set_status(*code); - break 'outer; - } - ShErrKind::LoopContinue(code) => { - state::set_status(*code); - continue 'outer; - } - _ => return Err(e), - } - } - } - } + for (var, val) in chunk_iter { + write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE)); + for_guard.vars.insert(var.to_string()); + } - Ok(()) - } - fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { - let NdRule::IfNode { - cond_nodes, - else_block, - } = if_stmt.class - else { - unreachable!(); - }; - // Pop the current frame and split it - let io_frame = self.io_stack.pop_frame(); - let (mut cond_frame, mut body_frame) = io_frame.split_frame(); - let (in_redirs, out_redirs) = if_stmt.redirs.split_by_channel(); - cond_frame.extend(in_redirs); // Condition gets input redirs - body_frame.extend(out_redirs); // Body gets output redirs + for node in body.clone() { + self.io_stack.push(body_frame.clone()); + if let Err(e) = self.dispatch_node(node) { + match e.kind() { + ShErrKind::LoopBreak(code) => { + state::set_status(*code); + break 'outer; + } + ShErrKind::LoopContinue(code) => { + state::set_status(*code); + continue 'outer; + } + _ => return Err(e), + } + } + } + } - for node in cond_nodes { - let CondNode { cond, body } = node; - self.io_stack.push(cond_frame.clone()); + Ok(()) + } + fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { + let NdRule::IfNode { + cond_nodes, + else_block, + } = if_stmt.class + else { + unreachable!(); + }; + // Pop the current frame and split it + let io_frame = self.io_stack.pop_frame(); + let (mut cond_frame, mut body_frame) = io_frame.split_frame(); + let (in_redirs, out_redirs) = if_stmt.redirs.split_by_channel(); + cond_frame.extend(in_redirs); // Condition gets input redirs + body_frame.extend(out_redirs); // Body gets output redirs - if let Err(e) = self.dispatch_node(*cond) { - state::set_status(1); - return Err(e); - } + for node in cond_nodes { + let CondNode { cond, body } = node; + self.io_stack.push(cond_frame.clone()); - match state::get_status() { - 0 => { - for body_node in body { - self.io_stack.push(body_frame.clone()); - self.dispatch_node(body_node)?; - } - } - _ => continue, - } - } + if let Err(e) = self.dispatch_node(*cond) { + state::set_status(1); + return Err(e); + } - if !else_block.is_empty() { - for node in else_block { - self.io_stack.push(body_frame.clone()); - self.dispatch_node(node)?; - } - } + match state::get_status() { + 0 => { + for body_node in body { + self.io_stack.push(body_frame.clone()); + self.dispatch_node(body_node)?; + } + } + _ => continue, + } + } - Ok(()) - } - fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { - let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { - unreachable!() - }; - self.job_stack.new_job(); - // Zip the commands and their respective pipes into an iterator - let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); + if !else_block.is_empty() { + for node in else_block { + self.io_stack.push(body_frame.clone()); + self.dispatch_node(node)?; + } + } - for ((rpipe, wpipe), cmd) in pipes_and_cmds { - if let Some(pipe) = rpipe { - self.io_stack.push_to_frame(pipe); - } - if let Some(pipe) = wpipe { - self.io_stack.push_to_frame(pipe); - } - self.dispatch_node(cmd)?; - } - let job = self.job_stack.finalize_job().unwrap(); - let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); - dispatch_job(job, is_bg)?; - Ok(()) - } - fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { - let NdRule::Command { - ref mut assignments, - ref mut argv, - } = &mut cmd.class - else { - unreachable!() - }; - let env_vars_to_unset = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; - let cmd_raw = argv.first().unwrap(); - let curr_job_mut = self.job_stack.curr_job_mut().unwrap(); - let io_stack_mut = &mut self.io_stack; + Ok(()) + } + fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { + let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { + unreachable!() + }; + self.job_stack.new_job(); + // Zip the commands and their respective pipes into an iterator + let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); - if cmd_raw.as_str() == "builtin" { - *argv = argv - .iter_mut() - .skip(1) - .map(|tk| tk.clone()) - .collect::>(); - return self.exec_builtin(cmd); - } else if cmd_raw.as_str() == "command" { - *argv = argv - .iter_mut() - .skip(1) - .map(|tk| tk.clone()) - .collect::>(); - return self.dispatch_cmd(cmd); - } + for ((rpipe, wpipe), cmd) in pipes_and_cmds { + if let Some(pipe) = rpipe { + self.io_stack.push_to_frame(pipe); + } + if let Some(pipe) = wpipe { + self.io_stack.push_to_frame(pipe); + } + self.dispatch_node(cmd)?; + } + let job = self.job_stack.finalize_job().unwrap(); + let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); + dispatch_job(job, is_bg)?; + Ok(()) + } + fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { + let NdRule::Command { + assignments, + argv, + } = &mut cmd.class + else { + unreachable!() + }; + let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; + let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); - flog!(TRACE, "doing builtin"); - let result = match cmd_raw.span.as_str() { - "echo" => echo(cmd, io_stack_mut, curr_job_mut), - "cd" => cd(cmd, curr_job_mut), - "export" => export(cmd, io_stack_mut, curr_job_mut), - "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), - "source" => source(cmd, curr_job_mut), - "shift" => shift(cmd, curr_job_mut), - "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), - "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), - "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), - "alias" => alias(cmd, io_stack_mut, curr_job_mut), - "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), - "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), - "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), - "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), - "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), - "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), - "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), - _ => unimplemented!( - "Have not yet added support for builtin '{}'", - cmd_raw.span.as_str() - ), - }; + let cmd_raw = argv.first().unwrap(); + let curr_job_mut = self.job_stack.curr_job_mut().unwrap(); + let io_stack_mut = &mut self.io_stack; - for var in env_vars_to_unset { - env::set_var(&var, ""); - } + if cmd_raw.as_str() == "builtin" { + *argv = argv + .iter_mut() + .skip(1) + .map(|tk| tk.clone()) + .collect::>(); + return self.exec_builtin(cmd); + } else if cmd_raw.as_str() == "command" { + *argv = argv + .iter_mut() + .skip(1) + .map(|tk| tk.clone()) + .collect::>(); + return self.dispatch_cmd(cmd); + } - if let Err(e) = result { - state::set_status(1); - return Err(e); - } - Ok(()) - } - fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { - let NdRule::Command { assignments, argv } = cmd.class else { - unreachable!() - }; - let mut env_vars_to_unset = vec![]; - if !assignments.is_empty() { - let assign_behavior = if argv.is_empty() { - AssignBehavior::Set - } else { - AssignBehavior::Export - }; - env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; - } + flog!(TRACE, "doing builtin"); + let result = match cmd_raw.span.as_str() { + "echo" => echo(cmd, io_stack_mut, curr_job_mut), + "cd" => cd(cmd, curr_job_mut), + "export" => export(cmd, io_stack_mut, curr_job_mut), + "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), + "source" => source(cmd, curr_job_mut), + "shift" => shift(cmd, curr_job_mut), + "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), + "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), + "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), + "alias" => alias(cmd, io_stack_mut, curr_job_mut), + "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), + "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), + "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), + "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), + "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), + "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), + "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), + _ => unimplemented!( + "Have not yet added support for builtin '{}'", + cmd_raw.span.as_str() + ), + }; - if argv.is_empty() { - return Ok(()); - } + if let Err(e) = result { + state::set_status(1); + return Err(e); + } + Ok(()) + } + fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { + let NdRule::Command { assignments, argv } = cmd.class else { + unreachable!() + }; + let mut env_vars_to_unset = vec![]; + if !assignments.is_empty() { + let assign_behavior = if argv.is_empty() { + AssignBehavior::Set + } else { + AssignBehavior::Export + }; + env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; + } - self.io_stack.append_to_frame(cmd.redirs); + if argv.is_empty() { + return Ok(()); + } - let exec_args = ExecArgs::new(argv)?; - let io_frame = self.io_stack.pop_frame(); - run_fork( - io_frame, - Some(exec_args), - self.job_stack.curr_job_mut().unwrap(), - def_child_action, - def_parent_action, - )?; + self.io_stack.append_to_frame(cmd.redirs); - for var in env_vars_to_unset { - std::env::set_var(&var, ""); - } + let exec_args = ExecArgs::new(argv)?; + let io_frame = self.io_stack.pop_frame(); + run_fork( + io_frame, + Some(exec_args), + self.job_stack.curr_job_mut().unwrap(), + def_child_action, + def_parent_action, + )?; - Ok(()) - } - fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> ShResult> { - let mut new_env_vars = vec![]; - match behavior { - AssignBehavior::Export => { - for assign in assigns { - let NdRule::Assignment { kind, var, val } = assign.class else { - unreachable!() - }; - 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, true)), - AssignKind::PlusEq => todo!(), - AssignKind::MinusEq => todo!(), - AssignKind::MultEq => todo!(), - AssignKind::DivEq => todo!(), - } - new_env_vars.push(var.to_string()); - } - } - AssignBehavior::Set => { - for assign in assigns { - let NdRule::Assignment { kind, var, val } = assign.class else { - unreachable!() - }; - 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, true)), - AssignKind::PlusEq => todo!(), - AssignKind::MinusEq => todo!(), - AssignKind::MultEq => todo!(), - AssignKind::DivEq => todo!(), - } - } - } - } - Ok(new_env_vars) - } + for var in env_vars_to_unset { + unsafe { std::env::set_var(&var, "") }; + } + + Ok(()) + } + fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> ShResult> { + let mut new_env_vars = vec![]; + match behavior { + AssignBehavior::Export => { + for assign in assigns { + let NdRule::Assignment { kind, var, val } = assign.class else { + unreachable!() + }; + 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::PlusEq => todo!(), + AssignKind::MinusEq => todo!(), + AssignKind::MultEq => todo!(), + AssignKind::DivEq => todo!(), + } + new_env_vars.push(var.to_string()); + } + } + AssignBehavior::Set => { + for assign in assigns { + let NdRule::Assignment { kind, var, val } = assign.class else { + unreachable!() + }; + 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::PlusEq => todo!(), + AssignKind::MinusEq => todo!(), + AssignKind::MultEq => todo!(), + AssignKind::DivEq => todo!(), + } + } + } + } + Ok(new_env_vars) + } } pub fn prepare_argv(argv: Vec) -> ShResult> { - let mut args = vec![]; + let mut args = vec![]; - for arg in argv { - let span = arg.span.clone(); - let expanded = arg.expand()?; - for exp in expanded.get_words() { - args.push((exp, span.clone())) - } - } - Ok(args) + for arg in argv { + let span = arg.span.clone(); + let expanded = arg.expand()?; + for exp in expanded.get_words() { + args.push((exp, span.clone())) + } + } + Ok(args) } pub fn run_fork( - io_frame: IoFrame, - exec_args: Option, - job: &mut JobBldr, - child_action: C, - parent_action: P, + io_frame: IoFrame, + exec_args: Option, + job: &mut JobBldr, + child_action: C, + parent_action: P, ) -> ShResult<()> where - C: Fn(IoFrame, Option), - P: Fn(&mut JobBldr, Option<&str>, Pid) -> ShResult<()>, + C: Fn(IoFrame, Option), + P: Fn(&mut JobBldr, Option<&str>, Pid) -> ShResult<()>, { - match unsafe { fork()? } { - ForkResult::Child => { - child_action(io_frame, exec_args); - exit(0); // Just in case - } - ForkResult::Parent { child } => { - let cmd = if let Some(args) = exec_args { - Some(args.cmd.0.to_str().unwrap().to_string()) - } else { - None - }; - parent_action(job, cmd.as_deref(), child) - } - } + match unsafe { fork()? } { + ForkResult::Child => { + child_action(io_frame, exec_args); + exit(0); // Just in case + } + ForkResult::Parent { child } => { + let cmd = if let Some(args) = exec_args { + Some(args.cmd.0.to_str().unwrap().to_string()) + } else { + None + }; + parent_action(job, cmd.as_deref(), child) + } + } } /// The default behavior for the child process after forking pub fn def_child_action(mut io_frame: IoFrame, exec_args: Option) { - if let Err(e) = io_frame.redirect() { - eprintln!("{e}"); - } - let exec_args = exec_args.unwrap(); - let cmd = &exec_args.cmd.0; - let span = exec_args.cmd.1; + if let Err(e) = io_frame.redirect() { + eprintln!("{e}"); + } + let exec_args = exec_args.unwrap(); + let cmd = &exec_args.cmd.0; + let span = exec_args.cmd.1; - let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); + let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); - let cmd = cmd.to_str().unwrap().to_string(); - match e { - Errno::ENOENT => { - let err = ShErr::full(ShErrKind::CmdNotFound(cmd), "", span); - eprintln!("{err}"); - } - _ => { - let err = ShErr::full(ShErrKind::Errno, format!("{e}"), span); - eprintln!("{err}"); - } - } - exit(e as i32) + let cmd = cmd.to_str().unwrap().to_string(); + match e { + Errno::ENOENT => { + let err = ShErr::full(ShErrKind::CmdNotFound(cmd), "", span); + eprintln!("{err}"); + } + _ => { + let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); + eprintln!("{err}"); + } + } + exit(e as i32) } /// The default behavior for the parent process after forking pub fn def_parent_action(job: &mut JobBldr, cmd: Option<&str>, child_pid: Pid) -> ShResult<()> { - let child_pgid = if let Some(pgid) = job.pgid() { - pgid - } else { - job.set_pgid(child_pid); - child_pid - }; - let child = ChildProc::new(child_pid, cmd, Some(child_pgid))?; - job.push_child(child); - Ok(()) + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(child_pid); + child_pid + }; + let child = ChildProc::new(child_pid, cmd, Some(child_pgid))?; + job.push_child(child); + Ok(()) } /// Initialize the pipes for a pipeline @@ -714,30 +750,30 @@ pub fn def_parent_action(job: &mut JobBldr, cmd: Option<&str>, child_pid: Pid) - /// Commands inbetween get `(RPipe, WPipe)` /// If there is only one command, it gets `(None, None)` pub fn get_pipe_stack(num_cmds: usize) -> Vec<(Option, Option)> { - let mut stack = Vec::with_capacity(num_cmds); - let mut prev_read: Option = None; + let mut stack = Vec::with_capacity(num_cmds); + let mut prev_read: Option = None; - for i in 0..num_cmds { - if i == num_cmds - 1 { - stack.push((prev_read.take(), None)); - } else { - let (rpipe, wpipe) = IoMode::get_pipes(); - let r_redir = Redir::new(rpipe, RedirType::Input); - let w_redir = Redir::new(wpipe, RedirType::Output); + for i in 0..num_cmds { + if i == num_cmds - 1 { + stack.push((prev_read.take(), None)); + } else { + let (rpipe, wpipe) = IoMode::get_pipes(); + let r_redir = Redir::new(rpipe, RedirType::Input); + let w_redir = Redir::new(wpipe, RedirType::Output); - // Push (prev_read, Some(w_redir)) and set prev_read to r_redir - stack.push((prev_read.take(), Some(w_redir))); - prev_read = Some(r_redir); - } - } - stack + // Push (prev_read, Some(w_redir)) and set prev_read to r_redir + stack.push((prev_read.take(), Some(w_redir))); + prev_read = Some(r_redir); + } + } + stack } pub fn is_func(tk: Option) -> bool { - let Some(tk) = tk else { return false }; - read_logic(|l| l.get_func(&tk.to_string())).is_some() + let Some(tk) = tk else { return false }; + read_logic(|l| l.get_func(&tk.to_string())).is_some() } pub fn is_subsh(tk: Option) -> bool { - tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH)) + tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH)) } diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 138b5a0..82eb5e2 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -26,11 +26,17 @@ fn get_prompt() -> ShResult { expand_prompt(&prompt) } -pub fn readline(edit_mode: FernEditMode) -> ShResult { +pub fn readline(edit_mode: FernEditMode, initial: Option<&str>) -> ShResult { let prompt = get_prompt()?; let mut reader: Box = match edit_mode { - FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?), - FernEditMode::Emacs => todo!(), + FernEditMode::Vi => { + let mut fern_vi = FernVi::new(Some(prompt))?; + if let Some(input) = initial { + fern_vi = fern_vi.with_initial(&input) + } + Box::new(fern_vi) as Box + } + FernEditMode::Emacs => todo!(), // idk if I'm ever gonna do this one actually, I don't use emacs }; reader.readline() } diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 88c3284..658f14b 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -622,17 +622,25 @@ impl LineBuf { .map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count()) .unwrap_or(0) } - pub fn is_sentence_punctuation(&mut self, pos: usize) -> bool { - if let Some(gr) = self.grapheme_at(pos) { - if PUNCTUATION.contains(&gr) && self.grapheme_after(pos).is_some() { + pub fn is_sentence_punctuation(&self, pos: usize) -> bool { + self.next_sentence_start_from_punctuation(pos).is_some() + } + + /// If position is at sentence-ending punctuation, returns the position of the next sentence start. + /// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation. + #[allow(clippy::collapsible_if)] + pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option { + if let Some(gr) = self.read_grapheme_at(pos) { + if PUNCTUATION.contains(&gr) && self.read_grapheme_after(pos).is_some() { let mut fwd_indices = (pos + 1..self.cursor.max).peekable(); + // Skip any closing delimiters after the punctuation if self - .grapheme_after(pos) + .read_grapheme_after(pos) .is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr)) { while let Some(idx) = fwd_indices.peek() { if self - .grapheme_after(*idx) + .read_grapheme_at(*idx) .is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr)) { fwd_indices.next(); @@ -641,16 +649,32 @@ impl LineBuf { } } } + // Now we should be at whitespace - skip it to find sentence start if let Some(idx) = fwd_indices.next() { - if let Some(gr) = self.grapheme_at(idx) { + if let Some(gr) = self.read_grapheme_at(idx) { if is_whitespace(gr) { - return true; + if gr == "\n" { + return Some(idx); + } + // Skip remaining whitespace to find actual sentence start + while let Some(idx) = fwd_indices.next() { + if let Some(gr) = self.read_grapheme_at(idx) { + if is_whitespace(gr) { + if gr == "\n" { + return Some(idx); + } + continue; + } else { + return Some(idx); + } + } + } } } } } } - false + None } pub fn is_sentence_start(&mut self, pos: usize) -> bool { if self.grapheme_before(pos).is_some_and(is_whitespace) { @@ -875,11 +899,7 @@ impl LineBuf { let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { self.cursor.get() } else { - self.end_of_word_forward_or_start_of_word_backward_from( - self.cursor.get(), - word, - Direction::Backward, - ) + self.start_of_word_backward(self.cursor.get(), word) }; let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, true); Some((start, end)) @@ -888,11 +908,7 @@ impl LineBuf { let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { self.cursor.get() } else { - self.end_of_word_forward_or_start_of_word_backward_from( - self.cursor.get(), - word, - Direction::Backward, - ) + self.start_of_word_backward(self.cursor.get(), word) }; let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, false); Some((start, end)) @@ -907,39 +923,26 @@ impl LineBuf { ) -> Option<(usize, usize)> { let mut start = None; let mut end = None; - let mut fwd_indices = start_pos..self.cursor.max; + let mut fwd_indices = (start_pos..self.cursor.max).peekable(); while let Some(idx) = fwd_indices.next() { - let Some(gr) = self.grapheme_at(idx) else { - end = Some(self.cursor.max); + if self.grapheme_at(idx).is_none() { break; - }; - if PUNCTUATION.contains(&gr) && self.is_sentence_punctuation(idx) { + } + + if let Some(next_sentence_start) = self.next_sentence_start_from_punctuation(idx) { match bound { Bound::Inside => { end = Some(idx); break; } Bound::Around => { - let mut end_pos = idx; - while let Some(idx) = fwd_indices.next() { - if !self.grapheme_at(idx).is_some_and(is_whitespace) { - end_pos += 1; - break; - } else { - end_pos += 1; - } - } - end = Some(end_pos); + end = Some(next_sentence_start); break; } } } } let mut end = end.unwrap_or(self.cursor.max); - flog!(DEBUG, end); - flog!(DEBUG, self.grapheme_at(end)); - flog!(DEBUG, self.grapheme_before(end)); - flog!(DEBUG, self.grapheme_after(end)); let mut bkwd_indices = (0..end).rev(); while let Some(idx) = bkwd_indices.next() { @@ -949,10 +952,6 @@ impl LineBuf { } } let start = start.unwrap_or(0); - flog!(DEBUG, start); - flog!(DEBUG, self.grapheme_at(start)); - flog!(DEBUG, self.grapheme_before(start)); - flog!(DEBUG, self.grapheme_after(start)); if count > 1 { if let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) { @@ -1321,7 +1320,6 @@ impl LineBuf { dir: Direction, include_last_char: bool, ) -> usize { - // Not sorry for these method names btw let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false); for i in 0..count { // We alter 'include_last_char' to only be true on the last iteration @@ -1331,16 +1329,12 @@ impl LineBuf { pos.set(match to { To::Start => { match dir { - Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from( - pos.get(), - word, - dir, - include_last_char_and_is_last_word, - ), + Direction::Forward => { + self.start_of_word_forward(pos.get(), word, include_last_char_and_is_last_word) + } Direction::Backward => 'backward: { // We also need to handle insert mode's Ctrl+W behaviors here - let target = - self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir); + let target = self.start_of_word_backward(pos.get(), word); // Check to see if we are in insert mode let Some(start_pos) = self.insert_mode_start_pos else { @@ -1361,38 +1355,18 @@ impl LineBuf { } } To::End => match dir { - Direction::Forward => { - self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir) - } - Direction::Backward => { - self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir, false) - } + Direction::Forward => self.end_of_word_forward(pos.get(), word), + Direction::Backward => self.end_of_word_backward(pos.get(), word, false), }, }); } pos.get() } - /// Finds the start of a word forward, or the end of a word backward - /// - /// Finding the start of a word in the forward direction, and finding the end - /// of a word in the backward direction are logically the same operation, if - /// you use a reversed iterator for the backward motion. - /// - /// Tied with 'end_of_word_forward_or_start_of_word_backward_from()' for the - /// longest method name I have ever written - pub fn start_of_word_forward_or_end_of_word_backward_from( - &mut self, - mut pos: usize, - word: Word, - dir: Direction, - include_last_char: bool, - ) -> usize { - let default = match dir { - Direction::Backward => 0, - Direction::Forward => self.grapheme_indices().len(), - }; - let mut indices_iter = self.directional_indices_iter_from(pos, dir).peekable(); // And make it peekable + /// Find the start of the next word forward + pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize { + let default = self.grapheme_indices().len(); + let mut indices_iter = (pos..self.cursor.max).peekable(); match word { Word::Big => { @@ -1404,7 +1378,6 @@ impl LineBuf { let Some(idx) = indices_iter.next() else { return default; }; - // We have a 'cw' call, do not include the trailing whitespace if include_last_char { return idx; } else { @@ -1412,15 +1385,14 @@ impl LineBuf { } } - // Check current grapheme let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default; }; let on_whitespace = is_whitespace(&cur_char); - // Find the next whitespace if !on_whitespace { - let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) + let Some(ws_pos) = + indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { return default; }; @@ -1429,11 +1401,9 @@ impl LineBuf { } } - // Return the next visible grapheme position - let non_ws_pos = indices_iter + indices_iter .find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) - .unwrap_or(default); - non_ws_pos + .unwrap_or(default) } Word::Normal => { let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { @@ -1462,7 +1432,6 @@ impl LineBuf { } let on_whitespace = is_whitespace(&cur_char); - // Advance until hitting whitespace or a different character class if !on_whitespace { let other_class_pos = indices_iter.find(|i| { self @@ -1472,7 +1441,6 @@ impl LineBuf { let Some(other_class_pos) = other_class_pos else { return default; }; - // If we hit a different character class, we return here if self .grapheme_at(other_class_pos) .is_some_and(|c| !is_whitespace(c)) @@ -1482,79 +1450,54 @@ impl LineBuf { } } - // We are now certainly on a whitespace character. Advance until a - // non-whitespace character. - let non_ws_pos = indices_iter + indices_iter .find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) - .unwrap_or(default); - non_ws_pos + .unwrap_or(default) } } } - /// Finds the end of a word forward, or the start of a word backward - /// - /// Finding the end of a word in the forward direction, and finding the start - /// of a word in the backward direction are logically the same operation, if - /// you use a reversed iterator for the backward motion. - pub fn end_of_word_forward_or_start_of_word_backward_from( - &mut self, - mut pos: usize, - word: Word, - dir: Direction, - ) -> usize { - let default = match dir { - Direction::Backward => 0, - Direction::Forward => self.grapheme_indices().len(), - }; - let mut indices_iter = self.directional_indices_iter_from(pos, dir).peekable(); + /// Find the end of the previous word backward + pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize { + let default = self.grapheme_indices().len(); + let mut indices_iter = (0..pos).rev().peekable(); match word { Word::Big => { - let Some(next_idx) = indices_iter.peek() else { + let Some(next) = indices_iter.peek() else { return default; }; - let on_boundary = self.grapheme_at(*next_idx).is_none_or(is_whitespace); + let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace); if on_boundary { let Some(idx) = indices_iter.next() else { return default; }; - pos = idx; + if include_last_char { + return idx; + } else { + pos = idx; + } } - // Check current grapheme + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default; }; let on_whitespace = is_whitespace(&cur_char); - // Advance iterator to next visible grapheme - if on_whitespace { - let Some(_non_ws_pos) = - indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) + if !on_whitespace { + let Some(ws_pos) = + indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { return default; }; - } - - // The position of the next whitespace will tell us where the end (or start) of - // the word is - let Some(next_ws_pos) = - indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) - else { - return default; - }; - pos = next_ws_pos; - - if pos == self.grapheme_indices().len() { - // We reached the end of the buffer - pos - } else { - // We hit some whitespace, so we will go back one - match dir { - Direction::Forward => pos.saturating_sub(1), - Direction::Backward => pos + 1, + if include_last_char { + return ws_pos; } } + + indices_iter + .find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) + .unwrap_or(default) } Word::Normal => { let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { @@ -1568,32 +1511,131 @@ impl LineBuf { .grapheme_at(*next_idx) .is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); if on_boundary { - let next_idx = indices_iter.next().unwrap(); - pos = next_idx + if include_last_char { + return *next_idx; + } else { + pos = *next_idx; + } + } + + let Some(next_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default; + }; + if is_other_class_not_ws(&cur_char, &next_char) { + return pos; + } + let on_whitespace = is_whitespace(&cur_char); + + if !on_whitespace { + let other_class_pos = indices_iter.find(|i| { + self + .grapheme_at(*i) + .is_some_and(|c| is_other_class_or_is_ws(c, &next_char)) + }); + let Some(other_class_pos) = other_class_pos else { + return default; + }; + if self + .grapheme_at(other_class_pos) + .is_some_and(|c| !is_whitespace(c)) + || include_last_char + { + return other_class_pos; + } + } + + indices_iter + .find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) + .unwrap_or(default) + } + } + } + + /// Find the end of the current/next word forward + pub fn end_of_word_forward(&mut self, mut pos: usize, word: Word) -> usize { + let default = self.cursor.max; + if pos >= default { + return default; + } + let mut fwd_indices = (pos + 1..default).peekable(); + + match word { + Word::Big => { + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default; + }; + let Some(next_idx) = fwd_indices.peek() else { + return default; + }; + let on_boundary = + !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(is_whitespace); + if on_boundary { + let Some(idx) = fwd_indices.next() else { + return default; + }; + pos = idx; } - // Check current grapheme let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default; }; let on_whitespace = is_whitespace(&cur_char); - // Proceed to next visible grapheme if on_whitespace { - let Some(non_ws_pos) = - indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) + let Some(_non_ws_pos) = + fwd_indices.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) else { return default; }; - pos = non_ws_pos + } + + let Some(next_ws_pos) = + fwd_indices.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) + else { + return default; + }; + pos = next_ws_pos; + + if pos == self.grapheme_indices().len() { + pos + } else { + pos.saturating_sub(1) + } + } + Word::Normal => { + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default; + }; + let Some(next_idx) = fwd_indices.peek() else { + return default; + }; + let on_boundary = !is_whitespace(&cur_char) + && self + .grapheme_at(*next_idx) + .is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); + if on_boundary { + let next_idx = fwd_indices.next().unwrap(); + pos = next_idx; + } + + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default; + }; + let on_whitespace = is_whitespace(&cur_char); + + if on_whitespace { + let Some(non_ws_pos) = + fwd_indices.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) + else { + return default; + }; + pos = non_ws_pos; } let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return self.grapheme_indices().len(); }; - // The position of the next differing character class will tell us where the end - // (or start) of the word is - let Some(next_ws_pos) = indices_iter.find(|i| { + let Some(next_ws_pos) = fwd_indices.find(|i| { self .grapheme_at(*i) .is_some_and(|c| is_other_class_or_is_ws(c, &cur_char)) @@ -1603,18 +1645,113 @@ impl LineBuf { pos = next_ws_pos; if pos == self.grapheme_indices().len() { - // We reached the end of the buffer pos } else { - // We hit some other character class, so we go back one - match dir { - Direction::Forward => pos.saturating_sub(1), - Direction::Backward => pos + 1, - } + pos.saturating_sub(1) } } } } + + /// Find the start of the current/previous word backward + pub fn start_of_word_backward(&mut self, mut pos: usize, word: Word) -> usize { + let default = 0; + let mut indices_iter = (0..pos).rev().peekable(); + + match word { + Word::Big => { + let on_boundary = 'bound_check: { + let Some(next_idx) = indices_iter.peek() else { + break 'bound_check false; + }; + self.grapheme_at(*next_idx).is_none_or(is_whitespace) + }; + if on_boundary { + let Some(idx) = indices_iter.next() else { + return default; + }; + pos = idx; + } + + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default; + }; + let on_whitespace = is_whitespace(&cur_char); + + if on_whitespace { + let Some(_non_ws_pos) = + indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) + else { + return default; + }; + } + + let Some(next_ws_pos) = + indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) + else { + return default; + }; + pos = next_ws_pos; + + if pos == self.grapheme_indices().len() { + pos + } else { + pos + 1 + } + } + Word::Normal => { + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default; + }; + let on_boundary = 'bound_check: { + let Some(next_idx) = indices_iter.peek() else { + break 'bound_check false; + }; + !is_whitespace(&cur_char) + && self + .grapheme_at(*next_idx) + .is_some_and(|c| is_other_class_or_is_ws(c, &cur_char)) + }; + if on_boundary { + let next_idx = indices_iter.next().unwrap(); + pos = next_idx; + } + + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default; + }; + let on_whitespace = is_whitespace(&cur_char); + + if on_whitespace { + let Some(non_ws_pos) = + indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) + else { + return default; + }; + pos = non_ws_pos; + } + + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return self.grapheme_indices().len(); + }; + let Some(next_ws_pos) = indices_iter.find(|i| { + self + .grapheme_at(*i) + .is_some_and(|c| is_other_class_or_is_ws(c, &cur_char)) + }) else { + return default; + }; + pos = next_ws_pos; + + if pos == 0 { + pos + } else { + pos + 1 + } + } + } + } + fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize { let mut col = 0; for (grapheme_index, g) in line.graphemes(true).enumerate() { diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 2b4ee6e..4bd1d56 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -48,7 +48,7 @@ impl Readline for FernVi { loop { raw_mode_guard.disable_for(|| self.print_line())?; - let Some(key) = self.reader.read_key() else { + let Some(key) = self.reader.read_key()? else { raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; std::mem::drop(raw_mode_guard); return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")); @@ -116,11 +116,17 @@ impl FernVi { old_layout: None, repeat_action: None, repeat_motion: None, - editor: LineBuf::new().with_initial(LOREM_IPSUM, 0), + editor: LineBuf::new(), history: History::new()?, }) } + pub fn with_initial(mut self, initial: &str) -> Self { + self.editor = LineBuf::new().with_initial(initial, 0); + self.history.update_pending_cmd(self.editor.as_str()); + self + } + pub fn get_layout(&mut self) -> Layout { let line = self.editor.to_string(); flog!(DEBUG, line); @@ -268,8 +274,10 @@ impl FernVi { self.repeat_action = mode.as_replay(); } - self.editor.exec_cmd(cmd)?; + // Set cursor clamp BEFORE executing the command so that motions + // (like EndOfLine for 'A') can reach positions valid in the new mode self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + self.editor.exec_cmd(cmd)?; if selecting { self diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index c7bba9b..7de5648 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -167,7 +167,7 @@ pub trait WidthCalculator { } pub trait KeyReader { - fn read_key(&mut self) -> Option; + fn read_key(&mut self) -> Result, ShErr>; } pub trait LineWriter { @@ -232,13 +232,11 @@ impl TermBuffer { impl Read for TermBuffer { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { assert!(isatty(self.tty).is_ok_and(|r| r)); - loop { - match nix::unistd::read(self.tty, buf) { - Ok(n) => return Ok(n), - Err(Errno::EINTR) => {} - Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)), - } - } + match nix::unistd::read(self.tty, buf) { + Ok(n) => Ok(n), + Err(Errno::EINTR) => Err(Errno::EINTR.into()), + Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)), + } } } @@ -420,24 +418,24 @@ impl TermReader { } impl KeyReader for TermReader { - fn read_key(&mut self) -> Option { + fn read_key(&mut self) -> Result, ShErr> { use core::str; let mut collected = Vec::with_capacity(4); loop { - let byte = self.next_byte().ok()?; + let byte = self.next_byte()?; flog!(DEBUG, "read byte: {:?}", byte as char); collected.push(byte); // If it's an escape seq, delegate to ESC sequence handler - if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO).ok()? { - return self.parse_esc_seq().ok(); + if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO)? { + return self.parse_esc_seq().map(Some); } // Try parse as valid UTF-8 if let Ok(s) = str::from_utf8(&collected) { - return Some(KeyEvent::new(s, ModKeys::empty())); + return Ok(Some(KeyEvent::new(s, ModKeys::empty()))); } // UTF-8 max 4 bytes — if it’s invalid at this point, bail @@ -446,7 +444,7 @@ impl KeyReader for TermReader { } } - None + Ok(None) } } diff --git a/src/signal.rs b/src/signal.rs index 0cb75c0..5853457 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -1,79 +1,178 @@ +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + +use nix::sys::signal::{SaFlags, SigAction, sigaction}; + use crate::{ - jobs::{take_term, JobCmdFlags, JobID}, - libsh::{error::ShResult, sys::sh_quit}, + jobs::{JobCmdFlags, JobID, take_term}, + libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit}, prelude::*, state::{read_jobs, write_jobs}, }; +static GOT_SIGINT: AtomicBool = AtomicBool::new(false); +static GOT_SIGHUP: AtomicBool = AtomicBool::new(false); +static GOT_SIGTSTP: AtomicBool = AtomicBool::new(false); +static GOT_SIGCHLD: AtomicBool = AtomicBool::new(false); +static REAPING_ENABLED: AtomicBool = AtomicBool::new(true); + +static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); +static QUIT_CODE: AtomicI32 = AtomicI32::new(0); + +pub fn signals_pending() -> bool { + GOT_SIGINT.load(Ordering::SeqCst) + || GOT_SIGHUP.load(Ordering::SeqCst) + || GOT_SIGTSTP.load(Ordering::SeqCst) + || (REAPING_ENABLED.load(Ordering::SeqCst) + && GOT_SIGCHLD.load(Ordering::SeqCst)) + || SHOULD_QUIT.load(Ordering::SeqCst) +} + +pub fn check_signals() -> ShResult<()> { + if GOT_SIGINT.swap(false, Ordering::SeqCst) { + interrupt()?; + return Err(ShErr::simple(ShErrKind::ClearReadline, "")); + } + if GOT_SIGHUP.swap(false, Ordering::SeqCst) { + hang_up(0); + } + if GOT_SIGTSTP.swap(false, Ordering::SeqCst) { + terminal_stop()?; + } + if REAPING_ENABLED.load(Ordering::SeqCst) && GOT_SIGCHLD.swap(false, Ordering::SeqCst) { + wait_child()?; + } + if SHOULD_QUIT.load(Ordering::SeqCst) { + let code = QUIT_CODE.load(Ordering::SeqCst); + return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit")); + } + Ok(()) +} + +pub fn disable_reaping() { + REAPING_ENABLED.store(false, Ordering::SeqCst); +} +pub fn enable_reaping() { + REAPING_ENABLED.store(true, Ordering::SeqCst); +} + pub fn sig_setup() { + let flags = SaFlags::empty(); + + let actions = [ + SigAction::new( + SigHandler::Handler(handle_sigchld), + flags, + SigSet::empty(), + ), + SigAction::new( + SigHandler::Handler(handle_sigquit), + flags, + SigSet::empty(), + ), + SigAction::new( + SigHandler::Handler(handle_sigtstp), + flags, + SigSet::empty(), + ), + SigAction::new( + SigHandler::Handler(handle_sighup), + flags, + SigSet::empty(), + ), + SigAction::new( + SigHandler::Handler(handle_sigint), + flags, + SigSet::empty(), + ), + SigAction::new( // SIGTTIN + SigHandler::SigIgn, + flags, + SigSet::empty(), + ), + SigAction::new( // SIGTTOU + SigHandler::SigIgn, + flags, + SigSet::empty(), + ), + ]; + + unsafe { - signal(Signal::SIGCHLD, SigHandler::Handler(handle_sigchld)).unwrap(); - signal(Signal::SIGQUIT, SigHandler::Handler(handle_sigquit)).unwrap(); - signal(Signal::SIGTSTP, SigHandler::Handler(handle_sigtstp)).unwrap(); - signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup)).unwrap(); - signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)).unwrap(); - signal(Signal::SIGTTIN, SigHandler::SigIgn).unwrap(); - signal(Signal::SIGTTOU, SigHandler::SigIgn).unwrap(); + sigaction(Signal::SIGCHLD, &actions[0]).unwrap(); + sigaction(Signal::SIGQUIT, &actions[1]).unwrap(); + sigaction(Signal::SIGTSTP, &actions[2]).unwrap(); + sigaction(Signal::SIGHUP, &actions[3]).unwrap(); + sigaction(Signal::SIGINT, &actions[4]).unwrap(); + sigaction(Signal::SIGTTIN, &actions[5]).unwrap(); + sigaction(Signal::SIGTTOU, &actions[6]).unwrap(); } } extern "C" fn handle_sighup(_: libc::c_int) { + GOT_SIGHUP.store(true, Ordering::SeqCst); + SHOULD_QUIT.store(true, Ordering::SeqCst); + QUIT_CODE.store(128 + libc::SIGHUP, Ordering::SeqCst); +} + +pub fn hang_up(_: libc::c_int) { write_jobs(|j| { for job in j.jobs_mut().iter_mut().flatten() { job.killpg(Signal::SIGTERM).ok(); } }); - std::process::exit(0); } extern "C" fn handle_sigtstp(_: libc::c_int) { + GOT_SIGTSTP.store(true, Ordering::SeqCst); +} + +pub fn terminal_stop() -> ShResult<()> { write_jobs(|j| { if let Some(job) = j.get_fg_mut() { - job.killpg(Signal::SIGTSTP).ok(); - } - }); + job.killpg(Signal::SIGTSTP) + } else { + Ok(()) + } + }) + // TODO: It seems like there is supposed to be a take_term() call here } extern "C" fn handle_sigint(_: libc::c_int) { - write_jobs(|j| { - if let Some(job) = j.get_fg_mut() { - job.killpg(Signal::SIGINT).ok(); - } - }); + GOT_SIGINT.store(true, Ordering::SeqCst); } -pub extern "C" fn ignore_sigchld(_: libc::c_int) { - /* - Do nothing - - This function exists because using SIGIGN to ignore SIGCHLD - will cause the kernel to automatically reap the child process, which is not what we want. - This handler will leave the signaling process as a zombie, allowing us - to handle it somewhere else. - - This handler is used when we want to handle SIGCHLD explicitly, - like in the case of handling foreground jobs - */ +pub fn interrupt() -> ShResult<()> { + write_jobs(|j| { + if let Some(job) = j.get_fg_mut() { + job.killpg(Signal::SIGINT) + } else { + Ok(()) + } + }) } extern "C" fn handle_sigquit(_: libc::c_int) { - sh_quit(0) + SHOULD_QUIT.store(true, Ordering::SeqCst); + QUIT_CODE.store(128 + libc::SIGQUIT, Ordering::SeqCst); } -pub extern "C" fn handle_sigchld(_: libc::c_int) { +extern "C" fn handle_sigchld(_: libc::c_int) { + GOT_SIGCHLD.store(true, Ordering::SeqCst); +} + +pub fn wait_child() -> ShResult<()> { let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED; while let Ok(status) = waitpid(None, Some(flags)) { - if let Err(e) = match status { - WtStat::Exited(pid, _) => child_exited(pid, status), - WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal), - WtStat::Stopped(pid, signal) => child_stopped(pid, signal), - WtStat::Continued(pid) => child_continued(pid), + match status { + WtStat::Exited(pid, _) => child_exited(pid, status)?, + WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal)?, + WtStat::Stopped(pid, signal) => child_stopped(pid, signal)?, + WtStat::Continued(pid) => child_continued(pid)?, WtStat::StillAlive => break, _ => unimplemented!(), - } { - eprintln!("{}", e) } } + Ok(()) } pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> { diff --git a/src/state.rs b/src/state.rs index 4d0e0d9..99b8c6e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,8 +1,5 @@ use std::{ - collections::{HashMap, VecDeque}, - ops::Deref, - sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}, - time::Duration, + collections::{HashMap, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, str::FromStr, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration }; use nix::unistd::{gethostname, getppid, User}; @@ -19,15 +16,248 @@ use crate::{ shopt::ShOpts, }; -pub static JOB_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(JobTab::new())); +pub struct Fern { + pub jobs: JobTab, + pub var_scopes: ScopeStack, + pub meta: MetaTab, + pub logic: LogTab, + pub shopts: ShOpts, +} -pub static VAR_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(VarTab::new())); +impl Fern { + pub fn new() -> Self { + Self { + jobs: JobTab::new(), + var_scopes: ScopeStack::new(), + meta: MetaTab::new(), + logic: LogTab::new(), + shopts: ShOpts::default(), + } + } + pub fn write_jobs(&mut self) -> &mut JobTab { + &mut self.jobs + } + pub fn write_vars(&mut self) -> &mut ScopeStack { + &mut self.var_scopes + } + pub fn write_meta(&mut self) -> &mut MetaTab { + &mut self.meta + } + pub fn write_logic(&mut self) -> &mut LogTab { + &mut self.logic + } + pub fn write_shopts(&mut self) -> &mut ShOpts { + &mut self.shopts + } + pub fn read_jobs(&self) -> &JobTab { + &self.jobs + } + pub fn read_vars(&self) -> &ScopeStack { + &self.var_scopes + } + pub fn read_meta(&self) -> &MetaTab { + &self.meta + } + pub fn read_logic(&self) -> &LogTab { + &self.logic + } + pub fn read_shopts(&self) -> &ShOpts { + &self.shopts + } +} -pub static META_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(MetaTab::new())); +impl Default for Fern { + fn default() -> Self { + Self::new() + } +} -pub static LOGIC_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(LogTab::new())); +#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)] +pub enum ShellParam { + // Global + Status, + ShPid, + LastJob, + ShellName, -pub static SHOPTS: LazyLock> = LazyLock::new(|| RwLock::new(ShOpts::default())); + // Local + Pos(usize), + AllArgs, + AllArgsStr, + ArgCount +} + +impl Display for ShellParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Status => write!(f, "?"), + Self::ShPid => write!(f, "$"), + Self::LastJob => write!(f, "!"), + Self::ShellName => write!(f, "0"), + Self::Pos(n) => write!(f, "{}", n), + Self::AllArgs => write!(f, "@"), + Self::AllArgsStr => write!(f, "*"), + Self::ArgCount => write!(f, "#"), + } + } +} + +impl FromStr for ShellParam { + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "?" => Ok(Self::Status), + "$" => Ok(Self::ShPid), + "!" => Ok(Self::LastJob), + "0" => Ok(Self::ShellName), + "@" => Ok(Self::AllArgs), + "*" => Ok(Self::AllArgsStr), + "#" => Ok(Self::ArgCount), + n if n.parse::().is_ok() => { + let idx = n.parse::().unwrap(); + Ok(Self::Pos(idx)) + } + _ => Err(ShErr::simple( + ShErrKind::InternalErr, + format!("Invalid shell parameter: {}", s), + )), + } + } +} + +#[derive(Clone, Default, Debug)] +pub struct ScopeStack { + // ALWAYS keep one scope. + // The bottom scope is the global variable space. + // Scopes that come after that are pushed in functions, + // and only contain variables that are defined using `local`. + scopes: Vec, + depth: u32, + + // Global parameters such as $?, $!, $$, etc + global_params: HashMap, +} + +impl ScopeStack { + pub fn new() -> Self { + let mut new = Self::default(); + new.scopes.push(VarTab::new()); + new + } + pub fn descend(&mut self, argv: Option>) { + let mut new_vars = VarTab::new(); + if let Some(argv) = argv { + for arg in argv { + new_vars.bpush_arg(arg); + } + } + self.scopes.push(new_vars); + self.depth += 1; + } + pub fn ascend(&mut self) { + if self.depth >= 1 { + self.scopes.pop(); + self.depth -= 1; + } + } + pub fn cur_scope(&self) -> &VarTab { + self.scopes.last().unwrap() + } + pub fn cur_scope_mut(&mut self) -> &mut VarTab { + self.scopes.last_mut().unwrap() + } + pub fn unset_var(&mut self, var_name: &str) { + for scope in self.scopes.iter_mut().rev() { + if scope.var_exists(var_name) { + scope.unset_var(var_name); + return; + } + } + } + pub fn export_var(&mut self, var_name: &str) { + for scope in self.scopes.iter_mut().rev() { + if scope.var_exists(var_name) { + scope.export_var(var_name); + return; + } + } + } + pub fn var_exists(&self, var_name: &str) -> bool { + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) { + return true; + } + } + false + } + pub fn flatten_vars(&self) -> HashMap { + let mut flat_vars = HashMap::new(); + for scope in self.scopes.iter() { + for (var_name, var) in scope.vars() { + flat_vars.insert(var_name.clone(), var.clone()); + } + } + flat_vars + } + pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { + if flags.contains(VarFlags::LOCAL) { + self.set_var_local(var_name, val, flags); + } else { + self.set_var_global(var_name, val, flags); + } + } + fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) { + if let Some(scope) = self.scopes.first_mut() { + scope.set_var(var_name, val, flags); + } + } + fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) { + if let Some(scope) = self.scopes.last_mut() { + scope.set_var(var_name, val, flags); + } + } + pub fn get_var(&self, var_name: &str) -> String { + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) { + return scope.get_var(var_name); + } + } + // Fallback to env var + std::env::var(var_name).unwrap_or_default() + } + pub fn get_param(&self, param: ShellParam) -> String { + for scope in self.scopes.iter().rev() { + let val = scope.get_param(param); + if !val.is_empty() { + return val; + } + } + // Fallback to empty string + "".into() + } + /// Set a shell parameter + /// Therefore, these are global state and we use the global scope + pub fn set_param(&mut self, param: ShellParam, val: &str) { + match param { + ShellParam::ShPid | + ShellParam::Status | + ShellParam::LastJob | + ShellParam::ShellName => { + self.global_params.insert(param.to_string(), val.to_string()); + } + ShellParam::Pos(_) | + ShellParam::AllArgs | + ShellParam::AllArgsStr | + ShellParam::ArgCount => { + if let Some(scope) = self.scopes.first_mut() { + scope.set_param(param, val); + } + } + } + } +} + +pub static FERN: LazyLock> = LazyLock::new(|| RwLock::new(Fern::new())); /// A shell function /// @@ -108,35 +338,143 @@ impl LogTab { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct VarFlags(u8); + +impl VarFlags { + pub const NONE : Self = Self(0); + pub const EXPORT : Self = Self(1 << 0); + pub const LOCAL : Self = Self(1 << 1); + pub const READONLY : Self = Self(1 << 2); +} + +impl BitOr for VarFlags { + type Output = Self; + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for VarFlags { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} + +impl BitAnd for VarFlags { + type Output = Self; + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } +} + +impl BitAndAssign for VarFlags { + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } +} + +impl VarFlags { + pub fn contains(&self, flag: Self) -> bool { + (self.0 & flag.0) == flag.0 + } + pub fn intersects(&self, flag: Self) -> bool { + (self.0 & flag.0) != 0 + } + pub fn is_empty(&self) -> bool { + self.0 == 0 + } + + pub fn insert(&mut self, flag: Self) { + self.0 |= flag.0; + } + pub fn remove(&mut self, flag: Self) { + self.0 &= !flag.0; + } + pub fn toggle(&mut self, flag: Self) { + self.0 ^= flag.0; + } + pub fn set(&mut self, flag: Self, value: bool) { + if value { + self.insert(flag); + } else { + self.remove(flag); + } + } +} + +#[derive(Clone, Debug)] +pub enum VarKind { + Str(String), + Int(i32), + Arr(Vec), + AssocArr(Vec<(String, String)>), +} + +impl Display for VarKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VarKind::Str(s) => write!(f, "{s}"), + VarKind::Int(i) => write!(f, "{i}"), + VarKind::Arr(items) => { + let mut item_iter = items.iter().peekable(); + while let Some(item) = item_iter.next() { + write!(f, "{item}")?; + if item_iter.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } + VarKind::AssocArr(items) => { + let mut item_iter = items.iter().peekable(); + while let Some(item) = item_iter.next() { + let (k,v) = item; + write!(f, "{k}={v}")?; + if item_iter.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } + } + } +} + #[derive(Clone, Debug)] pub struct Var { - export: bool, - value: String, + flags: VarFlags, + kind: VarKind, } impl Var { - pub fn new(value: String) -> Self { + pub fn new(kind: VarKind, flags: VarFlags) -> Self { Self { - export: false, - value, + flags, + kind } } + pub fn kind(&self) -> &VarKind { + &self.kind + } + pub fn kind_mut(&mut self) -> &mut VarKind { + &mut self.kind + } pub fn mark_for_export(&mut self) { - self.export = true; + self.flags.set(VarFlags::EXPORT, true); } } -impl Deref for Var { - type Target = String; - fn deref(&self) -> &Self::Target { - &self.value - } +impl Display for Var { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.kind.fmt(f) + } } #[derive(Default, Clone, Debug)] pub struct VarTab { vars: HashMap, - params: HashMap, + params: HashMap, sh_argv: VecDeque, /* Using a VecDeque makes the implementation of `shift` * straightforward */ } @@ -154,20 +492,19 @@ impl VarTab { var_tab.init_sh_argv(); var_tab } - fn init_params() -> HashMap { + fn init_params() -> HashMap { let mut params = HashMap::new(); - params.insert("?".into(), "0".into()); // Last command exit status - params.insert("#".into(), "0".into()); // Number of positional parameters + params.insert(ShellParam::ArgCount, "0".into()); // Number of positional parameters params.insert( - "0".into(), + ShellParam::Pos(0), std::env::current_exe() .unwrap() .to_str() .unwrap() .to_string(), ); // Name of the shell - params.insert("$".into(), Pid::this().to_string()); // PID of the shell - params.insert("!".into(), "".into()); // PID of the last background job (if any) + params.insert(ShellParam::ShPid, Pid::this().to_string()); // PID of the shell + params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any) params } fn init_env() { @@ -202,21 +539,23 @@ impl VarTab { .map(|hname| hname.to_string_lossy().to_string()) .unwrap_or_default(); - env::set_var("IFS", " \t\n"); - env::set_var("HOST", hostname.clone()); - env::set_var("UID", uid.to_string()); - env::set_var("PPID", getppid().to_string()); - env::set_var("TMPDIR", "/tmp"); - env::set_var("TERM", term); - env::set_var("LANG", "en_US.UTF-8"); - env::set_var("USER", username.clone()); - env::set_var("LOGNAME", username); - env::set_var("PWD", pathbuf_to_string(std::env::current_dir())); - env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir())); - env::set_var("HOME", home.clone()); - env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); - env::set_var("FERN_HIST", format!("{}/.fernhist", home)); - env::set_var("FERN_RC", format!("{}/.fernrc", home)); + unsafe { + env::set_var("IFS", " \t\n"); + env::set_var("HOST", hostname.clone()); + env::set_var("UID", uid.to_string()); + env::set_var("PPID", getppid().to_string()); + env::set_var("TMPDIR", "/tmp"); + env::set_var("TERM", term); + env::set_var("LANG", "en_US.UTF-8"); + env::set_var("USER", username.clone()); + env::set_var("LOGNAME", username); + env::set_var("PWD", pathbuf_to_string(std::env::current_dir())); + env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir())); + env::set_var("HOME", home.clone()); + env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); + env::set_var("FERN_HIST", format!("{}/.fernhist", home)); + env::set_var("FERN_RC", format!("{}/.fernrc", home)); + } } pub fn init_sh_argv(&mut self) { for arg in env::args() { @@ -226,10 +565,10 @@ impl VarTab { pub fn update_exports(&mut self) { for var_name in self.vars.keys() { let var = self.vars.get(var_name).unwrap(); - if var.export { - env::set_var(var_name, &var.value); + if var.flags.contains(VarFlags::EXPORT) { + unsafe { env::set_var(var_name, var.to_string()) }; } else { - env::set_var(var_name, ""); + unsafe { env::set_var(var_name, "") }; } } } @@ -247,8 +586,8 @@ impl VarTab { self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string()); } fn update_arg_params(&mut self) { - self.set_param("@", &self.sh_argv.clone().to_vec()[1..].join(" ")); - self.set_param("#", &(self.sh_argv.len() - 1).to_string()); + self.set_param(ShellParam::AllArgs, &self.sh_argv.clone().to_vec()[1..].join(" ")); + self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string()); } /// Push an arg to the front of the arg deque pub fn fpush_arg(&mut self, arg: String) { @@ -278,77 +617,84 @@ impl VarTab { pub fn vars_mut(&mut self) -> &mut HashMap { &mut self.vars } - pub fn params(&self) -> &HashMap { + pub fn params(&self) -> &HashMap { &self.params } - pub fn params_mut(&mut self) -> &mut HashMap { + pub fn params_mut(&mut self) -> &mut HashMap { &mut self.params } pub fn export_var(&mut self, var_name: &str) { if let Some(var) = self.vars.get_mut(var_name) { var.mark_for_export(); - env::set_var(var_name, &var.value); + unsafe { env::set_var(var_name, var.to_string()) }; } } pub fn get_var(&self, var: &str) -> String { - if var.chars().count() == 1 || var.parse::().is_ok() { - let param = self.get_param(var); + if let Ok(param) = var.parse::() { + let param = self.get_param(param); if !param.is_empty() { return param; } - } + } if let Some(var) = self.vars.get(var).map(|s| s.to_string()) { var } else { std::env::var(var).unwrap_or_default() } } - pub fn set_var(&mut self, var_name: &str, val: &str, export: bool) { + pub fn unset_var(&mut self, var_name: &str) { + self.vars.remove(var_name); + unsafe { env::remove_var(var_name) }; + } + pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { if let Some(var) = self.vars.get_mut(var_name) { - var.value = val.to_string(); - if var.export { - env::set_var(var_name, val); + var.kind = VarKind::Str(val.to_string()); + if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) { + if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) { + var.mark_for_export(); + } + unsafe { env::set_var(var_name, val) }; } } else { - let mut var = Var::new(val.to_string()); - if export { + let mut var = Var::new(VarKind::Str(val.to_string()), VarFlags::NONE); + if flags.contains(VarFlags::EXPORT) { var.mark_for_export(); - env::set_var(var_name, &*var); + unsafe { env::set_var(var_name, var.to_string()) }; } self.vars.insert(var_name.to_string(), var); } } pub fn var_exists(&self, var_name: &str) -> bool { - if var_name.parse::().is_ok() { - return self.params.contains_key(var_name); + if let Ok(param) = var_name.parse::() { + return self.params.contains_key(¶m); } - self.vars.contains_key(var_name) || (var_name.len() == 1 && self.params.contains_key(var_name)) + self.vars.contains_key(var_name) } - pub fn set_param(&mut self, param: &str, val: &str) { - self.params.insert(param.to_string(), val.to_string()); + pub fn set_param(&mut self, param: ShellParam, val: &str) { + self.params.insert(param, val.to_string()); } - pub fn get_param(&self, param: &str) -> String { - if param.parse::().is_ok() { - let argv_idx = param.to_string().parse::().unwrap(); - let arg = self - .sh_argv - .get(argv_idx) - .map(|s| s.to_string()) - .unwrap_or_default(); - arg - } else if param == "?" { - self - .params - .get(param) - .map(|s| s.to_string()) - .unwrap_or("0".into()) - } else { - self - .params - .get(param) - .map(|s| s.to_string()) - .unwrap_or_default() - } + pub fn get_param(&self, param: ShellParam) -> String { + match param { + ShellParam::Pos(n) => { + self + .sh_argv() + .get(n) + .map(|s| s.to_string()) + .unwrap_or_default() + } + ShellParam::Status => { + self + .params + .get(&ShellParam::Status) + .map(|s| s.to_string()) + .unwrap_or("0".into()) + } + _ => self + .params + .get(¶m) + .map(|s| s.to_string()) + .unwrap_or_default(), + } } } @@ -374,60 +720,77 @@ impl MetaTab { } /// Read from the job table -pub fn read_jobs) -> T>(f: F) -> T { - let lock = JOB_TABLE.read().unwrap(); - f(lock) +pub fn read_jobs T>(f: F) -> T { + let fern = FERN.read().unwrap(); + let jobs = fern.read_jobs(); + f(jobs) } /// Write to the job table -pub fn write_jobs) -> T>(f: F) -> T { - let lock = &mut JOB_TABLE.write().unwrap(); - f(lock) +pub fn write_jobs T>(f: F) -> T { + let mut fern = FERN.write().unwrap(); + let jobs = &mut fern.jobs; + f(jobs) } -/// Read from the variable table -pub fn read_vars) -> T>(f: F) -> T { - let lock = VAR_TABLE.read().unwrap(); - f(lock) +/// Read from the var scope stack +pub fn read_vars T>(f: F) -> T { + let fern = FERN.read().unwrap(); + let vars = fern.read_vars(); + f(vars) } /// Write to the variable table -pub fn write_vars) -> T>(f: F) -> T { - let lock = &mut VAR_TABLE.write().unwrap(); - f(lock) +pub fn write_vars T>(f: F) -> T { + let mut fern = FERN.write().unwrap(); + let vars = fern.write_vars(); + f(vars) } -pub fn read_meta) -> T>(f: F) -> T { - let lock = META_TABLE.read().unwrap(); - f(lock) +pub fn read_meta T>(f: F) -> T { + let fern = FERN.read().unwrap(); + let meta = fern.read_meta(); + f(meta) } /// Write to the variable table -pub fn write_meta) -> T>(f: F) -> T { - let lock = &mut META_TABLE.write().unwrap(); - f(lock) +pub fn write_meta T>(f: F) -> T { + let mut fern = FERN.write().unwrap(); + let meta = fern.write_meta(); + f(meta) } /// Read from the logic table -pub fn read_logic) -> T>(f: F) -> T { - let lock = LOGIC_TABLE.read().unwrap(); - f(lock) +pub fn read_logic T>(f: F) -> T { + let fern = FERN.read().unwrap(); + let logic = fern.read_logic(); + f(logic) } /// Write to the logic table -pub fn write_logic) -> T>(f: F) -> T { - let lock = &mut LOGIC_TABLE.write().unwrap(); - f(lock) +pub fn write_logic T>(f: F) -> T { + let mut fern = FERN.write().unwrap(); + let logic = &mut fern.logic; + f(logic) } -pub fn read_shopts) -> T>(f: F) -> T { - let lock = SHOPTS.read().unwrap(); - f(lock) +pub fn read_shopts T>(f: F) -> T { + let fern = FERN.read().unwrap(); + let shopts = fern.read_shopts(); + f(shopts) } -pub fn write_shopts) -> T>(f: F) -> T { - let lock = &mut SHOPTS.write().unwrap(); - f(lock) +pub fn write_shopts T>(f: F) -> T { + let mut fern = FERN.write().unwrap(); + let shopts = &mut fern.shopts; + f(shopts) +} + +pub fn descend_scope(argv: Option>) { + write_vars(|v| v.descend(argv)); +} +pub fn ascend_scope() { + write_vars(|v| v.ascend()); } /// This function is used internally and ideally never sees user input @@ -438,31 +801,11 @@ pub fn get_shopt(path: &str) -> String { } pub fn get_status() -> i32 { - read_vars(|v| v.get_param("?")).parse::().unwrap() + read_vars(|v| v.get_param(ShellParam::Status)).parse::().unwrap() } #[track_caller] pub fn set_status(code: i32) { - write_vars(|v| v.set_param("?", &code.to_string())) -} - -/// Save the current state of the logic and variable table, and the working -/// directory path -pub fn get_snapshots() -> (LogTab, VarTab, String) { - ( - read_logic(|l| l.clone()), - read_vars(|v| v.clone()), - env::var("PWD").unwrap_or_default(), - ) -} - -pub fn restore_snapshot(snapshot: (LogTab, VarTab, String)) { - write_logic(|l| **l = snapshot.0); - write_vars(|v| { - **v = snapshot.1; - v.update_exports(); - }); - env::set_current_dir(&snapshot.2).unwrap(); - env::set_var("PWD", &snapshot.2); + write_vars(|v| v.set_param(ShellParam::Status, &code.to_string())) } pub fn source_rc() -> ShResult<()> {