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) {
GOT_SIGINT.store(true, Ordering::SeqCst);
}
pub fn interrupt() -> 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::SIGINT).ok(); job.killpg(Signal::SIGINT)
} else {
Ok(())
} }
}); })
}
pub extern "C" fn ignore_sigchld(_: libc::c_int) {
/*
Do nothing
This function exists because using SIGIGN to ignore SIGCHLD
will cause the kernel to automatically reap the child process, which is not what we want.
This handler will leave the signaling process as a zombie, allowing us
to handle it somewhere else.
This handler is used when we want to handle SIGCHLD explicitly,
like in the case of handling foreground jobs
*/
} }
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<()> {