Implemented proper variable scoping

Extracted business logic out of signal handler functions

Consolidated state variables into a single struct

Implemented var types
This commit is contained in:
2026-01-28 19:30:48 -05:00
parent 5aead4fcdc
commit ae3c9a0445
17 changed files with 2154 additions and 1127 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ shell.nix
*~
TODO.md
rust-toolchain.toml
/ref
# cachix tmp file
store-path-pre-build

241
Cargo.lock generated
View File

@@ -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"

View File

@@ -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

View File

@@ -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(())

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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<Self> {
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<Self> {
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<Vec<String>> {
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<char> = 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<Vec<String>> {
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<Vec<String>> {
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<char> = 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<String> {
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<char> = 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<Vec<String>> {
// 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<Vec<String>> {
// 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<String> = (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<String> = (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<Chars<'_>>) -> ShResult<String> {
let mut result = String::new();
@@ -897,7 +1217,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
}
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<String> {
}
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<String> {
}
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())
}

View File

@@ -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<P: AsRef<Path>>(path: P, args: Vec<String>) {
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::<FernEditMode>().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;
}
}
}
};

View File

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

View File

@@ -388,7 +388,7 @@ impl From<std::env::VarError> for ShErr {
impl From<Errno> 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}")

File diff suppressed because it is too large Load Diff

View File

@@ -26,11 +26,17 @@ fn get_prompt() -> ShResult<String> {
expand_prompt(&prompt)
}
pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
pub fn readline(edit_mode: FernEditMode, initial: Option<&str>) -> ShResult<String> {
let prompt = get_prompt()?;
let mut reader: Box<dyn Readline> = 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<dyn Readline>
}
FernEditMode::Emacs => todo!(), // idk if I'm ever gonna do this one actually, I don't use emacs
};
reader.readline()
}

View File

@@ -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<usize> {
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() {

View File

@@ -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

View File

@@ -167,7 +167,7 @@ pub trait WidthCalculator {
}
pub trait KeyReader {
fn read_key(&mut self) -> Option<KeyEvent>;
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr>;
}
pub trait LineWriter {
@@ -232,13 +232,11 @@ impl TermBuffer {
impl Read for TermBuffer {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
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<KeyEvent> {
fn read_key(&mut self) -> Result<Option<KeyEvent>, 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 its invalid at this point, bail
@@ -446,7 +444,7 @@ impl KeyReader for TermReader {
}
}
None
Ok(None)
}
}

View File

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

View File

@@ -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<RwLock<JobTab>> = 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<RwLock<VarTab>> = 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<RwLock<MetaTab>> = LazyLock::new(|| RwLock::new(MetaTab::new()));
impl Default for Fern {
fn default() -> Self {
Self::new()
}
}
pub static LOGIC_TABLE: LazyLock<RwLock<LogTab>> = 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<RwLock<ShOpts>> = 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<Self, Self::Err> {
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::<usize>().is_ok() => {
let idx = n.parse::<usize>().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<VarTab>,
depth: u32,
// Global parameters such as $?, $!, $$, etc
global_params: HashMap<String, String>,
}
impl ScopeStack {
pub fn new() -> Self {
let mut new = Self::default();
new.scopes.push(VarTab::new());
new
}
pub fn descend(&mut self, argv: Option<Vec<String>>) {
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<String, Var> {
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<RwLock<Fern>> = 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<String>),
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<String, Var>,
params: HashMap<String, String>,
params: HashMap<ShellParam, String>,
sh_argv: VecDeque<String>, /* 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<String, String> {
fn init_params() -> HashMap<ShellParam, String> {
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<String, Var> {
&mut self.vars
}
pub fn params(&self) -> &HashMap<String, String> {
pub fn params(&self) -> &HashMap<ShellParam, String> {
&self.params
}
pub fn params_mut(&mut self) -> &mut HashMap<String, String> {
pub fn params_mut(&mut self) -> &mut HashMap<ShellParam, String> {
&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::<usize>().is_ok() {
let param = self.get_param(var);
if let Ok(param) = var.parse::<ShellParam>() {
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::<usize>().is_ok() {
return self.params.contains_key(var_name);
if let Ok(param) = var_name.parse::<ShellParam>() {
return self.params.contains_key(&param);
}
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::<usize>().is_ok() {
let argv_idx = param.to_string().parse::<usize>().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(&param)
.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: FnOnce(RwLockReadGuard<JobTab>) -> T>(f: F) -> T {
let lock = JOB_TABLE.read().unwrap();
f(lock)
pub fn read_jobs<T, F: FnOnce(&JobTab) -> 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: FnOnce(&mut RwLockWriteGuard<JobTab>) -> T>(f: F) -> T {
let lock = &mut JOB_TABLE.write().unwrap();
f(lock)
pub fn write_jobs<T, F: FnOnce(&mut JobTab) -> 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: FnOnce(RwLockReadGuard<VarTab>) -> T>(f: F) -> T {
let lock = VAR_TABLE.read().unwrap();
f(lock)
/// Read from the var scope stack
pub fn read_vars<T, F: FnOnce(&ScopeStack) -> 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: FnOnce(&mut RwLockWriteGuard<VarTab>) -> T>(f: F) -> T {
let lock = &mut VAR_TABLE.write().unwrap();
f(lock)
pub fn write_vars<T, F: FnOnce(&mut ScopeStack) -> T>(f: F) -> T {
let mut fern = FERN.write().unwrap();
let vars = fern.write_vars();
f(vars)
}
pub fn read_meta<T, F: FnOnce(RwLockReadGuard<MetaTab>) -> T>(f: F) -> T {
let lock = META_TABLE.read().unwrap();
f(lock)
pub fn read_meta<T, F: FnOnce(&MetaTab) -> 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: FnOnce(&mut RwLockWriteGuard<MetaTab>) -> T>(f: F) -> T {
let lock = &mut META_TABLE.write().unwrap();
f(lock)
pub fn write_meta<T, F: FnOnce(&mut MetaTab) -> 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: FnOnce(RwLockReadGuard<LogTab>) -> T>(f: F) -> T {
let lock = LOGIC_TABLE.read().unwrap();
f(lock)
pub fn read_logic<T, F: FnOnce(&LogTab) -> 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: FnOnce(&mut RwLockWriteGuard<LogTab>) -> T>(f: F) -> T {
let lock = &mut LOGIC_TABLE.write().unwrap();
f(lock)
pub fn write_logic<T, F: FnOnce(&mut LogTab) -> T>(f: F) -> T {
let mut fern = FERN.write().unwrap();
let logic = &mut fern.logic;
f(logic)
}
pub fn read_shopts<T, F: FnOnce(RwLockReadGuard<ShOpts>) -> T>(f: F) -> T {
let lock = SHOPTS.read().unwrap();
f(lock)
pub fn read_shopts<T, F: FnOnce(&ShOpts) -> T>(f: F) -> T {
let fern = FERN.read().unwrap();
let shopts = fern.read_shopts();
f(shopts)
}
pub fn write_shopts<T, F: FnOnce(&mut RwLockWriteGuard<ShOpts>) -> T>(f: F) -> T {
let lock = &mut SHOPTS.write().unwrap();
f(lock)
pub fn write_shopts<T, F: FnOnce(&mut ShOpts) -> T>(f: F) -> T {
let mut fern = FERN.write().unwrap();
let shopts = &mut fern.shopts;
f(shopts)
}
pub fn descend_scope(argv: Option<Vec<String>>) {
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::<i32>().unwrap()
read_vars(|v| v.get_param(ShellParam::Status)).parse::<i32>().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<()> {