brush-parser-0.4.0/.cargo_vcs_info.json 0000644 00000000152 10461020230 0013472 0 ustar {
"git": {
"sha1": "96a26d0c66cbc018a1517e9562944418fef5b272"
},
"path_in_vcs": "brush-parser"
} brush-parser-0.4.0/Cargo.lock 0000644 00000130453 10461020230 0011455 0 ustar # This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "backtrace"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-link",
]
[[package]]
name = "backtrace-ext"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
dependencies = [
"backtrace",
]
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
dependencies = [
"serde_core",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bon"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe"
dependencies = [
"bon-macros",
"rustversion",
]
[[package]]
name = "bon-macros"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c"
dependencies = [
"darling 0.23.0",
"ident_case",
"prettyplease",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "brush-parser"
version = "0.4.0"
dependencies = [
"anyhow",
"arbitrary",
"bon",
"cached",
"criterion",
"getrandom",
"indenter",
"insta",
"miette",
"peg",
"pretty_assertions",
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"tracing",
"utf8-chars",
"uuid",
"winnow",
]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "cached"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b6f5d101f0f6322c8646a45b7c581a673e476329040d97565815c2461dd0c4"
dependencies = [
"ahash",
"cached_proc_macro",
"cached_proc_macro_types",
"hashbrown 0.16.1",
"once_cell",
"parking_lot",
"thiserror",
"web-time",
]
[[package]]
name = "cached_proc_macro"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ebcf9c75f17a17d55d11afc98e46167d4790a263f428891b8705ab2f793eca3"
dependencies = [
"darling 0.20.11",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cached_proc_macro_types"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "console"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
dependencies = [
"encode_unicode",
"libc",
"windows-sys",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "criterion"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
"serde",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core 0.20.11",
"darling_macro 0.20.11",
]
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core 0.23.0",
"darling_macro 0.23.0",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core 0.20.11",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core 0.23.0",
"quote",
"syn",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasip2",
"wasip3",
"wasm-bindgen",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indenter"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.0",
"serde",
"serde_core",
]
[[package]]
name = "insta"
version = "1.47.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e"
dependencies = [
"console",
"globset",
"once_cell",
"pest",
"pest_derive",
"ron",
"serde",
"similar",
"tempfile",
"walkdir",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "miette"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
dependencies = [
"backtrace",
"backtrace-ext",
"cfg-if",
"miette-derive",
"owo-colors",
"supports-color",
"supports-hyperlinks",
"supports-unicode",
"terminal_size",
"textwrap",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "owo-colors"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[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 = "peg"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477"
dependencies = [
"peg-macros",
"peg-runtime",
]
[[package]]
name = "peg-macros"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71"
dependencies = [
"peg-runtime",
"proc-macro2",
"quote",
]
[[package]]
name = "peg-runtime"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca"
[[package]]
name = "pest"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
dependencies = [
"memchr",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
dependencies = [
"pest",
"sha2",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[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.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "ron"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc"
dependencies = [
"bitflags",
"once_cell",
"serde",
"serde_derive",
"typeid",
"unicode-ident",
]
[[package]]
name = "rustc-demangle"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[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.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "supports-color"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
dependencies = [
"is_ci",
]
[[package]]
name = "supports-hyperlinks"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
[[package]]
name = "supports-unicode"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "terminal_size"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys",
]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"unicode-linebreak",
"unicode-width 0.2.2",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8-chars"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe49e006d6df172d7f14794568a90fe41e05a1fa9e03dc276fa6da4bb747ec3"
dependencies = [
"arrayvec",
]
[[package]]
name = "uuid"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "web-sys"
version = "0.3.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[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.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
brush-parser-0.4.0/Cargo.toml 0000644 00000010715 10461020230 0011476 0 ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
rust-version = "1.88.0"
name = "brush-parser"
version = "0.4.0"
authors = ["reuben olinsky"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "POSIX/bash shell tokenizer and parsers (used by brush-shell)"
readme = "README.md"
keywords = [
"cli",
"shell",
"sh",
"bash",
"script",
]
categories = [
"command-line-utilities",
"development-tools",
]
license = "MIT"
repository = "https://github.com/reubeno/brush"
[features]
arbitrary = ["dep:arbitrary"]
debug-tracing = ["peg/trace"]
diagnostics = ["dep:miette"]
serde = ["dep:serde"]
winnow-parser = ["dep:winnow"]
[lib]
name = "brush_parser"
path = "src/lib.rs"
bench = false
[[example]]
name = "miette"
path = "examples/miette.rs"
required-features = ["diagnostics"]
[[example]]
name = "serde"
path = "examples/serde.rs"
required-features = ["serde"]
[[bench]]
name = "parser"
path = "benches/parser.rs"
harness = false
[dependencies.arbitrary]
version = "1.4.2"
features = ["derive"]
optional = true
[dependencies.bon]
version = "3.9.1"
[dependencies.cached]
version = "0.59.0"
[dependencies.indenter]
version = "0.3.4"
[dependencies.insta]
version = "1.47.1"
features = ["redactions"]
[dependencies.miette]
version = "7.6.0"
features = ["derive"]
optional = true
default-features = false
[dependencies.peg]
version = "0.8.5"
[dependencies.serde]
version = "1.0.228"
features = [
"derive",
"rc",
]
optional = true
[dependencies.thiserror]
version = "2.0.18"
[dependencies.tracing]
version = "0.1.44"
[dependencies.utf8-chars]
version = "3.0.6"
[dependencies.winnow]
version = "1.0.0"
optional = true
[dev-dependencies.anyhow]
version = "1.0.102"
[dev-dependencies.criterion]
version = "0.8.2"
features = ["html_reports"]
[dev-dependencies.insta]
version = "1.47.1"
features = [
"glob",
"ron",
"yaml",
"redactions",
]
[dev-dependencies.miette]
version = "7.6.0"
features = ["fancy"]
[dev-dependencies.pretty_assertions]
version = "1.4.1"
features = ["unstable"]
[dev-dependencies.serde]
version = "1.0.228"
features = [
"derive",
"rc",
]
[dev-dependencies.serde_json]
version = "1.0.149"
[dev-dependencies.serde_yaml]
version = "0.9.34"
[target.wasm32-unknown-unknown.dependencies.getrandom]
version = "0.4.2"
features = ["wasm_js"]
[target.wasm32-unknown-unknown.dependencies.uuid]
version = "1.23.1"
features = ["js"]
[lints.clippy]
bool_to_int_with_if = "allow"
cognitive_complexity = "allow"
collapsible_else_if = "allow"
collapsible_if = "allow"
expect_used = "deny"
format_push_string = "deny"
if_not_else = "allow"
if_same_then_else = "allow"
match_same_arms = "allow"
missing_errors_doc = "allow"
multiple_crate_versions = "allow"
multiple_unsafe_ops_per_block = "deny"
must_use_candidate = "allow"
option_if_let_else = "allow"
panic = "deny"
panic_in_result_fn = "deny"
redundant_closure_for_method_calls = "allow"
redundant_else = "allow"
redundant_pub_crate = "allow"
result_large_err = "allow"
similar_names = "allow"
string_lit_chars_any = "deny"
string_slice = "deny"
struct_excessive_bools = "allow"
tests_outside_test_module = "deny"
todo = "deny"
undocumented_unsafe_blocks = "deny"
unwrap_in_result = "deny"
unwrap_used = "deny"
[lints.clippy.all]
level = "deny"
priority = -1
[lints.clippy.cargo]
level = "deny"
priority = -1
[lints.clippy.nursery]
level = "deny"
priority = -1
[lints.clippy.pedantic]
level = "deny"
priority = -1
[lints.clippy.perf]
level = "deny"
priority = -1
[lints.rust]
unnameable_types = "deny"
unsafe_op_in_unsafe_fn = "deny"
unused_attributes = "deny"
unused_lifetimes = "deny"
unused_macro_rules = "deny"
[lints.rust.future_incompatible]
level = "deny"
priority = 0
[lints.rust.missing_docs]
level = "deny"
priority = 0
[lints.rust.nonstandard_style]
level = "deny"
priority = 0
[lints.rust.rust_2018_idioms]
level = "deny"
priority = -1
[lints.rust.unknown_lints]
level = "allow"
priority = -100
[lints.rust.warnings]
level = "deny"
priority = 0
[lints.rustdoc.all]
level = "deny"
priority = -1
brush-parser-0.4.0/Cargo.toml.orig 0000644 0000000 0000000 00000003353 10461020230 0015135 0 ustar 0000000 0000000 [package]
name = "brush-parser"
description = "POSIX/bash shell tokenizer and parsers (used by brush-shell)"
version = "0.4.0"
authors.workspace = true
categories.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[lib]
bench = false
[features]
arbitrary = ["dep:arbitrary"]
debug-tracing = ["peg/trace"]
diagnostics = ["dep:miette"]
serde = ["dep:serde"]
winnow-parser = ["dep:winnow"]
[dependencies]
arbitrary = { version = "1.4.2", optional = true, features = ["derive"] }
bon = "3.9.1"
cached = "0.59.0"
indenter = "0.3.4"
insta = { version = "1.47.1", features = ["redactions"] }
miette = { version = "7.6.0", optional = true, default-features = false, features = [
"derive",
] }
peg = "0.8.5"
serde = { version = "1.0.228", optional = true, features = ["derive", "rc"] }
thiserror = "2.0.18"
tracing = "0.1.44"
utf8-chars = "3.0.6"
winnow = { version = "1.0.0", optional = true }
[target.wasm32-unknown-unknown.dependencies]
getrandom = { version = "0.4.2", features = ["wasm_js"] }
uuid = { version = "1.23.1", features = ["js"] }
[dev-dependencies]
anyhow = "1.0.102"
criterion = { version = "0.8.2", features = ["html_reports"] }
insta = { version = "1.47.1", features = ["glob", "ron", "yaml", "redactions"] }
miette = { version = "7.6.0", features = ["fancy"] }
pretty_assertions = { version = "1.4.1", features = ["unstable"] }
serde = { version = "1.0.228", features = ["derive", "rc"] }
serde_json = "1.0.149"
serde_yaml = "0.9.34"
[[bench]]
name = "parser"
harness = false
[[example]]
name = "miette"
required-features = ["diagnostics"]
[[example]]
name = "serde"
required-features = ["serde"]
brush-parser-0.4.0/LICENSE 0000644 0000000 0000000 00000002057 10461020230 0013253 0 ustar 0000000 0000000 MIT License
Copyright (c) 2024 reuben olinsky
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
brush-parser-0.4.0/README.md 0000644 0000000 0000000 00000020167 10461020230 0013527 0 ustar 0000000 0000000
`brush` (**B**o(u)rn(e) **RU**sty **SH**ell) is a modern [bash-](https://www.gnu.org/software/bash/) and [POSIX-](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html) compatible shell written in Rust. Run your existing scripts and `.bashrc` unchanged -- with syntax highlighting and auto-suggestions built in.
## At a glance
β
Your existing `.bashrc` just worksβaliases, functions, completions, all of it.
β¨ Syntax highlighting and auto-suggestions built in.
π§ͺ Validated against bash with [~1700 compatibility tests](brush-shell/tests/cases).
π§© Easily embeddable in your Rust apps using `brush_core::Shell`.
> β οΈ **Not everything works yet:** `select` and some edge cases aren't supported. See the [Compatibility Reference](docs/reference/compatibility.md) for details.
### Quick start:
```console
$ cargo binstall brush-shell # using cargo-binstall
$ brew install brush # using Homebrew
$ pacman -S brush # Arch Linux
$ cargo install --locked brush-shell # Build from sources
```
`brush` is ready for use as a daily driver. We test every change against `bash` to keep it that way.
More detailed installation instructions are available below.
## β¨ Features
### π `bash` Compatibility
| | Feature | Description |
|--|---------|-------------|
| β
| **50+ builtins** | `echo`, `declare`, `read`, `complete`, `trap`, `ulimit`, ... |
| β
| **Full expansions** | brace, parameter, arithmetic, command/process substitution, globs, `extglob`, `globstar` |
| β
| **Control flow** | `if`/`for`/`while`/`until`/`case`, `&&`/`\|\|`, subshells, pipelines, etc. |
| β
| **Redirection** | here docs, here strings, fd duplication, process substitution redirects |
| β
| **Arrays & variables** | indexed/associative arrays, dynamic variables, standard well-known variables, etc. |
| β
| **Programmable completion** | Works with [bash-completion](https://github.com/scop/bash-completion) out of the box |
| β
| **Job control** | background jobs, suspend/resume, `fg`/`bg`/`jobs` |
| π· | **Traps & options** | `DEBUG`/`ERR`/`EXIT` traps work; signal traps and options in progress |
### β¨οΈ User Experience
| | Feature | Description |
|--|---------|-------------|
| β
| **Syntax highlighting** | Real-time as you type ([reedline](https://github.com/nushell/reedline)) |
| β
| **Auto-suggestions** | History-based hints as you type ([reedline](https://github.com/nushell/reedline)) |
| β
| **Rich prompts** | `PS1`/`PROMPT_COMMAND`, right prompts, [starship](https://starship.rs) compatible |
| β
| **TOML config** | `~/.config/brush/config.toml` for persistent settings |
| π§ͺ | **Extras** | `fzf`/`atuin` support, zsh-style `precmd`/`preexec` hooks (experimental), VS Code terminal integration |
## Installation
_When you run `brush`, it should look exactly as `bash` does on your system: it processes your `.bashrc` and
other standard configuration. If you'd like to distinguish the look of `brush` from the other shells
on your system, you may author a `~/.brushrc` file._
πΊ Installing using Homebrew (macOS/Linux)
Homebrew users can install using [the `brush` formula](https://formulae.brew.sh/formula/brush):
```bash
brew install brush
```
π§ Installing on Arch Linux
Arch Linux users can install `brush` from the official [extra repository](https://archlinux.org/packages/extra/x86_64/brush/):
```bash
pacman -S brush
```
π Installing prebuilt binaries via `cargo binstall`
You may use [cargo binstall](https://github.com/cargo-bins/cargo-binstall) to install pre-built `brush` binaries. Once you've installed `cargo-binstall` you can run:
```bash
cargo binstall brush-shell
```
π Installing prebuilt binaries from GitHub
We publish prebuilt binaries of `brush` for Linux (x86_64, aarch64) and macOS (aarch64) to GitHub for official [releases](https://github.com/reubeno/brush/releases). You can manually download and extract the `brush` binary from one of the archives published there, or otherwise use the GitHub CLI to download it, e.g.:
```bash
gh release download --repo reubeno/brush --pattern "brush-x86_64-unknown-linux-gnu.*"
```
After downloading the archive for your platform, you may verify its authenticity using the [GitHub CLI](https://cli.github.com/), e.g.:
```bash
gh attestation verify brush-x86_64-unknown-linux-gnu.tar.gz --repo reubeno/brush
```
π§ Installing using Nix
If you are a Nix user, you can use the registered version:
```bash
nix run 'github:NixOS/nixpkgs/nixpkgs-unstable#brush' -- --version
```
π¨ Building from sources
To build from sources, first install a working (and recent) `rust` toolchain; we recommend installing it via [`rustup`](https://rustup.rs/). Then run:
```bash
cargo install --locked brush-shell
```
## Community & Contributing
This project started out of curiosity and a desire to learnβwe're keeping that attitude. If something doesn't work the way you'd expect, [let us know](https://github.com/reubeno/brush/issues)!
* [Discord server](https://discord.gg/kPRgC9j3Tj) β chat with the community
* [Building from source](docs/how-to/build.md) β development workflow
* [Contribution guidelines](CONTRIBUTING.md) β how to submit changes
* [Technical docs](docs/README.md) β architecture and reference
## Related Projects
Other POSIX-ish shells implemented in non-C/C++ languages:
* [`nushell`](https://www.nushell.sh/) β modern Rust shell (provides `reedline`)
* [`fish`](https://fishshell.com) β user-friendly shell ([Rust port in 4.0](https://fishshell.com/blog/rustport/))
* [`Oils`](https://github.com/oils-for-unix/oils) β bash-compatible with new Oil language
* [`mvdan/sh`](https://github.com/mvdan/sh) β Go implementation
* [`rusty_bash`](https://github.com/shellgei/rusty_bash) β another Rust bash-like shell
π Credits
This project relies on many excellent OSS crates:
* [`reedline`](https://github.com/nushell/reedline) β readline-like input and interactive features
* [`clap`](https://github.com/clap-rs/clap) β command-line parsing
* [`fancy-regex`](https://github.com/fancy-regex/fancy-regex) β regex support
* [`tokio`](https://github.com/tokio-rs/tokio) β async runtime
* [`nix`](https://github.com/nix-rust/nix) β Unix/POSIX APIs
* [`criterion.rs`](https://github.com/bheisler/criterion.rs) β benchmarking
* [`bash-completion`](https://github.com/scop/bash-completion) β completion test suite
---
Licensed under the [MIT license](LICENSE).
brush-parser-0.4.0/benches/parser.rs 0000644 0000000 0000000 00000023663 10461020230 0015525 0 ustar 0000000 0000000 //! Benchmarks for the brush-parser crate.
//!
//! Compares parsing approaches:
//! 1. PEG parser (tokenize + peg parse)
//! 2. `Winnow_str` parser (direct string parse) - when winnow-parser feature enabled
#![allow(missing_docs)]
#![allow(clippy::unwrap_used)]
#[cfg(unix)]
mod unix {
use brush_parser::Token;
use criterion::Criterion;
fn uncached_tokenize(content: &str) -> Vec {
brush_parser::uncached_tokenize_str(content, &brush_parser::TokenizerOptions::default())
.unwrap()
}
fn cacheable_tokenize(content: &str) -> Vec {
brush_parser::tokenize_str_with_options(content, &brush_parser::TokenizerOptions::default())
.unwrap()
}
fn parse_peg(tokens: &[Token]) -> brush_parser::ast::Program {
brush_parser::parse_tokens(tokens, &brush_parser::ParserOptions::default()).unwrap()
}
#[cfg(feature = "winnow-parser")]
fn parse_winnow_str(content: &str) -> brush_parser::ast::Program {
use brush_parser::{ParserOptions, SourceInfo, winnow_str};
winnow_str::parse_program(content, &ParserOptions::default(), &SourceInfo::default())
.unwrap()
}
// Combined tokenize + parse functions for full pipeline comparison
fn tokenize_and_parse_peg(content: &str) -> brush_parser::ast::Program {
let tokens = uncached_tokenize(content);
parse_peg(&tokens)
}
const SAMPLE_SCRIPT: &str = r#"
for f in A B C; do
echo "${f@L}" >&2
done
"#;
const SIMPLE_SCRIPT: &str = "echo hello world";
const PIPELINE_SCRIPT: &str = "cat file.txt | grep pattern | wc -l";
const COMPLEX_SCRIPT: &str = r#"
#!/bin/bash
# Complex script with multiple constructs
function process_file() {
local file="$1"
if [[ -f "$file" ]]; then
while read -r line; do
case "$line" in
start*)
echo "Starting: $line"
;;
end*)
echo "Ending: $line"
;;
*)
echo "Processing: $line"
;;
esac
done < "$file"
fi
}
for i in {1..10}; do
if (( i % 2 == 0 )); then
echo "$i is even" | tee -a output.txt
else
echo "$i is odd" >> output.txt
fi
done
process_file "input.txt" && echo "Success" || echo "Failed"
"#;
const NESTED_EXPANSIONS_SCRIPT: &str = r"
# Script with deeply nested expansions (tests balanced delimiter parsing)
result=$(echo $(echo $((1 + (2 * (3 - 4))))))
fallback=${foo:-${bar:-${baz}}}
arithmetic=$((1 + (2 * (3 + (4 - 5)))))
command_subst=$(ls $(pwd))
mixed=$(echo $((1 + 2)) | cat)
backtick=`echo (nested parens)`
";
// Extended test expression benchmarks - various patterns
#[allow(dead_code)]
const EXTENDED_TEST_SIMPLE: &str = "[[ -f file.txt ]]";
#[allow(dead_code)]
const EXTENDED_TEST_BINARY: &str = "[[ $a == $b ]]";
#[allow(dead_code)]
const EXTENDED_TEST_REGEX: &str = "[[ $str =~ ^[0-9]+$ ]]";
#[allow(dead_code)]
const EXTENDED_TEST_COMPLEX_REGEX: &str = "[[ $input =~ ^(foo|bar)[0-9]+(baz|qux)$ ]]";
#[allow(dead_code)]
const EXTENDED_TEST_LOGICAL: &str = "[[ -f file.txt && -r file.txt || -w other.txt ]]";
#[allow(dead_code)]
const EXTENDED_TEST_NESTED: &str = "[[ ( -f $file && -r $file ) || ( -d $dir && -x $dir ) ]]";
#[allow(dead_code)]
const EXTENDED_TEST_COMPLEX: &str =
"[[ ! ( $a -eq 5 && $b -gt 10 ) || ( $c =~ pattern && -f $file ) ]]";
fn benchmark_parsing_script_using_caches(c: &mut Criterion, script_path: &std::path::Path) {
let contents = std::fs::read_to_string(script_path).unwrap();
let filename = script_path.file_name().unwrap().to_string_lossy();
c.bench_function(std::format!("parse_peg_{filename}").as_str(), |b| {
b.iter(|| parse_peg(&cacheable_tokenize(contents.as_str())));
});
}
pub(crate) fn criterion_benchmark(c: &mut Criterion) {
const POSSIBLE_BASH_COMPLETION_SCRIPT_PATH: &str =
"/usr/share/bash-completion/bash_completion";
// Tokenization benchmark (applies to both parsers)
c.bench_function("tokenize_sample_script", |b| {
b.iter(|| uncached_tokenize(SAMPLE_SCRIPT));
});
// Simple script benchmarks
let simple_tokens = uncached_tokenize(SIMPLE_SCRIPT);
c.bench_function("parse_peg_simple", |b| b.iter(|| parse_peg(&simple_tokens)));
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_simple", |b| {
b.iter(|| parse_winnow_str(SIMPLE_SCRIPT));
});
// Pipeline script benchmarks
let pipeline_tokens = uncached_tokenize(PIPELINE_SCRIPT);
c.bench_function("parse_peg_pipeline", |b| {
b.iter(|| parse_peg(&pipeline_tokens));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_pipeline", |b| {
b.iter(|| parse_winnow_str(PIPELINE_SCRIPT));
});
// Sample script (for loop) benchmarks
let sample_tokens = uncached_tokenize(SAMPLE_SCRIPT);
c.bench_function("parse_peg_for_loop", |b| {
b.iter(|| parse_peg(&sample_tokens));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_for_loop", |b| {
b.iter(|| parse_winnow_str(SAMPLE_SCRIPT));
});
// Complex script benchmarks
let complex_tokens = uncached_tokenize(COMPLEX_SCRIPT);
c.bench_function("parse_peg_complex", |b| {
b.iter(|| parse_peg(&complex_tokens));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_complex", |b| {
b.iter(|| parse_winnow_str(COMPLEX_SCRIPT));
});
// Real-world bash completion script (if available)
let well_known_complicated_script =
std::path::PathBuf::from(POSSIBLE_BASH_COMPLETION_SCRIPT_PATH);
if well_known_complicated_script.exists() {
benchmark_parsing_script_using_caches(c, &well_known_complicated_script);
}
// ========================================================================
// FULL PIPELINE BENCHMARKS (tokenize + parse)
// ========================================================================
// These benchmarks measure the complete parsing pipeline from string to AST,
// allowing fair comparison between different approaches:
// - tokenize_and_parse_peg: Legacy tokenizer + PEG parser
// - parse_winnow_str: Direct string parsing (no separate tokenization)
// Simple script full pipeline
c.bench_function("full_peg_simple", |b| {
b.iter(|| tokenize_and_parse_peg(SIMPLE_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_simple", |b| {
b.iter(|| parse_winnow_str(SIMPLE_SCRIPT));
});
// Pipeline script full pipeline
c.bench_function("full_peg_pipeline", |b| {
b.iter(|| tokenize_and_parse_peg(PIPELINE_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_pipeline", |b| {
b.iter(|| parse_winnow_str(PIPELINE_SCRIPT));
});
// For loop full pipeline
c.bench_function("full_peg_for_loop", |b| {
b.iter(|| tokenize_and_parse_peg(SAMPLE_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_for_loop", |b| {
b.iter(|| parse_winnow_str(SAMPLE_SCRIPT));
});
// Complex script full pipeline
c.bench_function("full_peg_complex", |b| {
b.iter(|| tokenize_and_parse_peg(COMPLEX_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_complex", |b| {
b.iter(|| parse_winnow_str(COMPLEX_SCRIPT));
});
// Nested expansions (balanced delimiter parsing stress test)
c.bench_function("full_peg_nested_expansions", |b| {
b.iter(|| tokenize_and_parse_peg(NESTED_EXPANSIONS_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_nested_expansions", |b| {
b.iter(|| parse_winnow_str(NESTED_EXPANSIONS_SCRIPT));
});
// ========================================================================
// EXTENDED TEST EXPRESSION BENCHMARKS
// ========================================================================
// Benchmarks for the refactored extended test ([[ ]]) parser
// Tests various patterns: simple, binary, regex, logical operators, nesting
#[cfg(feature = "winnow-parser")]
{
c.bench_function("extended_test_simple", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_SIMPLE));
});
c.bench_function("extended_test_binary", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_BINARY));
});
c.bench_function("extended_test_regex", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_REGEX));
});
c.bench_function("extended_test_complex_regex", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_COMPLEX_REGEX));
});
c.bench_function("extended_test_logical", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_LOGICAL));
});
c.bench_function("extended_test_nested", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_NESTED));
});
c.bench_function("extended_test_complex", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_COMPLEX));
});
}
}
}
#[cfg(unix)]
criterion::criterion_group! {
name = benches;
config = criterion::Criterion::default();
targets = unix::criterion_benchmark
}
#[cfg(unix)]
criterion::criterion_main!(benches);
#[cfg(not(unix))]
fn main() {}
brush-parser-0.4.0/examples/miette.rs 0000644 0000000 0000000 00000001064 10461020230 0015716 0 ustar 0000000 0000000 //! Simple example of miette usage
use std::io::Cursor;
use brush_parser::Parser;
use miette::{IntoDiagnostic, miette};
fn main() -> miette::Result<()> {
let f = std::env::args()
.nth(1)
.ok_or_else(|| miette!("Please provide a file name"))?;
let source = std::fs::read_to_string(&f).into_diagnostic()?;
let reader = Cursor::new(&source);
let mut parser = Parser::builder().build(reader);
let ast = parser
.parse_program()
.map_err(|e| e.to_pretty_error(&source))?;
println!("{ast:#?}");
Ok(())
}
brush-parser-0.4.0/examples/serde.rs 0000644 0000000 0000000 00000002034 10461020230 0015527 0 ustar 0000000 0000000 //! Example demonstrating AST serialization and deserialization with the `serde` feature.
//!
//! Run with: `cargo run --package brush-parser --example serde --features serde`
use brush_parser::{Parser, ParserOptions};
use std::io::BufReader;
fn main() -> Result<(), Box> {
// Parse a simple shell command
let input = "echo 'Hello, World!' && ls -la";
let reader = BufReader::new(input.as_bytes());
let options = ParserOptions::default();
let mut parser = Parser::new(reader, &options);
let program = parser.parse_program()?;
// Serialize the AST to JSON
let json = serde_json::to_string_pretty(&program)?;
println!("Parsed AST:");
println!("{json}");
// Demonstrate round-trip: deserialize the JSON back to AST
println!("\nRound-trip deserialization:");
let deserialized: brush_parser::ast::Program = serde_json::from_str(&json)?;
println!(
"Successfully deserialized AST with {} command(s)",
deserialized.complete_commands.len()
);
Ok(())
}
brush-parser-0.4.0/src/arithmetic.rs 0000644 0000000 0000000 00000022157 10461020230 0015537 0 ustar 0000000 0000000 //! Parser for shell arithmetic expressions.
use crate::ast;
use crate::error;
/// Parses a shell arithmetic expression.
///
/// # Arguments
///
/// * `input` - The arithmetic expression to parse, in string form.
pub fn parse(input: &str) -> Result {
cacheable_parse(input.to_owned())
}
#[cached::proc_macro::cached(size = 64, result = true)]
fn cacheable_parse(input: String) -> Result {
tracing::debug!(target: "arithmetic", "parsing arithmetic expression: '{input}'");
arithmetic::full_expression(input.as_str())
.map_err(|e| error::WordParseError::ArithmeticExpression(e.into()))
}
peg::parser! {
grammar arithmetic() for str {
pub(crate) rule full_expression() -> ast::ArithmeticExpr =
![_] { ast::ArithmeticExpr::Literal(0) } /
_ e:expression() _ { e }
pub(crate) rule expression() -> ast::ArithmeticExpr = precedence!{
x:(@) _ "," _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Comma, Box::new(x), Box::new(y)) }
--
x:lvalue() _ "*=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Multiply, x, Box::new(y)) }
x:lvalue() _ "/=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Divide, x, Box::new(y)) }
x:lvalue() _ "%=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Modulo, x, Box::new(y)) }
x:lvalue() _ "+=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Add, x, Box::new(y)) }
x:lvalue() _ "-=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Subtract, x, Box::new(y)) }
x:lvalue() _ "<<=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftLeft, x, Box::new(y)) }
x:lvalue() _ ">>=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftRight, x, Box::new(y)) }
x:lvalue() _ "&=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseAnd, x, Box::new(y)) }
x:lvalue() _ "|=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseOr, x, Box::new(y)) }
x:lvalue() _ "^=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseXor, x, Box::new(y)) }
x:lvalue() _ "=" _ y:(@) { ast::ArithmeticExpr::Assignment(x, Box::new(y)) }
--
x:@ _ "?" _ y:expression() _ ":" _ z:(@) { ast::ArithmeticExpr::Conditional(Box::new(x), Box::new(y), Box::new(z)) }
--
x:(@) _ "||" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalOr, Box::new(x), Box::new(y)) }
--
x:(@) _ "&&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalAnd, Box::new(x), Box::new(y)) }
--
x:(@) _ "|" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseOr, Box::new(x), Box::new(y)) }
--
x:(@) _ "^" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseXor, Box::new(x), Box::new(y)) }
--
x:(@) _ "&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseAnd, Box::new(x), Box::new(y)) }
--
x:(@) _ "==" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Equals, Box::new(x), Box::new(y)) }
x:(@) _ "!=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::NotEquals, Box::new(x), Box::new(y)) }
--
x:(@) _ "<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThan, Box::new(x), Box::new(y)) }
x:(@) _ ">" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThan, Box::new(x), Box::new(y)) }
x:(@) _ "<=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThanOrEqualTo, Box::new(x), Box::new(y)) }
x:(@) _ ">=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThanOrEqualTo, Box::new(x), Box::new(y)) }
--
x:(@) _ "<<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftLeft, Box::new(x), Box::new(y)) }
x:(@) _ ">>" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftRight, Box::new(x), Box::new(y)) }
--
x:(@) _ "+" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Add, Box::new(x), Box::new(y)) }
x:(@) _ "-" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Subtract, Box::new(x), Box::new(y)) }
--
x:(@) _ "*" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Multiply, Box::new(x), Box::new(y)) }
x:(@) _ "%" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Modulo, Box::new(x), Box::new(y)) }
x:(@) _ "/" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Divide, Box::new(x), Box::new(y)) }
--
x:@ _ "**" _ y:(@) { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Power, Box::new(x), Box::new(y)) }
--
"!" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::LogicalNot, Box::new(x)) }
"~" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::BitwiseNot, Box::new(x)) }
--
// NOTE: We add negative lookahead to avoid ambiguity with the pre-increment/pre-decrement operators.
"+" !['+'] _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryPlus, Box::new(x)) }
"-" !['-'] _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryMinus, Box::new(x)) }
--
"++" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixIncrement, x) }
"--" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixDecrement, x) }
--
x:lvalue() _ "++" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixIncrement, x) }
x:lvalue() _ "--" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixDecrement, x) }
--
n:literal_number() { ast::ArithmeticExpr::Literal(n) }
l:lvalue() { ast::ArithmeticExpr::Reference(l) }
"(" _ expr:expression() _ ")" { expr }
}
rule lvalue() -> ast::ArithmeticTarget =
name:variable_name() "[" index:expression() "]" {
ast::ArithmeticTarget::ArrayElement(name.to_owned(), Box::new(index))
} /
name:variable_name() {
ast::ArithmeticTarget::Variable(name.to_owned())
}
rule variable_name() -> &'input str =
$(['a'..='z' | 'A'..='Z' | '_'](['a'..='z' | 'A'..='Z' | '_' | '0'..='9']*))
rule _() -> () = quiet!{[' ' | '\t' | '\n' | '\r']*} {}
rule literal_number() -> i64 =
// Literal with explicit radix (format: #)
radix:decimal_literal() "#" s:$(['0'..='9' | 'a'..='z' | 'A'..='Z' | '@' | '_']+) {?
parse_shell_literal_number(s, radix.cast_unsigned())
} /
// Hex literal
"0" ['x' | 'X'] s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*) {?
i64::from_str_radix(s, 16).or(Err("i64"))
} /
// Octal literal
s:$("0" ['0'..='8']*) {?
i64::from_str_radix(s, 8).or(Err("i64"))
} /
// Decimal literal
decimal_literal()
rule decimal_literal() -> i64 =
s:$(['1'..='9'] ['0'..='9']*) {?
// Parse as u64 first, then cast to i64. This handles values like
// 9223372036854775808 (i64::MAX + 1) which is needed for INT64_MIN
// when preceded by unary minus: -(9223372036854775808) wraps to i64::MIN.
s.parse::().map(|v| v.cast_signed()).or(Err("i64"))
}
}
}
fn parse_shell_literal_number(s: &str, radix: u64) -> Result {
if !(2..=64).contains(&radix) {
return Err("invalid base");
}
// For bases <= 36: case-insensitive (a-z and A-Z both map to 10-35)
// For bases > 36 (bash extension):
// 0-9 = 0-9, a-z = 10-35, A-Z = 36-61, @ = 62, _ = 63
let mut result: i64 = 0;
for ch in s.chars() {
let digit_val = if radix <= 36 {
match ch {
'0'..='9' => (ch as u64) - ('0' as u64),
'a'..='z' => (ch as u64) - ('a' as u64) + 10,
'A'..='Z' => (ch as u64) - ('A' as u64) + 10,
_ => return Err("invalid digit"),
}
} else {
match ch {
'0'..='9' => (ch as u64) - ('0' as u64),
'a'..='z' => (ch as u64) - ('a' as u64) + 10,
'A'..='Z' => (ch as u64) - ('A' as u64) + 36,
'@' => 62,
'_' => 63,
_ => return Err("invalid digit"),
}
};
if digit_val >= radix {
return Err("value too great for base");
}
result = result
.wrapping_mul(radix.cast_signed())
.wrapping_add(digit_val.cast_signed());
}
Ok(result)
}
brush-parser-0.4.0/src/ast.rs 0000644 0000000 0000000 00000226041 10461020230 0014173 0 ustar 0000000 0000000 //! Defines the Abstract Syntax Tree (ast) for shell programs. Includes types and utilities
//! for manipulating the AST.
use std::fmt::{Display, Write};
use crate::{SourceSpan, tokenizer};
const DISPLAY_INDENT: &str = " ";
/// Trait implemented by all AST nodes. Used to aggregate traits expected
/// to be implemented.
pub trait Node: Display + SourceLocation {}
/// Provides the source location for the syntax item
pub trait SourceLocation {
/// The location of the syntax item, when known
fn location(&self) -> Option;
}
pub(crate) fn maybe_location(
start: Option<&SourceSpan>,
end: Option<&SourceSpan>,
) -> Option {
if let (Some(s), Some(e)) = (start, end) {
Some(SourceSpan::within(s, e))
} else {
None
}
}
/// Represents a complete shell program.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct Program {
/// A sequence of complete shell commands.
pub complete_commands: Vec,
}
impl Node for Program {}
impl SourceLocation for Program {
fn location(&self) -> Option {
let start = self
.complete_commands
.first()
.and_then(SourceLocation::location);
let end = self
.complete_commands
.last()
.and_then(SourceLocation::location);
maybe_location(start.as_ref(), end.as_ref())
}
}
impl Display for Program {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for complete_command in &self.complete_commands {
write!(f, "{complete_command}")?;
}
Ok(())
}
}
/// Represents a complete shell command.
pub type CompleteCommand = CompoundList;
/// Represents a complete shell command item.
pub type CompleteCommandItem = CompoundListItem;
// TODO(tracing): decide if we want to trace this location or consider it a whitespace separator
/// Indicates whether the preceding command is executed synchronously or asynchronously.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum SeparatorOperator {
/// The preceding command is executed asynchronously.
Async,
/// The preceding command is executed synchronously.
Sequence,
}
impl Display for SeparatorOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Async => write!(f, "&"),
Self::Sequence => write!(f, ";"),
}
}
}
/// Represents a sequence of command pipelines connected by boolean operators.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct AndOrList {
/// The first command pipeline.
pub first: Pipeline,
/// Any additional command pipelines, in sequence order.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub additional: Vec,
}
impl Node for AndOrList {}
impl SourceLocation for AndOrList {
fn location(&self) -> Option {
let start = self.first.location();
let last = self.additional.last();
let end = last.and_then(SourceLocation::location);
match (start, end) {
(Some(s), Some(e)) => Some(SourceSpan::within(&s, &e)),
(start, _) => start,
}
}
}
impl Display for AndOrList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.first)?;
for item in &self.additional {
write!(f, "{item}")?;
}
Ok(())
}
}
/// Represents a boolean operator used to connect command pipelines in an [`AndOrList`]
#[derive(PartialEq, Eq)]
pub enum PipelineOperator {
/// The command pipelines are connected by a boolean AND operator.
And,
/// The command pipelines are connected by a boolean OR operator.
Or,
}
impl PartialEq for PipelineOperator {
fn eq(&self, other: &AndOr) -> bool {
matches!(
(self, other),
(Self::And, AndOr::And(_)) | (Self::Or, AndOr::Or(_))
)
}
}
// We cannot losslessly convert into `AndOr`, hence we can only do `Into`.
#[expect(clippy::from_over_into)]
impl Into for AndOr {
fn into(self) -> PipelineOperator {
match self {
Self::And(_) => PipelineOperator::And,
Self::Or(_) => PipelineOperator::Or,
}
}
}
/// An iterator over the pipelines in an [`AndOrList`].
pub struct AndOrListIter<'a> {
first: Option<&'a Pipeline>,
additional_iter: std::slice::Iter<'a, AndOr>,
}
impl<'a> Iterator for AndOrListIter<'a> {
type Item = (PipelineOperator, &'a Pipeline);
fn next(&mut self) -> Option {
if let Some(first) = self.first.take() {
Some((PipelineOperator::And, first))
} else {
self.additional_iter.next().map(|and_or| match and_or {
AndOr::And(pipeline) => (PipelineOperator::And, pipeline),
AndOr::Or(pipeline) => (PipelineOperator::Or, pipeline),
})
}
}
}
impl<'a> IntoIterator for &'a AndOrList {
type Item = (PipelineOperator, &'a Pipeline);
type IntoIter = AndOrListIter<'a>;
fn into_iter(self) -> Self::IntoIter {
AndOrListIter {
first: Some(&self.first),
additional_iter: self.additional.iter(),
}
}
}
impl<'a> From<(PipelineOperator, &'a Pipeline)> for AndOr {
fn from(value: (PipelineOperator, &'a Pipeline)) -> Self {
match value.0 {
PipelineOperator::Or => Self::Or(value.1.to_owned()),
PipelineOperator::And => Self::And(value.1.to_owned()),
}
}
}
impl AndOrList {
/// Returns an iterator over the pipelines in this `AndOrList`.
pub fn iter(&self) -> AndOrListIter<'_> {
self.into_iter()
}
}
/// Represents a boolean operator used to connect command pipelines, along with the
/// succeeding pipeline.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum AndOr {
/// Boolean AND operator; the embedded pipeline is only to be executed if the
/// preceding command has succeeded.
And(Pipeline),
/// Boolean OR operator; the embedded pipeline is only to be executed if the
/// preceding command has not succeeded.
Or(Pipeline),
}
impl Node for AndOr {}
// TODO(source-location): add a loc to account for the operator
impl SourceLocation for AndOr {
fn location(&self) -> Option {
match self {
Self::And(p) => p.location(),
Self::Or(p) => p.location(),
}
}
}
impl Display for AndOr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::And(pipeline) => write!(f, " && {pipeline}"),
Self::Or(pipeline) => write!(f, " || {pipeline}"),
}
}
}
/// The type of timing requested for a pipeline.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum PipelineTimed {
/// The pipeline should be timed with bash-like output.
Timed(SourceSpan),
/// The pipeline should be timed with POSIX-like output.
TimedWithPosixOutput(SourceSpan),
}
impl Node for PipelineTimed {}
impl SourceLocation for PipelineTimed {
fn location(&self) -> Option {
match self {
Self::Timed(t) => Some(t.to_owned()),
Self::TimedWithPosixOutput(t) => Some(t.to_owned()),
}
}
}
impl Display for PipelineTimed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Timed(_) => write!(f, "time"),
Self::TimedWithPosixOutput(_) => write!(f, "time -p"),
}
}
}
impl PipelineTimed {
/// Returns true if the pipeline should be timed with POSIX-like output.
pub const fn is_posix_output(&self) -> bool {
matches!(self, Self::TimedWithPosixOutput(_))
}
}
/// A pipeline of commands, where each command's output is passed as standard input
/// to the command that follows it.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct Pipeline {
/// Indicates whether the pipeline's execution should be timed with reported
/// timings in output.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Option::is_none", default)
)]
pub timed: Option,
/// Indicates whether the result of the overall pipeline should be the logical
/// negation of the result of the pipeline.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
)]
pub bang: bool,
/// The sequence of commands in the pipeline.
pub seq: Vec,
}
impl Node for Pipeline {}
// TODO(source-location): Handle the case where `self.timed` is `None` but there is a bang.
impl SourceLocation for Pipeline {
fn location(&self) -> Option {
let start = self
.timed
.as_ref()
.and_then(SourceLocation::location)
.or_else(|| self.seq.first().and_then(SourceLocation::location));
let end = self.seq.last().and_then(SourceLocation::location);
maybe_location(start.as_ref(), end.as_ref())
}
}
impl Display for Pipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(timed) = &self.timed {
write!(f, "{timed} ")?;
}
if self.bang {
write!(f, "! ")?;
}
for (i, command) in self.seq.iter().enumerate() {
if i > 0 {
write!(f, " |")?;
}
write!(f, "{command}")?;
}
Ok(())
}
}
/// Represents a shell command.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum Command {
/// A simple command, directly invoking an external command, a built-in command,
/// a shell function, or similar.
Simple(SimpleCommand),
/// A compound command, composed of multiple commands.
Compound(CompoundCommand, Option),
/// A command whose side effect is to define a shell function.
Function(FunctionDefinition),
/// A command that evaluates an extended test expression.
ExtendedTest(ExtendedTestExprCommand, Option),
}
impl Node for Command {}
impl SourceLocation for Command {
fn location(&self) -> Option {
match self {
Self::Simple(s) => s.location(),
Self::Compound(c, r) => {
match (c.location(), r.as_ref().and_then(SourceLocation::location)) {
(Some(s), Some(e)) => Some(SourceSpan::within(&s, &e)),
(s, _) => s,
}
}
Self::Function(f) => f.location(),
Self::ExtendedTest(e, _) => e.location(),
}
}
}
impl Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Simple(simple_command) => write!(f, "{simple_command}"),
Self::Compound(compound_command, redirect_list) => {
write!(f, "{compound_command}")?;
if let Some(redirect_list) = redirect_list {
write!(f, "{redirect_list}")?;
}
Ok(())
}
Self::Function(function_definition) => write!(f, "{function_definition}"),
Self::ExtendedTest(extended_test_expr, redirect_list) => {
write!(f, "[[ {extended_test_expr} ]]")?;
if let Some(redirect_list) = redirect_list {
write!(f, "{redirect_list}")?;
}
Ok(())
}
}
}
}
/// Represents a compound command, potentially made up of multiple nested commands.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum CompoundCommand {
/// An arithmetic command, evaluating an arithmetic expression.
Arithmetic(ArithmeticCommand),
/// An arithmetic for clause, which loops until an arithmetic condition is reached.
ArithmeticForClause(ArithmeticForClauseCommand),
/// A brace group, which groups commands together.
BraceGroup(BraceGroupCommand),
/// A subshell, which executes commands in a subshell.
Subshell(SubshellCommand),
/// A for clause, which loops over a set of values.
ForClause(ForClauseCommand),
/// A case clause, which selects a command based on a value and a set of
/// pattern-based filters.
CaseClause(CaseClauseCommand),
/// An if clause, which conditionally executes a command.
IfClause(IfClauseCommand),
/// A while clause, which loops while a condition is met.
WhileClause(WhileOrUntilClauseCommand),
/// An until clause, which loops until a condition is met.
UntilClause(WhileOrUntilClauseCommand),
/// A coprocess, which runs a command asynchronously in a subshell.
Coprocess(CoprocessCommand),
}
impl Node for CompoundCommand {}
impl SourceLocation for CompoundCommand {
fn location(&self) -> Option {
match self {
Self::Arithmetic(a) => a.location(),
Self::ArithmeticForClause(a) => a.location(),
Self::BraceGroup(b) => b.location(),
Self::Subshell(s) => s.location(),
Self::ForClause(f) => f.location(),
Self::CaseClause(c) => c.location(),
Self::IfClause(i) => i.location(),
Self::WhileClause(w) => w.location(),
Self::UntilClause(u) => u.location(),
Self::Coprocess(c) => c.location(),
}
}
}
impl Display for CompoundCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Arithmetic(arithmetic_command) => write!(f, "{arithmetic_command}"),
Self::ArithmeticForClause(arithmetic_for_clause_command) => {
write!(f, "{arithmetic_for_clause_command}")
}
Self::BraceGroup(brace_group_command) => {
write!(f, "{brace_group_command}")
}
Self::Subshell(subshell_command) => write!(f, "{subshell_command}"),
Self::ForClause(for_clause_command) => write!(f, "{for_clause_command}"),
Self::CaseClause(case_clause_command) => {
write!(f, "{case_clause_command}")
}
Self::IfClause(if_clause_command) => write!(f, "{if_clause_command}"),
Self::WhileClause(while_or_until_clause_command) => {
write!(f, "while {while_or_until_clause_command}")
}
Self::UntilClause(while_or_until_clause_command) => {
write!(f, "until {while_or_until_clause_command}")
}
Self::Coprocess(coproc_clause_command) => {
write!(f, "{coproc_clause_command}")
}
}
}
}
/// An arithmetic command, evaluating an arithmetic expression.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct ArithmeticCommand {
/// The raw, unparsed and unexpanded arithmetic expression.
pub expr: UnexpandedArithmeticExpr,
/// Location of the command
pub loc: SourceSpan,
}
impl Node for ArithmeticCommand {}
impl SourceLocation for ArithmeticCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for ArithmeticCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(({}))", self.expr)
}
}
/// A subshell, which executes commands in a subshell.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct SubshellCommand {
/// Command list in the subshell
pub list: CompoundList,
/// Location of the subshell
pub loc: SourceSpan,
}
impl Node for SubshellCommand {}
impl SourceLocation for SubshellCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for SubshellCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "( ")?;
write!(f, "{}", self.list)?;
write!(f, " )")
}
}
/// A for clause, which loops over a set of values.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct ForClauseCommand {
/// The name of the iterator variable.
pub variable_name: String,
/// The values being iterated over.
pub values: Option>,
/// The command to run for each iteration of the loop.
pub body: DoGroupCommand,
/// Location of the for command.
pub loc: SourceSpan,
}
impl Node for ForClauseCommand {}
impl SourceLocation for ForClauseCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for ForClauseCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "for {} in ", self.variable_name)?;
if let Some(values) = &self.values {
for (i, value) in values.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{value}")?;
}
}
writeln!(f, ";")?;
write!(f, "{}", self.body)
}
}
/// An arithmetic for clause, which loops until an arithmetic condition is reached.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct ArithmeticForClauseCommand {
/// Optionally, the initializer expression evaluated before the first iteration of the loop.
pub initializer: Option,
/// Optionally, the expression evaluated as the exit condition of the loop.
pub condition: Option,
/// Optionally, the expression evaluated after each iteration of the loop.
pub updater: Option,
/// The command to run for each iteration of the loop.
pub body: DoGroupCommand,
/// Location of the clause
pub loc: SourceSpan,
}
impl Node for ArithmeticForClauseCommand {}
impl SourceLocation for ArithmeticForClauseCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for ArithmeticForClauseCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "for ((")?;
if let Some(initializer) = &self.initializer {
write!(f, "{initializer}")?;
}
write!(f, "; ")?;
if let Some(condition) = &self.condition {
write!(f, "{condition}")?;
}
write!(f, "; ")?;
if let Some(updater) = &self.updater {
write!(f, "{updater}")?;
}
writeln!(f, "))")?;
write!(f, "{}", self.body)
}
}
/// A case clause, which selects a command based on a value and a set of
/// pattern-based filters.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct CaseClauseCommand {
/// The value being matched on.
pub value: Word,
/// The individual case branches.
pub cases: Vec,
/// Location of the case command.
pub loc: SourceSpan,
}
impl Node for CaseClauseCommand {}
impl SourceLocation for CaseClauseCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for CaseClauseCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "case {} in", self.value)?;
for case in &self.cases {
write!(indenter::indented(f).with_str(DISPLAY_INDENT), "{case}")?;
}
writeln!(f)?;
write!(f, "esac")
}
}
/// A sequence of commands.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct CompoundList(pub Vec);
impl Node for CompoundList {}
// TODO(source-location): Handle the optional trailing separator.
impl SourceLocation for CompoundList {
fn location(&self) -> Option {
let start = self.0.first().and_then(SourceLocation::location);
let end = self.0.last().and_then(SourceLocation::location);
if let (Some(s), Some(e)) = (start, end) {
Some(SourceSpan::within(&s, &e))
} else {
None
}
}
}
impl Display for CompoundList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, item) in self.0.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
// Write the and-or list.
write!(f, "{}", item.0)?;
// Write the separator... unless we're on the list item and it's a ';'.
if i == self.0.len() - 1 && matches!(item.1, SeparatorOperator::Sequence) {
// Skip
} else {
write!(f, "{}", item.1)?;
}
}
Ok(())
}
}
/// An element of a compound command list.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct CompoundListItem(pub AndOrList, pub SeparatorOperator);
impl Node for CompoundListItem {}
// TODO(source-location): Account for the location of the separator operator.
impl SourceLocation for CompoundListItem {
fn location(&self) -> Option {
self.0.location()
}
}
impl Display for CompoundListItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)?;
write!(f, "{}", self.1)?;
Ok(())
}
}
/// An if clause, which conditionally executes a command.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct IfClauseCommand {
/// The command whose execution result is inspected.
pub condition: CompoundList,
/// The command to execute if the condition is true.
pub then: CompoundList,
/// Optionally, `else` clauses that will be evaluated if the condition is false.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Option::is_none", default)
)]
pub elses: Option>,
/// Location of the if clause
pub loc: SourceSpan,
}
impl Node for IfClauseCommand {}
impl SourceLocation for IfClauseCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for IfClauseCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "if {}; then", self.condition)?;
write!(
indenter::indented(f).with_str(DISPLAY_INDENT),
"{}",
self.then
)?;
if let Some(elses) = &self.elses {
for else_clause in elses {
write!(f, "{else_clause}")?;
}
}
writeln!(f)?;
write!(f, "fi")?;
Ok(())
}
}
/// Represents the `else` clause of a conditional command.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct ElseClause {
/// If present, the condition that must be met for this `else` clause to be executed.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Option::is_none", default)
)]
pub condition: Option,
/// The commands to execute if this `else` clause is selected.
pub body: CompoundList,
}
impl Display for ElseClause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f)?;
if let Some(condition) = &self.condition {
writeln!(f, "elif {condition}; then")?;
} else {
writeln!(f, "else")?;
}
write!(
indenter::indented(f).with_str(DISPLAY_INDENT),
"{}",
self.body
)
}
}
/// A coprocess command, which runs a command asynchronously in a subshell.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct CoprocessCommand {
/// The optional name for the coprocess.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Option::is_none", default)
)]
pub name: Option,
/// The command to run as a coprocess (can be simple or compound).
pub body: Box,
/// The location of this command in the source.
#[cfg_attr(any(test, feature = "serde"), serde(skip_serializing, default))]
pub loc: SourceSpan,
}
impl Node for CoprocessCommand {}
impl SourceLocation for CoprocessCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for CoprocessCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "coproc")?;
if let Some(name) = &self.name {
write!(f, " {name}")?;
}
write!(f, " {}", self.body)?;
Ok(())
}
}
/// An individual matching case item in a case clause.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct CaseItem {
/// The patterns that select this case branch.
pub patterns: Vec,
/// The commands to execute if this case branch is selected.
pub cmd: Option,
/// When the case branch is selected, the action to take after the command is executed.
pub post_action: CaseItemPostAction,
/// Location of the item
pub loc: Option,
}
impl Node for CaseItem {}
impl SourceLocation for CaseItem {
fn location(&self) -> Option {
self.loc.clone()
}
}
impl Display for CaseItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f)?;
for (i, pattern) in self.patterns.iter().enumerate() {
if i > 0 {
write!(f, "|")?;
}
write!(f, "{pattern}")?;
}
writeln!(f, ")")?;
if let Some(cmd) = &self.cmd {
write!(indenter::indented(f).with_str(DISPLAY_INDENT), "{cmd}")?;
}
writeln!(f)?;
write!(f, "{}", self.post_action)
}
}
/// Describes the action to take after executing the body command of a case clause.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum CaseItemPostAction {
/// The containing case should be exited.
ExitCase,
/// If one is present, the command body of the succeeding case item should be
/// executed (without evaluating its pattern).
UnconditionallyExecuteNextCaseItem,
/// The case should continue evaluating the remaining case items, as if this
/// item had not been executed.
ContinueEvaluatingCases,
}
impl Display for CaseItemPostAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExitCase => write!(f, ";;"),
Self::UnconditionallyExecuteNextCaseItem => write!(f, ";&"),
Self::ContinueEvaluatingCases => write!(f, ";;&"),
}
}
}
/// A while or until clause, whose looping is controlled by a condition.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct WhileOrUntilClauseCommand(pub CompoundList, pub DoGroupCommand, pub SourceSpan);
impl Node for WhileOrUntilClauseCommand {}
impl SourceLocation for WhileOrUntilClauseCommand {
fn location(&self) -> Option {
Some(self.2.clone())
}
}
impl Display for WhileOrUntilClauseCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}; {}", self.0, self.1)
}
}
/// Encapsulates the definition of a shell function.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct FunctionDefinition {
/// The name of the function.
pub fname: Word,
/// The body of the function.
pub body: FunctionBody,
}
impl Node for FunctionDefinition {}
// TODO(source-location): Account for the optional 'function' keyword that may
// precede the function name.
impl SourceLocation for FunctionDefinition {
fn location(&self) -> Option {
let start = self.fname.location();
let end = self.body.location();
if let (Some(s), Some(e)) = (start, end) {
Some(SourceSpan::within(&s, &e))
} else {
None
}
}
}
impl Display for FunctionDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{} () ", self.fname.value)?;
write!(f, "{}", self.body)?;
Ok(())
}
}
/// Encapsulates the body of a function definition.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct FunctionBody(pub CompoundCommand, pub Option);
impl Node for FunctionBody {}
impl SourceLocation for FunctionBody {
fn location(&self) -> Option {
let cmd_span = self.0.location();
let redirect_span = self.1.as_ref().and_then(SourceLocation::location);
match (cmd_span, redirect_span) {
// If there's a redirect, include it in the span.
(Some(cmd_span), Some(redirect_span)) => {
Some(SourceSpan::within(&cmd_span, &redirect_span))
}
// Otherwise, just return the command span.
(Some(cmd_span), None) => Some(cmd_span),
_ => None,
}
}
}
impl Display for FunctionBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)?;
if let Some(redirect_list) = &self.1 {
write!(f, "{redirect_list}")?;
}
Ok(())
}
}
/// A brace group, which groups commands together.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct BraceGroupCommand {
/// List of commands
pub list: CompoundList,
/// Location of the group
pub loc: SourceSpan,
}
impl Node for BraceGroupCommand {}
impl SourceLocation for BraceGroupCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for BraceGroupCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{{ ")?;
write!(
indenter::indented(f).with_str(DISPLAY_INDENT),
"{}",
self.list
)?;
writeln!(f)?;
write!(f, "}}")?;
Ok(())
}
}
/// A do group, which groups commands together.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct DoGroupCommand {
/// List of commands
pub list: CompoundList,
/// Location of the group
pub loc: SourceSpan,
}
impl Display for DoGroupCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "do")?;
write!(
indenter::indented(f).with_str(DISPLAY_INDENT),
"{}",
self.list
)?;
writeln!(f)?;
write!(f, "done")
}
}
/// Represents the invocation of a simple command.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct SimpleCommand {
/// Optionally, a prefix to the command.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Option::is_none", default)
)]
pub prefix: Option,
/// The name of the command to execute.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Option::is_none", default)
)]
pub word_or_name: Option,
/// Optionally, a suffix to the command.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "Option::is_none", default)
)]
pub suffix: Option,
}
impl Node for SimpleCommand {}
impl SourceLocation for SimpleCommand {
fn location(&self) -> Option {
let mid = &self
.word_or_name
.as_ref()
.and_then(SourceLocation::location);
let start = self.prefix.as_ref().and_then(SourceLocation::location);
let end = self.suffix.as_ref().and_then(SourceLocation::location);
match (start, mid, end) {
(Some(start), _, Some(end)) => Some(SourceSpan::within(&start, &end)),
(Some(start), Some(mid), None) => Some(SourceSpan::within(&start, mid)),
(Some(start), None, None) => Some(start),
(None, Some(mid), Some(end)) => Some(SourceSpan::within(mid, &end)),
(None, Some(mid), None) => Some(mid.clone()),
_ => None,
}
}
}
impl Display for SimpleCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut wrote_something = false;
if let Some(prefix) = &self.prefix {
if wrote_something {
write!(f, " ")?;
}
write!(f, "{prefix}")?;
wrote_something = true;
}
if let Some(word_or_name) = &self.word_or_name {
if wrote_something {
write!(f, " ")?;
}
write!(f, "{word_or_name}")?;
wrote_something = true;
}
if let Some(suffix) = &self.suffix {
if wrote_something {
write!(f, " ")?;
}
write!(f, "{suffix}")?;
}
Ok(())
}
}
/// Represents a prefix to a simple command.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct CommandPrefix(pub Vec);
impl Node for CommandPrefix {}
impl SourceLocation for CommandPrefix {
fn location(&self) -> Option {
let start = self.0.first().and_then(SourceLocation::location);
let end = self.0.last().and_then(SourceLocation::location);
maybe_location(start.as_ref(), end.as_ref())
}
}
impl Display for CommandPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, item) in self.0.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{item}")?;
}
Ok(())
}
}
/// Represents a suffix to a simple command; a word argument, declaration, or I/O redirection.
#[derive(Clone, Default, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct CommandSuffix(pub Vec);
impl Node for CommandSuffix {}
impl SourceLocation for CommandSuffix {
fn location(&self) -> Option {
let start = self.0.first().and_then(SourceLocation::location);
let end = self.0.last().and_then(SourceLocation::location);
maybe_location(start.as_ref(), end.as_ref())
}
}
impl Display for CommandSuffix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, item) in self.0.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{item}")?;
}
Ok(())
}
}
/// Represents the I/O direction of a process substitution.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum ProcessSubstitutionKind {
/// The process is read from.
Read,
/// The process is written to.
Write,
}
impl Display for ProcessSubstitutionKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Read => write!(f, "<"),
Self::Write => write!(f, ">"),
}
}
}
/// A prefix or suffix for a simple command.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum CommandPrefixOrSuffixItem {
/// An I/O redirection.
IoRedirect(IoRedirect),
/// A word.
Word(Word),
/// An assignment/declaration word.
AssignmentWord(Assignment, Word),
/// A process substitution.
ProcessSubstitution(ProcessSubstitutionKind, SubshellCommand),
}
impl Node for CommandPrefixOrSuffixItem {}
impl SourceLocation for CommandPrefixOrSuffixItem {
fn location(&self) -> Option {
match self {
Self::Word(w) => w.location(),
Self::IoRedirect(io_redirect) => io_redirect.location(),
Self::AssignmentWord(assignment, _word) => assignment.location(),
// TODO(source-location): account for the kind token
Self::ProcessSubstitution(_kind, cmd) => cmd.location(),
}
}
}
impl Display for CommandPrefixOrSuffixItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IoRedirect(io_redirect) => write!(f, "{io_redirect}"),
Self::Word(word) => write!(f, "{word}"),
Self::AssignmentWord(_assignment, word) => write!(f, "{word}"),
Self::ProcessSubstitution(kind, subshell_command) => {
write!(f, "{kind}({subshell_command})")
}
}
}
}
/// Encapsulates an assignment declaration.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct Assignment {
/// Name being assigned to.
pub name: AssignmentName,
/// Value being assigned.
pub value: AssignmentValue,
/// Whether or not to append to the preexisting value associated with the named variable.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
)]
pub append: bool,
/// Location of the assignment
pub loc: SourceSpan,
}
impl Node for Assignment {}
impl SourceLocation for Assignment {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for Assignment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)?;
if self.append {
write!(f, "+")?;
}
write!(f, "={}", self.value)
}
}
/// The target of an assignment.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum AssignmentName {
/// A named variable.
VariableName(String),
/// An element in a named array.
ArrayElementName(String, String),
}
impl Display for AssignmentName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::VariableName(name) => write!(f, "{name}"),
Self::ArrayElementName(name, index) => {
write!(f, "{name}[{index}]")
}
}
}
}
/// A value being assigned to a variable.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum AssignmentValue {
/// A scalar (word) value.
Scalar(Word),
/// An array of elements.
Array(Vec<(Option, Word)>),
}
impl Node for AssignmentValue {}
impl SourceLocation for AssignmentValue {
fn location(&self) -> Option {
match self {
Self::Scalar(word) => word.location(),
Self::Array(words) => {
// TODO(source-location): account for the surrounding parentheses
let first = words.first().and_then(|(_key, value)| value.location());
let last = words.last().and_then(|(_key, value)| value.location());
maybe_location(first.as_ref(), last.as_ref())
}
}
}
}
impl Display for AssignmentValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Scalar(word) => write!(f, "{word}"),
Self::Array(words) => {
write!(f, "(")?;
for (i, value) in words.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
match value {
(Some(key), value) => write!(f, "[{key}]={value}")?,
(None, value) => write!(f, "{value}")?,
}
}
write!(f, ")")
}
}
}
}
/// A list of I/O redirections to be applied to a command.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct RedirectList(pub Vec);
impl Node for RedirectList {}
impl SourceLocation for RedirectList {
fn location(&self) -> Option {
let first = self.0.first().and_then(SourceLocation::location);
let last = self.0.last().and_then(SourceLocation::location);
maybe_location(first.as_ref(), last.as_ref())
}
}
impl Display for RedirectList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for item in &self.0 {
write!(f, "{item}")?;
}
Ok(())
}
}
/// A file descriptor number.
pub type IoFd = i32;
/// An I/O redirection.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum IoRedirect {
/// Redirection to a file.
File(Option, IoFileRedirectKind, IoFileRedirectTarget),
/// Redirection from a here-document.
HereDocument(Option, IoHereDocument),
/// Redirection from a here-string.
HereString(Option, Word),
/// Redirection of both standard output and standard error (with optional append).
OutputAndError(Word, bool),
}
impl Node for IoRedirect {}
impl SourceLocation for IoRedirect {
fn location(&self) -> Option {
// TODO(source-location): complete
None
}
}
impl Display for IoRedirect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::File(fd_num, kind, target) => {
if let Some(fd_num) = fd_num {
write!(f, "{fd_num}")?;
}
write!(f, "{kind} {target}")?;
}
Self::OutputAndError(target, append) => {
write!(f, "&>")?;
if *append {
write!(f, ">")?;
}
write!(f, " {target}")?;
}
Self::HereDocument(fd_num, here_doc) => {
if let Some(fd_num) = fd_num {
write!(f, "{fd_num}")?;
}
write!(f, "<<{here_doc}")?;
}
Self::HereString(fd_num, s) => {
if let Some(fd_num) = fd_num {
write!(f, "{fd_num}")?;
}
write!(f, "<<< {s}")?;
}
}
Ok(())
}
}
/// Kind of file I/O redirection.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum IoFileRedirectKind {
/// Read (`<`).
Read,
/// Write (`>`).
Write,
/// Append (`>>`).
Append,
/// Read and write (`<>`).
ReadAndWrite,
/// Clobber (`>|`).
Clobber,
/// Duplicate input (`<&`).
DuplicateInput,
/// Duplicate output (`>&`).
DuplicateOutput,
}
impl Display for IoFileRedirectKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Read => write!(f, "<"),
Self::Write => write!(f, ">"),
Self::Append => write!(f, ">>"),
Self::ReadAndWrite => write!(f, "<>"),
Self::Clobber => write!(f, ">|"),
Self::DuplicateInput => write!(f, "<&"),
Self::DuplicateOutput => write!(f, ">&"),
}
}
}
/// Target for an I/O file redirection.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum IoFileRedirectTarget {
/// Path to a file.
Filename(Word),
/// File descriptor number.
Fd(IoFd),
/// Process substitution: substitution with the results of executing the given
/// command in a subshell.
ProcessSubstitution(ProcessSubstitutionKind, SubshellCommand),
/// Item to duplicate in a word redirection. After expansion, this could be a
/// filename, a file descriptor, or a file descriptor and a "-" to indicate
/// requested closure.
Duplicate(Word),
}
impl Display for IoFileRedirectTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Filename(word) => write!(f, "{word}"),
Self::Fd(fd) => write!(f, "{fd}"),
Self::ProcessSubstitution(kind, subshell_command) => {
write!(f, "{kind}{subshell_command}")
}
Self::Duplicate(word) => write!(f, "{word}"),
}
}
}
/// Represents an I/O here document.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct IoHereDocument {
/// Whether to remove leading tabs from the here document.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
)]
pub remove_tabs: bool,
/// Whether to basic-expand the contents of the here document.
#[cfg_attr(
any(test, feature = "serde"),
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
)]
pub requires_expansion: bool,
/// The delimiter marking the end of the here document.
pub here_end: Word,
/// The contents of the here document.
pub doc: Word,
}
impl Node for IoHereDocument {}
impl SourceLocation for IoHereDocument {
fn location(&self) -> Option {
// TODO(source-location): complete
None
}
}
impl Display for IoHereDocument {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.remove_tabs {
write!(f, "-")?;
}
writeln!(f, "{}", self.here_end)?;
write!(f, "{}", self.doc)?;
writeln!(f, "{}", self.here_end)?;
Ok(())
}
}
/// A (non-extended) test expression.
#[derive(Clone, Debug)]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum TestExpr {
/// Always evaluates to false.
False,
/// A literal string.
Literal(String),
/// Logical AND operation on two nested expressions.
And(Box, Box),
/// Logical OR operation on two nested expressions.
Or(Box, Box),
/// Logical NOT operation on a nested expression.
Not(Box),
/// A parenthesized expression.
Parenthesized(Box),
/// A unary test operation.
UnaryTest(UnaryPredicate, String),
/// A binary test operation.
BinaryTest(BinaryPredicate, String, String),
}
impl Node for TestExpr {}
impl SourceLocation for TestExpr {
fn location(&self) -> Option {
// TODO(source-location): complete
None
}
}
impl Display for TestExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::False => Ok(()),
Self::Literal(s) => write!(f, "{s}"),
Self::And(left, right) => write!(f, "{left} -a {right}"),
Self::Or(left, right) => write!(f, "{left} -o {right}"),
Self::Not(expr) => write!(f, "! {expr}"),
Self::Parenthesized(expr) => write!(f, "( {expr} )"),
Self::UnaryTest(pred, word) => write!(f, "{pred} {word}"),
Self::BinaryTest(left, op, right) => write!(f, "{left} {op} {right}"),
}
}
}
/// An extended test expression.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum ExtendedTestExpr {
/// Logical AND operation on two nested expressions.
And(Box, Box),
/// Logical OR operation on two nested expressions.
Or(Box, Box),
/// Logical NOT operation on a nested expression.
Not(Box),
/// A parenthesized expression.
Parenthesized(Box),
/// A unary test operation.
UnaryTest(UnaryPredicate, Word),
/// A binary test operation.
BinaryTest(BinaryPredicate, Word, Word),
}
impl Display for ExtendedTestExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::And(left, right) => {
write!(f, "{left} && {right}")
}
Self::Or(left, right) => {
write!(f, "{left} || {right}")
}
Self::Not(expr) => {
write!(f, "! {expr}")
}
Self::Parenthesized(expr) => {
write!(f, "( {expr} )")
}
Self::UnaryTest(pred, word) => {
write!(f, "{pred} {word}")
}
Self::BinaryTest(pred, left, right) => {
write!(f, "{left} {pred} {right}")
}
}
}
}
/// An extended test expression command.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct ExtendedTestExprCommand {
/// The extended test expression
pub expr: ExtendedTestExpr,
/// Location of the expression
pub loc: SourceSpan,
}
impl Node for ExtendedTestExprCommand {}
impl SourceLocation for ExtendedTestExprCommand {
fn location(&self) -> Option {
Some(self.loc.clone())
}
}
impl Display for ExtendedTestExprCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.expr.fmt(f)
}
}
/// A unary predicate usable in an extended test expression.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum UnaryPredicate {
/// Computes if the operand is a path to an existing file.
FileExists,
/// Computes if the operand is a path to an existing block device file.
FileExistsAndIsBlockSpecialFile,
/// Computes if the operand is a path to an existing character device file.
FileExistsAndIsCharSpecialFile,
/// Computes if the operand is a path to an existing directory.
FileExistsAndIsDir,
/// Computes if the operand is a path to an existing regular file.
FileExistsAndIsRegularFile,
/// Computes if the operand is a path to an existing file with the setgid bit set.
FileExistsAndIsSetgid,
/// Computes if the operand is a path to an existing symbolic link.
FileExistsAndIsSymlink,
/// Computes if the operand is a path to an existing file with the sticky bit set.
FileExistsAndHasStickyBit,
/// Computes if the operand is a path to an existing FIFO file.
FileExistsAndIsFifo,
/// Computes if the operand is a path to an existing file that is readable.
FileExistsAndIsReadable,
/// Computes if the operand is a path to an existing file with a non-zero length.
FileExistsAndIsNotZeroLength,
/// Computes if the operand is a file descriptor that is an open terminal.
FdIsOpenTerminal,
/// Computes if the operand is a path to an existing file with the setuid bit set.
FileExistsAndIsSetuid,
/// Computes if the operand is a path to an existing file that is writable.
FileExistsAndIsWritable,
/// Computes if the operand is a path to an existing file that is executable.
FileExistsAndIsExecutable,
/// Computes if the operand is a path to an existing file owned by the current context's
/// effective group ID.
FileExistsAndOwnedByEffectiveGroupId,
/// Computes if the operand is a path to an existing file that has been modified since last
/// being read.
FileExistsAndModifiedSinceLastRead,
/// Computes if the operand is a path to an existing file owned by the current context's
/// effective user ID.
FileExistsAndOwnedByEffectiveUserId,
/// Computes if the operand is a path to an existing socket file.
FileExistsAndIsSocket,
/// Computes if the operand is a 'set -o' option that is enabled.
ShellOptionEnabled,
/// Computes if the operand names a shell variable that is set and assigned a value.
ShellVariableIsSetAndAssigned,
/// Computes if the operand names a shell variable that is set and of nameref type.
ShellVariableIsSetAndNameRef,
/// Computes if the operand is a string with zero length.
StringHasZeroLength,
/// Computes if the operand is a string with non-zero length.
StringHasNonZeroLength,
}
impl Display for UnaryPredicate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FileExists => write!(f, "-e"),
Self::FileExistsAndIsBlockSpecialFile => write!(f, "-b"),
Self::FileExistsAndIsCharSpecialFile => write!(f, "-c"),
Self::FileExistsAndIsDir => write!(f, "-d"),
Self::FileExistsAndIsRegularFile => write!(f, "-f"),
Self::FileExistsAndIsSetgid => write!(f, "-g"),
Self::FileExistsAndIsSymlink => write!(f, "-h"),
Self::FileExistsAndHasStickyBit => write!(f, "-k"),
Self::FileExistsAndIsFifo => write!(f, "-p"),
Self::FileExistsAndIsReadable => write!(f, "-r"),
Self::FileExistsAndIsNotZeroLength => write!(f, "-s"),
Self::FdIsOpenTerminal => write!(f, "-t"),
Self::FileExistsAndIsSetuid => write!(f, "-u"),
Self::FileExistsAndIsWritable => write!(f, "-w"),
Self::FileExistsAndIsExecutable => write!(f, "-x"),
Self::FileExistsAndOwnedByEffectiveGroupId => write!(f, "-G"),
Self::FileExistsAndModifiedSinceLastRead => write!(f, "-N"),
Self::FileExistsAndOwnedByEffectiveUserId => write!(f, "-O"),
Self::FileExistsAndIsSocket => write!(f, "-S"),
Self::ShellOptionEnabled => write!(f, "-o"),
Self::ShellVariableIsSetAndAssigned => write!(f, "-v"),
Self::ShellVariableIsSetAndNameRef => write!(f, "-R"),
Self::StringHasZeroLength => write!(f, "-z"),
Self::StringHasNonZeroLength => write!(f, "-n"),
}
}
}
/// A binary predicate usable in an extended test expression.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum BinaryPredicate {
/// Computes if two files refer to the same device and inode numbers.
FilesReferToSameDeviceAndInodeNumbers,
/// Computes if the left file is newer than the right, or exists when the right does not.
LeftFileIsNewerOrExistsWhenRightDoesNot,
/// Computes if the left file is older than the right, or does not exist when the right does.
LeftFileIsOlderOrDoesNotExistWhenRightDoes,
/// Computes if a string exactly matches a pattern.
StringExactlyMatchesPattern,
/// Computes if a string does not exactly match a pattern.
StringDoesNotExactlyMatchPattern,
/// Computes if a string matches a regular expression.
StringMatchesRegex,
/// Computes if a string exactly matches another string.
StringExactlyMatchesString,
/// Computes if a string does not exactly match another string.
StringDoesNotExactlyMatchString,
/// Computes if a string contains a substring.
StringContainsSubstring,
/// Computes if the left value sorts before the right.
LeftSortsBeforeRight,
/// Computes if the left value sorts after the right.
LeftSortsAfterRight,
/// Computes if two values are equal via arithmetic comparison.
ArithmeticEqualTo,
/// Computes if two values are not equal via arithmetic comparison.
ArithmeticNotEqualTo,
/// Computes if the left value is less than the right via arithmetic comparison.
ArithmeticLessThan,
/// Computes if the left value is less than or equal to the right via arithmetic comparison.
ArithmeticLessThanOrEqualTo,
/// Computes if the left value is greater than the right via arithmetic comparison.
ArithmeticGreaterThan,
/// Computes if the left value is greater than or equal to the right via arithmetic comparison.
ArithmeticGreaterThanOrEqualTo,
}
impl Display for BinaryPredicate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FilesReferToSameDeviceAndInodeNumbers => write!(f, "-ef"),
Self::LeftFileIsNewerOrExistsWhenRightDoesNot => write!(f, "-nt"),
Self::LeftFileIsOlderOrDoesNotExistWhenRightDoes => write!(f, "-ot"),
Self::StringExactlyMatchesPattern => write!(f, "=="),
Self::StringDoesNotExactlyMatchPattern => write!(f, "!="),
Self::StringMatchesRegex => write!(f, "=~"),
Self::StringContainsSubstring => write!(f, "=~"),
Self::StringExactlyMatchesString => write!(f, "=="),
Self::StringDoesNotExactlyMatchString => write!(f, "!="),
Self::LeftSortsBeforeRight => write!(f, "<"),
Self::LeftSortsAfterRight => write!(f, ">"),
Self::ArithmeticEqualTo => write!(f, "-eq"),
Self::ArithmeticNotEqualTo => write!(f, "-ne"),
Self::ArithmeticLessThan => write!(f, "-lt"),
Self::ArithmeticLessThanOrEqualTo => write!(f, "-le"),
Self::ArithmeticGreaterThan => write!(f, "-gt"),
Self::ArithmeticGreaterThanOrEqualTo => write!(f, "-ge"),
}
}
}
/// Represents a shell word.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct Word {
/// Raw text of the word.
pub value: String,
/// Location of the word
pub loc: Option,
}
impl Node for Word {}
impl SourceLocation for Word {
fn location(&self) -> Option {
self.loc.clone()
}
}
impl Display for Word {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
impl From<&tokenizer::Token> for Word {
fn from(t: &tokenizer::Token) -> Self {
match t {
tokenizer::Token::Word(value, loc) => Self {
value: value.clone(),
loc: Some(loc.clone()),
},
tokenizer::Token::Operator(value, loc) => Self {
value: value.clone(),
loc: Some(loc.clone()),
},
}
}
}
impl From for Word {
fn from(s: String) -> Self {
Self {
value: s,
loc: None,
}
}
}
impl AsRef for Word {
fn as_ref(&self) -> &str {
&self.value
}
}
impl Word {
/// Constructs a new `Word` from a given string.
pub fn new(s: &str) -> Self {
Self {
value: s.to_owned(),
loc: None,
}
}
/// Constructs a new `Word` from a given string and location.
pub fn with_location(s: &str, loc: &SourceSpan) -> Self {
Self {
value: s.to_owned(),
loc: Some(loc.to_owned()),
}
}
/// Returns the raw text of the word, consuming the `Word`.
pub fn flatten(&self) -> String {
self.value.clone()
}
}
/// Encapsulates an unparsed arithmetic expression.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct UnexpandedArithmeticExpr {
/// The raw text of the expression.
pub value: String,
}
impl Display for UnexpandedArithmeticExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
/// An arithmetic expression.
#[derive(Clone, Debug)]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum ArithmeticExpr {
/// A literal integer value.
Literal(i64),
/// A dereference of a variable or array element.
Reference(ArithmeticTarget),
/// A unary operation on an the result of a given nested expression.
UnaryOp(UnaryOperator, Box),
/// A binary operation on two nested expressions.
BinaryOp(BinaryOperator, Box, Box),
/// A ternary conditional expression.
Conditional(Box, Box, Box),
/// An assignment operation.
Assignment(ArithmeticTarget, Box),
/// A binary assignment operation.
BinaryAssignment(BinaryOperator, ArithmeticTarget, Box),
/// A unary assignment operation.
UnaryAssignment(UnaryAssignmentOperator, ArithmeticTarget),
}
impl Node for ArithmeticExpr {}
impl SourceLocation for ArithmeticExpr {
fn location(&self) -> Option {
// TODO(source-location): complete and add loc for literal
None
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for ArithmeticExpr {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result {
let variant = u.choose(&[
"Literal",
"Reference",
"UnaryOp",
"BinaryOp",
"Conditional",
"Assignment",
"BinaryAssignment",
"UnaryAssignment",
])?;
match *variant {
"Literal" => Ok(Self::Literal(i64::arbitrary(u)?)),
"Reference" => Ok(Self::Reference(ArithmeticTarget::arbitrary(u)?)),
"UnaryOp" => Ok(Self::UnaryOp(
UnaryOperator::arbitrary(u)?,
Box::new(Self::arbitrary(u)?),
)),
"BinaryOp" => Ok(Self::BinaryOp(
BinaryOperator::arbitrary(u)?,
Box::new(Self::arbitrary(u)?),
Box::new(Self::arbitrary(u)?),
)),
"Conditional" => Ok(Self::Conditional(
Box::new(Self::arbitrary(u)?),
Box::new(Self::arbitrary(u)?),
Box::new(Self::arbitrary(u)?),
)),
"Assignment" => Ok(Self::Assignment(
ArithmeticTarget::arbitrary(u)?,
Box::new(Self::arbitrary(u)?),
)),
"BinaryAssignment" => Ok(Self::BinaryAssignment(
BinaryOperator::arbitrary(u)?,
ArithmeticTarget::arbitrary(u)?,
Box::new(Self::arbitrary(u)?),
)),
"UnaryAssignment" => Ok(Self::UnaryAssignment(
UnaryAssignmentOperator::arbitrary(u)?,
ArithmeticTarget::arbitrary(u)?,
)),
_ => unreachable!(),
}
}
}
impl Display for ArithmeticExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Literal(literal) => write!(f, "{literal}"),
Self::Reference(target) => write!(f, "{target}"),
Self::UnaryOp(op, operand) => write!(f, "{op}{operand}"),
Self::BinaryOp(op, left, right) => {
if matches!(op, BinaryOperator::Comma) {
write!(f, "{left}{op} {right}")
} else {
write!(f, "{left} {op} {right}")
}
}
Self::Conditional(condition, if_branch, else_branch) => {
write!(f, "{condition} ? {if_branch} : {else_branch}")
}
Self::Assignment(target, value) => write!(f, "{target} = {value}"),
Self::BinaryAssignment(op, target, operand) => {
write!(f, "{target} {op}= {operand}")
}
Self::UnaryAssignment(op, target) => match op {
UnaryAssignmentOperator::PrefixIncrement
| UnaryAssignmentOperator::PrefixDecrement => write!(f, "{op}{target}"),
UnaryAssignmentOperator::PostfixIncrement
| UnaryAssignmentOperator::PostfixDecrement => write!(f, "{target}{op}"),
},
}
}
}
/// A binary arithmetic operator.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum BinaryOperator {
/// Exponentiation (e.g., `x ** y`).
Power,
/// Multiplication (e.g., `x * y`).
Multiply,
/// Division (e.g., `x / y`).
Divide,
/// Modulo (e.g., `x % y`).
Modulo,
/// Comma (e.g., `x, y`).
Comma,
/// Addition (e.g., `x + y`).
Add,
/// Subtraction (e.g., `x - y`).
Subtract,
/// Bitwise left shift (e.g., `x << y`).
ShiftLeft,
/// Bitwise right shift (e.g., `x >> y`).
ShiftRight,
/// Less than (e.g., `x < y`).
LessThan,
/// Less than or equal to (e.g., `x <= y`).
LessThanOrEqualTo,
/// Greater than (e.g., `x > y`).
GreaterThan,
/// Greater than or equal to (e.g., `x >= y`).
GreaterThanOrEqualTo,
/// Equals (e.g., `x == y`).
Equals,
/// Not equals (e.g., `x != y`).
NotEquals,
/// Bitwise AND (e.g., `x & y`).
BitwiseAnd,
/// Bitwise exclusive OR (xor) (e.g., `x ^ y`).
BitwiseXor,
/// Bitwise OR (e.g., `x | y`).
BitwiseOr,
/// Logical AND (e.g., `x && y`).
LogicalAnd,
/// Logical OR (e.g., `x || y`).
LogicalOr,
}
impl Display for BinaryOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Power => write!(f, "**"),
Self::Multiply => write!(f, "*"),
Self::Divide => write!(f, "/"),
Self::Modulo => write!(f, "%"),
Self::Comma => write!(f, ","),
Self::Add => write!(f, "+"),
Self::Subtract => write!(f, "-"),
Self::ShiftLeft => write!(f, "<<"),
Self::ShiftRight => write!(f, ">>"),
Self::LessThan => write!(f, "<"),
Self::LessThanOrEqualTo => write!(f, "<="),
Self::GreaterThan => write!(f, ">"),
Self::GreaterThanOrEqualTo => write!(f, ">="),
Self::Equals => write!(f, "=="),
Self::NotEquals => write!(f, "!="),
Self::BitwiseAnd => write!(f, "&"),
Self::BitwiseXor => write!(f, "^"),
Self::BitwiseOr => write!(f, "|"),
Self::LogicalAnd => write!(f, "&&"),
Self::LogicalOr => write!(f, "||"),
}
}
}
/// A unary arithmetic operator.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum UnaryOperator {
/// Unary plus (e.g., `+x`).
UnaryPlus,
/// Unary minus (e.g., `-x`).
UnaryMinus,
/// Bitwise not (e.g., `~x`).
BitwiseNot,
/// Logical not (e.g., `!x`).
LogicalNot,
}
impl Display for UnaryOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnaryPlus => write!(f, "+"),
Self::UnaryMinus => write!(f, "-"),
Self::BitwiseNot => write!(f, "~"),
Self::LogicalNot => write!(f, "!"),
}
}
}
/// A unary arithmetic assignment operator.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum UnaryAssignmentOperator {
/// Prefix increment (e.g., `++x`).
PrefixIncrement,
/// Prefix increment (e.g., `--x`).
PrefixDecrement,
/// Postfix increment (e.g., `x++`).
PostfixIncrement,
/// Postfix decrement (e.g., `x--`).
PostfixDecrement,
}
impl Display for UnaryAssignmentOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PrefixIncrement => write!(f, "++"),
Self::PrefixDecrement => write!(f, "--"),
Self::PostfixIncrement => write!(f, "++"),
Self::PostfixDecrement => write!(f, "--"),
}
}
}
/// Identifies the target of an arithmetic assignment expression.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum ArithmeticTarget {
/// A named variable.
Variable(String),
/// An element in an array.
ArrayElement(String, Box),
}
impl Node for ArithmeticTarget {}
impl SourceLocation for ArithmeticTarget {
fn location(&self) -> Option {
// TODO(source-location): complete and add loc
None
}
}
impl Display for ArithmeticTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Variable(name) => write!(f, "{name}"),
Self::ArrayElement(name, index) => write!(f, "{name}[{index}]"),
}
}
}
#[cfg(test)]
#[allow(clippy::panic)]
mod tests {
use super::*;
use crate::{ParserOptions, SourcePosition};
use std::io::BufReader;
fn parse(input: &str) -> Program {
let reader = BufReader::new(input.as_bytes());
let mut parser = crate::Parser::new(reader, &ParserOptions::default());
parser.parse_program().unwrap()
}
#[test]
fn program_source_loc() {
const INPUT: &str = r"echo hi
echo there
";
let loc = parse(INPUT).location().unwrap();
assert_eq!(
*(loc.start),
SourcePosition {
line: 1,
column: 1,
index: 0
}
);
assert_eq!(
*(loc.end),
SourcePosition {
line: 2,
column: 11,
index: 18
}
);
}
#[test]
fn function_def_loc() {
const INPUT: &str = r"my_func() {
echo hi
echo there
}
my_func
";
let program = parse(INPUT);
let Command::Function(func_def) = &program.complete_commands[0].0[0].0.first.seq[0] else {
panic!("expected function definition");
};
let loc = func_def.location().unwrap();
assert_eq!(
*(loc.start),
SourcePosition {
line: 1,
column: 1,
index: 0
}
);
assert_eq!(
*(loc.end),
SourcePosition {
line: 4,
column: 2,
index: 36
}
);
}
#[test]
fn simple_cmd_loc() {
const INPUT: &str = r"var=value somecmd arg1 arg2
";
let program = parse(INPUT);
let Command::Simple(cmd) = &program.complete_commands[0].0[0].0.first.seq[0] else {
panic!("expected function definition");
};
let loc = cmd.location().unwrap();
assert_eq!(
*(loc.start),
SourcePosition {
line: 1,
column: 1,
index: 0
}
);
assert_eq!(
*(loc.end),
SourcePosition {
line: 1,
column: 28,
index: 27
}
);
}
}
brush-parser-0.4.0/src/error.rs 0000644 0000000 0000000 00000010765 10461020230 0014541 0 ustar 0000000 0000000 use crate::tokenizer;
/// Represents an error that occurred while parsing tokens.
#[derive(thiserror::Error, Debug)]
pub enum ParseError {
/// A parsing error occurred near the given position.
#[error("syntax error at line {} col {}", .0.line, .0.column)]
ParsingNear(crate::SourcePosition),
/// A parsing error occurred at the end of the input.
#[error("syntax error at end of input")]
ParsingAtEndOfInput,
/// An error occurred while tokenizing the input stream.
#[error("{} (detected near {})", .inner, .position.as_ref().map_or_else(|| String::from(""), |p| std::format!("line {} col {}", p.line, p.column)))]
Tokenizing {
/// The inner error.
inner: tokenizer::TokenizerError,
/// Optionally provides the position of the error.
position: Option,
},
}
#[cfg(feature = "diagnostics")]
#[allow(clippy::cast_sign_loss)]
#[allow(unused)] // Workaround unused warnings in nightly versions of the compiler
pub mod miette {
use super::ParseError;
use miette::SourceOffset;
impl ParseError {
/// Convert the original error to one miette can pretty print
pub fn to_pretty_error(self, input: impl Into) -> PrettyError {
let input = input.into();
let location = match self {
Self::ParsingNear(ref pos) => {
Some(SourceOffset::from_location(&input, pos.line, pos.column))
}
Self::Tokenizing { ref position, .. } => position
.as_ref()
.map(|p| SourceOffset::from_location(&input, p.line, p.column)),
Self::ParsingAtEndOfInput => {
Some(SourceOffset::from_location(&input, usize::MAX, usize::MAX))
}
};
PrettyError {
cause: self,
input,
location,
}
}
}
/// Represents an error that occurred while parsing tokens.
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("Cannot parse the input script")]
pub struct PrettyError {
cause: ParseError,
#[source_code]
input: String,
#[label("{cause}")]
location: Option,
}
}
/// Represents a parsing error with its location information
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct ParseErrorLocation {
#[from]
inner: peg::error::ParseError,
}
/// Represents an error that occurred while parsing a word.
#[derive(Debug, thiserror::Error)]
pub enum WordParseError {
/// An error occurred while parsing an arithmetic expression.
#[error("failed to parse arithmetic expression")]
ArithmeticExpression(ParseErrorLocation),
/// An error occurred while parsing a shell pattern.
#[error("failed to parse pattern")]
Pattern(ParseErrorLocation),
/// An error occurred while parsing a prompt string.
#[error("failed to parse prompt string")]
Prompt(ParseErrorLocation),
/// An error occurred while parsing a parameter.
#[error("failed to parse parameter '{0}'")]
Parameter(String, ParseErrorLocation),
/// An error occurred while parsing for brace expansion.
#[error("failed to parse for brace expansion: '{0}'")]
BraceExpansion(String, ParseErrorLocation),
/// An error occurred while parsing a word.
#[error("failed to parse word '{0}'")]
Word(String, ParseErrorLocation),
}
/// Represents an error that occurred while parsing a (non-extended) test command.
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct TestCommandParseError(#[from] peg::error::ParseError);
/// Represents an error that occurred while parsing a key-binding specification.
#[derive(Debug, thiserror::Error)]
pub enum BindingParseError {
/// An unknown error occurred while parsing a key-binding specification.
#[error("unknown error while parsing key-binding: '{0}'")]
Unknown(String),
/// A key code was missing from the key-binding specification.
#[error("missing key code in key-binding")]
MissingKeyCode,
}
pub(crate) fn convert_peg_parse_error(
err: &peg::error::ParseError,
tokens: &[crate::Token],
) -> ParseError {
let approx_token_index = err.location;
if approx_token_index < tokens.len() {
let token = &tokens[approx_token_index];
ParseError::ParsingNear((*token.location().start).clone())
} else {
ParseError::ParsingAtEndOfInput
}
}
brush-parser-0.4.0/src/lib.rs 0000644 0000000 0000000 00000001661 10461020230 0014151 0 ustar 0000000 0000000 //! Implements a tokenizer and parsers for POSIX / bash shell syntax.
// TODO(unwrap): remove or scope this allow attribute
#![allow(clippy::unwrap_used)]
pub mod arithmetic;
pub mod ast;
pub mod pattern;
pub mod prompt;
pub mod readline_binding;
pub mod test_command;
pub mod word;
mod error;
mod parser;
mod source;
mod tokenizer;
#[cfg(test)]
mod snapshot_tests;
pub use error::{
BindingParseError, ParseError, ParseErrorLocation, TestCommandParseError, WordParseError,
};
#[cfg(feature = "diagnostics")]
pub use error::miette::PrettyError;
#[cfg(feature = "winnow-parser")]
pub use parser::winnow_str;
pub use parser::{Parser, ParserBuilder, ParserImpl, ParserOptions, SourceInfo, parse_tokens};
pub use source::{SourcePosition, SourcePositionOffset, SourceSpan};
pub use tokenizer::{
Token, TokenLocation, TokenizerError, TokenizerOptions, tokenize_str,
tokenize_str_with_options, uncached_tokenize_str, unquote_str,
};
brush-parser-0.4.0/src/parser/mod.rs 0000644 0000000 0000000 00000020520 10461020230 0015451 0 ustar 0000000 0000000 use std::path::PathBuf;
use bon::bon;
use crate::ast;
use crate::tokenizer::{Token, TokenEndReason, Tokenizer, TokenizerOptions, Tokens};
pub mod peg;
#[cfg(feature = "winnow-parser")]
pub mod winnow_str;
/// Parser implementation to use
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)]
pub enum ParserImpl {
/// PEG-based parser (token-based)
#[default]
Peg,
/// Winnow-based parser (string-based, direct)
#[cfg(feature = "winnow-parser")]
Winnow,
}
/// Options used to control the behavior of the parser.
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct ParserOptions {
/// Whether or not to enable extended globbing (a.k.a. `extglob`).
pub enable_extended_globbing: bool,
/// Whether or not to enable POSIX compliance mode.
pub posix_mode: bool,
/// Whether or not to enable maximal compatibility with the `sh` shell.
pub sh_mode: bool,
/// Whether or not to perform tilde expansion for tildes at the start of words.
pub tilde_expansion_at_word_start: bool,
/// Whether or not to perform tilde expansion for tildes after colons.
pub tilde_expansion_after_colon: bool,
/// Select the parser internal implementation
pub parser_impl: ParserImpl,
}
impl Default for ParserOptions {
fn default() -> Self {
Self {
enable_extended_globbing: true,
posix_mode: false,
sh_mode: false,
tilde_expansion_at_word_start: true,
tilde_expansion_after_colon: false,
parser_impl: ParserImpl::default(),
}
}
}
impl ParserOptions {
/// Returns the tokenizer options implied by these parser options.
pub const fn tokenizer_options(&self) -> TokenizerOptions {
TokenizerOptions {
enable_extended_globbing: self.enable_extended_globbing,
posix_mode: self.posix_mode,
sh_mode: self.sh_mode,
}
}
}
/// Information about the source of tokens.
#[derive(Clone, Debug, Default)]
#[allow(dead_code)]
pub struct SourceInfo {
/// The source of the tokens.
pub source: String,
}
impl From for SourceInfo {
fn from(path: PathBuf) -> Self {
Self {
source: path.to_string_lossy().to_string(),
}
}
}
/// Implements parsing for shell programs.
pub struct Parser {
/// The reader to use for input
reader: R,
/// Parsing options
options: ParserOptions,
}
#[bon]
impl Parser {
///
/// # Arguments
///
/// * `reader` - The reader to use for input.
/// * `options` - The options to use when parsing.
pub fn new(reader: R, options: &ParserOptions) -> Self {
Self {
reader,
options: options.clone(),
}
}
/// Create a new parser instance through a builder
#[builder(
finish_fn(doc {
/// Instantiate a parser with the provided reader as input
})
)]
pub const fn builder(
/// The reader to use for input
#[builder(finish_fn)]
reader: R,
#[builder(default = true)]
/// Whether or not to enable extended globbing (a.k.a. `extglob`).
enable_extended_globbing: bool,
#[builder(default = false)]
/// Whether or not to enable POSIX compliance mode.
posix_mode: bool,
#[builder(default = false)]
/// Whether or not to enable maximal compatibility with the `sh` shell.
sh_mode: bool,
#[builder(default = true)]
/// Whether or not to perform tilde expansion for tildes at the start of words.
tilde_expansion_at_word_start: bool,
#[builder(default = false)]
/// Whether or not to perform tilde expansion for tildes after colons.
tilde_expansion_after_colon: bool,
#[builder(default)]
/// Select the parser internal implementation
parser_impl: ParserImpl,
) -> Self {
let options = ParserOptions {
enable_extended_globbing,
posix_mode,
sh_mode,
tilde_expansion_at_word_start,
tilde_expansion_after_colon,
parser_impl,
};
Self { reader, options }
}
/// Parses the input into an abstract syntax tree (AST) of a shell program.
pub fn parse_program(&mut self) -> Result {
//
// References:
// * https://www.gnu.org/software/bash/manual/bash.html#Shell-Syntax
// * https://mywiki.wooledge.org/BashParser
// * https://aosabook.org/en/v1/bash.html
// * https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
//
match self.options.parser_impl {
ParserImpl::Peg => {
let tokens = self.tokenize()?;
parse_tokens(&tokens, &self.options)
}
#[cfg(feature = "winnow-parser")]
ParserImpl::Winnow => {
// Read entire input to string for winnow_str parser
let mut input_str = String::new();
std::io::Read::read_to_string(&mut self.reader, &mut input_str).map_err(|e| {
crate::error::ParseError::Tokenizing {
inner: crate::tokenizer::TokenizerError::from(e),
position: None,
}
})?;
winnow_str::parse_program(&input_str, &self.options, &SourceInfo::default())
.map_err(|_e| {
// Convert winnow error to ParseError
// TODO: Extract position information from winnow error
crate::error::ParseError::ParsingAtEndOfInput
})
}
}
}
/// Parses a function definition body from the input. The body is expected to be
/// preceded by "()", but no function name.
pub fn parse_function_parens_and_body(
&mut self,
) -> Result {
let tokens = self.tokenize()?;
let parse_result =
peg::token_parser::function_parens_and_body(&Tokens { tokens: &tokens }, &self.options);
parse_result_to_error(parse_result, &tokens)
}
fn tokenize(&mut self) -> Result, crate::error::ParseError> {
// First we tokenize the input, according to the policy implied by provided options.
let mut tokenizer = Tokenizer::new(&mut self.reader, &self.options.tokenizer_options());
tracing::debug!(target: "tokenize", "Tokenizing...");
let mut tokens = vec![];
loop {
let result = match tokenizer.next_token() {
Ok(result) => result,
Err(e) => {
return Err(crate::error::ParseError::Tokenizing {
inner: e,
position: tokenizer.current_location(),
});
}
};
let reason = result.reason;
if let Some(token) = result.token {
tracing::debug!(target: "tokenize", "TOKEN {}: {:?} {reason:?}", tokens.len(), token);
tokens.push(token);
}
if matches!(reason, TokenEndReason::EndOfInput) {
break;
}
}
tracing::debug!(target: "tokenize", " => {} token(s)", tokens.len());
Ok(tokens)
}
}
/// Parses a sequence of tokens into the abstract syntax tree (AST) of a shell program.
///
/// # Arguments
///
/// * `tokens` - The tokens to parse.
/// * `options` - The options to use when parsing.
pub fn parse_tokens(
tokens: &[Token],
options: &ParserOptions,
) -> Result {
let parse_result = peg::token_parser::program(&Tokens { tokens }, options);
parse_result_to_error(parse_result, tokens)
}
fn parse_result_to_error(
parse_result: Result>,
tokens: &[Token],
) -> Result
where
R: std::fmt::Debug,
{
match parse_result {
Ok(program) => {
tracing::debug!(target: "parse", "PROG: {:?}", program);
Ok(program)
}
Err(parse_error) => {
tracing::debug!(target: "parse", "Parse error: {:?}", parse_error);
Err(crate::error::convert_peg_parse_error(&parse_error, tokens))
}
}
}
#[cfg(test)]
mod tests;
brush-parser-0.4.0/src/parser/peg.rs 0000644 0000000 0000000 00000114332 10461020230 0015452 0 ustar 0000000 0000000 //! PEG-based parser implementation for shell scripts.
use crate::SourceSpan;
use crate::ast::{self, SeparatorOperator, SourceLocation, maybe_location};
use crate::tokenizer::Token;
use crate::word;
use super::{ParserOptions, Tokens};
peg::parser! {
pub grammar token_parser<'a>(parser_options: &ParserOptions) for Tokens<'a> {
pub(crate) rule program() -> ast::Program =
linebreak() c:complete_commands() linebreak() { ast::Program { complete_commands: c } } /
linebreak() { ast::Program { complete_commands: vec![] } }
rule complete_commands() -> Vec =
c:complete_command() ++ newline_list()
rule complete_command() -> ast::CompleteCommand =
first:and_or() remainder:(s:separator_op() l:and_or() { (s, l) })* last_sep:separator_op()? {
let mut and_ors = vec![first];
let mut seps = vec![];
for (sep, ao) in remainder {
seps.push(sep);
and_ors.push(ao);
}
// N.B. We default to synchronous if no separator op is given.
seps.push(last_sep.unwrap_or(SeparatorOperator::Sequence));
let mut items = vec![];
for (i, ao) in and_ors.into_iter().enumerate() {
items.push(ast::CompoundListItem(ao, seps[i].clone()));
}
ast::CompoundList(items)
}
rule and_or() -> ast::AndOrList =
first:pipeline() additional:_and_or_item()* { ast::AndOrList { first, additional } }
rule _and_or_item() -> ast::AndOr =
op:_and_or_op() linebreak() p:pipeline() { op(p) }
rule _and_or_op() -> fn(ast::Pipeline) -> ast::AndOr =
specific_operator("&&") { ast::AndOr::And } /
specific_operator("||") { ast::AndOr::Or }
rule pipeline() -> ast::Pipeline =
timed:pipeline_timed()? bang:bang()* seq:pipe_sequence() {?
if timed.is_none() && bang.is_empty() && seq.is_empty() {
Err("empty pipeline")
} else {
let invert = bang.len() % 2 == 1;
Ok(ast::Pipeline { timed, bang: invert, seq })
}
}
rule pipeline_timed() -> ast::PipelineTimed =
non_posix_extensions_enabled() s:specific_word("time") posix_output:specific_word("-p")? {
let start = s.location();
if let Some(end) = posix_output {
ast::PipelineTimed::TimedWithPosixOutput(SourceSpan::within(start, end.location()))
} else {
ast::PipelineTimed::Timed(start.to_owned())
}
}
rule bang() -> bool = specific_word("!") { true }
pub(crate) rule pipe_sequence() -> Vec =
c:(c:command() r:&pipe_extension_redirection()? {? // check for `|&` without consuming the stream.
let mut c = c;
if r.is_some() {
add_pipe_extension_redirection(&mut c)?;
}
Ok(c)
}) ** (pipe_operator() linebreak()) {
c
}
rule pipe_operator() =
specific_operator("|") /
pipe_extension_redirection()
rule pipe_extension_redirection() -> &'input Token =
non_posix_extensions_enabled() p:specific_operator("|&") { p }
// N.B. We needed to move the function definition branch up to avoid conflicts with array assignment syntax.
rule command() -> ast::Command =
f:function_definition() { ast::Command::Function(f) } /
c:simple_command() { ast::Command::Simple(c) } /
c:compound_command() r:redirect_list()? { ast::Command::Compound(c, r) } /
// N.B. Extended test commands are bash extensions.
non_posix_extensions_enabled() c:extended_test_command() r:redirect_list()? { ast::Command::ExtendedTest(c, r) } /
expected!("command")
// N.B. The arithmetic command is a non-sh extension.
// N.B. The arithmetic for clause command is a non-sh extension.
pub(crate) rule compound_command() -> ast::CompoundCommand =
non_posix_extensions_enabled() a:arithmetic_command() { ast::CompoundCommand::Arithmetic(a) } /
non_posix_extensions_enabled() c:coproc_clause() { ast::CompoundCommand::Coprocess(c) } /
b:brace_group() { ast::CompoundCommand::BraceGroup(b) } /
s:subshell() { ast::CompoundCommand::Subshell(s) } /
f:for_clause() { ast::CompoundCommand::ForClause(f) } /
c:case_clause() { ast::CompoundCommand::CaseClause(c) } /
i:if_clause() { ast::CompoundCommand::IfClause(i) } /
w:while_clause() { ast::CompoundCommand::WhileClause(w) } /
u:until_clause() { ast::CompoundCommand::UntilClause(u) } /
non_posix_extensions_enabled() c:arithmetic_for_clause() { ast::CompoundCommand::ArithmeticForClause(c) } /
expected!("compound command")
pub(crate) rule arithmetic_command() -> ast::ArithmeticCommand =
start:specific_operator("(") specific_operator("(") expr:arithmetic_expression() specific_operator(")") end:specific_operator(")") {
let loc = SourceSpan::within(
start.location(),
end.location()
);
ast::ArithmeticCommand { expr, loc }
}
pub(crate) rule arithmetic_expression() -> ast::UnexpandedArithmeticExpr =
raw_expr:$(arithmetic_expression_piece()*) { ast::UnexpandedArithmeticExpr { value: raw_expr } }
rule arithmetic_expression_piece() =
// Allow a parenthesized expression (with matching opening and closing parens).
specific_operator("(") (!specific_operator(")") arithmetic_expression_piece())* specific_operator(")") {} /
// Otherwise consume any token that's neither the normal end of the entire arithmetic expression, nor an
// unexpected mismatched closing parenthesis. In the latter case, it may be that this really was never an
// arithmetic expression in the first place and we need to backtrack and instead try parsing as a subshell
// command instead.
!arithmetic_end() !specific_operator(")") [_] {}
// TODO(arithmetic): evaluate arithmetic end; the semicolon is used in arithmetic for loops.
rule arithmetic_end() -> () =
specific_operator(")") specific_operator(")") {} /
specific_operator(";") {}
rule subshell() -> ast::SubshellCommand =
start:specific_operator("(") list:compound_list() end:specific_operator(")") {
let loc = SourceSpan::within(start.location(), end.location());
ast::SubshellCommand { list, loc }
}
rule compound_list() -> ast::CompoundList =
linebreak() first:and_or() remainder:(s:separator() l:and_or() { (s, l) })* last_sep:separator()? {
let mut and_ors = vec![first];
let mut seps = vec![];
for (sep, ao) in remainder {
seps.push(sep.unwrap_or(SeparatorOperator::Sequence));
and_ors.push(ao);
}
// N.B. We default to synchronous if no separator op is given.
let last_sep = last_sep.unwrap_or(None);
seps.push(last_sep.unwrap_or(SeparatorOperator::Sequence));
let mut items = vec![];
for (i, ao) in and_ors.into_iter().enumerate() {
items.push(ast::CompoundListItem(ao, seps[i].clone()));
}
ast::CompoundList(items)
}
rule for_clause() -> ast::ForClauseCommand =
s:specific_word("for") n:name() linebreak() _in() w:wordlist()? sequential_sep() d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::ForClauseCommand { variable_name: n.to_owned(), values: w, body: d, loc }
} /
s:specific_word("for") n:name() sequential_sep()? d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::ForClauseCommand { variable_name: n.to_owned(), values: None, body: d, loc }
}
// N.B. The arithmetic for loop is a non-sh extension.
rule arithmetic_for_clause() -> ast::ArithmeticForClauseCommand =
s:specific_word("for")
specific_operator("(") specific_operator("(")
initializer:arithmetic_expression()? specific_operator(";")
condition:arithmetic_expression()? specific_operator(";")
updater:arithmetic_expression()?
specific_operator(")") specific_operator(")")
body:arithmetic_for_body() {
let start = s.location();
let end = &body.loc;
let loc = SourceSpan::within(start, end);
ast::ArithmeticForClauseCommand { initializer, condition, updater, body, loc }
}
rule arithmetic_for_body() -> ast::DoGroupCommand =
sequential_sep()? body:do_group() { body } /
body:brace_group() { ast::DoGroupCommand { list: body.list, loc: body.loc } }
rule extended_test_command() -> ast::ExtendedTestExprCommand =
s:specific_word("[[") linebreak() expr:extended_test_expression() linebreak() e:specific_word("]]") {
let start = s.location();
let end = e.location();
let loc = SourceSpan::within(start, end);
ast::ExtendedTestExprCommand { expr, loc }
}
rule extended_test_expression() -> ast::ExtendedTestExpr = precedence! {
left:(@) linebreak() specific_operator("||") linebreak() right:@ { ast::ExtendedTestExpr::Or(Box::from(left), Box::from(right)) }
--
left:(@) linebreak() specific_operator("&&") linebreak() right:@ { ast::ExtendedTestExpr::And(Box::from(left), Box::from(right)) }
--
specific_word("!") e:@ { ast::ExtendedTestExpr::Not(Box::from(e)) }
--
specific_operator("(") e:extended_test_expression() specific_operator(")") { ast::ExtendedTestExpr::Parenthesized(Box::from(e)) }
--
// Arithmetic operators
left:word() specific_word("-eq") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticEqualTo, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-ne") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticNotEqualTo, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-lt") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticLessThan, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-le") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticLessThanOrEqualTo, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-gt") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticGreaterThan, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-ge") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo, ast::Word::from(left), ast::Word::from(right)) }
// Non-arithmetic binary operators
left:word() specific_word("-ef") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-nt") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-ot") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes, ast::Word::from(left), ast::Word::from(right)) }
left:word() (specific_word("==") / specific_word("=")) right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringExactlyMatchesPattern, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("!=") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringDoesNotExactlyMatchPattern, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("=~") right:regex_word() {
if right.value.starts_with(['\'', '\"']) {
// TODO(test): Confirm it ends with that too?
ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringContainsSubstring, ast::Word::from(left), right)
} else {
ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringMatchesRegex, ast::Word::from(left), right)
}
}
left:word() specific_operator("<") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftSortsBeforeRight, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_operator(">") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftSortsAfterRight, ast::Word::from(left), ast::Word::from(right)) }
--
p:extended_unary_predicate() f:word() { ast::ExtendedTestExpr::UnaryTest(p, ast::Word::from(f)) }
--
w:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::StringHasNonZeroLength, ast::Word::from(w)) }
}
rule extended_unary_predicate() -> ast::UnaryPredicate =
specific_word("-a") { ast::UnaryPredicate::FileExists } /
specific_word("-b") { ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile } /
specific_word("-c") { ast::UnaryPredicate::FileExistsAndIsCharSpecialFile } /
specific_word("-d") { ast::UnaryPredicate::FileExistsAndIsDir } /
specific_word("-e") { ast::UnaryPredicate::FileExists } /
specific_word("-f") { ast::UnaryPredicate::FileExistsAndIsRegularFile } /
specific_word("-g") { ast::UnaryPredicate::FileExistsAndIsSetgid } /
specific_word("-h") { ast::UnaryPredicate::FileExistsAndIsSymlink } /
specific_word("-k") { ast::UnaryPredicate::FileExistsAndHasStickyBit } /
specific_word("-n") { ast::UnaryPredicate::StringHasNonZeroLength } /
specific_word("-o") { ast::UnaryPredicate::ShellOptionEnabled } /
specific_word("-p") { ast::UnaryPredicate::FileExistsAndIsFifo } /
specific_word("-r") { ast::UnaryPredicate::FileExistsAndIsReadable } /
specific_word("-s") { ast::UnaryPredicate::FileExistsAndIsNotZeroLength } /
specific_word("-t") { ast::UnaryPredicate::FdIsOpenTerminal } /
specific_word("-u") { ast::UnaryPredicate::FileExistsAndIsSetuid } /
specific_word("-v") { ast::UnaryPredicate::ShellVariableIsSetAndAssigned } /
specific_word("-w") { ast::UnaryPredicate::FileExistsAndIsWritable } /
specific_word("-x") { ast::UnaryPredicate::FileExistsAndIsExecutable } /
specific_word("-z") { ast::UnaryPredicate::StringHasZeroLength } /
specific_word("-G") { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId } /
specific_word("-L") { ast::UnaryPredicate::FileExistsAndIsSymlink } /
specific_word("-N") { ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead } /
specific_word("-O") { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId } /
specific_word("-R") { ast::UnaryPredicate::ShellVariableIsSetAndNameRef } /
specific_word("-S") { ast::UnaryPredicate::FileExistsAndIsSocket }
// N.B. For some reason we seem to need to allow a select subset
// of unescaped operators in regex words.
rule regex_word() -> ast::Word =
value:$((!specific_word("]]") regex_word_piece())+) {
ast::Word::from(value)
}
rule regex_word_piece() =
word() {} /
specific_operator("|") {} /
specific_operator("(") parenthesized_regex_word()* specific_operator(")") {}
rule parenthesized_regex_word() =
regex_word_piece() /
!specific_operator(")") !specific_operator("]]") [_]
rule name() -> &'input str =
w:[Token::Word(_, _)] { w.to_str() }
rule _in() -> () =
specific_word("in") { }
rule wordlist() -> Vec =
(w:word() { ast::Word::from(w) })+
pub(crate) rule case_clause() -> ast::CaseClauseCommand =
start:specific_word("case") w:word() linebreak() _in() linebreak() first_items:case_item()* last_item:case_item_ns()? end:specific_word("esac") {
let mut cases = first_items;
if let Some(last_item) = last_item {
cases.push(last_item);
}
let loc = SourceSpan::within(start.location(), end.location());
ast::CaseClauseCommand { value: ast::Word::from(w), cases, loc }
}
pub(crate) rule case_item_ns() -> ast::CaseItem =
s:specific_operator("(")? p:pattern() specific_operator(")") c:compound_list() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = c.location();
let loc = maybe_location(start, end.as_ref());
ast::CaseItem { patterns: p, cmd: Some(c), post_action: ast::CaseItemPostAction::ExitCase, loc }
} /
s:specific_operator("(")? p:pattern() e:specific_operator(")") linebreak() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = Some(e.location());
let loc = maybe_location(start, end);
ast::CaseItem { patterns: p, cmd: None, post_action: ast::CaseItemPostAction::ExitCase, loc }
}
pub(crate) rule case_item() -> ast::CaseItem =
s:specific_operator("(")? p:pattern() specific_operator(")") linebreak() post_action:case_item_post_action() linebreak() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = Some(post_action.1);
let loc = maybe_location(start, end);
ast::CaseItem { patterns: p, cmd: None, post_action: post_action.0, loc }
} /
s:specific_operator("(")? p:pattern() specific_operator(")") c:compound_list() post_action:case_item_post_action() linebreak() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = Some(post_action.1);
let loc = maybe_location(start, end);
ast::CaseItem { patterns: p, cmd: Some(c), post_action: post_action.0, loc }
}
rule case_item_post_action() -> (ast::CaseItemPostAction, &'input SourceSpan) =
s:specific_operator(";;") {
(ast::CaseItemPostAction::ExitCase, s.location())
} /
non_posix_extensions_enabled() s:specific_operator(";;&") {
(ast::CaseItemPostAction::ContinueEvaluatingCases, s.location())
} /
non_posix_extensions_enabled() s:specific_operator(";&") {
(ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem, s.location())
}
rule pattern() -> Vec =
(w:word() { ast::Word::from(w) }) ++ specific_operator("|")
rule if_clause() -> ast::IfClauseCommand =
s:specific_word("if") condition:compound_list() specific_word("then") then:compound_list() elses:else_part()? e:specific_word("fi") {
let start = s.location();
let end = s.location();
let loc = SourceSpan::within(start, end);
ast::IfClauseCommand {
condition,
then,
elses,
loc
}
}
rule else_part() -> Vec =
cs:_conditional_else_part()+ u:_unconditional_else_part()? {
let mut parts = vec![];
for c in cs {
parts.push(c);
}
if let Some(uncond) = u {
parts.push(uncond);
}
parts
} /
e:_unconditional_else_part() { vec![e] }
rule _conditional_else_part() -> ast::ElseClause =
specific_word("elif") condition:compound_list() specific_word("then") body:compound_list() {
ast::ElseClause { condition: Some(condition), body }
}
rule _unconditional_else_part() -> ast::ElseClause =
specific_word("else") body:compound_list() {
ast::ElseClause { condition: None, body }
}
rule while_clause() -> ast::WhileOrUntilClauseCommand =
s:specific_word("while") c:compound_list() d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::WhileOrUntilClauseCommand(c, d, loc)
}
rule until_clause() -> ast::WhileOrUntilClauseCommand =
s:specific_word("until") c:compound_list() d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::WhileOrUntilClauseCommand(c, d, loc)
}
// N.B. Coproc is a bash extension.
rule coproc_clause() -> ast::CoprocessCommand =
s:specific_word("coproc") linebreak() name:coproc_name()? body:command() {
let start = s.location();
let loc = body.location().unwrap_or_else(|| start.clone());
ast::CoprocessCommand { name, body: Box::new(body), loc: SourceSpan::within(start, &loc) }
}
// N.B. The name must be followed by a compound command start ({ or ()
rule coproc_name() -> ast::Word =
w:fname() linebreak() &(specific_word("{") / specific_operator("(")) {
w
}
// N.B. Non-sh extensions allows use of the 'function' word to indicate a function definition.
// N.B. Without the 'function' keyword, reserved words cannot be used as function names
// (bash rejects e.g. `for (){ :; }`). With the 'function' keyword, reserved words are
// allowed as function names (bash accepts e.g. `function for { :; }`).
rule function_definition() -> ast::FunctionDefinition =
specific_word("function") fname:fname() body:function_parens_and_body() {
ast::FunctionDefinition { fname, body }
} /
fname:non_reserved_fname() body:function_parens_and_body() {
ast::FunctionDefinition { fname, body }
} /
specific_word("function") fname:fname() linebreak() body:function_body() {
ast::FunctionDefinition { fname, body }
} /
expected!("function definition")
pub(crate) rule function_parens_and_body() -> ast::FunctionBody =
specific_operator("(") specific_operator(")") linebreak() body:function_body() { body }
rule function_body() -> ast::FunctionBody =
c:compound_command() r:redirect_list()? { ast::FunctionBody(c, r) }
rule fname() -> ast::Word =
// Special-case: don't allow it to end with an equals sign, to avoid the challenge of
// misinterpreting certain declaration assignments as function definitions.
// TODO(parser): Find a way to make this still work without requiring this targeted exception.
w:[Token::Word(word, l) if !word.ends_with('=')] { ast::Word::with_location(word, l) }
rule non_reserved_fname() -> ast::Word =
!reserved_word() w:fname() { w }
rule brace_group() -> ast::BraceGroupCommand =
start:specific_word("{") list:compound_list() end:specific_word("}") {
let loc = SourceSpan::within(start.location(), end.location());
ast::BraceGroupCommand { list, loc }
}
rule do_group() -> ast::DoGroupCommand =
start:specific_word("do") list:compound_list() end:specific_word("done") {
let loc = SourceSpan::within(start.location(), end.location());
ast::DoGroupCommand { list, loc }
}
rule simple_command() -> ast::SimpleCommand =
prefix:cmd_prefix() word_and_suffix:(word_or_name:cmd_word() suffix:cmd_suffix()? { (word_or_name, suffix) })? {
match word_and_suffix {
Some((word_or_name, suffix)) => {
ast::SimpleCommand { prefix: Some(prefix), word_or_name: Some(ast::Word::from(word_or_name)), suffix }
}
None => {
ast::SimpleCommand { prefix: Some(prefix), word_or_name: None, suffix: None }
}
}
} /
word_or_name:cmd_name() suffix:cmd_suffix()? {
ast::SimpleCommand { prefix: None, word_or_name: Some(ast::Word::from(word_or_name)), suffix } } /
expected!("simple command")
rule cmd_name() -> &'input Token =
non_reserved_word()
rule cmd_word() -> &'input Token =
!assignment_word() w:non_reserved_word() { w }
rule cmd_prefix() -> ast::CommandPrefix =
p:(
i:io_redirect() { ast::CommandPrefixOrSuffixItem::IoRedirect(i) } /
assignment_and_word:assignment_word() {
let (assignment, word) = assignment_and_word;
ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, word)
}
)+ { ast::CommandPrefix(p) }
rule cmd_suffix() -> ast::CommandSuffix =
s:(
non_posix_extensions_enabled() sub:process_substitution() {
let (kind, subshell) = sub;
ast::CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell)
} /
i:io_redirect() {
ast::CommandPrefixOrSuffixItem::IoRedirect(i)
} /
assignment_and_word:assignment_word() {
let (assignment, word) = assignment_and_word;
ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, word)
} /
w:word() {
ast::CommandPrefixOrSuffixItem::Word(ast::Word::from(w))
}
)+ { ast::CommandSuffix(s) }
rule redirect_list() -> ast::RedirectList =
r:io_redirect()+ { ast::RedirectList(r) } /
expected!("redirect list")
// N.B. here strings are extensions to the POSIX standard.
rule io_redirect() -> ast::IoRedirect =
n:io_number()? f:io_file() {
let (kind, target) = f;
ast::IoRedirect::File(n, kind, target)
} /
non_posix_extensions_enabled() specific_operator("&>>") target:filename() { ast::IoRedirect::OutputAndError(ast::Word::from(target), true) } /
non_posix_extensions_enabled() specific_operator("&>") target:filename() { ast::IoRedirect::OutputAndError(ast::Word::from(target), false) } /
non_posix_extensions_enabled() n:io_number()? specific_operator("<<<") w:word() { ast::IoRedirect::HereString(n, ast::Word::from(w)) } /
n:io_number()? h:io_here() { ast::IoRedirect::HereDocument(n, h) } /
expected!("I/O redirect")
// N.B. Process substitution forms are extensions to the POSIX standard.
rule io_file() -> (ast::IoFileRedirectKind, ast::IoFileRedirectTarget) =
specific_operator("<") f:io_filename() { (ast::IoFileRedirectKind::Read, f) } /
specific_operator("<&") f:io_fd_duplication_source() { (ast::IoFileRedirectKind::DuplicateInput, f) } /
specific_operator(">") f:io_filename() { (ast::IoFileRedirectKind::Write, f) } /
specific_operator(">&") f:io_fd_duplication_source() { (ast::IoFileRedirectKind::DuplicateOutput, f) } /
specific_operator(">>") f:io_filename() { (ast::IoFileRedirectKind::Append, f) } /
specific_operator("<>") f:io_filename() { (ast::IoFileRedirectKind::ReadAndWrite, f) } /
specific_operator(">|") f:io_filename() { (ast::IoFileRedirectKind::Clobber, f) }
rule io_fd_duplication_source() -> ast::IoFileRedirectTarget =
w:word() { ast::IoFileRedirectTarget::Duplicate(ast::Word::from(w)) }
rule io_fd() -> u32 =
w:[Token::Word(_, _)] {? w.to_str().parse().or(Err("io_fd u32")) }
rule io_filename() -> ast::IoFileRedirectTarget =
non_posix_extensions_enabled() sub:process_substitution() {
let (kind, subshell) = sub;
ast::IoFileRedirectTarget::ProcessSubstitution(kind, subshell)
} /
f:filename() { ast::IoFileRedirectTarget::Filename(ast::Word::from(f)) }
rule filename() -> &'input Token =
word()
pub(crate) rule io_here() -> ast::IoHereDocument =
specific_operator("<<-") here_tag:here_tag() doc:[_] closing_tag:here_tag() {
let requires_expansion = !here_tag.to_str().contains(['\'', '"', '\\']);
ast::IoHereDocument {
remove_tabs: true,
requires_expansion,
here_end: ast::Word::from(here_tag),
doc: ast::Word::from(doc)
}
} /
specific_operator("<<") here_tag:here_tag() doc:[_] closing_tag:here_tag() {
let requires_expansion = !here_tag.to_str().contains(['\'', '"', '\\']);
ast::IoHereDocument {
remove_tabs: false,
requires_expansion,
here_end: ast::Word::from(here_tag),
doc: ast::Word::from(doc)
}
}
rule here_tag() -> &'input Token =
word()
rule process_substitution() -> (ast::ProcessSubstitutionKind, ast::SubshellCommand) =
specific_operator("<") s:subshell() { (ast::ProcessSubstitutionKind::Read, s) } /
specific_operator(">") s:subshell() { (ast::ProcessSubstitutionKind::Write, s) }
rule newline_list() -> () =
newline()+ {}
rule linebreak() -> () =
quiet! {
newline()* {}
}
rule separator_op() -> ast::SeparatorOperator =
specific_operator("&") { ast::SeparatorOperator::Async } /
specific_operator(";") { ast::SeparatorOperator::Sequence }
rule separator() -> Option =
s:separator_op() linebreak() { Some(s) } /
newline_list() { None }
rule sequential_sep() -> () =
specific_operator(";") linebreak() /
newline_list()
//
// Token interpretation
//
rule non_reserved_word() -> &'input Token =
!reserved_word() w:word() { w }
rule word() -> &'input Token =
[Token::Word(_, _)]
rule reserved_word() -> &'input Token =
[Token::Word(w, _) if matches!(w.as_str(),
"!" |
"{" |
"}" |
"case" |
"do" |
"done" |
"elif" |
"else" |
"esac" |
"fi" |
"for" |
"if" |
"in" |
"then" |
"until" |
"while"
)] /
// N.B. bash also treats the following as reserved.
non_posix_extensions_enabled() token:non_posix_reserved_word_token() { token }
rule non_posix_reserved_word_token() -> &'input Token =
specific_word("[[") /
specific_word("]]") /
specific_word("function") /
specific_word("select") /
specific_word("coproc")
rule newline() -> () = quiet! {
specific_operator("\n") {}
}
pub(crate) rule assignment_word() -> (ast::Assignment, ast::Word) =
non_posix_extensions_enabled() [Token::Word(w, l)] specific_operator("(") elements:array_elements() end:specific_operator(")") {?
let mut parsed = word::parse_array_assignment(w.as_str(), elements.as_slice())?;
let mut all_as_word = w.to_owned();
all_as_word.push('(');
for (i, e) in elements.iter().enumerate() {
if i > 0 {
all_as_word.push(' ');
}
all_as_word.push_str(e);
}
all_as_word.push(')');
let loc = SourceSpan::within(l, end.location());
parsed.loc = loc.clone();
Ok((parsed, ast::Word::with_location(&all_as_word, &loc)))
} /
[Token::Word(w, l)] {?
let mut parsed = word::parse_assignment_word(w.as_str()).map_err(|_| "not assignment word")?;
parsed.loc = l.clone();
Ok((parsed, ast::Word::with_location(w, l)))
}
rule array_elements() -> Vec<&'input String> =
linebreak() e:array_element()* { e }
rule array_element() -> &'input String =
linebreak() [Token::Word(e, _)] linebreak() { e }
// N.B. An I/O number must be a string of only digits, and it must be
// followed by a '<' or '>' character (but not consume them). We also
// need to make sure that there was no space between the number and the
// redirection operator; unfortunately we don't have the space anymore
// but we can infer it by looking at the tokens' locations.
rule io_number() -> ast::IoFd =
[Token::Word(w, num_loc) if w.chars().all(|c: char| c.is_ascii_digit())]
&([Token::Operator(o, redir_loc) if
o.starts_with(['<', '>']) &&
locations_are_contiguous(num_loc, redir_loc)]) {
w.parse().unwrap()
}
//
// Helpers
//
rule specific_operator(expected: &str) -> &'input Token =
[Token::Operator(w, _) if w.as_str() == expected]
rule specific_word(expected: &str) -> &'input Token =
[Token::Word(w, _) if w.as_str() == expected]
rule non_posix_extensions_enabled() -> () =
&[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
}
}
// add `2>&1` to the command if the pipeline is `|&`
fn add_pipe_extension_redirection(c: &mut ast::Command) -> Result<(), &'static str> {
fn add_to_redirect_list(l: &mut Option, r: ast::IoRedirect) {
if let Some(l) = l {
l.0.push(r);
} else {
let v = vec![r];
*l = Some(ast::RedirectList(v));
}
}
let r = ast::IoRedirect::File(
Some(2),
ast::IoFileRedirectKind::DuplicateOutput,
ast::IoFileRedirectTarget::Fd(1),
);
match c {
ast::Command::Simple(c) => {
let r = ast::CommandPrefixOrSuffixItem::IoRedirect(r);
if let Some(l) = &mut c.suffix {
l.0.push(r);
} else {
c.suffix = Some(ast::CommandSuffix(vec![r]));
}
}
ast::Command::Compound(_, l) => add_to_redirect_list(l, r),
ast::Command::Function(f) => add_to_redirect_list(&mut f.body.1, r),
ast::Command::ExtendedTest(..) => return Err("|& unimplemented for extended tests"),
}
Ok(())
}
#[inline]
fn locations_are_contiguous(loc_left: &crate::SourceSpan, loc_right: &crate::SourceSpan) -> bool {
loc_left.end.index == loc_right.start.index
}
impl peg::Parse for Tokens<'_> {
type PositionRepr = usize;
#[inline]
fn start(&self) -> usize {
0
}
#[inline]
fn is_eof(&self, p: usize) -> bool {
p >= self.tokens.len()
}
#[inline]
fn position_repr(&self, p: usize) -> Self::PositionRepr {
p
}
}
impl<'a> peg::ParseElem<'a> for Tokens<'a> {
type Element = &'a Token;
#[inline]
fn parse_elem(&'a self, pos: usize) -> peg::RuleResult {
match self.tokens.get(pos) {
Some(c) => peg::RuleResult::Matched(pos + 1, c),
None => peg::RuleResult::Failed,
}
}
}
impl<'a> peg::ParseSlice<'a> for Tokens<'a> {
type Slice = String;
/// Reconstructs a source string from a slice of tokens.
///
/// Uses each token's source position to detect whether whitespace existed
/// between adjacent tokens in the original source, preserving it as a
/// single space. This matters for constructs like `[[ x =~ (a| *) ]]`
/// where the space inside the regex group is significant.
///
/// N.B. This relies on tokens having accurate, contiguous source
/// positions. All tokens produced by the normal tokenizer path satisfy
/// this. If positions are missing or non-monotonic (as can happen with
/// synthetic here-document end-tag tokens), the gap check evaluates
/// false and no space is inserted β a safe degradation to the previous
/// behavior, which also omitted spaces between non-word tokens.
fn parse_slice(&'a self, start: usize, end: usize) -> Self::Slice {
let mut result = String::new();
let mut prev_end_index: Option = None;
for token in &self.tokens[start..end] {
let loc = token.location();
if let Some(prev_end) = prev_end_index {
if loc.start.index > prev_end {
result.push(' ');
}
}
result.push_str(token.to_str());
prev_end_index = Some(loc.end.index);
}
result
}
}
././@LongLink 0000644 0000000 0000000 00000000153 00000000000 0007772 L ustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_arith_and_non_arith_parens.snap brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_arith_and_non_arith_paren0000644 0000000 0000000 00000011545 10461020230 0032663 0 ustar 0000000 0000000 ---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "( : && ( (( 0 )) || : ) )",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: ":",
loc: Some(SourceSpan(
start: SourcePosition(
index: 2,
line: 1,
column: 3,
),
end: SourcePosition(
index: 3,
line: 1,
column: 4,
),
)),
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "0",
),
loc: SourceSpan(
start: SourcePosition(
index: 9,
line: 1,
column: 10,
),
end: SourcePosition(
index: 16,
line: 1,
column: 17,
),
),
)), None),
],
),
additional: [
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: ":",
loc: Some(SourceSpan(
start: SourcePosition(
index: 20,
line: 1,
column: 21,
),
end: SourcePosition(
index: 21,
line: 1,
column: 22,
),
)),
)),
)),
],
)),
],
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 7,
line: 1,
column: 8,
),
end: SourcePosition(
index: 23,
line: 1,
column: 24,
),
),
)), None),
],
)),
],
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 25,
line: 1,
column: 26,
),
),
)), None),
],
),
), Sequence),
]),
],
),
)
brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_case.snap 0000644 0000000 0000000 00000010301 10461020230 0027344 0 ustar 0000000 0000000 ---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "\\\ncase x in\nx)\n echo y;;\nesac\\\n",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 7,
line: 2,
column: 6,
),
end: SourcePosition(
index: 8,
line: 2,
column: 7,
),
)),
),
cases: [
CaseItem(
patterns: [
Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 13,
line: 3,
column: 2,
),
)),
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 19,
line: 4,
column: 5,
),
end: SourcePosition(
index: 23,
line: 4,
column: 9,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "y",
loc: Some(SourceSpan(
start: SourcePosition(
index: 24,
line: 4,
column: 10,
),
end: SourcePosition(
index: 25,
line: 4,
column: 11,
),
)),
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 27,
line: 4,
column: 13,
),
)),
),
],
loc: SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 34,
line: 6,
column: 1,
),
),
)), None),
],
),
), Sequence),
]),
],
),
)
brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_case_ns.snap 0000644 0000000 0000000 00000010277 10461020230 0030060 0 ustar 0000000 0000000 ---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "\\\ncase x in\nx)\n echo y\nesac\\\n",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 7,
line: 2,
column: 6,
),
end: SourcePosition(
index: 8,
line: 2,
column: 7,
),
)),
),
cases: [
CaseItem(
patterns: [
Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 13,
line: 3,
column: 2,
),
)),
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 19,
line: 4,
column: 5,
),
end: SourcePosition(
index: 23,
line: 4,
column: 9,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "y",
loc: Some(SourceSpan(
start: SourcePosition(
index: 24,
line: 4,
column: 10,
),
end: SourcePosition(
index: 25,
line: 4,
column: 11,
),
)),
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 25,
line: 4,
column: 11,
),
)),
),
],
loc: SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 32,
line: 6,
column: 1,
),
),
)), None),
],
),
), Sequence),
]),
],
),
)
././@LongLink 0000644 0000000 0000000 00000000161 00000000000 0007771 L ustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redirection-2.snap brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redire0000644 0000000 0000000 00000007263 10461020230 0032735 0 ustar 0000000 0000000 ---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "foo() { echo 1; } |& cat",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Function(FunctionDefinition(
fname: Word(
value: "foo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 3,
line: 1,
column: 4,
),
)),
),
body: FunctionBody(BraceGroup(BraceGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 8,
line: 1,
column: 9,
),
end: SourcePosition(
index: 12,
line: 1,
column: 13,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "1",
loc: Some(SourceSpan(
start: SourcePosition(
index: 13,
line: 1,
column: 14,
),
end: SourcePosition(
index: 14,
line: 1,
column: 15,
),
)),
)),
])),
)),
],
),
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 6,
line: 1,
column: 7,
),
end: SourcePosition(
index: 17,
line: 1,
column: 18,
),
),
)), Some(RedirectList([
File(Some(2), DuplicateOutput, Fd(1)),
]))),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: Some(SourceSpan(
start: SourcePosition(
index: 21,
line: 1,
column: 22,
),
end: SourcePosition(
index: 24,
line: 1,
column: 25,
),
)),
)),
)),
],
),
), Sequence),
]),
],
),
)
././@LongLink 0000644 0000000 0000000 00000000157 00000000000 0007776 L ustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redirection.snap brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redire0000644 0000000 0000000 00000010222 10461020230 0032722 0 ustar 0000000 0000000 ---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "foo() { echo 1; } 2>&1 | cat",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Function(FunctionDefinition(
fname: Word(
value: "foo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 3,
line: 1,
column: 4,
),
)),
),
body: FunctionBody(BraceGroup(BraceGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 8,
line: 1,
column: 9,
),
end: SourcePosition(
index: 12,
line: 1,
column: 13,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "1",
loc: Some(SourceSpan(
start: SourcePosition(
index: 13,
line: 1,
column: 14,
),
end: SourcePosition(
index: 14,
line: 1,
column: 15,
),
)),
)),
])),
)),
],
),
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 6,
line: 1,
column: 7,
),
end: SourcePosition(
index: 17,
line: 1,
column: 18,
),
),
)), Some(RedirectList([
File(Some(2), DuplicateOutput, Duplicate(Word(
value: "1",
loc: Some(SourceSpan(
start: SourcePosition(
index: 21,
line: 1,
column: 22,
),
end: SourcePosition(
index: 22,
line: 1,
column: 23,
),
)),
))),
]))),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: Some(SourceSpan(
start: SourcePosition(
index: 25,
line: 1,
column: 26,
),
end: SourcePosition(
index: 28,
line: 1,
column: 29,
),
)),
)),
)),
],
),
), Sequence),
]),
],
),
)
././@LongLink 0000644 0000000 0000000 00000000162 00000000000 0007772 L ustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_here_doc_with_no_trailing_newline.snap brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_here_doc_with_no_trailing0000644 0000000 0000000 00000004162 10461020230 0032671 0 ustar 0000000 0000000 ---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cat <