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 8ad53f09b3
commit ad0e4277cb
17 changed files with 2154 additions and 1127 deletions

1
.gitignore vendored
View File

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

241
Cargo.lock generated
View File

@@ -4,18 +4,18 @@ version = 4
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@@ -28,50 +28,50 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.10" version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [ dependencies = [
"utf8parse", "utf8parse",
] ]
[[package]] [[package]]
name = "anstyle-query" name = "anstyle-query"
version = "1.1.2" version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.7" version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell", "once_cell_polyfill",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.8.0" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "cfg_aliases" name = "cfg_aliases"
@@ -81,9 +81,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.38" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -91,9 +91,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.38" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -103,9 +103,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.32" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -115,15 +115,15 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.4" version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]] [[package]]
name = "console" name = "console"
@@ -134,7 +134,7 @@ dependencies = [
"encode_unicode", "encode_unicode",
"libc", "libc",
"once_cell", "once_cell",
"windows-sys", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -149,6 +149,22 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 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]] [[package]]
name = "fern" name = "fern"
version = "0.1.0" version = "0.1.0"
@@ -165,10 +181,22 @@ dependencies = [
] ]
[[package]] [[package]]
name = "glob" name = "getrandom"
version = "0.3.2" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "heck" name = "heck"
@@ -178,40 +206,39 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.42.2" version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8"
dependencies = [ dependencies = [
"console", "console",
"linked-hash-map",
"once_cell", "once_cell",
"pin-project",
"similar", "similar",
"tempfile",
] ]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]] [[package]]
name = "linked-hash-map" name = "linux-raw-sys"
version = "0.5.6" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]] [[package]]
name = "nix" name = "nix"
@@ -227,29 +254,15 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.1" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "pin-project" name = "once_cell_polyfill"
version = "1.1.10" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
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",
]
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
@@ -263,27 +276,33 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.93" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "regex" name = "r-efi"
version = "1.11.1" version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -293,9 +312,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.9" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -304,9 +323,22 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "similar" name = "similar"
@@ -322,9 +354,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.98" version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -332,10 +364,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "unicode-ident" name = "tempfile"
version = "1.0.17" version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
@@ -345,9 +390,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.0" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
@@ -355,6 +400,21 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@@ -364,6 +424,15 @@ dependencies = [
"windows-targets", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -428,6 +497,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "1.0.1" version = "1.0.1"

View File

@@ -4,7 +4,7 @@ description = "A linux shell written in rust"
publish = false publish = false
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[profile.release] [profile.release]
debug = true debug = true

View File

@@ -44,7 +44,7 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
env::set_current_dir(new_dir).unwrap(); env::set_current_dir(new_dir).unwrap();
let new_dir = env::current_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); state::set_status(0);
Ok(()) Ok(())

View File

@@ -3,8 +3,8 @@ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state::{self, write_vars}, state::{self, VarFlags, write_vars},
}; };
use super::setup_builtin; use super::setup_builtin;
@@ -34,7 +34,7 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
} else { } else {
for (arg, _) in argv { for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') { 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' // 'foo=bar'
} else { } else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if 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 { 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::collections::HashSet;
use std::iter::Peekable; use std::iter::Peekable;
use std::mem::take;
use std::str::{Chars, FromStr}; use std::str::{Chars, FromStr};
use glob::Pattern; 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::parse::{Redir, RedirType};
use crate::prelude::*; use crate::prelude::*;
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; 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']; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
@@ -40,7 +41,7 @@ impl Tk {
pub fn expand(self) -> ShResult<Self> { pub fn expand(self) -> ShResult<Self> {
let flags = self.flags; let flags = self.flags;
let span = self.span.clone(); let span = self.span.clone();
let exp = Expander::new(self).expand()?; let exp = Expander::new(self)?.expand()?;
let class = TkRule::Expanded { exp }; let class = TkRule::Expanded { exp };
Ok(Self { class, span, flags }) Ok(Self { class, span, flags })
} }
@@ -58,9 +59,11 @@ pub struct Expander {
} }
impl Expander { impl Expander {
pub fn new(raw: Tk) -> Self { pub fn new(raw: Tk) -> ShResult<Self> {
let unescaped = unescape_str(raw.span.as_str()); let mut raw = raw.span.as_str().to_string();
Self { raw: unescaped } raw = expand_braces_full(&raw)?.join(" ");
let unescaped = unescape_str(&raw);
Ok(Self { raw: unescaped })
} }
pub fn expand(&mut self) -> ShResult<Vec<String>> { pub fn expand(&mut self) -> ShResult<Vec<String>> {
let mut chars = self.raw.chars().peekable(); 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> { pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let mut result = String::new(); let mut result = String::new();
@@ -897,7 +1217,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::SetDefaultUnsetOrNull(default) => { ParamExp::SetDefaultUnsetOrNull(default) => {
if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { 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) Ok(default)
} else { } else {
Ok(vars.get_var(&var_name)) Ok(vars.get_var(&var_name))
@@ -905,7 +1225,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::SetDefaultUnset(default) => { ParamExp::SetDefaultUnset(default) => {
if !vars.var_exists(&var_name) { 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) Ok(default)
} else { } else {
Ok(vars.get_var(&var_name)) Ok(vars.get_var(&var_name))
@@ -1061,7 +1381,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::VarNamesWithPrefix(prefix) => { ParamExp::VarNamesWithPrefix(prefix) => {
let mut match_vars = vec![]; let mut match_vars = vec![];
for var in vars.vars().keys() { for var in vars.flatten_vars().keys() {
if var.starts_with(&prefix) { if var.starts_with(&prefix) {
match_vars.push(var.clone()) match_vars.push(var.clone())
} }

View File

@@ -18,10 +18,11 @@ pub mod state;
#[cfg(test)] #[cfg(test)]
pub mod tests; pub mod tests;
use crate::libsh::error::ShErrKind;
use crate::libsh::sys::{save_termios, set_termios}; use crate::libsh::sys::{save_termios, set_termios};
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::signal::sig_setup; use crate::signal::{check_signals, sig_setup, signals_pending};
use crate::state::source_rc; use crate::state::source_rc;
use clap::Parser; use clap::Parser;
use shopt::FernEditMode; use shopt::FernEditMode;
@@ -78,9 +79,9 @@ fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
exit(1); 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 { 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) { if let Err(e) = exec_input(input, None) {
@@ -100,18 +101,40 @@ fn fern_interactive() {
let mut readline_err_count: u32 = 0; 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 // Main loop
let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode"))
.unwrap() .unwrap()
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default()) .map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
.unwrap(); .unwrap();
let input = match prompt::readline(edit_mode) { let input = match prompt::readline(edit_mode, Some(&partial_input)) {
Ok(line) => { Ok(line) => {
readline_err_count = 0; readline_err_count = 0;
partial_input.clear();
line line
} }
Err(e) => { Err(e) => {
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}"); eprintln!("{e}");
readline_err_count += 1; readline_err_count += 1;
if readline_err_count == 20 { if readline_err_count == 20 {
@@ -121,6 +144,7 @@ fn fern_interactive() {
continue; continue;
} }
} }
}
}; };
if let Err(e) = exec_input(input, None) { if let Err(e) = exec_input(input, None) {

View File

@@ -2,10 +2,7 @@ use crate::{
libsh::{ libsh::{
error::ShResult, error::ShResult,
term::{Style, Styled}, term::{Style, Styled},
}, }, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, set_status, write_jobs}
prelude::*,
procio::{borrow_fd, IoMode},
state::{self, set_status, write_jobs},
}; };
pub const SIG_EXIT_OFFSET: i32 = 128; pub const SIG_EXIT_OFFSET: i32 = 128;
@@ -643,29 +640,6 @@ pub fn take_term() -> ShResult<()> {
Ok(()) 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 /// Waits on the current foreground job and updates the shell's last status code
pub fn wait_fg(job: Job) -> ShResult<()> { pub fn wait_fg(job: Job) -> ShResult<()> {
if job.children().is_empty() { if job.children().is_empty() {
@@ -674,7 +648,7 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
flog!(TRACE, "Waiting on foreground job"); flog!(TRACE, "Waiting on foreground job");
let mut code = 0; let mut code = 0;
attach_tty(job.pgid())?; attach_tty(job.pgid())?;
disable_reaping()?; disable_reaping();
let statuses = write_jobs(|j| j.new_fg(job))?; let statuses = write_jobs(|j| j.new_fg(job))?;
for status in statuses { for status in statuses {
match status { match status {
@@ -697,7 +671,7 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
take_term()?; take_term()?;
set_status(code); set_status(code);
flog!(TRACE, "exit code: {}", code); flog!(TRACE, "exit code: {}", code);
enable_reaping()?; enable_reaping();
Ok(()) Ok(())
} }

View File

@@ -388,7 +388,7 @@ impl From<std::env::VarError> for ShErr {
impl From<Errno> for ShErr { impl From<Errno> for ShErr {
fn from(value: Errno) -> Self { 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, HistoryReadErr,
ResourceLimitExceeded, ResourceLimitExceeded,
BadPermission, BadPermission,
Errno, Errno(Errno),
FileNotFound(String), FileNotFound(String),
CmdNotFound(String), CmdNotFound(String),
ReadlineIntr(String),
ReadlineErr,
// Not really errors, more like internal signals
CleanExit(i32), CleanExit(i32),
FuncReturn(i32), FuncReturn(i32),
LoopContinue(i32), LoopContinue(i32),
LoopBreak(i32), LoopBreak(i32),
ReadlineErr, ClearReadline,
Null, Null,
} }
@@ -424,14 +428,16 @@ impl Display for ShErrKind {
Self::ExecFail => "Execution Failed", Self::ExecFail => "Execution Failed",
Self::ResourceLimitExceeded => "Resource Limit Exceeded", Self::ResourceLimitExceeded => "Resource Limit Exceeded",
Self::BadPermission => "Bad Permissions", Self::BadPermission => "Bad Permissions",
Self::Errno => "ERRNO", Self::Errno(e) => &format!("Errno: {}", e.desc()),
Self::FileNotFound(file) => &format!("File not found: {file}"), Self::FileNotFound(file) => &format!("File not found: {file}"),
Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"),
Self::CleanExit(_) => "", Self::CleanExit(_) => "",
Self::FuncReturn(_) => "", Self::FuncReturn(_) => "",
Self::LoopContinue(_) => "", Self::LoopContinue(_) => "",
Self::LoopBreak(_) => "", Self::LoopBreak(_) => "",
Self::ReadlineErr => "Line Read Error", Self::ReadlineIntr(_) => "",
Self::ReadlineErr => "Readline Error",
Self::ClearReadline => "",
Self::Null => "", Self::Null => "",
}; };
write!(f, "{output}") write!(f, "{output}")

View File

@@ -7,7 +7,7 @@ use crate::{
echo::echo, echo::echo,
export::export, export::export,
flowctl::flowctl, flowctl::flowctl,
jobctl::{continue_job, jobs, JobBehavior}, jobctl::{JobBehavior, continue_job, jobs},
pwd::pwd, pwd::pwd,
shift::shift, shift::shift,
shopt::shopt, shopt::shopt,
@@ -16,7 +16,7 @@ use crate::{
zoltraak::zoltraak, zoltraak::zoltraak,
}, },
expand::expand_aliases, expand::expand_aliases,
jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, jobs::{ChildProc, JobBldr, JobStack, dispatch_job},
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt}, error::{ShErr, ShErrKind, ShResult, ShResultExt},
utils::RedirVecUtils, utils::RedirVecUtils,
@@ -24,8 +24,7 @@ use crate::{
prelude::*, prelude::*,
procio::{IoFrame, IoMode, IoStack}, procio::{IoFrame, IoMode, IoStack},
state::{ state::{
self, get_snapshots, read_logic, restore_snapshot, write_logic, write_meta, write_vars, ShFunc, self, FERN, ShFunc, VarFlags, read_logic, write_logic, write_meta, write_vars
VarTab, LOGIC_TABLE,
}, },
}; };
@@ -35,6 +34,49 @@ use super::{
ParsedSrc, Redir, RedirType, ParsedSrc, Redir, RedirType,
}; };
pub struct ScopeGuard;
impl ScopeGuard {
pub fn exclusive_scope(args: Option<Vec<(String,Span)>>) -> Self {
let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::<Vec<_>>());
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 <command> <args>'
/// or for-loop variables
struct VarCtxGuard {
vars: HashSet<String>
}
impl VarCtxGuard {
fn new(vars: HashSet<String>) -> 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 { pub enum AssignBehavior {
Export, Export,
Set, Set,
@@ -76,9 +118,13 @@ impl ExecArgs {
pub fn exec_input(input: String, io_stack: Option<IoStack>) -> ShResult<()> { pub fn exec_input(input: String, io_stack: Option<IoStack>) -> ShResult<()> {
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
let log_tab = LOGIC_TABLE.read().unwrap(); 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 input = expand_aliases(input, HashSet::new(), &log_tab);
mem::drop(log_tab); // Release lock ASAP
let mut parser = ParsedSrc::new(Arc::new(input)); let mut parser = ParsedSrc::new(Arc::new(input));
if let Err(errors) = parser.parse_src() { if let Err(errors) = parser.parse_src() {
for error in errors { for error in errors {
@@ -137,10 +183,10 @@ impl Dispatcher {
let Some(cmd) = node.get_command() else { let Some(cmd) = node.get_command() else {
return self.exec_cmd(node); // Argv is empty, probably an assignment return self.exec_cmd(node); // Argv is empty, probably an assignment
}; };
if cmd.flags.contains(TkFlags::BUILTIN) { if is_func(node.get_command().cloned()) {
self.exec_builtin(node)
} else if is_func(node.get_command().cloned()) {
self.exec_func(node) self.exec_func(node)
} else if cmd.flags.contains(TkFlags::BUILTIN) {
self.exec_builtin(node)
} else if is_subsh(node.get_command().cloned()) { } else if is_subsh(node.get_command().cloned()) {
self.exec_subsh(node) self.exec_subsh(node)
} else { } else {
@@ -216,20 +262,17 @@ impl Dispatcher {
unreachable!() 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(subsh.redirs); self.io_stack.append_to_frame(subsh.redirs);
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
let subsh = argv.remove(0); let subsh = argv.remove(0);
let subsh_body = subsh.0.to_string(); let subsh_body = subsh.0.to_string();
let snapshot = get_snapshots(); let _guard = ScopeGuard::shared_scope();
if let Err(e) = exec_input(subsh_body, None) { exec_input(subsh_body, None)?;
restore_snapshot(snapshot);
return Err(e);
}
restore_snapshot(snapshot);
Ok(()) Ok(())
} }
fn exec_func(&mut self, func: Node) -> ShResult<()> { fn exec_func(&mut self, func: Node) -> ShResult<()> {
@@ -242,36 +285,26 @@ impl Dispatcher {
unreachable!() 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 func_name = argv.remove(0).span.as_str().to_string();
let argv = prepare_argv(argv)?; let argv = prepare_argv(argv)?;
if let Some(func) = read_logic(|l| l.get_func(&func_name)) { if let Some(func) = read_logic(|l| l.get_func(&func_name)) {
let snapshot = get_snapshots(); let _guard = ScopeGuard::exclusive_scope(Some(argv));
// Set up the inner scope
write_vars(|v| {
**v = VarTab::new();
v.clear_args();
for (arg, _) in argv {
v.bpush_arg(arg.to_string());
}
});
if let Err(e) = self.exec_brc_grp((*func).clone()) { if let Err(e) = self.exec_brc_grp((*func).clone()) {
restore_snapshot(snapshot);
match e.kind() { match e.kind() {
ShErrKind::FuncReturn(code) => { ShErrKind::FuncReturn(code) => {
state::set_status(*code); state::set_status(*code);
return Ok(()); return Ok(());
} }
_ => return { Err(e) }, _ => return Err(e),
} }
} }
// Return to the outer scope
restore_snapshot(snapshot);
Ok(()) Ok(())
} else { } else {
Err(ShErr::full( Err(ShErr::full(
@@ -384,9 +417,13 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> {
let NdRule::ForNode { vars, arr, body } = for_stmt.class else { let NdRule::ForNode { vars, arr, body } = for_stmt.class else {
unreachable!(); unreachable!();
}; };
let mut for_guard = VarCtxGuard::new(
vars.iter().map(|v| v.to_string()).collect()
);
let io_frame = self.io_stack.pop_frame(); let io_frame = self.io_stack.pop_frame();
let (_, mut body_frame) = io_frame.split_frame(); let (_, mut body_frame) = io_frame.split_frame();
@@ -400,7 +437,8 @@ impl Dispatcher {
); );
for (var, val) in chunk_iter { for (var, val) in chunk_iter {
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), false)); write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE));
for_guard.vars.insert(var.to_string());
} }
for node in body.clone() { for node in body.clone() {
@@ -491,13 +529,15 @@ impl Dispatcher {
} }
fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
ref mut assignments, assignments,
ref mut argv, argv,
} = &mut cmd.class } = &mut cmd.class
else { else {
unreachable!() unreachable!()
}; };
let env_vars_to_unset = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?;
let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect());
let cmd_raw = argv.first().unwrap(); let cmd_raw = argv.first().unwrap();
let curr_job_mut = self.job_stack.curr_job_mut().unwrap(); let curr_job_mut = self.job_stack.curr_job_mut().unwrap();
let io_stack_mut = &mut self.io_stack; let io_stack_mut = &mut self.io_stack;
@@ -543,10 +583,6 @@ impl Dispatcher {
), ),
}; };
for var in env_vars_to_unset {
env::set_var(&var, "");
}
if let Err(e) = result { if let Err(e) = result {
state::set_status(1); state::set_status(1);
return Err(e); return Err(e);
@@ -584,7 +620,7 @@ impl Dispatcher {
)?; )?;
for var in env_vars_to_unset { for var in env_vars_to_unset {
std::env::set_var(&var, ""); unsafe { std::env::set_var(&var, "") };
} }
Ok(()) Ok(())
@@ -600,7 +636,7 @@ impl Dispatcher {
let var = var.span.as_str(); let var = var.span.as_str();
let val = val.expand()?.get_words().join(" "); let val = val.expand()?.get_words().join(" ");
match kind { match kind {
AssignKind::Eq => write_vars(|v| v.set_var(var, &val, true)), AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::EXPORT)),
AssignKind::PlusEq => todo!(), AssignKind::PlusEq => todo!(),
AssignKind::MinusEq => todo!(), AssignKind::MinusEq => todo!(),
AssignKind::MultEq => todo!(), AssignKind::MultEq => todo!(),
@@ -617,7 +653,7 @@ impl Dispatcher {
let var = var.span.as_str(); let var = var.span.as_str();
let val = val.expand()?.get_words().join(" "); let val = val.expand()?.get_words().join(" ");
match kind { match kind {
AssignKind::Eq => write_vars(|v| v.set_var(var, &val, true)), AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::NONE)),
AssignKind::PlusEq => todo!(), AssignKind::PlusEq => todo!(),
AssignKind::MinusEq => todo!(), AssignKind::MinusEq => todo!(),
AssignKind::MultEq => todo!(), AssignKind::MultEq => todo!(),
@@ -688,7 +724,7 @@ pub fn def_child_action(mut io_frame: IoFrame, exec_args: Option<ExecArgs>) {
eprintln!("{err}"); eprintln!("{err}");
} }
_ => { _ => {
let err = ShErr::full(ShErrKind::Errno, format!("{e}"), span); let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span);
eprintln!("{err}"); eprintln!("{err}");
} }
} }

View File

@@ -26,11 +26,17 @@ fn get_prompt() -> ShResult<String> {
expand_prompt(&prompt) 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 prompt = get_prompt()?;
let mut reader: Box<dyn Readline> = match edit_mode { let mut reader: Box<dyn Readline> = match edit_mode {
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?), FernEditMode::Vi => {
FernEditMode::Emacs => todo!(), 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() reader.readline()
} }

View File

@@ -622,17 +622,25 @@ impl LineBuf {
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count()) .map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count())
.unwrap_or(0) .unwrap_or(0)
} }
pub fn is_sentence_punctuation(&mut self, pos: usize) -> bool { pub fn is_sentence_punctuation(&self, pos: usize) -> bool {
if let Some(gr) = self.grapheme_at(pos) { self.next_sentence_start_from_punctuation(pos).is_some()
if PUNCTUATION.contains(&gr) && self.grapheme_after(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(); let mut fwd_indices = (pos + 1..self.cursor.max).peekable();
// Skip any closing delimiters after the punctuation
if self if self
.grapheme_after(pos) .read_grapheme_after(pos)
.is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr)) .is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr))
{ {
while let Some(idx) = fwd_indices.peek() { while let Some(idx) = fwd_indices.peek() {
if self if self
.grapheme_after(*idx) .read_grapheme_at(*idx)
.is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr)) .is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr))
{ {
fwd_indices.next(); 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(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) { 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 { pub fn is_sentence_start(&mut self, pos: usize) -> bool {
if self.grapheme_before(pos).is_some_and(is_whitespace) { 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) { let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get() self.cursor.get()
} else { } else {
self.end_of_word_forward_or_start_of_word_backward_from( self.start_of_word_backward(self.cursor.get(), word)
self.cursor.get(),
word,
Direction::Backward,
)
}; };
let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, true); let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, true);
Some((start, end)) Some((start, end))
@@ -888,11 +908,7 @@ impl LineBuf {
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get() self.cursor.get()
} else { } else {
self.end_of_word_forward_or_start_of_word_backward_from( self.start_of_word_backward(self.cursor.get(), word)
self.cursor.get(),
word,
Direction::Backward,
)
}; };
let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, false); let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, false);
Some((start, end)) Some((start, end))
@@ -907,39 +923,26 @@ impl LineBuf {
) -> Option<(usize, usize)> { ) -> Option<(usize, usize)> {
let mut start = None; let mut start = None;
let mut end = 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() { while let Some(idx) = fwd_indices.next() {
let Some(gr) = self.grapheme_at(idx) else { if self.grapheme_at(idx).is_none() {
end = Some(self.cursor.max);
break; 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 { match bound {
Bound::Inside => { Bound::Inside => {
end = Some(idx); end = Some(idx);
break; break;
} }
Bound::Around => { Bound::Around => {
let mut end_pos = idx; end = Some(next_sentence_start);
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);
break; break;
} }
} }
} }
} }
let mut end = end.unwrap_or(self.cursor.max); 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(); let mut bkwd_indices = (0..end).rev();
while let Some(idx) = bkwd_indices.next() { while let Some(idx) = bkwd_indices.next() {
@@ -949,10 +952,6 @@ impl LineBuf {
} }
} }
let start = start.unwrap_or(0); 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 count > 1 {
if let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) { if let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) {
@@ -1321,7 +1320,6 @@ impl LineBuf {
dir: Direction, dir: Direction,
include_last_char: bool, include_last_char: bool,
) -> usize { ) -> usize {
// Not sorry for these method names btw
let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false); let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false);
for i in 0..count { for i in 0..count {
// We alter 'include_last_char' to only be true on the last iteration // We alter 'include_last_char' to only be true on the last iteration
@@ -1331,16 +1329,12 @@ impl LineBuf {
pos.set(match to { pos.set(match to {
To::Start => { To::Start => {
match dir { match dir {
Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from( Direction::Forward => {
pos.get(), self.start_of_word_forward(pos.get(), word, include_last_char_and_is_last_word)
word, }
dir,
include_last_char_and_is_last_word,
),
Direction::Backward => 'backward: { Direction::Backward => 'backward: {
// We also need to handle insert mode's Ctrl+W behaviors here // We also need to handle insert mode's Ctrl+W behaviors here
let target = let target = self.start_of_word_backward(pos.get(), word);
self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir);
// Check to see if we are in insert mode // Check to see if we are in insert mode
let Some(start_pos) = self.insert_mode_start_pos else { let Some(start_pos) = self.insert_mode_start_pos else {
@@ -1361,38 +1355,18 @@ impl LineBuf {
} }
} }
To::End => match dir { To::End => match dir {
Direction::Forward => { Direction::Forward => self.end_of_word_forward(pos.get(), word),
self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir) Direction::Backward => self.end_of_word_backward(pos.get(), word, false),
}
Direction::Backward => {
self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir, false)
}
}, },
}); });
} }
pos.get() pos.get()
} }
/// Finds the start of a word forward, or the end of a word backward /// 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 {
/// Finding the start of a word in the forward direction, and finding the end let default = self.grapheme_indices().len();
/// of a word in the backward direction are logically the same operation, if let mut indices_iter = (pos..self.cursor.max).peekable();
/// 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
match word { match word {
Word::Big => { Word::Big => {
@@ -1404,7 +1378,6 @@ impl LineBuf {
let Some(idx) = indices_iter.next() else { let Some(idx) = indices_iter.next() else {
return default; return default;
}; };
// We have a 'cw' call, do not include the trailing whitespace
if include_last_char { if include_last_char {
return idx; return idx;
} else { } else {
@@ -1412,15 +1385,14 @@ impl LineBuf {
} }
} }
// Check current grapheme
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
return default; return default;
}; };
let on_whitespace = is_whitespace(&cur_char); let on_whitespace = is_whitespace(&cur_char);
// Find the next whitespace
if !on_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 { else {
return default; return default;
}; };
@@ -1429,11 +1401,9 @@ impl LineBuf {
} }
} }
// Return the next visible grapheme position indices_iter
let non_ws_pos = indices_iter
.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) .find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
.unwrap_or(default); .unwrap_or(default)
non_ws_pos
} }
Word::Normal => { Word::Normal => {
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { 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); let on_whitespace = is_whitespace(&cur_char);
// Advance until hitting whitespace or a different character class
if !on_whitespace { if !on_whitespace {
let other_class_pos = indices_iter.find(|i| { let other_class_pos = indices_iter.find(|i| {
self self
@@ -1472,7 +1441,6 @@ impl LineBuf {
let Some(other_class_pos) = other_class_pos else { let Some(other_class_pos) = other_class_pos else {
return default; return default;
}; };
// If we hit a different character class, we return here
if self if self
.grapheme_at(other_class_pos) .grapheme_at(other_class_pos)
.is_some_and(|c| !is_whitespace(c)) .is_some_and(|c| !is_whitespace(c))
@@ -1482,79 +1450,54 @@ impl LineBuf {
} }
} }
// We are now certainly on a whitespace character. Advance until a indices_iter
// non-whitespace character.
let non_ws_pos = indices_iter
.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) .find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
.unwrap_or(default); .unwrap_or(default)
non_ws_pos
} }
} }
} }
/// 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 { match word {
Word::Big => { Word::Big => {
let Some(next_idx) = indices_iter.peek() else { let Some(next) = indices_iter.peek() else {
return default; 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 { if on_boundary {
let Some(idx) = indices_iter.next() else { let Some(idx) = indices_iter.next() else {
return default; return default;
}; };
if include_last_char {
return idx;
} else {
pos = idx; pos = idx;
} }
// Check current grapheme }
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
return default; return default;
}; };
let on_whitespace = is_whitespace(&cur_char); let on_whitespace = is_whitespace(&cur_char);
// Advance iterator to next visible grapheme if !on_whitespace {
if on_whitespace { let Some(ws_pos) =
let Some(_non_ws_pos) =
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
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)) indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
else { else {
return default; return default;
}; };
pos = next_ws_pos; if include_last_char {
return ws_pos;
}
}
if pos == self.grapheme_indices().len() { indices_iter
// We reached the end of the buffer .find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
pos .unwrap_or(default)
} else {
// We hit some whitespace, so we will go back one
match dir {
Direction::Forward => pos.saturating_sub(1),
Direction::Backward => pos + 1,
}
}
} }
Word::Normal => { Word::Normal => {
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
@@ -1568,32 +1511,131 @@ impl LineBuf {
.grapheme_at(*next_idx) .grapheme_at(*next_idx)
.is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); .is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
if on_boundary { if on_boundary {
let next_idx = indices_iter.next().unwrap(); if include_last_char {
pos = next_idx 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 { let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
return default; return default;
}; };
let on_whitespace = is_whitespace(&cur_char); let on_whitespace = is_whitespace(&cur_char);
// Proceed to next visible grapheme
if on_whitespace { if on_whitespace {
let Some(non_ws_pos) = let Some(_non_ws_pos) =
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) fwd_indices.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
else { else {
return default; 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 { let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
return self.grapheme_indices().len(); return self.grapheme_indices().len();
}; };
// The position of the next differing character class will tell us where the end let Some(next_ws_pos) = fwd_indices.find(|i| {
// (or start) of the word is
let Some(next_ws_pos) = indices_iter.find(|i| {
self self
.grapheme_at(*i) .grapheme_at(*i)
.is_some_and(|c| is_other_class_or_is_ws(c, &cur_char)) .is_some_and(|c| is_other_class_or_is_ws(c, &cur_char))
@@ -1603,18 +1645,113 @@ impl LineBuf {
pos = next_ws_pos; pos = next_ws_pos;
if pos == self.grapheme_indices().len() { if pos == self.grapheme_indices().len() {
// We reached the end of the buffer
pos pos
} else { } else {
// We hit some other character class, so we go back one pos.saturating_sub(1)
match dir {
Direction::Forward => pos.saturating_sub(1),
Direction::Backward => pos + 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 { fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize {
let mut col = 0; let mut col = 0;
for (grapheme_index, g) in line.graphemes(true).enumerate() { for (grapheme_index, g) in line.graphemes(true).enumerate() {

View File

@@ -48,7 +48,7 @@ impl Readline for FernVi {
loop { loop {
raw_mode_guard.disable_for(|| self.print_line())?; 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"))?; raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard); std::mem::drop(raw_mode_guard);
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")); return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
@@ -116,11 +116,17 @@ impl FernVi {
old_layout: None, old_layout: None,
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0), editor: LineBuf::new(),
history: History::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 { pub fn get_layout(&mut self) -> Layout {
let line = self.editor.to_string(); let line = self.editor.to_string();
flog!(DEBUG, line); flog!(DEBUG, line);
@@ -268,8 +274,10 @@ impl FernVi {
self.repeat_action = mode.as_replay(); 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.set_cursor_clamp(self.mode.clamp_cursor());
self.editor.exec_cmd(cmd)?;
if selecting { if selecting {
self self

View File

@@ -167,7 +167,7 @@ pub trait WidthCalculator {
} }
pub trait KeyReader { pub trait KeyReader {
fn read_key(&mut self) -> Option<KeyEvent>; fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr>;
} }
pub trait LineWriter { pub trait LineWriter {
@@ -232,12 +232,10 @@ impl TermBuffer {
impl Read for TermBuffer { impl Read for TermBuffer {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
assert!(isatty(self.tty).is_ok_and(|r| r)); assert!(isatty(self.tty).is_ok_and(|r| r));
loop {
match nix::unistd::read(self.tty, buf) { match nix::unistd::read(self.tty, buf) {
Ok(n) => return Ok(n), Ok(n) => Ok(n),
Err(Errno::EINTR) => {} Err(Errno::EINTR) => Err(Errno::EINTR.into()),
Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)), Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
}
} }
} }
} }
@@ -420,24 +418,24 @@ impl TermReader {
} }
impl KeyReader for TermReader { impl KeyReader for TermReader {
fn read_key(&mut self) -> Option<KeyEvent> { fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
use core::str; use core::str;
let mut collected = Vec::with_capacity(4); let mut collected = Vec::with_capacity(4);
loop { loop {
let byte = self.next_byte().ok()?; let byte = self.next_byte()?;
flog!(DEBUG, "read byte: {:?}", byte as char); flog!(DEBUG, "read byte: {:?}", byte as char);
collected.push(byte); collected.push(byte);
// If it's an escape seq, delegate to ESC sequence handler // If it's an escape seq, delegate to ESC sequence handler
if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO).ok()? { if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO)? {
return self.parse_esc_seq().ok(); return self.parse_esc_seq().map(Some);
} }
// Try parse as valid UTF-8 // Try parse as valid UTF-8
if let Ok(s) = str::from_utf8(&collected) { 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 // 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::{ use crate::{
jobs::{take_term, JobCmdFlags, JobID}, jobs::{JobCmdFlags, JobID, take_term},
libsh::{error::ShResult, sys::sh_quit}, libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit},
prelude::*, prelude::*,
state::{read_jobs, write_jobs}, 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() { 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 { unsafe {
signal(Signal::SIGCHLD, SigHandler::Handler(handle_sigchld)).unwrap(); sigaction(Signal::SIGCHLD, &actions[0]).unwrap();
signal(Signal::SIGQUIT, SigHandler::Handler(handle_sigquit)).unwrap(); sigaction(Signal::SIGQUIT, &actions[1]).unwrap();
signal(Signal::SIGTSTP, SigHandler::Handler(handle_sigtstp)).unwrap(); sigaction(Signal::SIGTSTP, &actions[2]).unwrap();
signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup)).unwrap(); sigaction(Signal::SIGHUP, &actions[3]).unwrap();
signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)).unwrap(); sigaction(Signal::SIGINT, &actions[4]).unwrap();
signal(Signal::SIGTTIN, SigHandler::SigIgn).unwrap(); sigaction(Signal::SIGTTIN, &actions[5]).unwrap();
signal(Signal::SIGTTOU, SigHandler::SigIgn).unwrap(); sigaction(Signal::SIGTTOU, &actions[6]).unwrap();
} }
} }
extern "C" fn handle_sighup(_: libc::c_int) { 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| { write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() { for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok(); job.killpg(Signal::SIGTERM).ok();
} }
}); });
std::process::exit(0);
} }
extern "C" fn handle_sigtstp(_: libc::c_int) { extern "C" fn handle_sigtstp(_: libc::c_int) {
GOT_SIGTSTP.store(true, Ordering::SeqCst);
}
pub fn terminal_stop() -> ShResult<()> {
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.get_fg_mut() { 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) { extern "C" fn handle_sigint(_: libc::c_int) {
write_jobs(|j| { GOT_SIGINT.store(true, Ordering::SeqCst);
if let Some(job) = j.get_fg_mut() {
job.killpg(Signal::SIGINT).ok();
}
});
} }
pub extern "C" fn ignore_sigchld(_: libc::c_int) { pub fn interrupt() -> ShResult<()> {
/* write_jobs(|j| {
Do nothing if let Some(job) = j.get_fg_mut() {
job.killpg(Signal::SIGINT)
This function exists because using SIGIGN to ignore SIGCHLD } else {
will cause the kernel to automatically reap the child process, which is not what we want. Ok(())
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
*/
} }
extern "C" fn handle_sigquit(_: libc::c_int) { 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; let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED;
while let Ok(status) = waitpid(None, Some(flags)) { while let Ok(status) = waitpid(None, Some(flags)) {
if let Err(e) = match status { match status {
WtStat::Exited(pid, _) => child_exited(pid, status), WtStat::Exited(pid, _) => child_exited(pid, status)?,
WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal), WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal)?,
WtStat::Stopped(pid, signal) => child_stopped(pid, signal), WtStat::Stopped(pid, signal) => child_stopped(pid, signal)?,
WtStat::Continued(pid) => child_continued(pid), WtStat::Continued(pid) => child_continued(pid)?,
WtStat::StillAlive => break, WtStat::StillAlive => break,
_ => unimplemented!(), _ => unimplemented!(),
} {
eprintln!("{}", e)
} }
} }
Ok(())
} }
pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> { pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> {

View File

@@ -1,8 +1,5 @@
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, str::FromStr, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration
ops::Deref,
sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
time::Duration,
}; };
use nix::unistd::{gethostname, getppid, User}; use nix::unistd::{gethostname, getppid, User};
@@ -19,15 +16,248 @@ use crate::{
shopt::ShOpts, 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 /// 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)] #[derive(Clone, Debug)]
pub struct Var { pub struct Var {
export: bool, flags: VarFlags,
value: String, kind: VarKind,
} }
impl Var { impl Var {
pub fn new(value: String) -> Self { pub fn new(kind: VarKind, flags: VarFlags) -> Self {
Self { Self {
export: false, flags,
value, 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) { pub fn mark_for_export(&mut self) {
self.export = true; self.flags.set(VarFlags::EXPORT, true);
} }
} }
impl Deref for Var { impl Display for Var {
type Target = String; fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn deref(&self) -> &Self::Target { self.kind.fmt(f)
&self.value
} }
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct VarTab { pub struct VarTab {
vars: HashMap<String, Var>, vars: HashMap<String, Var>,
params: HashMap<String, String>, params: HashMap<ShellParam, String>,
sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift` sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift`
* straightforward */ * straightforward */
} }
@@ -154,20 +492,19 @@ impl VarTab {
var_tab.init_sh_argv(); var_tab.init_sh_argv();
var_tab var_tab
} }
fn init_params() -> HashMap<String, String> { fn init_params() -> HashMap<ShellParam, String> {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("?".into(), "0".into()); // Last command exit status params.insert(ShellParam::ArgCount, "0".into()); // Number of positional parameters
params.insert("#".into(), "0".into()); // Number of positional parameters
params.insert( params.insert(
"0".into(), ShellParam::Pos(0),
std::env::current_exe() std::env::current_exe()
.unwrap() .unwrap()
.to_str() .to_str()
.unwrap() .unwrap()
.to_string(), .to_string(),
); // Name of the shell ); // Name of the shell
params.insert("$".into(), Pid::this().to_string()); // PID of the shell params.insert(ShellParam::ShPid, Pid::this().to_string()); // PID of the shell
params.insert("!".into(), "".into()); // PID of the last background job (if any) params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
params params
} }
fn init_env() { fn init_env() {
@@ -202,6 +539,7 @@ impl VarTab {
.map(|hname| hname.to_string_lossy().to_string()) .map(|hname| hname.to_string_lossy().to_string())
.unwrap_or_default(); .unwrap_or_default();
unsafe {
env::set_var("IFS", " \t\n"); env::set_var("IFS", " \t\n");
env::set_var("HOST", hostname.clone()); env::set_var("HOST", hostname.clone());
env::set_var("UID", uid.to_string()); env::set_var("UID", uid.to_string());
@@ -218,6 +556,7 @@ impl VarTab {
env::set_var("FERN_HIST", format!("{}/.fernhist", home)); env::set_var("FERN_HIST", format!("{}/.fernhist", home));
env::set_var("FERN_RC", format!("{}/.fernrc", home)); env::set_var("FERN_RC", format!("{}/.fernrc", home));
} }
}
pub fn init_sh_argv(&mut self) { pub fn init_sh_argv(&mut self) {
for arg in env::args() { for arg in env::args() {
self.bpush_arg(arg); self.bpush_arg(arg);
@@ -226,10 +565,10 @@ impl VarTab {
pub fn update_exports(&mut self) { pub fn update_exports(&mut self) {
for var_name in self.vars.keys() { for var_name in self.vars.keys() {
let var = self.vars.get(var_name).unwrap(); let var = self.vars.get(var_name).unwrap();
if var.export { if var.flags.contains(VarFlags::EXPORT) {
env::set_var(var_name, &var.value); unsafe { env::set_var(var_name, var.to_string()) };
} else { } 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()); self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
} }
fn update_arg_params(&mut self) { fn update_arg_params(&mut self) {
self.set_param("@", &self.sh_argv.clone().to_vec()[1..].join(" ")); self.set_param(ShellParam::AllArgs, &self.sh_argv.clone().to_vec()[1..].join(" "));
self.set_param("#", &(self.sh_argv.len() - 1).to_string()); self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
} }
/// Push an arg to the front of the arg deque /// Push an arg to the front of the arg deque
pub fn fpush_arg(&mut self, arg: String) { pub fn fpush_arg(&mut self, arg: String) {
@@ -278,21 +617,21 @@ impl VarTab {
pub fn vars_mut(&mut self) -> &mut HashMap<String, Var> { pub fn vars_mut(&mut self) -> &mut HashMap<String, Var> {
&mut self.vars &mut self.vars
} }
pub fn params(&self) -> &HashMap<String, String> { pub fn params(&self) -> &HashMap<ShellParam, String> {
&self.params &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 &mut self.params
} }
pub fn export_var(&mut self, var_name: &str) { pub fn export_var(&mut self, var_name: &str) {
if let Some(var) = self.vars.get_mut(var_name) { if let Some(var) = self.vars.get_mut(var_name) {
var.mark_for_export(); 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 { pub fn get_var(&self, var: &str) -> String {
if var.chars().count() == 1 || var.parse::<usize>().is_ok() { if let Ok(param) = var.parse::<ShellParam>() {
let param = self.get_param(var); let param = self.get_param(param);
if !param.is_empty() { if !param.is_empty() {
return param; return param;
} }
@@ -303,52 +642,59 @@ impl VarTab {
std::env::var(var).unwrap_or_default() 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) { if let Some(var) = self.vars.get_mut(var_name) {
var.value = val.to_string(); var.kind = VarKind::Str(val.to_string());
if var.export { if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
env::set_var(var_name, val); if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
var.mark_for_export();
}
unsafe { env::set_var(var_name, val) };
} }
} else { } else {
let mut var = Var::new(val.to_string()); let mut var = Var::new(VarKind::Str(val.to_string()), VarFlags::NONE);
if export { if flags.contains(VarFlags::EXPORT) {
var.mark_for_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); self.vars.insert(var_name.to_string(), var);
} }
} }
pub fn var_exists(&self, var_name: &str) -> bool { pub fn var_exists(&self, var_name: &str) -> bool {
if var_name.parse::<usize>().is_ok() { if let Ok(param) = var_name.parse::<ShellParam>() {
return self.params.contains_key(var_name); 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) { pub fn set_param(&mut self, param: ShellParam, val: &str) {
self.params.insert(param.to_string(), val.to_string()); self.params.insert(param, val.to_string());
} }
pub fn get_param(&self, param: &str) -> String { pub fn get_param(&self, param: ShellParam) -> String {
if param.parse::<usize>().is_ok() { match param {
let argv_idx = param.to_string().parse::<usize>().unwrap(); ShellParam::Pos(n) => {
let arg = self
.sh_argv
.get(argv_idx)
.map(|s| s.to_string())
.unwrap_or_default();
arg
} else if param == "?" {
self self
.params .sh_argv()
.get(param) .get(n)
.map(|s| s.to_string())
.unwrap_or("0".into())
} else {
self
.params
.get(param)
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_default() .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 /// Read from the job table
pub fn read_jobs<T, F: FnOnce(RwLockReadGuard<JobTab>) -> T>(f: F) -> T { pub fn read_jobs<T, F: FnOnce(&JobTab) -> T>(f: F) -> T {
let lock = JOB_TABLE.read().unwrap(); let fern = FERN.read().unwrap();
f(lock) let jobs = fern.read_jobs();
f(jobs)
} }
/// Write to the job table /// Write to the job table
pub fn write_jobs<T, F: FnOnce(&mut RwLockWriteGuard<JobTab>) -> T>(f: F) -> T { pub fn write_jobs<T, F: FnOnce(&mut JobTab) -> T>(f: F) -> T {
let lock = &mut JOB_TABLE.write().unwrap(); let mut fern = FERN.write().unwrap();
f(lock) let jobs = &mut fern.jobs;
f(jobs)
} }
/// Read from the variable table /// Read from the var scope stack
pub fn read_vars<T, F: FnOnce(RwLockReadGuard<VarTab>) -> T>(f: F) -> T { pub fn read_vars<T, F: FnOnce(&ScopeStack) -> T>(f: F) -> T {
let lock = VAR_TABLE.read().unwrap(); let fern = FERN.read().unwrap();
f(lock) let vars = fern.read_vars();
f(vars)
} }
/// Write to the variable table /// Write to the variable table
pub fn write_vars<T, F: FnOnce(&mut RwLockWriteGuard<VarTab>) -> T>(f: F) -> T { pub fn write_vars<T, F: FnOnce(&mut ScopeStack) -> T>(f: F) -> T {
let lock = &mut VAR_TABLE.write().unwrap(); let mut fern = FERN.write().unwrap();
f(lock) let vars = fern.write_vars();
f(vars)
} }
pub fn read_meta<T, F: FnOnce(RwLockReadGuard<MetaTab>) -> T>(f: F) -> T { pub fn read_meta<T, F: FnOnce(&MetaTab) -> T>(f: F) -> T {
let lock = META_TABLE.read().unwrap(); let fern = FERN.read().unwrap();
f(lock) let meta = fern.read_meta();
f(meta)
} }
/// Write to the variable table /// Write to the variable table
pub fn write_meta<T, F: FnOnce(&mut RwLockWriteGuard<MetaTab>) -> T>(f: F) -> T { pub fn write_meta<T, F: FnOnce(&mut MetaTab) -> T>(f: F) -> T {
let lock = &mut META_TABLE.write().unwrap(); let mut fern = FERN.write().unwrap();
f(lock) let meta = fern.write_meta();
f(meta)
} }
/// Read from the logic table /// Read from the logic table
pub fn read_logic<T, F: FnOnce(RwLockReadGuard<LogTab>) -> T>(f: F) -> T { pub fn read_logic<T, F: FnOnce(&LogTab) -> T>(f: F) -> T {
let lock = LOGIC_TABLE.read().unwrap(); let fern = FERN.read().unwrap();
f(lock) let logic = fern.read_logic();
f(logic)
} }
/// Write to the logic table /// Write to the logic table
pub fn write_logic<T, F: FnOnce(&mut RwLockWriteGuard<LogTab>) -> T>(f: F) -> T { pub fn write_logic<T, F: FnOnce(&mut LogTab) -> T>(f: F) -> T {
let lock = &mut LOGIC_TABLE.write().unwrap(); let mut fern = FERN.write().unwrap();
f(lock) let logic = &mut fern.logic;
f(logic)
} }
pub fn read_shopts<T, F: FnOnce(RwLockReadGuard<ShOpts>) -> T>(f: F) -> T { pub fn read_shopts<T, F: FnOnce(&ShOpts) -> T>(f: F) -> T {
let lock = SHOPTS.read().unwrap(); let fern = FERN.read().unwrap();
f(lock) let shopts = fern.read_shopts();
f(shopts)
} }
pub fn write_shopts<T, F: FnOnce(&mut RwLockWriteGuard<ShOpts>) -> T>(f: F) -> T { pub fn write_shopts<T, F: FnOnce(&mut ShOpts) -> T>(f: F) -> T {
let lock = &mut SHOPTS.write().unwrap(); let mut fern = FERN.write().unwrap();
f(lock) 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 /// 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 { 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] #[track_caller]
pub fn set_status(code: i32) { pub fn set_status(code: i32) {
write_vars(|v| v.set_param("?", &code.to_string())) write_vars(|v| v.set_param(ShellParam::Status, &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);
} }
pub fn source_rc() -> ShResult<()> { pub fn source_rc() -> ShResult<()> {