initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target
|
||||
534
Cargo.lock
generated
Normal file
534
Cargo.lock
generated
Normal file
@@ -0,0 +1,534 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "assert_cmd"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"bstr",
|
||||
"libc",
|
||||
"predicates 3.1.3",
|
||||
"predicates-core",
|
||||
"predicates-tree",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "difference"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df"
|
||||
dependencies = [
|
||||
"difference",
|
||||
"float-cmp",
|
||||
"normalize-line-endings",
|
||||
"predicates-core",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"difflib",
|
||||
"predicates-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "rush"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assert_cmd",
|
||||
"predicates 1.0.8",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "rush"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0"
|
||||
predicates = "1.0"
|
||||
6
README.md
Normal file
6
README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# rush - A Modern Shell Scripting Language Interpreter
|
||||
|
||||
## Overview
|
||||
Rush is a modern shell scripting language interpreter designed to execute scripts written in the `.rsh` format. It supports features such as variable assignment, control flow, and parallel execution, making it a powerful tool for automating tasks and scripting.
|
||||
|
||||
More info coming soon
|
||||
14
src/error/mod.rs
Normal file
14
src/error/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
#[derive(Debug)]
|
||||
pub enum RushError {
|
||||
SyntaxError(String),
|
||||
RuntimeError(String),
|
||||
VariableError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RushError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RushError {}
|
||||
367
src/interpreter/executor.rs
Normal file
367
src/interpreter/executor.rs
Normal file
@@ -0,0 +1,367 @@
|
||||
use crate::parser::ast::{AstNode, Command, Statement};
|
||||
use crate::runtime::builtins;
|
||||
use crate::runtime::environment::Environment;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
pub struct Executor<'a> {
|
||||
env: &'a mut Environment,
|
||||
}
|
||||
|
||||
impl<'a> Executor<'a> {
|
||||
pub fn new(env: &'a mut Environment) -> Self {
|
||||
Executor { env }
|
||||
}
|
||||
|
||||
pub fn execute_node(&mut self, node: AstNode) {
|
||||
match node {
|
||||
AstNode::VariableAssignment { name, value } => {
|
||||
if let AstNode::Literal(val) = *value {
|
||||
self.env.set_variable(name, val);
|
||||
}
|
||||
}
|
||||
AstNode::Command { name, args } => {
|
||||
// convert args to strings
|
||||
let arg_strings: Vec<String> = args
|
||||
.into_iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
// Check if its a variable reference
|
||||
if s.starts_with('$') {
|
||||
self.env.get_variable(&s[1..]).map(|v| v.clone())
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// built in commands :D
|
||||
//todo: move and add more builtins
|
||||
if name == "echo" {
|
||||
builtins::echo(arg_strings);
|
||||
} else if name == "exit" {
|
||||
let code = arg_strings
|
||||
.first()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
builtins::exit(code);
|
||||
} else if name == "require_root" {
|
||||
builtins::require_root();
|
||||
} else {
|
||||
// execute external cmd
|
||||
let cmd = Command {
|
||||
name: name.clone(),
|
||||
args: arg_strings,
|
||||
};
|
||||
self.execute_command(cmd);
|
||||
}
|
||||
}
|
||||
AstNode::ControlFlow {
|
||||
condition: _,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
println!("TODO: Execute if statement");
|
||||
for node in then_branch {
|
||||
self.execute_node(node);
|
||||
}
|
||||
if let Some(else_nodes) = else_branch {
|
||||
for node in else_nodes {
|
||||
self.execute_node(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::For { var, items, body } => {
|
||||
// execute for loop by iterating over items
|
||||
for item in items {
|
||||
// set loop variable
|
||||
self.env.set_variable(var.clone(), item);
|
||||
|
||||
// Execute body
|
||||
for node in &body {
|
||||
self.execute_node(node.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::Parallel { blocks } => {
|
||||
// create multithreaded runtime
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(4) //todo: make configurable or dynamic
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async {
|
||||
let mut handles = vec![];
|
||||
for block in blocks {
|
||||
// clone the environment for this block to access variables
|
||||
let env = self.env.clone();
|
||||
|
||||
// Spawn each block as a separate task (can run on different threads)
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
for node in block {
|
||||
// Execute node
|
||||
if let AstNode::Command { name, args } = node {
|
||||
let arg_strings: Vec<String> = args
|
||||
.into_iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
// Check if its a variable reference
|
||||
if s.starts_with('$') {
|
||||
env.get_variable(&s[1..]).map(|v| v.clone())
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if name == "echo" {
|
||||
println!("{}", arg_strings.join(" "));
|
||||
} else {
|
||||
// execute external cmd
|
||||
match std::process::Command::new(&name)
|
||||
.args(&arg_strings)
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
if !output.stdout.is_empty() {
|
||||
print!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// wait for all
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
AstNode::Workers { count, body } => {
|
||||
// execute with limited concurrency using a semaphore and multi-threaded runtime
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(count.max(2)) // use at least 2 threads, or the worker count
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
// clone environment for workers
|
||||
let env = self.env.clone();
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(count));
|
||||
let mut handles = vec![];
|
||||
|
||||
for node in body {
|
||||
let sem = semaphore.clone();
|
||||
let env_clone = env.clone();
|
||||
|
||||
// clone the node for the async task
|
||||
let node_clone = node.clone();
|
||||
|
||||
// Use spawn_blocking for CPU bound work to run on thread pool
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
// Acquire semaphore permit (limits concurrency)
|
||||
// Note: We need to do this in a blocking context
|
||||
let rt_inner = tokio::runtime::Handle::current();
|
||||
let _permit = rt_inner.block_on(async { sem.acquire().await.unwrap() });
|
||||
|
||||
// execute the node based on its type
|
||||
match node_clone {
|
||||
AstNode::Command { name, args } => {
|
||||
let arg_strings: Vec<String> = args
|
||||
.into_iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
// Check if it's a variable reference
|
||||
if s.starts_with('$') {
|
||||
env_clone
|
||||
.get_variable(&s[1..])
|
||||
.map(|v| v.clone())
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if name == "echo" {
|
||||
println!("{}", arg_strings.join(" "));
|
||||
} else {
|
||||
match std::process::Command::new(&name)
|
||||
.args(&arg_strings)
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
if !output.stdout.is_empty() {
|
||||
print!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::For {
|
||||
var,
|
||||
items,
|
||||
body: for_body,
|
||||
} => {
|
||||
// execute for loop
|
||||
for item in items {
|
||||
for for_node in &for_body {
|
||||
// simple execution for commands in for loops
|
||||
if let AstNode::Command { name, args } = for_node {
|
||||
let arg_strings: Vec<String> = args
|
||||
.iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
|
||||
if s.starts_with('$') {
|
||||
let var_name = &s[1..];
|
||||
if var_name == var {
|
||||
// loop var
|
||||
Some(item.clone())
|
||||
} else {
|
||||
// Environment var
|
||||
env_clone
|
||||
.get_variable(var_name)
|
||||
.map(|v| v.clone())
|
||||
}
|
||||
} else {
|
||||
Some(s.clone())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if name == "echo" {
|
||||
println!("{}", arg_strings.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
//todo: other node types not yet supported in workers
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// wait for all tasks to complete
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
AstNode::Literal(_) => {
|
||||
// literals dont execute on their own
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&mut self, statement: Statement) {
|
||||
match statement {
|
||||
Statement::Command(cmd) => self.execute_command(cmd),
|
||||
Statement::Assignment(var, value) => self.env.set_variable(var, value),
|
||||
Statement::For { var, items, body } => {
|
||||
// todo: Implement for loop execution
|
||||
println!(
|
||||
"todo: Execute for loop with var '{}' over {} items",
|
||||
var,
|
||||
items.len()
|
||||
);
|
||||
for _stmt in body {
|
||||
// execute each statement in the loop body
|
||||
}
|
||||
}
|
||||
Statement::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
// todo: Implement if statement execution
|
||||
println!("todo: Execute if with condition '{}'", condition);
|
||||
for _stmt in then_branch {
|
||||
// Execute then branch
|
||||
}
|
||||
if let Some(else_stmts) = else_branch {
|
||||
for _stmt in else_stmts {
|
||||
// Execute else branch
|
||||
}
|
||||
}
|
||||
}
|
||||
Statement::Parallel { blocks } => {
|
||||
// todo: Implement parallel execution
|
||||
println!("todo: Execute {} parallel blocks", blocks.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_command(&mut self, command: Command) {
|
||||
let result = StdCommand::new(&command.name).args(&command.args).output();
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
if !output.stdout.is_empty() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"Command '{}' failed with exit code: {:?}",
|
||||
command.name,
|
||||
output.status.code()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute command '{}': {}", command.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/interpreter/mod.rs
Normal file
32
src/interpreter/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
pub mod executor;
|
||||
pub mod parallel;
|
||||
|
||||
use crate::parser::Parser;
|
||||
use crate::runtime::environment::Environment;
|
||||
use executor::Executor;
|
||||
|
||||
/// Main entry point for executing rush scripts
|
||||
pub fn execute(content: &str) {
|
||||
// create a new parser
|
||||
let parser = Parser::new();
|
||||
|
||||
// parse the script content
|
||||
let program = match parser.parse(content) {
|
||||
Ok(prog) => prog,
|
||||
Err(e) => {
|
||||
eprintln!("Parse error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// create a new environment
|
||||
let mut env = Environment::new();
|
||||
|
||||
// create an executor
|
||||
let mut executor = Executor::new(&mut env);
|
||||
|
||||
// execute each statement in the program
|
||||
for statement in program.statements {
|
||||
executor.execute_node(statement);
|
||||
}
|
||||
}
|
||||
39
src/interpreter/parallel.rs
Normal file
39
src/interpreter/parallel.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use tokio::task;
|
||||
|
||||
pub async fn execute_parallel<F, Fut>(tasks: Vec<F>)
|
||||
where
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let handles: Vec<_> = tasks
|
||||
.into_iter()
|
||||
.map(|task| {
|
||||
task::spawn(async move {
|
||||
task().await;
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
|
||||
// simpler version for synchronous tasks
|
||||
pub async fn execute_parallel_sync<F>(tasks: Vec<F>)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
let handles: Vec<_> = tasks
|
||||
.into_iter()
|
||||
.map(|task| {
|
||||
task::spawn_blocking(move || {
|
||||
task();
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
123
src/lexer/mod.rs
Normal file
123
src/lexer/mod.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Token {
|
||||
Identifier(String),
|
||||
Number(i64),
|
||||
Assign,
|
||||
Plus,
|
||||
Minus,
|
||||
Multiply,
|
||||
Divide,
|
||||
If,
|
||||
Else,
|
||||
While,
|
||||
Print,
|
||||
Eof,
|
||||
}
|
||||
|
||||
pub struct Lexer<'a> {
|
||||
input: &'a str,
|
||||
position: usize,
|
||||
current_char: Option<char>,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Lexer {
|
||||
input,
|
||||
position: 0,
|
||||
current_char: input.chars().next(),
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.position += 1;
|
||||
self.current_char = self.input.chars().nth(self.position);
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_whitespace() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_token(&mut self) -> Token {
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_whitespace() {
|
||||
self.skip_whitespace();
|
||||
continue;
|
||||
}
|
||||
|
||||
if c.is_alphabetic() {
|
||||
let identifier = self.collect_identifier();
|
||||
return match identifier.as_str() {
|
||||
"if" => Token::If,
|
||||
"else" => Token::Else,
|
||||
"while" => Token::While,
|
||||
"print" => Token::Print,
|
||||
_ => Token::Identifier(identifier),
|
||||
};
|
||||
}
|
||||
|
||||
if c.is_digit(10) {
|
||||
return Token::Number(self.collect_number());
|
||||
}
|
||||
|
||||
if c == '=' {
|
||||
self.advance();
|
||||
return Token::Assign;
|
||||
}
|
||||
|
||||
if c == '+' {
|
||||
self.advance();
|
||||
return Token::Plus;
|
||||
}
|
||||
|
||||
if c == '-' {
|
||||
self.advance();
|
||||
return Token::Minus;
|
||||
}
|
||||
|
||||
if c == '*' {
|
||||
self.advance();
|
||||
return Token::Multiply;
|
||||
}
|
||||
|
||||
if c == '/' {
|
||||
self.advance();
|
||||
return Token::Divide;
|
||||
}
|
||||
|
||||
self.advance();
|
||||
}
|
||||
|
||||
Token::Eof
|
||||
}
|
||||
|
||||
fn collect_identifier(&mut self) -> String {
|
||||
let start_pos = self.position;
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_alphanumeric() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.input[start_pos..self.position].to_string()
|
||||
}
|
||||
|
||||
fn collect_number(&mut self) -> i64 {
|
||||
let start_pos = self.position;
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_digit(10) {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.input[start_pos..self.position].parse().unwrap()
|
||||
}
|
||||
}
|
||||
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod error;
|
||||
pub mod interpreter;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod runtime;
|
||||
20
src/main.rs
Normal file
20
src/main.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: rush <script.rsh>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let script_path = &args[1];
|
||||
|
||||
let content = std::fs::read_to_string(script_path).expect("Failed to read the script file");
|
||||
|
||||
let stripped_content = if content.starts_with("#!") {
|
||||
content.lines().skip(1).collect::<Vec<&str>>().join("\n")
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
rush::interpreter::execute(&stripped_content);
|
||||
}
|
||||
60
src/parser/ast.rs
Normal file
60
src/parser/ast.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AstNode {
|
||||
VariableAssignment {
|
||||
name: String,
|
||||
value: Box<AstNode>,
|
||||
},
|
||||
ControlFlow {
|
||||
condition: Box<AstNode>,
|
||||
then_branch: Vec<AstNode>,
|
||||
else_branch: Option<Vec<AstNode>>,
|
||||
},
|
||||
Command {
|
||||
name: String,
|
||||
args: Vec<AstNode>,
|
||||
},
|
||||
For {
|
||||
var: String,
|
||||
items: Vec<String>,
|
||||
body: Vec<AstNode>,
|
||||
},
|
||||
Parallel {
|
||||
blocks: Vec<Vec<AstNode>>,
|
||||
},
|
||||
Workers {
|
||||
count: usize,
|
||||
body: Vec<AstNode>,
|
||||
},
|
||||
Literal(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Program {
|
||||
pub statements: Vec<AstNode>,
|
||||
}
|
||||
|
||||
// types for executor compatibility
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Command {
|
||||
pub name: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Statement {
|
||||
Command(Command),
|
||||
Assignment(String, String),
|
||||
For {
|
||||
var: String,
|
||||
items: Vec<String>,
|
||||
body: Vec<Statement>,
|
||||
},
|
||||
If {
|
||||
condition: String,
|
||||
then_branch: Vec<Statement>,
|
||||
else_branch: Option<Vec<Statement>>,
|
||||
},
|
||||
Parallel {
|
||||
blocks: Vec<Vec<Statement>>,
|
||||
},
|
||||
}
|
||||
393
src/parser/mod.rs
Normal file
393
src/parser/mod.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
pub mod ast;
|
||||
|
||||
use crate::error::RushError;
|
||||
pub use ast::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct Parser {
|
||||
// todo: add fields for the parser state
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new() -> Self {
|
||||
Parser {
|
||||
// todo: init fields
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&self, input: &str) -> Result<Program, RushError> {
|
||||
let mut statements = Vec::new();
|
||||
let lines: Vec<&str> = input.lines().collect();
|
||||
let mut i = 0;
|
||||
|
||||
while i < lines.len() {
|
||||
let (node, next_i) = self.parse_statement(&lines, i)?;
|
||||
if let Some(n) = node {
|
||||
statements.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
|
||||
let program = Program { statements };
|
||||
|
||||
// validate variables before returning
|
||||
self.validate_variables(&program)?;
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
fn validate_variables(&self, program: &Program) -> Result<(), RushError> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut defined_vars: HashSet<String> = HashSet::new();
|
||||
|
||||
for statement in &program.statements {
|
||||
self.validate_node(statement, &mut defined_vars)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_node(
|
||||
&self,
|
||||
node: &AstNode,
|
||||
defined_vars: &mut HashSet<String>,
|
||||
) -> Result<(), RushError> {
|
||||
match node {
|
||||
AstNode::VariableAssignment { name, value } => {
|
||||
// first check if the value uses any undefined variables
|
||||
self.check_node_for_undefined_vars(value, defined_vars)?;
|
||||
// then add this variable to the defined set
|
||||
defined_vars.insert(name.clone());
|
||||
}
|
||||
AstNode::Command { name: _, args } => {
|
||||
// check all arguments for variable references
|
||||
for arg in args {
|
||||
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||
}
|
||||
}
|
||||
AstNode::For {
|
||||
var,
|
||||
items: _,
|
||||
body,
|
||||
} => {
|
||||
// create a new scope for the for loop variable
|
||||
let mut loop_scope = defined_vars.clone();
|
||||
loop_scope.insert(var.clone());
|
||||
|
||||
// validate the body with the loop variable in scope
|
||||
for body_node in body {
|
||||
self.validate_node(body_node, &mut loop_scope)?;
|
||||
}
|
||||
}
|
||||
AstNode::Parallel { blocks } => {
|
||||
// each parallel block sees the current scope but can't define new vars
|
||||
for block in blocks {
|
||||
for block_node in block {
|
||||
let mut parallel_scope = defined_vars.clone();
|
||||
self.validate_node(block_node, &mut parallel_scope)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::Workers { count: _, body } => {
|
||||
// workers blocks see the current scope
|
||||
for worker_node in body {
|
||||
let mut worker_scope = defined_vars.clone();
|
||||
self.validate_node(worker_node, &mut worker_scope)?;
|
||||
}
|
||||
}
|
||||
AstNode::ControlFlow {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
self.check_node_for_undefined_vars(condition, defined_vars)?;
|
||||
for node in then_branch {
|
||||
self.validate_node(node, defined_vars)?;
|
||||
}
|
||||
if let Some(else_nodes) = else_branch {
|
||||
for node in else_nodes {
|
||||
self.validate_node(node, defined_vars)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::Literal(_) => {
|
||||
// literals dont use vars
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_node_for_undefined_vars(
|
||||
&self,
|
||||
node: &AstNode,
|
||||
defined_vars: &HashSet<String>,
|
||||
) -> Result<(), RushError> {
|
||||
match node {
|
||||
AstNode::Literal(s) => {
|
||||
// check if this is a variable reference
|
||||
if s.starts_with('$') {
|
||||
let var_name = &s[1..];
|
||||
if !defined_vars.contains(var_name) {
|
||||
return Err(RushError::VariableError(format!(
|
||||
"Variable '{}' is used before being defined",
|
||||
var_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::VariableAssignment { name: _, value } => {
|
||||
self.check_node_for_undefined_vars(value, defined_vars)?;
|
||||
}
|
||||
AstNode::Command { name: _, args } => {
|
||||
for arg in args {
|
||||
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// other node types dont directly contain variable references
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_statement(
|
||||
&self,
|
||||
lines: &[&str],
|
||||
mut i: usize,
|
||||
) -> Result<(Option<AstNode>, usize), RushError> {
|
||||
let line = lines[i].trim();
|
||||
|
||||
// skip empty lines and comments
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return Ok((None, i + 1));
|
||||
}
|
||||
|
||||
// parse variable assignment: name = "value"
|
||||
// make sure it's actually an assignment (not a command with = in args)
|
||||
if line.contains('=')
|
||||
&& !line.starts_with("if")
|
||||
&& !line.starts_with("for")
|
||||
&& !line.starts_with("echo")
|
||||
{
|
||||
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
||||
if parts.len() == 2 {
|
||||
let var_name = parts[0].trim();
|
||||
// only treat as assignment if var_name is a valid identifier (no spaces, no special chars)
|
||||
if !var_name.is_empty() && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||
{
|
||||
let value = parts[1].trim().trim_matches('"').to_string();
|
||||
return Ok((
|
||||
Some(AstNode::VariableAssignment {
|
||||
name: var_name.to_string(),
|
||||
value: Box::new(AstNode::Literal(value)),
|
||||
}),
|
||||
i + 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse for loop: for var in items { ... }
|
||||
if line.starts_with("for ") {
|
||||
// parse: for i in 1 2 3 {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 4 && parts[0] == "for" && parts[2] == "in" {
|
||||
let var_name = parts[1].to_string();
|
||||
|
||||
// collect items until we hit '{'
|
||||
let mut items = Vec::new();
|
||||
for part in &parts[3..] {
|
||||
if *part == "{" {
|
||||
break;
|
||||
}
|
||||
items.push(part.trim_matches('"').to_string());
|
||||
}
|
||||
|
||||
// parse body
|
||||
i += 1;
|
||||
let mut body = Vec::new();
|
||||
while i < lines.len() && !lines[i].trim().starts_with('}') {
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
body.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
i += 1; // skip closing brace
|
||||
|
||||
return Ok((
|
||||
Some(AstNode::For {
|
||||
var: var_name,
|
||||
items,
|
||||
body,
|
||||
}),
|
||||
i,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// parse parallel block: parallel { ... }
|
||||
if line.starts_with("parallel") && line.contains('{') {
|
||||
i += 1;
|
||||
let mut blocks = Vec::new();
|
||||
|
||||
while i < lines.len() {
|
||||
let inner_line = lines[i].trim();
|
||||
|
||||
// end of parallel block
|
||||
if inner_line.starts_with('}') {
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// handle 'run {' blocks inside parallel
|
||||
if inner_line.starts_with("run") && inner_line.contains('{') {
|
||||
let mut current_block = Vec::new();
|
||||
|
||||
// check if single line
|
||||
if inner_line.contains('}') {
|
||||
|
||||
let start = inner_line.find('{').unwrap() + 1;
|
||||
let end = inner_line.rfind('}').unwrap();
|
||||
let content = &inner_line[start..end].trim();
|
||||
|
||||
// parse commands separated by semicolons
|
||||
for cmd_part in content.split(';') {
|
||||
let cmd_trimmed = cmd_part.trim();
|
||||
if !cmd_trimmed.is_empty() {
|
||||
let parts: Vec<&str> = cmd_trimmed.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
let cmd_name = parts[0].to_string();
|
||||
let args: Vec<AstNode> = parts[1..]
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
AstNode::Literal(arg.trim_matches('"').to_string())
|
||||
})
|
||||
.collect();
|
||||
current_block.push(AstNode::Command {
|
||||
name: cmd_name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
blocks.push(current_block);
|
||||
i += 1;
|
||||
} else {
|
||||
// multi line run block
|
||||
i += 1;
|
||||
|
||||
while i < lines.len() {
|
||||
let run_line = lines[i].trim();
|
||||
|
||||
// end of this run block
|
||||
if run_line.starts_with('}') {
|
||||
blocks.push(current_block);
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// handle multiple commands on one line separated by semicolons
|
||||
if run_line.contains(';') {
|
||||
for cmd_part in run_line.split(';') {
|
||||
let cmd_trimmed = cmd_part.trim();
|
||||
if !cmd_trimmed.is_empty() {
|
||||
let parts: Vec<&str> =
|
||||
cmd_trimmed.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
let cmd_name = parts[0].to_string();
|
||||
let args: Vec<AstNode> = parts[1..]
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
AstNode::Literal(
|
||||
arg.trim_matches('"').to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
current_block.push(AstNode::Command {
|
||||
name: cmd_name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
} else {
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
current_block.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// direct statement in parallel block (not inside a run {})
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
blocks.push(vec![n]);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok((Some(AstNode::Parallel { blocks }), i));
|
||||
}
|
||||
|
||||
// parse workers N {} block
|
||||
if line.starts_with("workers ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let worker_count = parts[1].parse::<usize>().unwrap_or(1);
|
||||
|
||||
i += 1;
|
||||
let mut body = Vec::new();
|
||||
while i < lines.len() && !lines[i].trim().starts_with('}') {
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
body.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
i += 1; // skip closing brace
|
||||
|
||||
return Ok((
|
||||
Some(AstNode::Workers {
|
||||
count: worker_count,
|
||||
body,
|
||||
}),
|
||||
i,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// parse command: echo "text" or any other command
|
||||
if !line.is_empty() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
let cmd_name = parts[0].to_string();
|
||||
let args: Vec<AstNode> = parts[1..]
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
// kep $ for variable references, remove quotes
|
||||
let cleaned = arg.trim_matches('"');
|
||||
AstNode::Literal(cleaned.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Ok((
|
||||
Some(AstNode::Command {
|
||||
name: cmd_name,
|
||||
args,
|
||||
}),
|
||||
i + 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((None, i + 1))
|
||||
}
|
||||
}
|
||||
|
||||
//todo: add additional functions and implementations for the parser
|
||||
31
src/runtime/builtins.rs
Normal file
31
src/runtime/builtins.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
pub fn echo(args: Vec<String>) {
|
||||
println!("{}", args.join(" "));
|
||||
}
|
||||
|
||||
pub fn require_root() {
|
||||
if !is_root() {
|
||||
eprintln!("This command requires root privileges.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit(code: i32) {
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
fn is_root() -> bool {
|
||||
// check if the current user is root by checking UID
|
||||
// on Unix systems, root has UID 0
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// try to get process UID using USER env variable
|
||||
std::env::var("USER").map(|u| u == "root").unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
//todo
|
||||
// On non-Unix systems, assume not root
|
||||
false
|
||||
}
|
||||
}
|
||||
26
src/runtime/environment.rs
Normal file
26
src/runtime/environment.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Environment {
|
||||
variables: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn new() -> Self {
|
||||
Environment {
|
||||
variables: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_variable(&mut self, name: String, value: String) {
|
||||
self.variables.insert(name, value);
|
||||
}
|
||||
|
||||
pub fn get_variable(&self, name: &str) -> Option<&String> {
|
||||
self.variables.get(name)
|
||||
}
|
||||
|
||||
pub fn remove_variable(&mut self, name: &str) {
|
||||
self.variables.remove(name);
|
||||
}
|
||||
}
|
||||
2
src/runtime/mod.rs
Normal file
2
src/runtime/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod builtins;
|
||||
pub mod environment;
|
||||
254
tests/interpreter_tests.rs
Normal file
254
tests/interpreter_tests.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use rush::interpreter::executor::Executor;
|
||||
use rush::parser::Parser;
|
||||
use rush::runtime::environment::Environment;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_variable_assignment() {
|
||||
// Test variable assignment functionality
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse("name = \"test\"");
|
||||
assert!(result.is_ok());
|
||||
|
||||
let program = result.unwrap();
|
||||
let mut env = Environment::new();
|
||||
let mut executor = Executor::new(&mut env);
|
||||
|
||||
for stmt in program.statements {
|
||||
executor.execute_node(stmt);
|
||||
}
|
||||
|
||||
assert_eq!(env.get_variable("name"), Some(&"test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_command() {
|
||||
// Test basic command execution doesn't panic
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse("echo \"test\"");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_parsing() {
|
||||
// Test the parsing of a complete .rsh script
|
||||
let script = r#"
|
||||
name = "Louis"
|
||||
echo "Hello" $name
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert!(program.statements.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undefined_variable_detection() {
|
||||
// Test that using an undefined variable is caught
|
||||
let script = r#"
|
||||
echo "Hello" $undefined_var
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{:?}", result.unwrap_err());
|
||||
assert!(err_msg.contains("undefined_var"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_order_variable_detection() {
|
||||
// Test that using a variable before it's defined is caught
|
||||
let script = r#"
|
||||
echo "Hello" $name
|
||||
name = "Louis"
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{:?}", result.unwrap_err());
|
||||
assert!(err_msg.contains("name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_variable_order() {
|
||||
// Test that defining before using works correctly
|
||||
let script = r#"
|
||||
name = "Louis"
|
||||
echo "Hello" $name
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_variable_scope() {
|
||||
// Test that for loop variables are properly scoped
|
||||
let script = r#"
|
||||
for i in 1 2 3 {
|
||||
echo "Number:" $i
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_variable_isolation() {
|
||||
// Test that for loop variables don't leak to outer scope
|
||||
let script = r#"
|
||||
for i in 1 2 3 {
|
||||
echo "Inside:" $i
|
||||
}
|
||||
echo "Outside:" $i
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{:?}", result.unwrap_err());
|
||||
assert!(err_msg.contains("i"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_block_variable_access() {
|
||||
// Test that parallel blocks can access outer scope variables
|
||||
let script = r#"
|
||||
name = "Test"
|
||||
|
||||
parallel {
|
||||
run { echo "Task 1:" $name }
|
||||
run { echo "Task 2:" $name }
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_block_undefined_variable() {
|
||||
// Test that parallel blocks catch undefined variables
|
||||
let script = r#"
|
||||
parallel {
|
||||
run { echo "Task:" $undefined }
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workers_block_variable_access() {
|
||||
// Test that workers blocks can access outer scope variables
|
||||
let script = r#"
|
||||
msg = "Worker"
|
||||
|
||||
workers 2 {
|
||||
echo $msg "Task 1"
|
||||
echo $msg "Task 2"
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workers_with_for_loop() {
|
||||
// Test that workers can contain for loops with variables
|
||||
let script = r#"
|
||||
prefix = "Item"
|
||||
|
||||
workers 2 {
|
||||
for num in 1 2 3 {
|
||||
echo $prefix $num
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_for_loops() {
|
||||
// Test nested for loops with proper scoping
|
||||
let script = r#"
|
||||
for i in 1 2 {
|
||||
for j in a b {
|
||||
echo $i $j
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_variables() {
|
||||
// Test multiple variable definitions and usage
|
||||
let script = r#"
|
||||
user = "Alice"
|
||||
project = "RushShell"
|
||||
version = "1.0"
|
||||
|
||||
echo $user "is working on" $project "version" $version
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_with_outer_variables() {
|
||||
// Test for loops can access both loop vars and outer vars
|
||||
let script = r#"
|
||||
prefix = "Item"
|
||||
|
||||
for num in 1 2 3 {
|
||||
echo $prefix $num
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comprehensive_variable_usage() {
|
||||
// Test a comprehensive script with all variable features
|
||||
let script = r#"
|
||||
user = "Alice"
|
||||
project = "RushShell"
|
||||
|
||||
echo "Welcome" $user "to" $project
|
||||
|
||||
for item in "docs" "tests" "src" {
|
||||
echo $user "is checking" $item
|
||||
}
|
||||
|
||||
parallel {
|
||||
run { echo "Parallel 1:" $user }
|
||||
run { echo "Parallel 2:" $project }
|
||||
}
|
||||
|
||||
workers 2 {
|
||||
for num in 1 2 {
|
||||
echo $project "worker task" $num
|
||||
}
|
||||
}
|
||||
|
||||
echo "Done with" $project
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
42
tests/lexer_tests.rs
Normal file
42
tests/lexer_tests.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use rush::lexer::{Lexer, Token};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tokenization() {
|
||||
let mut lexer = Lexer::new("x = 10");
|
||||
assert_eq!(lexer.next_token(), Token::Identifier("x".to_string()));
|
||||
assert_eq!(lexer.next_token(), Token::Assign);
|
||||
assert_eq!(lexer.next_token(), Token::Number(10));
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identifier_tokens() {
|
||||
let mut lexer = Lexer::new("foo bar");
|
||||
assert_eq!(lexer.next_token(), Token::Identifier("foo".to_string()));
|
||||
assert_eq!(lexer.next_token(), Token::Identifier("bar".to_string()));
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_control_flow_tokens() {
|
||||
let mut lexer = Lexer::new("if else while");
|
||||
assert_eq!(lexer.next_token(), Token::If);
|
||||
assert_eq!(lexer.next_token(), Token::Else);
|
||||
assert_eq!(lexer.next_token(), Token::While);
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_operators() {
|
||||
let mut lexer = Lexer::new("+ - * /");
|
||||
assert_eq!(lexer.next_token(), Token::Plus);
|
||||
assert_eq!(lexer.next_token(), Token::Minus);
|
||||
assert_eq!(lexer.next_token(), Token::Multiply);
|
||||
assert_eq!(lexer.next_token(), Token::Divide);
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
}
|
||||
64
tests/parser_tests.rs
Normal file
64
tests/parser_tests.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use rush::parser::{AstNode, Parser};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_variable_assignment() {
|
||||
// Test parsing a variable assignment
|
||||
let parser = Parser::new();
|
||||
let input = "x = \"10\"";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 1);
|
||||
|
||||
match &program.statements[0] {
|
||||
AstNode::VariableAssignment { name, .. } => {
|
||||
assert_eq!(name, "x");
|
||||
}
|
||||
_ => panic!("Expected variable assignment"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_parsing() {
|
||||
// Test parsing a simple command
|
||||
let parser = Parser::new();
|
||||
let input = "echo \"Hello World\"";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 1);
|
||||
|
||||
match &program.statements[0] {
|
||||
AstNode::Command { name, .. } => {
|
||||
assert_eq!(name, "echo");
|
||||
}
|
||||
_ => panic!("Expected command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_statements() {
|
||||
// Test parsing multiple statements
|
||||
let parser = Parser::new();
|
||||
let input = "name = \"Louis\"\necho \"Hello\" $name";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_comments() {
|
||||
// Test that comments are skipped
|
||||
let parser = Parser::new();
|
||||
let input = "# This is a comment\necho \"test\"";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user