brush-parser-0.4.0/.cargo_vcs_info.json0000644000000001521046102023000134720ustar { "git": { "sha1": "96a26d0c66cbc018a1517e9562944418fef5b272" }, "path_in_vcs": "brush-parser" }brush-parser-0.4.0/Cargo.lock0000644000001304531046102023000114550ustar # 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.toml0000644000000107151046102023000114760ustar # 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.orig000064400000000000000000000033531046102023000151350ustar 00000000000000[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/LICENSE000064400000000000000000000020571046102023000132530ustar 00000000000000MIT 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.md000064400000000000000000000201671046102023000135270ustar 00000000000000


1389 compatibility tests Packaging status Discord invite


`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.rs000064400000000000000000000236631046102023000155250ustar 00000000000000//! 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.rs000064400000000000000000000010641046102023000157160ustar 00000000000000//! 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.rs000064400000000000000000000020341046102023000155270ustar 00000000000000//! 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.rs000064400000000000000000000221571046102023000155370ustar 00000000000000//! 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.rs000064400000000000000000002260411046102023000141730ustar 00000000000000//! 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.rs000064400000000000000000000107651046102023000145410ustar 00000000000000use 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.rs000064400000000000000000000016611046102023000141510ustar 00000000000000//! 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.rs000064400000000000000000000205201046102023000154510ustar 00000000000000use 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.rs000064400000000000000000001143321046102023000154520ustar 00000000000000//! 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 } } ././@LongLink00006440000000000000000000000153000000000000007772Lustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_arith_and_non_arith_parens.snapbrush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_arith_and_non_arith_paren000064400000000000000000000115451046102023000326630ustar 00000000000000--- 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.snap000064400000000000000000000103011046102023000273440ustar 00000000000000--- 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.snap000064400000000000000000000102771046102023000300600ustar 00000000000000--- 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), ]), ], ), ) ././@LongLink00006440000000000000000000000161000000000000007771Lustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redirection-2.snapbrush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redire000064400000000000000000000072631046102023000327350ustar 00000000000000--- 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), ]), ], ), ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redirection.snapbrush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_function_with_pipe_redire000064400000000000000000000102221046102023000327220ustar 00000000000000--- 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), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_here_doc_with_no_trailing_newline.snapbrush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_here_doc_with_no_trailing000064400000000000000000000041621046102023000326710ustar 00000000000000--- source: brush-parser/src/parser/mod.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cat <&2\n\n done\n\n", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ForClause(ForClauseCommand( variable_name: "f", values: Some([ Word( value: "A", loc: Some(SourceSpan( start: SourcePosition( index: 32, line: 5, column: 10, ), end: SourcePosition( index: 33, line: 5, column: 11, ), )), ), Word( value: "B", loc: Some(SourceSpan( start: SourcePosition( index: 34, line: 5, column: 12, ), end: SourcePosition( index: 35, line: 5, column: 13, ), )), ), Word( value: "C", loc: Some(SourceSpan( start: SourcePosition( index: 36, line: 5, column: 14, ), end: SourcePosition( index: 37, line: 5, column: 15, ), )), ), ]), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: Some(SourceSpan( start: SourcePosition( index: 60, line: 8, column: 5, ), end: SourcePosition( index: 64, line: 8, column: 9, ), )), )), suffix: Some(CommandSuffix([ Word(Word( value: "\"${f@L}\"", loc: Some(SourceSpan( start: SourcePosition( index: 65, line: 8, column: 10, ), end: SourcePosition( index: 73, line: 8, column: 18, ), )), )), IoRedirect(File(None, DuplicateOutput, Duplicate(Word( value: "2", loc: Some(SourceSpan( start: SourcePosition( index: 76, line: 8, column: 21, ), end: SourcePosition( index: 77, line: 8, column: 22, ), )), )))), ])), )), ], ), ), Sequence), ]), loc: SourceSpan( start: SourcePosition( index: 39, line: 5, column: 17, ), end: SourcePosition( index: 86, line: 10, column: 8, ), ), ), loc: SourceSpan( start: SourcePosition( index: 23, line: 5, column: 1, ), end: SourcePosition( index: 86, line: 10, column: 8, ), ), )), None), ], ), ), Sequence), ]), ], ), ) brush-parser-0.4.0/src/parser/snapshots/brush_parser__parser__tests__parse_redirection.snap000064400000000000000000000030241046102023000307440ustar 00000000000000--- source: brush-parser/src/parser/mod.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo |& wc", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: Some(SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 4, line: 1, column: 5, ), )), )), suffix: Some(CommandSuffix([ IoRedirect(File(Some(2), DuplicateOutput, Fd(1))), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "wc", loc: Some(SourceSpan( start: SourcePosition( index: 8, line: 1, column: 9, ), end: SourcePosition( index: 10, line: 1, column: 11, ), )), )), )), ], ), ), Sequence), ]), ], ), ) brush-parser-0.4.0/src/parser/tests/and_or_lists.rs000064400000000000000000000034241046102023000205200ustar 00000000000000//! Tests for and/or list parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; #[test] fn parse_simple_and() -> Result<()> { let input = "true && echo yes"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_simple_or() -> Result<()> { let input = "false || echo no"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_chained_and() -> Result<()> { let input = "cmd1 && cmd2 && cmd3"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_chained_or() -> Result<()> { let input = "cmd1 || cmd2 || cmd3"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_mixed_and_or() -> Result<()> { let input = "cmd1 && cmd2 || cmd3"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_and_or_with_pipes() -> Result<()> { let input = "cmd1 | cmd2 && cmd3 | cmd4"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_and_or_in_sequence() -> Result<()> { let input = "cmd1 && cmd2; cmd3 || cmd4"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/assignments.rs000064400000000000000000000124211046102023000203700ustar 00000000000000//! Tests for assignment parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; // Scalar assignments #[test] fn parse_assignment_simple() -> Result<()> { let input = "x=value"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_empty() -> Result<()> { let input = "x="; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_quoted() -> Result<()> { let input = r#"x="hello world""#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_single_quoted() -> Result<()> { let input = "x='hello world'"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_with_expansion() -> Result<()> { let input = "x=$HOME/bin"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_with_command_substitution() -> Result<()> { let input = "x=$(pwd)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Append assignments #[test] fn parse_assignment_append() -> Result<()> { let input = "x+=more"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_append_quoted() -> Result<()> { let input = r#"x+=" more text""#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Array assignments #[test] fn parse_assignment_array() -> Result<()> { let input = "arr=(a b c)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_array_empty() -> Result<()> { let input = "arr=()"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_array_with_indices() -> Result<()> { let input = "arr=([0]=a [1]=b [2]=c)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_array_mixed() -> Result<()> { let input = "arr=(a [5]=b c)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_array_quoted_elements() -> Result<()> { let input = r#"arr=("hello world" "foo bar")"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Array element assignment #[test] fn parse_assignment_array_element() -> Result<()> { let input = "arr[0]=value"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_array_element_expression() -> Result<()> { let input = "arr[i+1]=value"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Multiple assignments #[test] fn parse_multiple_assignments() -> Result<()> { let input = "x=1 y=2 z=3"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_assignment_with_command() -> Result<()> { let input = "VAR=value command arg1 arg2"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_multiple_assignments_with_command() -> Result<()> { let input = "A=1 B=2 command"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Export/local with assignment #[test] fn parse_export_assignment() -> Result<()> { let input = "export VAR=value"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_local_assignment() -> Result<()> { let input = "local x=5"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_declare_assignment() -> Result<()> { let input = "declare -i x=5"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/complex.rs000064400000000000000000000127511046102023000175120ustar 00000000000000//! Complex and integration tests that combine multiple parser features. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; #[test] fn parse_shebang_and_program() -> Result<()> { let input = r#"#!/usr/bin/env bash for f in A B C; do # sdfsdf echo "${f@L}" >&2 done "#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_case_with_newlines() -> Result<()> { let input = r"case x in x) echo y;; esac"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_case_no_semicolon() -> Result<()> { let input = r"case x in x) echo y esac"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_nested_if_while() -> Result<()> { let input = r#"if true; then while read line; do echo "$line" done < file.txt fi"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_with_if() -> Result<()> { let input = r#"myfunc() { if [[ -z "$1" ]]; then echo "No argument" return 1 fi echo "Got: $1" }"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_pipeline_with_redirections() -> Result<()> { let input = "cat < input.txt | grep pattern | tee output.txt > /dev/null 2>&1"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_command_substitution_nested() -> Result<()> { let input = "echo \"$(echo $(pwd))\""; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_for_with_command_substitution() -> Result<()> { let input = "for f in $(ls *.txt); do cat \"$f\"; done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_arithmetic_in_condition() -> Result<()> { let input = "if (( x > 5 )); then echo big; else echo small; fi"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_brace_expansion_context() -> Result<()> { let input = "echo {a,b,c}"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] #[allow(clippy::literal_string_with_formatting_args)] fn parse_parameter_expansion_complex() -> Result<()> { let input = r#"echo "${var:-default}" "${var:+alt}" "${var:=assign}""#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_subshell_with_assignments() -> Result<()> { let input = "( x=1; y=2; echo $((x + y)) )"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_coprocess() -> Result<()> { let input = "coproc cat"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_multiple_here_docs() -> Result<()> { let input = r"cmd < Result<()> { let input = "cmd1 & cmd2 & cmd3"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_mixed_sequences() -> Result<()> { let input = "cmd1; cmd2 && cmd3 || cmd4; cmd5"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_complex_array_operations() -> Result<()> { let input = r#"arr=($(seq 1 10)); echo "${arr[@]}"; echo "${#arr[@]}""#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_glob_patterns() -> Result<()> { let input = "ls *.txt **/foo.* file?.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extglob_patterns() -> Result<()> { let input = "ls !(*.txt) +(foo|bar) ?(a|b)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_tilde_expansion() -> Result<()> { let input = "cd ~/projects; ls ~user/home"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/compound_commands.rs000064400000000000000000000147201046102023000215460ustar 00000000000000//! Tests for compound command parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; // Arithmetic commands #[test] fn parse_arithmetic_simple() -> Result<()> { let input = "(( 1 + 2 ))"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_arithmetic_increment() -> Result<()> { let input = "(( x++ ))"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_arithmetic_complex() -> Result<()> { let input = "(( x = 5 + 3 * 2 ))"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Arithmetic for clause #[test] fn parse_arithmetic_for() -> Result<()> { let input = "for (( i = 0; i < 10; i++ )); do echo $i; done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_arithmetic_for_empty_parts() -> Result<()> { let input = "for (( ; ; )); do echo loop; done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Brace group #[test] fn parse_brace_group() -> Result<()> { let input = "{ echo hello; }"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_brace_group_multiline() -> Result<()> { let input = r"{ echo hello echo world }"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Subshell #[test] fn parse_subshell() -> Result<()> { let input = "( echo hello )"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_subshell_multiple_commands() -> Result<()> { let input = "( echo hello; echo world )"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_nested_subshell() -> Result<()> { let input = "( ( echo nested ) )"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // For clause #[test] fn parse_for_in() -> Result<()> { let input = "for x in a b c; do echo $x; done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_for_in_multiline() -> Result<()> { let input = r"for x in a b c do echo $x done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_for_no_in() -> Result<()> { let input = "for x; do echo $x; done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Case clause #[test] fn parse_case_simple() -> Result<()> { let input = "case x in a) echo a;; esac"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_case_multiple_patterns() -> Result<()> { let input = r"case x in a|b) echo ab;; c) echo c;; *) echo default;; esac"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_case_fallthrough() -> Result<()> { let input = "case x in a) echo a;& b) echo b;; esac"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_case_continue() -> Result<()> { let input = "case x in a) echo a;;& b) echo b;; esac"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // If clause #[test] fn parse_if_simple() -> Result<()> { let input = "if true; then echo yes; fi"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_if_else() -> Result<()> { let input = "if true; then echo yes; else echo no; fi"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_if_elif() -> Result<()> { let input = "if false; then echo one; elif true; then echo two; else echo three; fi"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_if_multiline() -> Result<()> { let input = r"if true then echo yes else echo no fi"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // While/Until #[test] fn parse_while() -> Result<()> { let input = "while true; do echo loop; done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_until() -> Result<()> { let input = "until false; do echo loop; done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_while_multiline() -> Result<()> { let input = r"while true do echo loop break done"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Mixed/nested #[test] fn parse_arith_and_non_arith_parens() -> Result<()> { let input = "( : && ( (( 0 )) || : ) )"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/extended_test.rs000064400000000000000000000135751046102023000207070ustar 00000000000000//! Tests for extended test expression [[ ]] parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; // File tests #[test] fn parse_extended_test_file_exists() -> Result<()> { let input = "[[ -f file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_directory() -> Result<()> { let input = "[[ -d /path/to/dir ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_readable() -> Result<()> { let input = "[[ -r file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_writable() -> Result<()> { let input = "[[ -w file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_executable() -> Result<()> { let input = "[[ -x file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // String tests #[test] fn parse_extended_test_string_zero_length() -> Result<()> { let input = r#"[[ -z "$var" ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_string_non_zero() -> Result<()> { let input = r#"[[ -n "$var" ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_string_equal() -> Result<()> { let input = r#"[[ "$a" == "$b" ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_string_not_equal() -> Result<()> { let input = r#"[[ "$a" != "$b" ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_string_pattern() -> Result<()> { let input = r#"[[ "$str" == *pattern* ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_regex() -> Result<()> { let input = r#"[[ "$str" =~ ^[0-9]+$ ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_regex_with_spaces() -> Result<()> { let input = r#"[[ "x" =~ (a| *) ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Logical operators #[test] fn parse_extended_test_and() -> Result<()> { let input = "[[ -f file && -r file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_or() -> Result<()> { let input = "[[ -f file || -d file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_not() -> Result<()> { let input = "[[ ! -f file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_parenthesized() -> Result<()> { let input = "[[ ( -f file ) ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_complex() -> Result<()> { let input = "[[ ( -f file && -r file ) || -d file ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Comparison operators #[test] fn parse_extended_test_less_than() -> Result<()> { let input = r#"[[ "$a" < "$b" ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_greater_than() -> Result<()> { let input = r#"[[ "$a" > "$b" ]]"#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Arithmetic comparison #[test] fn parse_extended_test_arith_equal() -> Result<()> { let input = "[[ 5 -eq 5 ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_arith_not_equal() -> Result<()> { let input = "[[ 5 -ne 3 ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_arith_less_than() -> Result<()> { let input = "[[ 3 -lt 5 ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_extended_test_arith_greater_than() -> Result<()> { let input = "[[ 5 -gt 3 ]]"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/functions.rs000064400000000000000000000042331046102023000200470ustar 00000000000000//! Tests for function definition parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; #[test] fn parse_function_basic() -> Result<()> { let input = "foo() { echo hello; }"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_keyword() -> Result<()> { let input = "function foo { echo hello; }"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_keyword_with_parens() -> Result<()> { let input = "function foo() { echo hello; }"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_with_redirect() -> Result<()> { let input = "foo() { echo 1; } 2>&1 | cat"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_with_stderr_redirect() -> Result<()> { let input = "foo() { echo 1; } |& cat"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_multiline() -> Result<()> { let input = r"foo() { echo hello echo world }"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_with_subshell_body() -> Result<()> { let input = "foo() ( echo subshell )"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_function_with_local_vars() -> Result<()> { let input = r"foo() { local x=1 echo $x }"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/here_docs.rs000064400000000000000000000047601046102023000177770ustar 00000000000000//! Tests for here-document parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; #[test] fn parse_here_doc_basic() -> Result<()> { let input = r"cat < Result<()> { let input = r"cat < Result<()> { let input = "cat <<-EOF\n\tcontent with tab\nEOF\n"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_here_doc_quoted_delimiter() -> Result<()> { let input = r"cat <<'EOF' $variable should not expand EOF "; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_here_doc_double_quoted_delimiter() -> Result<()> { let input = r#"cat <<"EOF" $variable should not expand EOF "#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_here_doc_with_expansion() -> Result<()> { let input = r"cat < Result<()> { let input = r"cat < Result<()> { let input = r"cat < Result<()> { let input = r"command 3< { pub input: &'a str, pub result: &'a T, } /// Macro to assert snapshots with location information redacted. /// This makes snapshots stable across parser changes that only affect source locations. #[macro_export] macro_rules! assert_snapshot_redacted { ($value:expr) => {{ let mut settings = insta::Settings::clone_current(); settings.add_redaction(".**.loc", "[location]"); settings.bind(|| { insta::assert_ron_snapshot!($value); }); }}; } /// Recursively redact location fields from a JSON value. /// This normalizes AST representations for comparison by removing source location info. #[cfg(feature = "winnow-parser")] fn redact_locations(value: &mut Value) { match value { Value::Object(map) => { // Remove location-related fields map.remove("loc"); // Also handle tuple structs that store SourceSpan as positional element // These appear as arrays with SourceSpan objects // Recursively process remaining fields for (_, v) in map.iter_mut() { // If this value is a SourceSpan object, normalize it if is_source_span(v) { normalize_source_span(v); } else { redact_locations(v); } } } Value::Array(arr) => { // Check if this array looks like a tuple struct containing SourceSpan at the end // SourceSpan has: start: SourcePosition, end: SourcePosition // SourcePosition has: index, line, column if let Some(last) = arr.last() { if is_source_span(last) { arr.pop(); } } for item in arr.iter_mut() { redact_locations(item); } } _ => {} } } /// Check if a value looks like a `SourceSpan` object #[cfg(feature = "winnow-parser")] fn is_source_span(value: &Value) -> bool { if let Value::Object(map) = value { map.contains_key("start") && map.contains_key("end") && map.len() == 2 } else { false } } /// Normalize a `SourceSpan` object by replacing its positions with placeholder values. /// This allows comparing ASTs without position differences. #[cfg(feature = "winnow-parser")] fn normalize_source_span(value: &mut Value) { if let Value::Object(map) = value { let placeholder_pos = serde_json::json!({ "index": 0, "line": 0, "column": 0 }); map.insert("start".to_string(), placeholder_pos.clone()); map.insert("end".to_string(), placeholder_pos); } } /// Convert a Program to a normalized JSON value with locations redacted #[cfg(feature = "winnow-parser")] #[allow(clippy::expect_used)] fn normalize_ast(program: &Program) -> Value { let mut value = serde_json::to_value(program).expect("Failed to serialize Program to JSON"); redact_locations(&mut value); value } /// A named parser configuration for test output clarity #[derive(Debug, Clone)] pub struct ParserConfig { pub name: &'static str, pub parser_impl: ParserImpl, } /// Returns all available parser implementations for testing. /// - Without `winnow-parser`: returns only Peg /// - With `winnow-parser`: returns both Peg and Winnow pub fn parser_configs() -> Vec { #[allow(unused_mut)] let mut configs = vec![ParserConfig { name: "peg", parser_impl: ParserImpl::Peg, }]; #[cfg(feature = "winnow-parser")] configs.push(ParserConfig { name: "winnow", parser_impl: ParserImpl::Winnow, }); configs } /// Helper to parse input with a specific parser configuration pub fn parse_with_config(input: &str, config: &ParserConfig) -> Result { let options = ParserOptions { parser_impl: config.parser_impl, ..Default::default() }; let mut parser = Parser::new(std::io::Cursor::new(input), &options); parser.parse_program() } /// Verify all parser implementations produce the same result. /// /// This function parses the input with each available parser and verifies /// they all produce structurally equivalent ASTs. Returns an error if /// parsing fails or if the results differ. #[cfg(feature = "winnow-parser")] #[allow(dead_code)] pub fn test_all_parsers_match(input: &str) -> Result<()> { let configs = parser_configs(); // Parse with each configuration let mut results: Vec<(&str, Program)> = Vec::new(); for config in &configs { let result = parse_with_config(input, config).map_err(|e| { anyhow::anyhow!( "Parser '{}' failed to parse input: {}\nInput: {}", config.name, e, input ) })?; results.push((config.name, result)); } // Compare all results against the first (peg) implementation // Normalize ASTs by redacting location info before comparison if results.len() > 1 { let (base_name, base_result) = &results[0]; let base_normalized = normalize_ast(base_result); for (name, result) in results.iter().skip(1) { let result_normalized = normalize_ast(result); pretty_assertions::assert_eq!( base_normalized, result_normalized, "Parser outputs differ between '{}' and '{}'.\nInput: {}", base_name, name, input ); } } Ok(()) } /// Run a test and create snapshot for peg parser (canonical implementation). /// /// This function parses the input with the peg parser and returns the result /// for snapshot testing. When the winnow-parser feature is enabled, it also /// verifies that winnow produces the same result. pub fn test_with_snapshot(input: &str) -> Result { // Always parse with peg (canonical implementation) let peg_config = ParserConfig { name: "peg", parser_impl: ParserImpl::Peg, }; let peg_result = parse_with_config(input, &peg_config) .map_err(|e| anyhow::anyhow!("Peg parser failed: {e}\nInput: {input}"))?; // When winnow is enabled, verify it matches (ignoring location differences) #[cfg(feature = "winnow-parser")] { let winnow_config = ParserConfig { name: "winnow", parser_impl: ParserImpl::Winnow, }; let winnow_result = parse_with_config(input, &winnow_config) .map_err(|e| anyhow::anyhow!("Winnow parser failed: {e}\nInput: {input}"))?; // Normalize both ASTs by redacting location info before comparison let peg_normalized = normalize_ast(&peg_result); let winnow_normalized = normalize_ast(&winnow_result); pretty_assertions::assert_eq!( peg_normalized, winnow_normalized, "Parser outputs differ between 'peg' and 'winnow'.\nInput: {}", input ); } Ok(peg_result) } #[cfg(test)] mod harness_tests { use super::*; #[test] fn test_parser_configs_includes_peg() { let configs = parser_configs(); assert!(!configs.is_empty()); assert_eq!(configs[0].name, "peg"); } #[test] #[cfg(feature = "winnow-parser")] fn test_parser_configs_includes_winnow() { let configs = parser_configs(); assert!(configs.len() >= 2); assert!(configs.iter().any(|c| c.name == "winnow")); } #[test] fn test_parse_with_config_basic() { let config = ParserConfig { name: "peg", parser_impl: ParserImpl::Peg, }; let result = parse_with_config("echo hello", &config); assert!(result.is_ok()); } } brush-parser-0.4.0/src/parser/tests/pipelines.rs000064400000000000000000000040371046102023000200310ustar 00000000000000//! Tests for pipeline parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; #[test] fn parse_simple_pipe() -> Result<()> { let input = "echo hello | grep world"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_multi_stage_pipe() -> Result<()> { let input = "cat file | grep pattern | wc -l"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_pipe_with_stderr() -> Result<()> { let input = "echo |& wc"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_timed_pipeline() -> Result<()> { let input = "time echo hello"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_timed_pipeline_posix() -> Result<()> { let input = "time -p echo hello"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_negated_pipeline() -> Result<()> { let input = "! echo hello"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_negated_timed_pipeline() -> Result<()> { let input = "time ! echo hello"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_pipe_with_multiple_commands() -> Result<()> { let input = "ls -la | head -10 | tail -5"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/redirections.rs000064400000000000000000000101311046102023000205230ustar 00000000000000//! Tests for redirection parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; // File redirections #[test] fn parse_redirect_output() -> Result<()> { let input = "echo hello > file.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_input() -> Result<()> { let input = "cat < input.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_append() -> Result<()> { let input = "echo hello >> file.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_clobber() -> Result<()> { let input = "echo hello >| file.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_read_write() -> Result<()> { let input = "cat <> file.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // FD operations #[test] fn parse_redirect_stderr_to_stdout() -> Result<()> { let input = "command 2>&1"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_stdout_to_stderr() -> Result<()> { let input = "command 1>&2"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_fd_close() -> Result<()> { let input = "command 2>&-"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_stdin_dup() -> Result<()> { let input = "command <&3"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Combined redirections #[test] fn parse_redirect_output_and_error() -> Result<()> { let input = "command &> file.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_output_and_error_append() -> Result<()> { let input = "command &>> file.txt"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_redirect_multiple() -> Result<()> { let input = "command < input.txt > output.txt 2>&1"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Process substitution #[test] fn parse_process_substitution_read() -> Result<()> { let input = "diff <(sort file1) <(sort file2)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_process_substitution_write() -> Result<()> { let input = "tee >(grep error > errors.txt)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } // Here string #[test] fn parse_here_string() -> Result<()> { let input = "cat <<< 'hello world'"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_here_string_with_variable() -> Result<()> { let input = "cat <<< $variable"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } brush-parser-0.4.0/src/parser/tests/simple_commands.rs000064400000000000000000000056271046102023000212210ustar 00000000000000//! Tests for simple command parsing. use super::{ParseResult, test_with_snapshot}; use crate::assert_snapshot_redacted; use anyhow::Result; #[test] fn parse_echo_hello() -> Result<()> { let input = "echo hello"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_ls() -> Result<()> { let input = "ls"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_colon() -> Result<()> { let input = ":"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_true() -> Result<()> { let input = "true"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_echo_hello_world() -> Result<()> { let input = "echo hello world"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_ls_la_home() -> Result<()> { let input = "ls -la /home"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_command_with_quoted_args() -> Result<()> { let input = r#"echo "hello world""#; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_command_with_single_quotes() -> Result<()> { let input = "echo 'hello world'"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_command_with_backslash_escape() -> Result<()> { let input = r"echo hello\ world"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_command_with_variable() -> Result<()> { let input = "echo $HOME"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_command_with_command_substitution() -> Result<()> { let input = "echo $(whoami)"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } #[test] fn parse_command_with_backtick_substitution() -> Result<()> { let input = "echo `whoami`"; let result = test_with_snapshot(input)?; assert_snapshot_redacted!(ParseResult { input, result: &result }); Ok(()) } ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_and_or_in_sequence.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_and_o000064400000000000000000000027551046102023000326300ustar 00000000000000--- source: brush-parser/src/parser/tests/and_or_lists.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd1 && cmd2; cmd3 || cmd4", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd1", loc: "[location]", )), )), ], ), additional: [ And(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd2", loc: "[location]", )), )), ], )), ], ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd3", loc: "[location]", )), )), ], ), additional: [ Or(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd4", loc: "[location]", )), )), ], )), ], ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000166000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_and_or_with_pipes.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_and_o000064400000000000000000000023651046102023000326250ustar 00000000000000--- source: brush-parser/src/parser/tests/and_or_lists.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd1 | cmd2 && cmd3 | cmd4", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd1", loc: "[location]", )), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd2", loc: "[location]", )), )), ], ), additional: [ And(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd3", loc: "[location]", )), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd4", loc: "[location]", )), )), ], )), ], ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_chained_and.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_chain000064400000000000000000000022061046102023000326210ustar 00000000000000--- source: brush-parser/src/parser/tests/and_or_lists.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd1 && cmd2 && cmd3", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd1", loc: "[location]", )), )), ], ), additional: [ And(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd2", loc: "[location]", )), )), ], )), And(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd3", loc: "[location]", )), )), ], )), ], ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_chained_or.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_chain000064400000000000000000000022041046102023000326170ustar 00000000000000--- source: brush-parser/src/parser/tests/and_or_lists.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd1 || cmd2 || cmd3", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd1", loc: "[location]", )), )), ], ), additional: [ Or(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd2", loc: "[location]", )), )), ], )), Or(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd3", loc: "[location]", )), )), ], )), ], ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000161000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_mixed_and_or.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_mixed000064400000000000000000000022051046102023000326440ustar 00000000000000--- source: brush-parser/src/parser/tests/and_or_lists.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd1 && cmd2 || cmd3", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd1", loc: "[location]", )), )), ], ), additional: [ And(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd2", loc: "[location]", )), )), ], )), Or(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd3", loc: "[location]", )), )), ], )), ], ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_simple_and.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_simpl000064400000000000000000000020671046102023000326700ustar 00000000000000--- source: brush-parser/src/parser/tests/and_or_lists.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "true && echo yes", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), additional: [ And(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "yes", loc: "[location]", )), ])), )), ], )), ], ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000156000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_simple_or.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__and_or_lists__parse_simpl000064400000000000000000000020661046102023000326670ustar 00000000000000--- source: brush-parser/src/parser/tests/and_or_lists.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "false || echo no", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "false", loc: "[location]", )), )), ], ), additional: [ Or(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "no", loc: "[location]", )), ])), )), ], )), ], ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_append.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000016271046102023000327040ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x+=more", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "more", loc: "[location]", )), append: true, loc: "[location]", ), Word( value: "x+=more", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000174000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_append_quoted.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000016651046102023000327060ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x+=\" more text\"", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "\" more text\"", loc: "[location]", )), append: true, loc: "[location]", ), Word( value: "x+=\" more text\"", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000164000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_array.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000023201046102023000326730ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr=(a b c)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("arr"), value: Array([ (None, Word( value: "a", loc: "[location]", )), (None, Word( value: "b", loc: "[location]", )), (None, Word( value: "c", loc: "[location]", )), ]), loc: "[location]", ), Word( value: "arr=(a b c)", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000174000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_array_element.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000016131046102023000326770ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr[0]=value", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: ArrayElementName("arr", "0"), value: Scalar(Word( value: "value", loc: "[location]", )), loc: "[location]", ), Word( value: "arr[0]=value", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000207000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_array_element_expression.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000016211046102023000326760ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr[i+1]=value", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: ArrayElementName("arr", "i+1"), value: Scalar(Word( value: "value", loc: "[location]", )), loc: "[location]", ), Word( value: "arr[i+1]=value", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000172000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_array_empty.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000014151046102023000326770ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr=()", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("arr"), value: Array([]), loc: "[location]", ), Word( value: "arr=()", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000172000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_array_mixed.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000025061046102023000327010ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr=(a [5]=b c)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("arr"), value: Array([ (None, Word( value: "a", loc: "[location]", )), (Some(Word( value: "5", loc: "[location]", )), Word( value: "b", loc: "[location]", )), (None, Word( value: "c", loc: "[location]", )), ]), loc: "[location]", ), Word( value: "arr=(a [5]=b c)", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000204000000000000007767Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_array_quoted_elements.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000022101046102023000326710ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr=(\"hello world\" \"foo bar\")", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("arr"), value: Array([ (None, Word( value: "\"hello world\"", loc: "[location]", )), (None, Word( value: "\"foo bar\"", loc: "[location]", )), ]), loc: "[location]", ), Word( value: "arr=(\"hello world\" \"foo bar\")", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000201000000000000007764Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_array_with_indices.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000030621046102023000326770ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr=([0]=a [1]=b [2]=c)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("arr"), value: Array([ (Some(Word( value: "0", loc: "[location]", )), Word( value: "a", loc: "[location]", )), (Some(Word( value: "1", loc: "[location]", )), Word( value: "b", loc: "[location]", )), (Some(Word( value: "2", loc: "[location]", )), Word( value: "c", loc: "[location]", )), ]), loc: "[location]", ), Word( value: "arr=([0]=a [1]=b [2]=c)", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000164000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_empty.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000015471046102023000327050ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x=", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "", loc: "[location]", )), loc: "[location]", ), Word( value: "x=", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_quoted.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000016241046102023000327010ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x=\"hello world\"", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "\"hello world\"", loc: "[location]", )), loc: "[location]", ), Word( value: "x=\"hello world\"", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_simple.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000015661046102023000327060ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x=value", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "value", loc: "[location]", )), loc: "[location]", ), Word( value: "x=value", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000174000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_single_quoted.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000016241046102023000327010ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x=\'hello world\'", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "\'hello world\'", loc: "[location]", )), loc: "[location]", ), Word( value: "x=\'hello world\'", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000173000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_with_command.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000025201046102023000326750ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "VAR=value command arg1 arg2", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("VAR"), value: Scalar(Word( value: "value", loc: "[location]", )), loc: "[location]", ), Word( value: "VAR=value", loc: "[location]", )), ])), word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "arg1", loc: "[location]", )), Word(Word( value: "arg2", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000210000000000000007764Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_with_command_substitution.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000015711046102023000327020ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x=$(pwd)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "$(pwd)", loc: "[location]", )), loc: "[location]", ), Word( value: "x=$(pwd)", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000175000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assignment_with_expansion.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_assign000064400000000000000000000016021046102023000326750ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x=$HOME/bin", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "$HOME/bin", loc: "[location]", )), loc: "[location]", ), Word( value: "x=$HOME/bin", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000166000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_declare_assignment.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_declar000064400000000000000000000021661046102023000326510ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "declare -i x=5", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "declare", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "-i", loc: "[location]", )), AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "5", loc: "[location]", )), loc: "[location]", ), Word( value: "x=5", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_export_assignment.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_export000064400000000000000000000020101046102023000327240ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "export VAR=value", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "export", loc: "[location]", )), suffix: Some(CommandSuffix([ AssignmentWord(Assignment( name: VariableName("VAR"), value: Scalar(Word( value: "value", loc: "[location]", )), loc: "[location]", ), Word( value: "VAR=value", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000164000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_local_assignment.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_local_000064400000000000000000000017641046102023000326530ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "local x=5", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "local", loc: "[location]", )), suffix: Some(CommandSuffix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "5", loc: "[location]", )), loc: "[location]", ), Word( value: "x=5", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_multiple_assignments.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_multip000064400000000000000000000031761046102023000327330ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "x=1 y=2 z=3", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "1", loc: "[location]", )), loc: "[location]", ), Word( value: "x=1", loc: "[location]", )), AssignmentWord(Assignment( name: VariableName("y"), value: Scalar(Word( value: "2", loc: "[location]", )), loc: "[location]", ), Word( value: "y=2", loc: "[location]", )), AssignmentWord(Assignment( name: VariableName("z"), value: Scalar(Word( value: "3", loc: "[location]", )), loc: "[location]", ), Word( value: "z=3", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000205000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_multiple_assignments_with_command.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__assignments__parse_multip000064400000000000000000000026021046102023000327240ustar 00000000000000--- source: brush-parser/src/parser/tests/assignments.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "A=1 B=2 command", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("A"), value: Scalar(Word( value: "1", loc: "[location]", )), loc: "[location]", ), Word( value: "A=1", loc: "[location]", )), AssignmentWord(Assignment( name: VariableName("B"), value: Scalar(Word( value: "2", loc: "[location]", )), loc: "[location]", ), Word( value: "B=2", loc: "[location]", )), ])), word_or_name: Some(Word( value: "command", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_arithmetic_in_condition.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_arithmetic000064400000000000000000000052331046102023000326620ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "if (( x > 5 )); then echo big; else echo small; fi", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(IfClause(IfClauseCommand( condition: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Arithmetic(ArithmeticCommand( expr: UnexpandedArithmeticExpr( value: "x > 5", ), loc: "[location]", )), None), ], ), ), Sequence), ]), then: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "big", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), elses: Some([ ElseClause( body: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "small", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_backgrounded_commands.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_background000064400000000000000000000022441046102023000326470ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd1 & cmd2 & cmd3", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd1", loc: "[location]", )), )), ], ), ), Async), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd2", loc: "[location]", )), )), ], ), ), Async), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd3", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_brace_expansion_context.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_brace_expa000064400000000000000000000013541046102023000326220ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo {a,b,c}", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "{a,b,c}", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000161000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_case_no_semicolon.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_case_no_se000064400000000000000000000034151046102023000326270ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "case x in\nx)\n echo y\nesac", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(CaseClause(CaseClauseCommand( value: Word( value: "x", loc: "[location]", ), cases: [ CaseItem( patterns: [ Word( value: "x", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "y", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), ], loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_case_with_newlines.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_case_with_000064400000000000000000000034171046102023000326400ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "case x in\nx)\n echo y;;\nesac", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(CaseClause(CaseClauseCommand( value: Word( value: "x", loc: "[location]", ), cases: [ CaseItem( patterns: [ Word( value: "x", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "y", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), ], loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000173000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_command_substitution_nested.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_command_su000064400000000000000000000014021046102023000326500ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo \"$(echo $(pwd))\"", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"$(echo $(pwd))\"", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_complex_array_operations.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_complex_ar000064400000000000000000000037761046102023000326740ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "arr=($(seq 1 10)); echo \"${arr[@]}\"; echo \"${#arr[@]}\"", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("arr"), value: Array([ (None, Word( value: "$(seq 1 10)", loc: "[location]", )), ]), loc: "[location]", ), Word( value: "arr=($(seq 1 10))", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"${arr[@]}\"", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"${#arr[@]}\"", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000151000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_coprocess.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_coprocess.000064400000000000000000000012051046102023000326020ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "coproc cat", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Coprocess(CoprocessCommand( body: Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), )), )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_extglob_patterns.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_extglob_pa000064400000000000000000000017761046102023000326650ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "ls !(*.txt) +(foo|bar) ?(a|b)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "ls", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "!(*.txt)", loc: "[location]", )), Word(Word( value: "+(foo|bar)", loc: "[location]", )), Word(Word( value: "?(a|b)", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000175000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_for_with_command_substitution.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_for_with_c000064400000000000000000000031021046102023000326450ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "for f in $(ls *.txt); do cat \"$f\"; done", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ForClause(ForClauseCommand( variable_name: "f", values: Some([ Word( value: "$(ls *.txt)", loc: "[location]", ), ]), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"$f\"", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_function_with_if.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_function_w000064400000000000000000000107621046102023000327070ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "myfunc() {\n if [[ -z \"$1\" ]]; then\n echo \"No argument\"\n return 1\n fi\n echo \"Got: $1\"\n}", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Function(FunctionDefinition( fname: Word( value: "myfunc", loc: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(IfClause(IfClauseCommand( condition: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(StringHasZeroLength, Word( value: "\"$1\"", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), then: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"No argument\"", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "return", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "1", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"Got: $1\"", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000155000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_glob_patterns.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_glob_patte000064400000000000000000000017721046102023000326550ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "ls *.txt **/foo.* file?.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "ls", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "*.txt", loc: "[location]", )), Word(Word( value: "**/foo.*", loc: "[location]", )), Word(Word( value: "file?.txt", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_mixed_sequences.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_mixed_sequ000064400000000000000000000034061046102023000326740ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd1; cmd2 && cmd3 || cmd4; cmd5", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd1", loc: "[location]", )), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd2", loc: "[location]", )), )), ], ), additional: [ And(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd3", loc: "[location]", )), )), ], )), Or(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd4", loc: "[location]", )), )), ], )), ], ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cmd5", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_multiple_here_docs.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_multiple_h000064400000000000000000000026701046102023000326750ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cmd < /dev/null 2>&1", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(None, Read, Filename(Word( value: "input.txt", loc: "[location]", )))), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "grep", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "pattern", loc: "[location]", )), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "tee", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "output.txt", loc: "[location]", )), IoRedirect(File(None, Write, Filename(Word( value: "/dev/null", loc: "[location]", )))), IoRedirect(File(Some(2), DuplicateOutput, Duplicate(Word( value: "1", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000163000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_shebang_and_program.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_shebang_an000064400000000000000000000040631046102023000326160ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "#!/usr/bin/env bash\n\nfor f in A B C; do\n\n # sdfsdf\n echo \"${f@L}\" >&2\n\n done\n", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ForClause(ForClauseCommand( variable_name: "f", values: Some([ Word( value: "A", loc: "[location]", ), Word( value: "B", loc: "[location]", ), Word( value: "C", loc: "[location]", ), ]), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"${f@L}\"", loc: "[location]", )), IoRedirect(File(None, DuplicateOutput, Duplicate(Word( value: "2", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000171000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_subshell_with_assignments.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_subshell_w000064400000000000000000000056061046102023000327040ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "( x=1; y=2; echo $((x + y)) )", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Subshell(SubshellCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "1", loc: "[location]", )), loc: "[location]", ), Word( value: "x=1", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( prefix: Some(CommandPrefix([ AssignmentWord(Assignment( name: VariableName("y"), value: Scalar(Word( value: "2", loc: "[location]", )), loc: "[location]", ), Word( value: "y=2", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$((x + y))", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_tilde_expansion.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__complex__parse_tilde_expa000064400000000000000000000023751046102023000326530ustar 00000000000000--- source: brush-parser/src/parser/tests/complex.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cd ~/projects; ls ~user/home", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cd", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "~/projects", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "ls", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "~user/home", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000204000000000000007767Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_arith_and_non_arith_parens.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000050171046102023000326460ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.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: "[location]", )), )), ], ), additional: [ And(Pipeline( seq: [ Compound(Subshell(SubshellCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Arithmetic(ArithmeticCommand( expr: UnexpandedArithmeticExpr( value: "0", ), loc: "[location]", )), None), ], ), additional: [ Or(Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: ":", loc: "[location]", )), )), ], )), ], ), Sequence), ]), loc: "[location]", )), None), ], )), ], ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000174000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_arithmetic_complex.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000011401046102023000326370ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "(( x = 5 + 3 * 2 ))", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Arithmetic(ArithmeticCommand( expr: UnexpandedArithmeticExpr( value: "x = 5 + 3 * 2", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_arithmetic_for.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000033341046102023000326460ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "for (( i = 0; i < 10; i++ )); do echo $i; done", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ArithmeticForClause(ArithmeticForClauseCommand( initializer: Some(UnexpandedArithmeticExpr( value: "i = 0", )), condition: Some(UnexpandedArithmeticExpr( value: "i < 10", )), updater: Some(UnexpandedArithmeticExpr( value: "i++", )), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$i", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000204000000000000007767Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_arithmetic_for_empty_parts.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000033031046102023000326420ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "for (( ; ; )); do echo loop; done", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ArithmeticForClause(ArithmeticForClauseCommand( initializer: Some(UnexpandedArithmeticExpr( value: "", )), condition: Some(UnexpandedArithmeticExpr( value: "", )), updater: Some(UnexpandedArithmeticExpr( value: "", )), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "loop", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_arithmetic_increment.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000011141046102023000326400ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "(( x++ ))", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Arithmetic(ArithmeticCommand( expr: UnexpandedArithmeticExpr( value: "x++", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000173000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_arithmetic_simple.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000011201046102023000326350ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "(( 1 + 2 ))", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Arithmetic(ArithmeticCommand( expr: UnexpandedArithmeticExpr( value: "1 + 2", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_brace_group.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000023311046102023000326420ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "{ echo hello; }", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000177000000000000010000Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_brace_group_multiline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000036371046102023000326540ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "{\n echo hello\n echo world\n}", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "world", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_case_continue.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000056471046102023000326570ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "case x in a) echo a;;& b) echo b;; esac", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(CaseClause(CaseClauseCommand( value: Word( value: "x", loc: "[location]", ), cases: [ CaseItem( patterns: [ Word( value: "a", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "a", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ContinueEvaluatingCases, loc: "[location]", ), CaseItem( patterns: [ Word( value: "b", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "b", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), ], loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000172000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_case_fallthrough.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000056611046102023000326530ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "case x in a) echo a;& b) echo b;; esac", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(CaseClause(CaseClauseCommand( value: Word( value: "x", loc: "[location]", ), cases: [ CaseItem( patterns: [ Word( value: "a", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "a", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: UnconditionallyExecuteNextCaseItem, loc: "[location]", ), CaseItem( patterns: [ Word( value: "b", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "b", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), ], loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000200000000000000007763Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_case_multiple_patterns.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000103001046102023000326350ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "case x in\n a|b) echo ab;;\n c) echo c;;\n *) echo default;;\nesac", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(CaseClause(CaseClauseCommand( value: Word( value: "x", loc: "[location]", ), cases: [ CaseItem( patterns: [ Word( value: "a", loc: "[location]", ), Word( value: "b", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "ab", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), CaseItem( patterns: [ Word( value: "c", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "c", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), CaseItem( patterns: [ Word( value: "*", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "default", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), ], loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_case_simple.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000034221046102023000326440ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "case x in a) echo a;; esac", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(CaseClause(CaseClauseCommand( value: Word( value: "x", loc: "[location]", ), cases: [ CaseItem( patterns: [ Word( value: "a", loc: "[location]", ), ], cmd: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "a", loc: "[location]", )), ])), )), ], ), ), Sequence), ])), post_action: ExitCase, loc: "[location]", ), ], loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_for_in.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000034361046102023000326510ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "for x in a b c; do echo $x; done", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ForClause(ForClauseCommand( variable_name: "x", values: Some([ Word( value: "a", loc: "[location]", ), Word( value: "b", loc: "[location]", ), Word( value: "c", loc: "[location]", ), ]), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$x", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000172000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_for_in_multiline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000034431046102023000326470ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "for x in a b c\ndo\n echo $x\ndone", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ForClause(ForClauseCommand( variable_name: "x", values: Some([ Word( value: "a", loc: "[location]", ), Word( value: "b", loc: "[location]", ), Word( value: "c", loc: "[location]", ), ]), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$x", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000163000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_for_no_in.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000026441046102023000326510ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "for x; do echo $x; done", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ForClause(ForClauseCommand( variable_name: "x", values: None, body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$x", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000161000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_if_elif.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000100711046102023000326420ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "if false; then echo one; elif true; then echo two; else echo three; fi", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(IfClause(IfClauseCommand( condition: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "false", loc: "[location]", )), )), ], ), ), Sequence), ]), then: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "one", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), elses: Some([ ElseClause( condition: Some(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), ), Sequence), ])), body: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "two", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ), ElseClause( body: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "three", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000161000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_if_else.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000051731046102023000326510ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "if true; then echo yes; else echo no; fi", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(IfClause(IfClauseCommand( condition: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), ), Sequence), ]), then: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "yes", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), elses: Some([ ElseClause( body: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "no", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000166000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_if_multiline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000052051046102023000326450ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "if true\nthen\n echo yes\nelse\n echo no\nfi", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(IfClause(IfClauseCommand( condition: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), ), Sequence), ]), then: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "yes", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), elses: Some([ ElseClause( body: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "no", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000163000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_if_simple.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000033211046102023000326420ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "if true; then echo yes; fi", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(IfClause(IfClauseCommand( condition: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), ), Sequence), ]), then: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "yes", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000171000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_nested_subshell.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000011361046102023000326440ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "( ( echo nested ) )", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(Arithmetic(ArithmeticCommand( expr: UnexpandedArithmeticExpr( value: "echo nested", ), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_subshell.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000023241046102023000326440ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "( echo hello )", 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: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000204000000000000007767Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_subshell_multiple_commands.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000036211046102023000326450ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "( echo hello; echo world )", 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: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "world", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_until.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000037711046102023000326530ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "until false; do echo loop; done", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(UntilClause(WhileOrUntilClauseCommand(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "false", loc: "[location]", )), )), ], ), ), Sequence), ]), DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "loop", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 31, line: 1, column: 32, ), ))), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_while.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000037671046102023000326600ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "while true; do echo loop; done", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(WhileClause(WhileOrUntilClauseCommand(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), ), Sequence), ]), DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "loop", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", ), SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 30, line: 1, column: 31, ), ))), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000171000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_while_multiline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__compound_commands__parse_000064400000000000000000000046741046102023000326560ustar 00000000000000--- source: brush-parser/src/parser/tests/compound_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "while true\ndo\n echo loop\n break\ndone", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(WhileClause(WhileOrUntilClauseCommand(CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), ), Sequence), ]), DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "loop", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "break", loc: "[location]", )), )), ], ), ), Sequence), ]), loc: "[location]", ), SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 42, line: 5, column: 5, ), ))), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_and.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000014261046102023000326660ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -f file && -r file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: And(UnaryTest(FileExistsAndIsRegularFile, Word( value: "file", loc: "[location]", )), UnaryTest(FileExistsAndIsReadable, Word( value: "file", loc: "[location]", ))), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000177000000000000010000Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_arith_equal.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013241046102023000326630ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ 5 -eq 5 ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(ArithmeticEqualTo, Word( value: "5", loc: "[location]", ), Word( value: "5", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000206000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_arith_greater_than.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013301046102023000326600ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ 5 -gt 3 ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(ArithmeticGreaterThan, Word( value: "5", loc: "[location]", ), Word( value: "3", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000203000000000000007766Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_arith_less_than.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013251046102023000326640ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ 3 -lt 5 ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(ArithmeticLessThan, Word( value: "3", loc: "[location]", ), Word( value: "5", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000203000000000000007766Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_arith_not_equal.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013271046102023000326660ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ 5 -ne 3 ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(ArithmeticNotEqualTo, Word( value: "5", loc: "[location]", ), Word( value: "3", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000173000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_complex.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000016661046102023000326740ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ ( -f file && -r file ) || -d file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: Or(Parenthesized(And(UnaryTest(FileExistsAndIsRegularFile, Word( value: "file", loc: "[location]", )), UnaryTest(FileExistsAndIsReadable, Word( value: "file", loc: "[location]", )))), UnaryTest(FileExistsAndIsDir, Word( value: "file", loc: "[location]", ))), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000175000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_directory.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012131046102023000326600ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -d /path/to/dir ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(FileExistsAndIsDir, Word( value: "/path/to/dir", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_executable.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012021046102023000326560ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -x file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(FileExistsAndIsExecutable, Word( value: "file", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000177000000000000010000Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_file_exists.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012031046102023000326570ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -f file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(FileExistsAndIsRegularFile, Word( value: "file", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000200000000000000007763Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_greater_than.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013501046102023000326620ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ \"$a\" > \"$b\" ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(LeftSortsAfterRight, Word( value: "\"$a\"", loc: "[location]", ), Word( value: "\"$b\"", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000175000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_less_than.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013511046102023000326630ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ \"$a\" < \"$b\" ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(LeftSortsBeforeRight, Word( value: "\"$a\"", loc: "[location]", ), Word( value: "\"$b\"", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_not.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012121046102023000326570ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ ! -f file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: Not(UnaryTest(FileExistsAndIsRegularFile, Word( value: "file", loc: "[location]", ))), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000166000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_or.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000014201046102023000326600ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -f file || -d file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: Or(UnaryTest(FileExistsAndIsRegularFile, Word( value: "file", loc: "[location]", )), UnaryTest(FileExistsAndIsDir, Word( value: "file", loc: "[location]", ))), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000201000000000000007764Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_parenthesized.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012261046102023000326640ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ ( -f file ) ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: Parenthesized(UnaryTest(FileExistsAndIsRegularFile, Word( value: "file", loc: "[location]", ))), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000174000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_readable.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012001046102023000326540ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -r file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(FileExistsAndIsReadable, Word( value: "file", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000171000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_regex.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013601046102023000326630ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ \"$str\" =~ ^[0-9]+$ ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(StringMatchesRegex, Word( value: "\"$str\"", loc: "[location]", ), Word( value: "^[0-9]+$", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000205000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_regex_with_spaces.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013461046102023000326670ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ \"x\" =~ (a| *) ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(StringMatchesRegex, Word( value: "\"x\"", loc: "[location]", ), Word( value: "(a| *)", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000200000000000000007763Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_string_equal.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013611046102023000326640ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ \"$a\" == \"$b\" ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(StringExactlyMatchesPattern, Word( value: "\"$a\"", loc: "[location]", ), Word( value: "\"$b\"", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000203000000000000007766Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_string_non_zero.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012071046102023000326630ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -n \"$var\" ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(StringHasNonZeroLength, Word( value: "\"$var\"", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000204000000000000007767Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_string_not_equal.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013661046102023000326710ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ \"$a\" != \"$b\" ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(StringDoesNotExactlyMatchPattern, Word( value: "\"$a\"", loc: "[location]", ), Word( value: "\"$b\"", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000202000000000000007765Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_string_pattern.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000013731046102023000326670ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ \"$str\" == *pattern* ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: BinaryTest(StringExactlyMatchesPattern, Word( value: "\"$str\"", loc: "[location]", ), Word( value: "*pattern*", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000206000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_string_zero_length.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012041046102023000326600ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -z \"$var\" ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(StringHasZeroLength, Word( value: "\"$var\"", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000174000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_extended_test_writable.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__extended_test__parse_exte000064400000000000000000000012001046102023000326540ustar 00000000000000--- source: brush-parser/src/parser/tests/extended_test.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "[[ -w file ]]", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ ExtendedTest(ExtendedTestExprCommand( expr: UnaryTest(FileExistsAndIsWritable, Word( value: "file", loc: "[location]", )), loc: "[location]", ), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_basic.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000027011046102023000327140ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "foo() { echo hello; }", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Function(FunctionDefinition( fname: Word( value: "foo", loc: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_keyword.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000027101046102023000327140ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "function foo { echo hello; }", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Function(FunctionDefinition( fname: Word( value: "foo", loc: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_keyword_with_parens.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000027121046102023000327160ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "function foo() { echo hello; }", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Function(FunctionDefinition( fname: Word( value: "foo", loc: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000164000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_multiline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000042531046102023000327200ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "foo() {\n echo hello\n echo world\n}", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Function(FunctionDefinition( fname: Word( value: "foo", loc: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "world", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000172000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_with_local_vars.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000050011046102023000327100ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "foo() {\n local x=1\n echo $x\n}", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Function(FunctionDefinition( fname: Word( value: "foo", loc: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "local", loc: "[location]", )), suffix: Some(CommandSuffix([ AssignmentWord(Assignment( name: VariableName("x"), value: Scalar(Word( value: "1", loc: "[location]", )), loc: "[location]", ), Word( value: "x=1", loc: "[location]", )), ])), )), ], ), ), Sequence), CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$x", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_with_redirect.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000034761046102023000327260ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.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: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "1", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), Some(RedirectList([ File(Some(2), DuplicateOutput, Duplicate(Word( value: "1", loc: "[location]", ))), ]))), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000177000000000000010000Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_with_stderr_redirect.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000033241046102023000327160ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.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: "[location]", ), body: FunctionBody(BraceGroup(BraceGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "1", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), Some(RedirectList([ File(Some(2), DuplicateOutput, Fd(1)), ]))), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000175000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function_with_subshell_body.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__functions__parse_function000064400000000000000000000027021046102023000327150ustar 00000000000000--- source: brush-parser/src/parser/tests/functions.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "foo() ( echo subshell )", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Function(FunctionDefinition( fname: Word( value: "foo", loc: "[location]", ), body: FunctionBody(Subshell(SubshellCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "subshell", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), None), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__here_docs__parse_here_doc_basic.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__here_docs__parse_here_doc000064400000000000000000000020761046102023000325670ustar 00000000000000--- source: brush-parser/src/parser/tests/here_docs.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cat <&2\n\n done\n\n", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Compound(ForClause(ForClauseCommand( variable_name: "f", values: Some([ Word( value: "A", loc: Some(SourceSpan( start: SourcePosition( index: 32, line: 5, column: 10, ), end: SourcePosition( index: 33, line: 5, column: 11, ), )), ), Word( value: "B", loc: Some(SourceSpan( start: SourcePosition( index: 34, line: 5, column: 12, ), end: SourcePosition( index: 35, line: 5, column: 13, ), )), ), Word( value: "C", loc: Some(SourceSpan( start: SourcePosition( index: 36, line: 5, column: 14, ), end: SourcePosition( index: 37, line: 5, column: 15, ), )), ), ]), body: DoGroupCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: Some(SourceSpan( start: SourcePosition( index: 60, line: 8, column: 5, ), end: SourcePosition( index: 64, line: 8, column: 9, ), )), )), suffix: Some(CommandSuffix([ Word(Word( value: "\"${f@L}\"", loc: Some(SourceSpan( start: SourcePosition( index: 65, line: 8, column: 10, ), end: SourcePosition( index: 73, line: 8, column: 18, ), )), )), IoRedirect(File(None, DuplicateOutput, Duplicate(Word( value: "2", loc: Some(SourceSpan( start: SourcePosition( index: 76, line: 8, column: 21, ), end: SourcePosition( index: 77, line: 8, column: 22, ), )), )))), ])), )), ], ), ), Sequence), ]), loc: SourceSpan( start: SourcePosition( index: 39, line: 5, column: 17, ), end: SourcePosition( index: 86, line: 10, column: 8, ), ), ), loc: SourceSpan( start: SourcePosition( index: 23, line: 5, column: 1, ), end: SourcePosition( index: 86, line: 10, column: 8, ), ), )), None), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_multi_stage_pipe.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_multi_st000064400000000000000000000027541046102023000327170ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cat file | grep pattern | wc -l", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "file", loc: "[location]", )), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "grep", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "pattern", loc: "[location]", )), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "wc", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "-l", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_negated_pipeline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_negated_000064400000000000000000000014041046102023000326140ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "! echo hello", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( bang: true, seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_negated_timed_pipeline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_negated_000064400000000000000000000021121046102023000326110ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "time ! echo hello", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( timed: Some(Timed(SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 4, line: 1, column: 5, ), ))), bang: true, seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000175000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_pipe_with_multiple_commands.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_pipe_wit000064400000000000000000000027441046102023000326760ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "ls -la | head -10 | tail -5", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "ls", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "-la", loc: "[location]", )), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "head", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "-10", loc: "[location]", )), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "tail", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "-5", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_pipe_with_stderr.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_pipe_wit000064400000000000000000000015501046102023000326700ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo |& wc", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(Some(2), DuplicateOutput, Fd(1))), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "wc", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000155000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_simple_pipe.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_simple_p000064400000000000000000000021601046102023000326560ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo hello | grep world", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), Simple(SimpleCommand( word_or_name: Some(Word( value: "grep", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "world", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_timed_pipeline.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_timed_pi000064400000000000000000000020601046102023000326370ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "time echo hello", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( timed: Some(Timed(SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 4, line: 1, column: 5, ), ))), seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000166000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_timed_pipeline_posix.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__pipelines__parse_timed_pi000064400000000000000000000021021046102023000326340ustar 00000000000000--- source: brush-parser/src/parser/tests/pipelines.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "time -p echo hello", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( timed: Some(TimedWithPosixOutput(SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 7, line: 1, column: 8, ), ))), seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_here_string.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_here_000064400000000000000000000014331046102023000326340ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cat <<< \'hello world\'", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(HereString(None, Word( value: "\'hello world\'", loc: "[location]", ))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_here_string_with_variable.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_here_000064400000000000000000000014171046102023000326360ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cat <<< $variable", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(HereString(None, Word( value: "$variable", loc: "[location]", ))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_process_substitution_read.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_proce000064400000000000000000000047551046102023000326740ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "diff <(sort file1) <(sort file2)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "diff", loc: "[location]", )), suffix: Some(CommandSuffix([ ProcessSubstitution(Read, SubshellCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "sort", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "file1", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), ProcessSubstitution(Read, SubshellCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "sort", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "file2", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000177000000000000010000Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_process_substitution_write.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_proce000064400000000000000000000034251046102023000326650ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "tee >(grep error > errors.txt)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "tee", loc: "[location]", )), suffix: Some(CommandSuffix([ ProcessSubstitution(Write, SubshellCommand( list: CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "grep", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "error", loc: "[location]", )), IoRedirect(File(None, Write, Filename(Word( value: "errors.txt", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000164000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_append.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000016361046102023000326640ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo hello >> file.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), IoRedirect(File(None, Append, Filename(Word( value: "file.txt", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000165000000000000007775Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_clobber.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000016371046102023000326650ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo hello >| file.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), IoRedirect(File(None, Clobber, Filename(Word( value: "file.txt", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000166000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_fd_close.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014371046102023000326630ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "command 2>&-", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(Some(2), DuplicateOutput, Duplicate(Word( value: "-", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000163000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_input.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014271046102023000326620ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cat < input.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(None, Read, Filename(Word( value: "input.txt", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000166000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_multiple.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000022021046102023000326520ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "command < input.txt > output.txt 2>&1", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(None, Read, Filename(Word( value: "input.txt", loc: "[location]", )))), IoRedirect(File(None, Write, Filename(Word( value: "output.txt", loc: "[location]", )))), IoRedirect(File(Some(2), DuplicateOutput, Duplicate(Word( value: "1", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000164000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_output.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000016341046102023000326620ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo hello > file.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), IoRedirect(File(None, Write, Filename(Word( value: "file.txt", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_output_and_error.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014311046102023000326550ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "command &> file.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(OutputAndError(Word( value: "file.txt", loc: "[location]", ), false)), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000205000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_output_and_error_append.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014311046102023000326550ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "command &>> file.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(OutputAndError(Word( value: "file.txt", loc: "[location]", ), true)), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_read_write.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014361046102023000326620ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "cat <> file.txt", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "cat", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(None, ReadAndWrite, Filename(Word( value: "file.txt", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_stderr_to_stdout.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014371046102023000326630ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "command 2>&1", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(Some(2), DuplicateOutput, Duplicate(Word( value: "1", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_stdin_dup.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014321046102023000326560ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "command <&3", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(None, DuplicateInput, Duplicate(Word( value: "3", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000176000000000000007777Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redirect_stdout_to_stderr.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__redirections__parse_redir000064400000000000000000000014371046102023000326630ustar 00000000000000--- source: brush-parser/src/parser/tests/redirections.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "command 1>&2", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "command", loc: "[location]", )), suffix: Some(CommandSuffix([ IoRedirect(File(Some(1), DuplicateOutput, Duplicate(Word( value: "2", loc: "[location]", )))), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000155000000000000007774Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_colon.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_co000064400000000000000000000010441046102023000326310ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: ":", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: ":", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000205000000000000007770Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_command_with_backslash_escape.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_co000064400000000000000000000014001046102023000326250ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo hello\\ world", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello\\ world", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000212000000000000007766Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_command_with_backtick_substitution.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_co000064400000000000000000000013661046102023000326400ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo `whoami`", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "`whoami`", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000211000000000000007765Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_command_with_command_substitution.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_co000064400000000000000000000013701046102023000326330ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo $(whoami)", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$(whoami)", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000200000000000000007763Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_command_with_quoted_args.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_co000064400000000000000000000014041046102023000326310ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo \"hello world\"", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\"hello world\"", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000202000000000000007765Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_command_with_single_quotes.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_co000064400000000000000000000014041046102023000326310ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo \'hello world\'", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "\'hello world\'", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000175000000000000007776Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_command_with_variable.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_co000064400000000000000000000013601046102023000326320ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo $HOME", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "$HOME", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_echo_hello.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_ec000064400000000000000000000013601046102023000326200ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo hello", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_echo_hello_world.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_ec000064400000000000000000000015641046102023000326260ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "echo hello world", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "echo", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "hello", loc: "[location]", )), Word(Word( value: "world", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000152000000000000007771Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_ls.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_ls000064400000000000000000000010461046102023000326500ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "ls", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "ls", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_ls_la_home.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_ls000064400000000000000000000015541046102023000326540ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "ls -la /home", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "ls", loc: "[location]", )), suffix: Some(CommandSuffix([ Word(Word( value: "-la", loc: "[location]", )), Word(Word( value: "/home", loc: "[location]", )), ])), )), ], ), ), Sequence), ]), ], ), ) ././@LongLink00006440000000000000000000000154000000000000007773Lustar brush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_true.snapbrush-parser-0.4.0/src/parser/tests/snapshots/brush_parser__parser__tests__simple_commands__parse_tr000064400000000000000000000010521046102023000326540ustar 00000000000000--- source: brush-parser/src/parser/tests/simple_commands.rs expression: "ParseResult { input, result: &result }" --- ParseResult( input: "true", result: Program( complete_commands: [ CompoundList([ CompoundListItem(AndOrList( first: Pipeline( seq: [ Simple(SimpleCommand( word_or_name: Some(Word( value: "true", loc: "[location]", )), )), ], ), ), Sequence), ]), ], ), ) brush-parser-0.4.0/src/parser/winnow_str.rs000064400000000000000000000010251046102023000171020ustar 00000000000000//! String-based winnow parser use winnow::error::ContextError; use crate::ast; use crate::parser::{ParserOptions, SourceInfo}; /// Type alias for parser error type PError = winnow::error::ErrMode; /// Parse a shell program from a string with full source location tracking /// /// This is not yet implemented. pub fn parse_program( _input: &str, _options: &ParserOptions, _source_info: &SourceInfo, ) -> Result { unimplemented!("winnow string parser is not yet implemented") } brush-parser-0.4.0/src/pattern.rs000064400000000000000000000303241046102023000150560ustar 00000000000000//! Implements parsing for shell glob and extglob patterns. use crate::error; /// Represents the kind of an extended glob. pub enum ExtendedGlobKind { /// The `+` extended glob; matches one or more occurrences of the inner pattern. Plus, /// The `@` extended glob; allows matching an alternation of inner patterns. At, /// The `!` extended glob; matches the negation of the inner pattern. Exclamation, /// The `?` extended glob; matches zero or one occurrence of the inner pattern. Question, /// The `*` extended glob; matches zero or more occurrences of the inner pattern. Star, } /// Converts a shell pattern to a regular expression string. /// /// # Arguments /// /// * `pattern` - The shell pattern to convert. /// * `enable_extended_globbing` - Whether to enable extended globbing (extglob). pub fn pattern_to_regex_str( pattern: &str, enable_extended_globbing: bool, ) -> Result { let regex_str = pattern_to_regex_translator::pattern(pattern, enable_extended_globbing) .map_err(|e| error::WordParseError::Pattern(e.into()))?; Ok(regex_str) } peg::parser! { grammar pattern_to_regex_translator(enable_extended_globbing: bool) for str { pub(crate) rule pattern() -> String = pieces:(pattern_piece()*) { pieces.join("") } rule pattern_piece() -> String = escape_sequence() / bracket_expression() / extglob_enabled() s:extended_glob_pattern() { s } / wildcard() / [c if regex_char_needs_escaping(c)] { let mut s = '\\'.to_string(); s.push(c); s } / [c] { c.to_string() } rule escape_sequence() -> String = sequence:$(['\\'] [c if regex_char_needs_escaping(c)]) { sequence.to_owned() } / ['\\'] [c] { c.to_string() } rule bracket_expression() -> String = "[" invert:(invert_char()?) members:bracket_member()+ "]" { let mut members = members.into_iter().flatten().collect::>(); // If we completed the parse but ended up with no valid members // of the bracket expression, then return a regex that matches nothing. // (Or in the inverted case, matches everything.) if members.is_empty() { if invert.is_some() { String::from(".") } else { String::from("(?!)") } } else { if invert.is_some() { members.insert(0, String::from("^")); } std::format!("[{}]", members.join("")) } } rule invert_char() -> bool = ['!' | '^'] { true } rule bracket_member() -> Option = e:char_class_expression() { Some(e) } / r:char_range() { r } / m:single_char_bracket_member() { let (char_str, _) = m; Some(char_str) } rule char_class_expression() -> String = e:$("[:" char_class() ":]") { e.to_owned() } rule char_class() = "alnum" / "alpha" / "blank" / "cntrl" / "digit" / "graph" / "lower" / "print" / "punct" / "space" / "upper"/ "xdigit" rule char_range() -> Option = from:single_char_bracket_member() "-" to:single_char_bracket_member() { let (from_str, from_c) = from; let (to_str, to_c) = to; // Evaluate if the range is valid. if from_c <= to_c { Some(std::format!("{from_str}-{to_str}")) } else { None } } rule single_char_bracket_member() -> (String, char) = // Preserve escaped characters as-is. ['\\'] [c] { (std::format!("\\{c}"), c) } / // Escape opening bracket. ['['] { (String::from(r"\["), '[') } / // Any other character except closing bracket gets added as-is. [c if c != ']'] { (c.to_string(), c) } rule wildcard() -> String = "?" { String::from(".") } / "*" { String::from(".*") } rule extglob_enabled() -> () = &[_] {? if enable_extended_globbing { Ok(()) } else { Err("extglob disabled") } } pub(crate) rule extended_glob_pattern() -> String = kind:extended_glob_prefix() "(" branches:extended_glob_body() ")" { let mut s = String::new(); // fancy_regex uses ?! to indicate a negative lookahead. if matches!(kind, ExtendedGlobKind::Exclamation) { if !branches.is_empty() { s.push_str("(?:(?!"); s.push_str(&branches.join("|")); s.push_str(").*|(?>"); s.push_str(&branches.join("|")); s.push_str(").+?|)"); } else { s.push_str("(?:.+)"); } } else { s.push('('); s.push_str(&branches.join("|")); s.push(')'); match kind { ExtendedGlobKind::Plus => s.push('+'), ExtendedGlobKind::Question => s.push('?'), ExtendedGlobKind::Star => s.push('*'), ExtendedGlobKind::At | ExtendedGlobKind::Exclamation => (), } } s } rule extended_glob_prefix() -> ExtendedGlobKind = "+" { ExtendedGlobKind::Plus } / "@" { ExtendedGlobKind::At } / "!" { ExtendedGlobKind::Exclamation } / "?" { ExtendedGlobKind::Question } / "*" { ExtendedGlobKind::Star } pub(crate) rule extended_glob_body() -> Vec = // Cover case with *no* branches. &[')'] { vec![] } / // Otherwise, look for branches separated by '|'. extended_glob_branch() ** "|" rule extended_glob_branch() -> String = // Cover case of empty branch. &['|' | ')'] { String::new() } / pieces:(!['|' | ')'] piece:pattern_piece() { piece })+ { pieces.join("") } // A glob metacharacter construct: wildcard, bracket expression, or extglob. rule glob_piece() = bracket_expression() / extglob_enabled() extended_glob_pattern() / wildcard() // A non-glob piece: an escape sequence or any character not starting a glob. rule non_glob_piece() = escape_sequence() / !glob_piece() [_] // Succeeds (returning true) if the pattern contains at least one glob // metacharacter. The same bracket_expression, wildcard, and // extended_glob_pattern rules used for regex conversion are reused here // via negative lookaheads, keeping a single source of truth. pub(crate) rule has_glob_metacharacters() -> bool = non_glob_piece()* glob_piece() [_]* { true } } } /// Returns whether a pattern string contains any glob metacharacters. /// /// Uses the same PEG grammar rules that `pattern_to_regex_str` uses, keeping /// a single source of truth for what constitutes a glob metacharacter. /// /// # Arguments /// /// * `pattern` - The shell pattern to check. /// * `enable_extended_globbing` - Whether to enable extended globbing (extglob). pub fn pattern_has_glob_metacharacters(pattern: &str, enable_extended_globbing: bool) -> bool { pattern_to_regex_translator::has_glob_metacharacters(pattern, enable_extended_globbing) .unwrap_or(false) } /// Returns whether or not a given character needs to be escaped in a regular expression. /// /// # Arguments /// /// * `c` - The character to check. pub const fn regex_char_needs_escaping(c: char) -> bool { matches!( c, '[' | ']' | '(' | ')' | '{' | '}' | '*' | '?' | '.' | '+' | '^' | '$' | '|' | '\\' | '-' ) } #[cfg(test)] #[expect(clippy::panic_in_result_fn)] mod tests { use super::*; use anyhow::Result; #[test] fn test_bracket_exprs() -> Result<()> { assert_eq!(pattern_to_regex_str("[a-z]", true)?, "[a-z]"); assert_eq!(pattern_to_regex_str("[z-a]", true)?, "(?!)"); assert_eq!(pattern_to_regex_str("[+-/]", true)?, "[+-/]"); assert_eq!(pattern_to_regex_str(r"[\*-/]", true)?, r"[\*-/]"); assert_eq!(pattern_to_regex_str("[abc]", true)?, "[abc]"); assert_eq!(pattern_to_regex_str(r"[\(]", true)?, r"[\(]"); assert_eq!(pattern_to_regex_str(r"[(]", true)?, "[(]"); assert_eq!(pattern_to_regex_str("[[:digit:]]", true)?, "[[:digit:]]"); assert_eq!(pattern_to_regex_str(r"[-(),!]*", true)?, r"[-(),!].*"); assert_eq!(pattern_to_regex_str(r"[-\(\),\!]*", true)?, r"[-\(\),\!].*"); assert_eq!(pattern_to_regex_str(r"[a\-b]", true)?, r"[a\-b]"); assert_eq!(pattern_to_regex_str(r"[a\-\*]", true)?, r"[a\-\*]"); Ok(()) } #[test] fn test_extended_glob() -> Result<()> { assert_eq!( pattern_to_regex_translator::extended_glob_pattern("@(a|b)", true)?, "(a|b)" ); assert_eq!( pattern_to_regex_translator::extended_glob_pattern("@(|a)", true)?, "(|a)" ); assert_eq!( pattern_to_regex_translator::extended_glob_pattern("@(|)", true)?, "(|)" ); assert_eq!( pattern_to_regex_translator::extended_glob_body("ab|ac", true)?, vec!["ab", "ac"], ); assert_eq!( pattern_to_regex_translator::extended_glob_pattern("*(ab|ac)", true)?, "(ab|ac)*" ); assert_eq!( pattern_to_regex_translator::extended_glob_body("", true)?, Vec::::new(), ); Ok(()) } #[test] fn test_has_glob_metacharacters() { // Basic metacharacters. assert!(pattern_has_glob_metacharacters("*", false)); assert!(pattern_has_glob_metacharacters("?", false)); assert!(pattern_has_glob_metacharacters("a*b", false)); assert!(pattern_has_glob_metacharacters("a?b", false)); // Valid bracket expressions. assert!(pattern_has_glob_metacharacters("[abc]", false)); assert!(pattern_has_glob_metacharacters("[a-z]", false)); assert!(pattern_has_glob_metacharacters("[!a]", false)); // Lone `]` is NOT a glob metacharacter. assert!(!pattern_has_glob_metacharacters("]", false)); assert!(!pattern_has_glob_metacharacters("foo]", false)); assert!(!pattern_has_glob_metacharacters("a]b", false)); // Lone `[` without matching `]` is NOT a glob metacharacter. assert!(!pattern_has_glob_metacharacters("[", false)); assert!(!pattern_has_glob_metacharacters("[abc", false)); assert!(!pattern_has_glob_metacharacters("a[b", false)); // Plain text β€” no glob chars. assert!(!pattern_has_glob_metacharacters("hello", false)); assert!(!pattern_has_glob_metacharacters("", false)); // Backslash-escaped metacharacters are not globs. assert!(!pattern_has_glob_metacharacters(r"\*", false)); assert!(!pattern_has_glob_metacharacters(r"\?", false)); assert!(!pattern_has_glob_metacharacters(r"\[abc]", false)); // Extglob patterns β€” not detected without extended globbing. assert!(!pattern_has_glob_metacharacters("@(a)", false)); assert!(!pattern_has_glob_metacharacters("!(a)", false)); assert!(!pattern_has_glob_metacharacters("+(a)", false)); // Extglob patterns β€” detected with extended globbing. assert!(pattern_has_glob_metacharacters("@(a)", true)); assert!(pattern_has_glob_metacharacters("!(a)", true)); assert!(pattern_has_glob_metacharacters("+(a)", true)); // *( and ?( are already caught by * and ? checks. assert!(pattern_has_glob_metacharacters("*(a)", false)); assert!(pattern_has_glob_metacharacters("?(a)", false)); } } brush-parser-0.4.0/src/prompt.rs000064400000000000000000000155111046102023000147230ustar 00000000000000//! Parser for shell prompt syntax (e.g., `PS1`). use crate::error; /// A piece of a prompt string. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum PromptPiece { /// An ASCII character. AsciiCharacter(u32), /// A backslash character. Backslash, /// The bell character. BellCharacter, /// A carriage return character. CarriageReturn, /// The current command number. CurrentCommandNumber, /// The current history number. CurrentHistoryNumber, /// The name of the current user. CurrentUser, /// Path to the current working directory. CurrentWorkingDirectory { /// Whether or not to apply tilde-replacement before expanding. tilde_replaced: bool, /// Whether or not to only expand to the basename of the directory. basename: bool, }, /// The current date, using the given format. Date(PromptDateFormat), /// The dollar or pound character. DollarOrPound, /// Special marker indicating the end of a non-printing sequence of characters. EndNonPrintingSequence, /// The escape character. EscapeCharacter, /// An escaped sequence not otherwise recognized. EscapedSequence(String), /// The hostname of the system. Hostname { /// Whether or not to include only up to the first dot of the name. only_up_to_first_dot: bool, }, /// A literal string. Literal(String), /// A newline character. Newline, /// The number of actively managed jobs. NumberOfManagedJobs, /// The base name of the shell. ShellBaseName, /// The release of the shell. ShellRelease, /// The version of the shell. ShellVersion, /// Special marker indicating the start of a non-printing sequence of characters. StartNonPrintingSequence, /// The base name of the terminal device. TerminalDeviceBaseName, /// The current time, using the given format. Time(PromptTimeFormat), } /// Format for a date in a prompt. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum PromptDateFormat { /// A format including weekday, month, and date. WeekdayMonthDate, /// A customer string format. Custom(String), } /// Format for a time in a prompt. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum PromptTimeFormat { /// A twelve-hour time format with AM/PM. TwelveHourAM, /// A twelve-hour time format (HHMMSS). TwelveHourHHMMSS, /// A twenty-four-hour time format (HHMM). TwentyFourHourHHMM, /// A twenty-four-hour time format (HHMMSS). TwentyFourHourHHMMSS, } peg::parser! { grammar prompt_parser() for str { pub(crate) rule prompt() -> Vec = pieces:prompt_piece()* rule prompt_piece() -> PromptPiece = special_sequence() / literal_sequence() // // Reference: https://www.gnu.org/software/bash/manual/bash.html#Controlling-the-Prompt // rule special_sequence() -> PromptPiece = "\\a" { PromptPiece::BellCharacter } / "\\A" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMM) } / "\\d" { PromptPiece::Date(PromptDateFormat::WeekdayMonthDate) } / "\\D{" f:date_format() "}" { PromptPiece::Date(PromptDateFormat::Custom(f)) } / "\\e" { PromptPiece::EscapeCharacter } / "\\h" { PromptPiece::Hostname { only_up_to_first_dot: true } } / "\\H" { PromptPiece::Hostname { only_up_to_first_dot: false } } / "\\j" { PromptPiece::NumberOfManagedJobs } / "\\l" { PromptPiece::TerminalDeviceBaseName } / "\\n" { PromptPiece::Newline } / "\\r" { PromptPiece::CarriageReturn } / "\\s" { PromptPiece::ShellBaseName } / "\\t" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMMSS ) } / "\\T" { PromptPiece::Time(PromptTimeFormat::TwelveHourHHMMSS ) } / "\\@" { PromptPiece::Time(PromptTimeFormat::TwelveHourAM ) } / "\\u" { PromptPiece::CurrentUser } / "\\v" { PromptPiece::ShellVersion } / "\\V" { PromptPiece::ShellRelease } / "\\w" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: false, } } / "\\W" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: true, } } / "\\!" { PromptPiece::CurrentHistoryNumber } / "\\#" { PromptPiece::CurrentCommandNumber } / "\\$" { PromptPiece::DollarOrPound } / "\\" n:octal_number() { PromptPiece::AsciiCharacter(n) } / "\\\\" { PromptPiece::Backslash } / "\\[" { PromptPiece::StartNonPrintingSequence } / "\\]" { PromptPiece::EndNonPrintingSequence } / s:$("\\" [_]) { PromptPiece::EscapedSequence(s.to_owned()) } rule literal_sequence() -> PromptPiece = s:$((!special_sequence() [c])+) { PromptPiece::Literal(s.to_owned()) } rule date_format() -> String = s:$([c if c != '}']*) { s.to_owned() } rule octal_number() -> u32 = s:$(['0'..='7']*<1,3>) {? u32::from_str_radix(s, 8).or(Err("invalid octal number")) } } } /// Parses a shell prompt string. /// /// # Arguments /// /// * `s` - The prompt string to parse. pub fn parse(s: &str) -> Result, error::WordParseError> { let result = prompt_parser::prompt(s).map_err(|e| error::WordParseError::Prompt(e.into()))?; Ok(result) } #[cfg(test)] mod tests { use super::*; use anyhow::Result; use pretty_assertions::assert_eq; #[test] fn basic_prompt() -> Result<()> { assert_eq!( parse(r"\u@\h:\w$ ")?, &[ PromptPiece::CurrentUser, PromptPiece::Literal("@".to_owned()), PromptPiece::Hostname { only_up_to_first_dot: true }, PromptPiece::Literal(":".to_owned()), PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: false }, PromptPiece::Literal("$ ".to_owned()), ] ); Ok(()) } #[test] fn brackets_and_vars() -> Result<()> { assert_eq!( parse(r"\[$foo\]\u > ")?, &[ PromptPiece::StartNonPrintingSequence, PromptPiece::Literal("$foo".to_owned()), PromptPiece::EndNonPrintingSequence, PromptPiece::CurrentUser, PromptPiece::Literal(" > ".to_owned()), ] ); Ok(()) } } brush-parser-0.4.0/src/readline_binding.rs000064400000000000000000000200221046102023000166500ustar 00000000000000//! Implements a parser for readline binding syntax. use crate::error; /// Represents a key-sequence-to-shell-command binding. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct KeySequenceShellCommandBinding { /// Key sequence to bind pub seq: KeySequence, /// Shell command to bind to the sequence pub shell_cmd: String, } /// Represents a key-sequence-to-readline-command binding. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct KeySequenceReadlineBinding { /// Key sequence to bind pub seq: KeySequence, /// Readline target to bind to the sequence pub target: ReadlineTarget, } /// Represents a readline target. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum ReadlineTarget { /// A named readline function. Function(String), /// A readline command macro. Macro(String), } /// Represents a key sequence. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct KeySequence(pub Vec); /// Represents an element of a key sequence. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum KeySequenceItem { /// Control Control, /// Meta Meta, /// Regular character Byte(u8), } /// Represents a single key stroke. #[derive(Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct KeyStroke { /// Meta key is held down pub meta: bool, /// Control key is held down pub control: bool, /// Primary key code pub key_code: Vec, } /// Parses a key sequence. /// /// # Arguments /// /// * `input` - The input string to parse pub fn parse_key_sequence(input: &str) -> Result { readline_binding::key_sequence(input) .map_err(|_err| error::BindingParseError::Unknown(input.to_owned())) } /// Parses a binding specification that maps a key sequence /// to a shell command. /// /// # Arguments /// /// * `input` - The input string to parse pub fn parse_key_sequence_shell_cmd_binding( input: &str, ) -> Result { readline_binding::key_sequence_shell_cmd_binding(input) .map_err(|_err| error::BindingParseError::Unknown(input.to_owned())) } /// Parses a binding specification that maps a key sequence /// to a readline target. /// /// # Arguments /// /// * `input` - The input string to parse pub fn parse_key_sequence_readline_binding( input: &str, ) -> Result { readline_binding::key_sequence_readline_binding(input) .map_err(|_err| error::BindingParseError::Unknown(input.to_owned())) } /// Converts a `KeySequence` to a vector of `KeyStroke`. /// /// # Arguments /// /// * `seq` - The key sequence to convert pub fn key_sequence_to_strokes( seq: &KeySequence, ) -> Result, error::BindingParseError> { let mut strokes = vec![]; let mut current_stroke = KeyStroke::default(); for item in &seq.0 { if matches!( item, KeySequenceItem::Control | KeySequenceItem::Meta | KeySequenceItem::Byte(b'\x1b') ) && !current_stroke.key_code.is_empty() { strokes.push(current_stroke); current_stroke = KeyStroke::default(); } match item { KeySequenceItem::Control => current_stroke.control = true, KeySequenceItem::Meta => current_stroke.meta = true, KeySequenceItem::Byte(b) => { current_stroke.key_code.push(*b); // If this is a control or meta stroke, the modifier only applies to this one byte, // so we need to push the stroke and start fresh for subsequent bytes. if current_stroke.control || current_stroke.meta { strokes.push(current_stroke); current_stroke = KeyStroke::default(); } } } } if current_stroke.key_code.is_empty() { if current_stroke.control || current_stroke.meta { return Err(error::BindingParseError::MissingKeyCode); } } else { strokes.push(current_stroke); } Ok(strokes) } peg::parser! { grammar readline_binding() for str { rule _() = [' ' | '\t' | '\n']* pub rule key_sequence_shell_cmd_binding() -> KeySequenceShellCommandBinding = _ "\"" seq:key_sequence() "\"" _ ":" _ cmd:shell_cmd() _ { KeySequenceShellCommandBinding { seq, shell_cmd: cmd } } pub rule key_sequence_readline_binding() -> KeySequenceReadlineBinding = _ "\"" seq:key_sequence() "\"" _ ":" _ "\"" cmd:readline_cmd() "\"" _ { KeySequenceReadlineBinding { seq, target: ReadlineTarget::Macro(cmd) } } / _ "\"" seq:key_sequence() "\"" _ ":" _ func:readline_function() _ { KeySequenceReadlineBinding { seq, target: ReadlineTarget::Function(func) } } rule readline_cmd() -> String = s:$([^'"']*) { s.to_string() } rule shell_cmd() -> String = s:$([_]*) { s.to_string() } rule readline_function() -> String = s:$([_]*) { s.to_string() } // Main rule for parsing a key sequence pub rule key_sequence() -> KeySequence = items:key_sequence_item()* { KeySequence(items) } rule key_sequence_item() -> KeySequenceItem = "\\C-" { KeySequenceItem::Control } / "\\M-" { KeySequenceItem::Meta } / "\\e" { KeySequenceItem::Byte(b'\x1b') } / "\\\\" { KeySequenceItem::Byte(b'\\') } / "\\\"" { KeySequenceItem::Byte(b'"') } / "\\'" { KeySequenceItem::Byte(b'\'') } / "\\a" { KeySequenceItem::Byte(b'\x07') } / "\\b" { KeySequenceItem::Byte(b'\x08') } / "\\d" { KeySequenceItem::Byte(b'\x7f') } / "\\f" { KeySequenceItem::Byte(b'\x0c') } / "\\n" { KeySequenceItem::Byte(b'\n') } / "\\r" { KeySequenceItem::Byte(b'\r') } / "\\t" { KeySequenceItem::Byte(b'\t') } / "\\v" { KeySequenceItem::Byte(b'\x0b') } / "\\" n:octal_number() { KeySequenceItem::Byte(n) } / "\\" n:hex_number() { KeySequenceItem::Byte(n) } / [c if c != '"'] { KeySequenceItem::Byte(c as u8) } rule octal_number() -> u8 = s:$(['0'..='7']*<1,3>) {? u8::from_str_radix(s, 8).or(Err("invalid octal number")) } rule hex_number() -> u8 = s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*<1,2>) {? u8::from_str_radix(s, 16).or(Err("invalid hex number")) } } } #[cfg(test)] #[expect(clippy::panic_in_result_fn)] mod tests { use super::*; use anyhow::Result; #[test] fn test_basic_shell_cmd_binding_parse() -> Result<()> { let binding = parse_key_sequence_shell_cmd_binding(r#""\C-k": xyz"#)?; assert_eq!( binding.seq.0, [KeySequenceItem::Control, KeySequenceItem::Byte(b'k')] ); assert_eq!(binding.shell_cmd, "xyz"); Ok(()) } #[test] fn test_basic_readline_func_binding_parse() -> Result<()> { let binding = parse_key_sequence_readline_binding(r#""\M-x": some-function"#)?; assert_eq!( binding.seq.0, [KeySequenceItem::Meta, KeySequenceItem::Byte(b'x')] ); assert_eq!( binding.target, ReadlineTarget::Function("some-function".to_string()) ); Ok(()) } #[test] fn test_basic_readline_cmd_binding_parse() -> Result<()> { let binding = parse_key_sequence_readline_binding(r#""\C-k": "xyz""#)?; assert_eq!( binding.seq.0, [KeySequenceItem::Control, KeySequenceItem::Byte(b'k')] ); assert_eq!(binding.target, ReadlineTarget::Macro(String::from("xyz"))); Ok(()) } } brush-parser-0.4.0/src/snapshot_tests.rs000064400000000000000000000066131046102023000164660ustar 00000000000000use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; #[derive(Clone, Debug, Deserialize, Serialize)] struct TestCaseSet { /// Name of the test case set pub name: Option, /// Set of test cases pub cases: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] struct TestCase { /// Name of the test case pub name: Option, #[serde(default)] pub stdin: Option, } #[test] #[ignore = "not yet ready for default-enablement"] fn test_parser_using_yaml_test_cases() { insta::glob!("../../brush-shell", "tests/cases/**/*.yaml", |path| { test_parser_using_yaml(path).unwrap(); }); } fn test_parser_using_yaml(path: &Path) -> Result<()> { let yaml_file = std::fs::File::open(path)?; let test_case_set: TestCaseSet = serde_yaml::from_reader(yaml_file) .context(format!("parsing {}", path.to_string_lossy()))?; test_parser_using_test_case_set(&test_case_set); Ok(()) } fn test_parser_using_test_case_set(test_case_set: &TestCaseSet) { let name = test_case_set.name.as_deref().unwrap_or_default(); for test_case in &test_case_set.cases { parse(name, test_case); } } // NOTE: The name of this function affects the name of the snapshot generated. fn parse(test_case_set_name: &str, test_case: &TestCase) { #[derive(serde::Serialize, serde::Deserialize)] struct TestCaseInfo { test_case_set: String, test_case: String, } let name = test_case.name.as_deref().unwrap_or_default(); let script_content = test_case.stdin.as_deref().unwrap_or_default(); if script_content.is_empty() { return; } let summary = parse_script_content(script_content); let info = TestCaseInfo { test_case_set: test_case_set_name.to_string(), test_case: name.to_string(), }; // Generate a cleaned-up name. let snapshot_suffix = std::format!("{test_case_set_name}-{name}") .to_lowercase() .replace(' ', "_") .replace(|c: char| !c.is_ascii_alphanumeric(), "_"); insta::with_settings!({ info => &info, prepend_module_to_snapshot => false, omit_expression => true, snapshot_suffix => snapshot_suffix, }, { insta::assert_ron_snapshot!(summary); }); } #[cfg_attr(test, derive(serde::Serialize))] struct ParseSummary<'a> { input: Vec<&'a str>, result: ParseResult, } #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] enum ParseResult { Success(crate::ast::Program), Failure(String), } fn parse_script_content(s: &str) -> ParseSummary<'_> { let input_lines: Vec<_> = s.lines().collect(); let tokens = match crate::tokenize_str_with_options(s, &crate::TokenizerOptions::default()) { Ok(tokens) => tokens, Err(err) => { return ParseSummary { input: input_lines, result: ParseResult::Failure(err.to_string()), }; } }; let parsed_program = match crate::parse_tokens(&tokens, &crate::ParserOptions::default()) { Ok(parsed_program) => parsed_program, Err(err) => { return ParseSummary { input: input_lines, result: ParseResult::Failure(err.to_string()), }; } }; ParseSummary { input: input_lines, result: ParseResult::Success(parsed_program), } } brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_arithmetic_expression.snap000064400000000000000000000011321046102023000330030ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"a$((1+2))b c\")?" --- TokenizerResult( input: "a$((1+2))b c", result: [ Word("a$((1+2))b", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 10, line: 1, column: 11, ), )), Word("c", SourceSpan( start: SourcePosition( index: 11, line: 1, column: 12, ), end: SourcePosition( index: 12, line: 1, column: 13, ), )), ], ) ././@LongLink00006440000000000000000000000161000000000000007771Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_arithmetic_expression_with_parens.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_arithmetic_expression_with000064400000000000000000000005721046102023000331050ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$(( (0) ))\")?" --- TokenizerResult( input: "$(( (0) ))", result: [ Word("$(( (0) ))", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 10, line: 1, column: 11, ), )), ], ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_arithmetic_expression_with_space.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_arithmetic_expression_with000064400000000000000000000005621046102023000331040ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$(( 1 ))\")?" --- TokenizerResult( input: "$(( 1 ))", result: [ Word("$(( 1 ))", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 8, line: 1, column: 9, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_backquote_with_escape.snap000064400000000000000000000011431046102023000327260ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(r\"echo `echo\\`hi`\")?" --- TokenizerResult( input: "echo `echo\\`hi`", result: [ Word("echo", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 4, line: 1, column: 5, ), )), Word("`echo\\`hi`", SourceSpan( start: SourcePosition( index: 5, line: 1, column: 6, ), end: SourcePosition( index: 15, line: 1, column: 16, ), )), ], ) ././@LongLink00006440000000000000000000000154000000000000007773Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_braced_parameter_expansion-2.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_braced_parameter_expansion000064400000000000000000000005541046102023000330060ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"a${x}b\")?" --- TokenizerResult( input: "a${x}b", result: [ Word("a${x}b", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 6, line: 1, column: 7, ), )), ], ) ././@LongLink00006440000000000000000000000152000000000000007771Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_braced_parameter_expansion.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_braced_parameter_expansion000064400000000000000000000005461046102023000330070ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"${x}\")?" --- TokenizerResult( input: "${x}", result: [ Word("${x}", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 4, line: 1, column: 5, ), )), ], ) ././@LongLink00006440000000000000000000000170000000000000007771Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_braced_parameter_expansion_with_escaping.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_braced_parameter_expansion000064400000000000000000000005661046102023000330110ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(r\"a${x\\}}b\")?" --- TokenizerResult( input: "a${x\\}}b", result: [ Word("a${x\\}}b", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 8, line: 1, column: 9, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_command_substitution.snap000064400000000000000000000011401046102023000326440ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"a$(echo hi)b c\")?" --- TokenizerResult( input: "a$(echo hi)b c", result: [ Word("a$(echo hi)b", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 12, line: 1, column: 13, ), )), Word("c", SourceSpan( start: SourcePosition( index: 13, line: 1, column: 14, ), end: SourcePosition( index: 14, line: 1, column: 15, ), )), ], ) ././@LongLink00006440000000000000000000000167000000000000007777Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_command_substitution_containing_extglob.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_command_substitution_conta000064400000000000000000000011451046102023000330750ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"echo $(echo !(x))\")?" --- TokenizerResult( input: "echo $(echo !(x))", result: [ Word("echo", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 4, line: 1, column: 5, ), )), Word("$(echo !(x))", SourceSpan( start: SourcePosition( index: 5, line: 1, column: 6, ), end: SourcePosition( index: 17, line: 1, column: 18, ), )), ], ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_command_substitution_with_subshell.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_command_substitution_with_000064400000000000000000000005621046102023000331050ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$( (:) )\")?" --- TokenizerResult( input: "$( (:) )", result: [ Word("$( (:) )", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 8, line: 1, column: 9, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_comment.snap000064400000000000000000000011221046102023000300340ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(r\"a #comment\n\")?" --- TokenizerResult( input: "a #comment\n", result: [ Word("a", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 1, line: 1, column: 2, ), )), Operator("\n", SourceSpan( start: SourcePosition( index: 2, line: 1, column: 3, ), end: SourcePosition( index: 11, line: 2, column: 1, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_comment_at_eof.snap000064400000000000000000000005601046102023000313560ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(r\"a #comment\")?" --- TokenizerResult( input: "a #comment", result: [ Word("a", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 1, line: 1, column: 2, ), )), ], ) ././@LongLink00006440000000000000000000000171000000000000007772Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_complex_here_docs_in_command_substitution.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_complex_here_docs_in_comma000064400000000000000000000013621046102023000327640ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(r\"echo $(cat <>b\")?" --- TokenizerResult( input: "a>>b", result: [ Word("a", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 1, line: 1, column: 2, ), )), Operator(">>", SourceSpan( start: SourcePosition( index: 1, line: 1, column: 2, ), end: SourcePosition( index: 3, line: 1, column: 4, ), )), Word("b", SourceSpan( start: SourcePosition( index: 3, line: 1, column: 4, ), end: SourcePosition( index: 4, line: 1, column: 5, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_simple_backquote.snap000064400000000000000000000011351046102023000317250ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(r\"echo `echo hi`\")?" --- TokenizerResult( input: "echo `echo hi`", result: [ Word("echo", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 4, line: 1, column: 5, ), )), Word("`echo hi`", SourceSpan( start: SourcePosition( index: 5, line: 1, column: 6, ), end: SourcePosition( index: 14, line: 1, column: 15, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_single_quote.snap000064400000000000000000000005641046102023000311010ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(r\"x'a b'y\")?" --- TokenizerResult( input: "x\'a b\'y", result: [ Word("x\'a b\'y", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 7, line: 1, column: 8, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_special_parameters-2.snap000064400000000000000000000005401046102023000323770ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$@\")?" --- TokenizerResult( input: "$@", result: [ Word("$@", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 2, line: 1, column: 3, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_special_parameters-3.snap000064400000000000000000000005401046102023000324000ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$!\")?" --- TokenizerResult( input: "$!", result: [ Word("$!", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 2, line: 1, column: 3, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_special_parameters-4.snap000064400000000000000000000005401046102023000324010ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$?\")?" --- TokenizerResult( input: "$?", result: [ Word("$?", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 2, line: 1, column: 3, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_special_parameters-5.snap000064400000000000000000000005401046102023000324020ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$*\")?" --- TokenizerResult( input: "$*", result: [ Word("$*", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 2, line: 1, column: 3, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_special_parameters.snap000064400000000000000000000005401046102023000322400ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$$\")?" --- TokenizerResult( input: "$$", result: [ Word("$$", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 2, line: 1, column: 3, ), )), ], ) ././@LongLink00006440000000000000000000000156000000000000007775Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_unbraced_parameter_expansion-2.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_unbraced_parameter_expansi000064400000000000000000000005431046102023000330120ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"a$x\")?" --- TokenizerResult( input: "a$x", result: [ Word("a$x", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 3, line: 1, column: 4, ), )), ], ) ././@LongLink00006440000000000000000000000154000000000000007773Lustar brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_unbraced_parameter_expansion.snapbrush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_unbraced_parameter_expansi000064400000000000000000000005401046102023000330070ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"$x\")?" --- TokenizerResult( input: "$x", result: [ Word("$x", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 2, line: 1, column: 3, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__tokenizer__tests__tokenize_whitespace.snap000064400000000000000000000014251046102023000305340ustar 00000000000000--- source: brush-parser/src/tokenizer.rs expression: "test_tokenizer(\"1 2 3\")?" --- TokenizerResult( input: "1 2 3", result: [ Word("1", SourceSpan( start: SourcePosition( index: 0, line: 1, column: 1, ), end: SourcePosition( index: 1, line: 1, column: 2, ), )), Word("2", SourceSpan( start: SourcePosition( index: 2, line: 1, column: 3, ), end: SourcePosition( index: 3, line: 1, column: 4, ), )), Word("3", SourceSpan( start: SourcePosition( index: 4, line: 1, column: 5, ), end: SourcePosition( index: 5, line: 1, column: 6, ), )), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__brace_expansion_parsing-2.snap000064400000000000000000000006271046102023000304560ustar 00000000000000--- source: brush-parser/src/word.rs expression: "super::parse_brace_expansions(input,\n&options)?.ok_or_else(||\nanyhow::anyhow!(\"Expected brace expansion to be parsed successfully\"))?" --- [ Expr([ Child([ Text("a"), ]), Child([ Text("b"), Expr([ Child([ Text("1"), ]), Child([ Text("2"), ]), ]), ]), ]), ] brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__brace_expansion_parsing.snap000064400000000000000000000004701046102023000303130ustar 00000000000000--- source: brush-parser/src/word.rs expression: "super::parse_brace_expansions(input,\n&options)?.ok_or_else(||\nanyhow::anyhow!(\"Expected brace expansion to be parsed successfully\"))?" --- [ Text("x"), Expr([ Child([ Text("a"), ]), Child([ Text("b"), ]), ]), Text("y"), ] brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_ansi_c_quoted_escape_seq.snap000064400000000000000000000003741046102023000316320ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(r\"$'\\\\'\")?" --- ParseTestResults( input: "$\'\\\\\'", result: [ WordPieceWithSource( piece: AnsiCQuotedText("\\\\"), start_index: 0, end_index: 5, ), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_ansi_c_quoted_text.snap000064400000000000000000000004301046102023000304770ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(r\"$'hi\\nthere\\t'\")?" --- ParseTestResults( input: "$\'hi\\nthere\\t\'", result: [ WordPieceWithSource( piece: AnsiCQuotedText("hi\\nthere\\t"), start_index: 0, end_index: 14, ), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_arithmetic_expansion.snap000064400000000000000000000004531046102023000310400ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"$((0))\")?" --- ParseTestResults( input: "$((0))", result: [ WordPieceWithSource( piece: ArithmeticExpression(UnexpandedArithmeticExpr( value: "0", )), start_index: 0, end_index: 6, ), ], ) ././@LongLink00006440000000000000000000000150000000000000007767Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_arithmetic_expansion_with_parens.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_arithmetic_expansion_with_parens.s000064400000000000000000000005041046102023000327410ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"$((((1+2)*3)))\")?" --- ParseTestResults( input: "$((((1+2)*3)))", result: [ WordPieceWithSource( piece: ArithmeticExpression(UnexpandedArithmeticExpr( value: "((1+2)*3)", )), start_index: 0, end_index: 14, ), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_backquoted_command.snap000064400000000000000000000004161046102023000304420ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"`echo hi`\")?" --- ParseTestResults( input: "`echo hi`", result: [ WordPieceWithSource( piece: BackquotedCommandSubstitution("echo hi"), start_index: 0, end_index: 9, ), ], ) ././@LongLink00006440000000000000000000000153000000000000007772Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_backquoted_command_in_double_quotes.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_backquoted_command_in_double_quote000064400000000000000000000006471046102023000327450ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(r#\"\"`echo hi`\"\"#)?" --- ParseTestResults( input: "\"`echo hi`\"", result: [ WordPieceWithSource( piece: DoubleQuotedSequence([ WordPieceWithSource( piece: BackquotedCommandSubstitution("echo hi"), start_index: 1, end_index: 10, ), ]), start_index: 0, end_index: 11, ), ], ) ././@LongLink00006440000000000000000000000153000000000000007772Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_balanced_backticks.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_balanced_backtick000064400000000000000000000007431046102023000326530ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"\\\"$(cat <<'EOF'\\n`hello`\\nEOF\\n)\\\"\")?" --- ParseTestResults( input: "\"$(cat <<\'EOF\'\n`hello`\nEOF\n)\"", result: [ WordPieceWithSource( piece: DoubleQuotedSequence([ WordPieceWithSource( piece: CommandSubstitution("cat <<\'EOF\'\n`hello`\nEOF\n"), start_index: 1, end_index: 28, ), ]), start_index: 0, end_index: 29, ), ], ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_balanced_double_quotes.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_balanced_double_q000064400000000000000000000007551046102023000326750ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"\\\"$(cat <<'EOF'\\n\\\"hello\\\"\\nEOF\\n)\\\"\")?" --- ParseTestResults( input: "\"$(cat <<\'EOF\'\n\"hello\"\nEOF\n)\"", result: [ WordPieceWithSource( piece: DoubleQuotedSequence([ WordPieceWithSource( piece: CommandSubstitution("cat <<\'EOF\'\n\"hello\"\nEOF\n"), start_index: 1, end_index: 28, ), ]), start_index: 0, end_index: 29, ), ], ) ././@LongLink00006440000000000000000000000157000000000000007776Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_balanced_single_quotes.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_balanced_single_q000064400000000000000000000007471046102023000327050ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"\\\"$(cat <<'EOF'\\n'hello'\\nEOF\\n)\\\"\")?" --- ParseTestResults( input: "\"$(cat <<\'EOF\'\n\'hello\'\nEOF\n)\"", result: [ WordPieceWithSource( piece: DoubleQuotedSequence([ WordPieceWithSource( piece: CommandSubstitution("cat <<\'EOF\'\n\'hello\'\nEOF\n"), start_index: 1, end_index: 28, ), ]), start_index: 0, end_index: 29, ), ], ) ././@LongLink00006440000000000000000000000154000000000000007773Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_unbalanced_backtick.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_unbalanced_backti000064400000000000000000000007351046102023000327010ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"\\\"$(cat <<'EOF'\\na ` b\\nEOF\\n)\\\"\")?" --- ParseTestResults( input: "\"$(cat <<\'EOF\'\na ` b\nEOF\n)\"", result: [ WordPieceWithSource( piece: DoubleQuotedSequence([ WordPieceWithSource( piece: CommandSubstitution("cat <<\'EOF\'\na ` b\nEOF\n"), start_index: 1, end_index: 26, ), ]), start_index: 0, end_index: 27, ), ], ) ././@LongLink00006440000000000000000000000160000000000000007770Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_unbalanced_single_quote.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_sub_with_unbalanced_single000064400000000000000000000007531046102023000327250ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"\\\"$(cat <<'EOF'\\nit's here\\nEOF\\n)\\\"\")?" --- ParseTestResults( input: "\"$(cat <<\'EOF\'\nit\'s here\nEOF\n)\"", result: [ WordPieceWithSource( piece: DoubleQuotedSequence([ WordPieceWithSource( piece: CommandSubstitution("cat <<\'EOF\'\nit\'s here\nEOF\n"), start_index: 1, end_index: 30, ), ]), start_index: 0, end_index: 31, ), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_substitution.snap000064400000000000000000000004071046102023000310740ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"$(echo hi)\")?" --- ParseTestResults( input: "$(echo hi)", result: [ WordPieceWithSource( piece: CommandSubstitution("echo hi"), start_index: 0, end_index: 10, ), ], ) ././@LongLink00006440000000000000000000000162000000000000007772Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_substitution_with_embedded_extglob.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_substitution_with_embedded000064400000000000000000000004151046102023000327770ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"$(echo !(x))\")?" --- ParseTestResults( input: "$(echo !(x))", result: [ WordPieceWithSource( piece: CommandSubstitution("echo !(x)"), start_index: 0, end_index: 12, ), ], ) ././@LongLink00006440000000000000000000000161000000000000007771Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_substitution_with_embedded_quotes.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_command_substitution_with_embedded000064400000000000000000000004261046102023000330010ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(r#\"$(echo \"hi\")\"#)?" --- ParseTestResults( input: "$(echo \"hi\")", result: [ WordPieceWithSource( piece: CommandSubstitution("echo \"hi\""), start_index: 0, end_index: 12, ), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_double_quoted_text.snap000064400000000000000000000013241046102023000305200ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(r#\"\"a ${b} c\"\"#)?" --- ParseTestResults( input: "\"a ${b} c\"", result: [ WordPieceWithSource( piece: DoubleQuotedSequence([ WordPieceWithSource( piece: Text("a "), start_index: 1, end_index: 3, ), WordPieceWithSource( piece: ParameterExpansion(Parameter( parameter: Named("b"), indirect: false, )), start_index: 3, end_index: 7, ), WordPieceWithSource( piece: Text(" c"), start_index: 7, end_index: 9, ), ]), start_index: 0, end_index: 10, ), ], ) ././@LongLink00006440000000000000000000000147000000000000007775Lustar brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_extglob_with_embedded_parameter.snapbrush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_extglob_with_embedded_parameter.sn000064400000000000000000000010151046102023000326450ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(\"+([$var])\")?" --- ParseTestResults( input: "+([$var])", result: [ WordPieceWithSource( piece: Text("+(["), start_index: 0, end_index: 3, ), WordPieceWithSource( piece: ParameterExpansion(Parameter( parameter: Named("var"), indirect: false, )), start_index: 3, end_index: 7, ), WordPieceWithSource( piece: Text("])"), start_index: 7, end_index: 9, ), ], ) brush-parser-0.4.0/src/snapshots/brush_parser__word__tests__parse_gettext_double_quoted_text.snap000064400000000000000000000013361046102023000322670ustar 00000000000000--- source: brush-parser/src/word.rs expression: "test_parse(r#\"$\"a ${b} c\"\"#)?" --- ParseTestResults( input: "$\"a ${b} c\"", result: [ WordPieceWithSource( piece: GettextDoubleQuotedSequence([ WordPieceWithSource( piece: Text("a "), start_index: 2, end_index: 4, ), WordPieceWithSource( piece: ParameterExpansion(Parameter( parameter: Named("b"), indirect: false, )), start_index: 4, end_index: 8, ), WordPieceWithSource( piece: Text(" c"), start_index: 8, end_index: 10, ), ]), start_index: 0, end_index: 11, ), ], ) brush-parser-0.4.0/src/source.rs000064400000000000000000000051651046102023000147060ustar 00000000000000use std::{fmt::Display, sync::Arc}; /// Represents a position in source text. #[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 SourcePosition { /// The 0-based index of the character in the input stream. pub index: usize, /// The 1-based line number. pub line: usize, /// The 1-based column number. pub column: usize, } impl Display for SourcePosition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{},{}", self.line, self.column)) } } impl SourcePosition { /// Returns a new `SourcePosition` offset by the given `SourcePositionOffset`. /// /// # Arguments /// /// * `offset` - The offset to apply. #[must_use] pub const fn offset(&self, offset: &SourcePositionOffset) -> Self { Self { index: self.index + offset.index, line: self.line + offset.line, column: if offset.line == 0 { self.column + offset.column } else { offset.column + 1 }, } } } #[cfg(feature = "diagnostics")] impl From<&SourcePosition> for miette::SourceOffset { #[allow(clippy::cast_sign_loss)] fn from(position: &SourcePosition) -> Self { position.index.into() } } /// Represents an offset in source text. #[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 SourcePositionOffset { /// The 0-based character offset. pub index: usize, /// The 0-based line offset. pub line: usize, /// The 0-based column offset. pub column: usize, } /// Represents a span within source text. #[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 SourceSpan { /// The start position. pub start: Arc, /// The end position of the span (exclusive). pub end: Arc, } impl SourceSpan { /// Returns the length of the token in characters. pub fn length(&self) -> usize { self.end.index - self.start.index } pub(crate) fn within(start: &Self, end: &Self) -> Self { Self { start: start.start.clone(), end: end.end.clone(), } } } brush-parser-0.4.0/src/test_command.rs000064400000000000000000000127271046102023000160650ustar 00000000000000//! Parser for shell test commands. use crate::{ast, error}; /// Parses a test command expression. /// /// # Arguments /// /// * `input` - The test command expression to parse, in string form. pub fn parse>(input: &[S]) -> Result { let expr = test_command::full_expression(&input.iter().map(AsRef::as_ref).collect::>())?; Ok(expr) } peg::parser! { grammar test_command<'a>() for [&'a str] { pub(crate) rule full_expression() -> ast::TestExpr = end() { ast::TestExpr::False } / e:one_arg_expr() end() { e } / e:two_arg_expr() end() { e } / e:three_arg_expr() end() { e } / e:four_arg_expr() end() { e } / expression() rule one_arg_expr() -> ast::TestExpr = [s] { ast::TestExpr::Literal(s.to_owned()) } rule two_arg_expr() -> ast::TestExpr = ["!"] e:one_arg_expr() { ast::TestExpr::Not(Box::from(e)) } / op:unary_op() [s] { ast::TestExpr::UnaryTest(op, s.to_owned()) } rule three_arg_expr() -> ast::TestExpr = [left] ["-a"] [right] { ast::TestExpr::And(Box::from(ast::TestExpr::Literal(left.to_owned())), Box::from(ast::TestExpr::Literal(right.to_owned()))) } / [left] ["-o"] [right] { ast::TestExpr::Or(Box::from(ast::TestExpr::Literal(left.to_owned())), Box::from(ast::TestExpr::Literal(right.to_owned()))) } / [left] op:binary_op() [right] { ast::TestExpr::BinaryTest(op, left.to_owned(), right.to_owned()) } / ["!"] e:two_arg_expr() { ast::TestExpr::Not(Box::from(e)) } / ["("] e:one_arg_expr() [")"] { e } rule four_arg_expr() -> ast::TestExpr = ["!"] e:three_arg_expr() { ast::TestExpr::Not(Box::from(e)) } rule expression() -> ast::TestExpr = precedence! { left:(@) ["-a"] right:@ { ast::TestExpr::And(Box::from(left), Box::from(right)) } left:(@) ["-o"] right:@ { ast::TestExpr::Or(Box::from(left), Box::from(right)) } -- ["("] e:expression() [")"] { ast::TestExpr::Parenthesized(Box::from(e)) } -- ["!"] e:@ { ast::TestExpr::Not(Box::from(e)) } -- [left] op:binary_op() [right] { ast::TestExpr::BinaryTest(op, left.to_owned(), right.to_owned()) } -- op:unary_op() [operand] { ast::TestExpr::UnaryTest(op, operand.to_owned()) } -- [s] { ast::TestExpr::Literal(s.to_owned()) } } rule unary_op() -> ast::UnaryPredicate = ["-a"] { ast::UnaryPredicate::FileExists } / ["-b"] { ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile } / ["-c"] { ast::UnaryPredicate::FileExistsAndIsCharSpecialFile } / ["-d"] { ast::UnaryPredicate::FileExistsAndIsDir } / ["-e"] { ast::UnaryPredicate::FileExists } / ["-f"] { ast::UnaryPredicate::FileExistsAndIsRegularFile } / ["-g"] { ast::UnaryPredicate::FileExistsAndIsSetgid } / ["-h"] { ast::UnaryPredicate::FileExistsAndIsSymlink } / ["-k"] { ast::UnaryPredicate::FileExistsAndHasStickyBit } / ["-n"] { ast::UnaryPredicate::StringHasNonZeroLength } / ["-o"] { ast::UnaryPredicate::ShellOptionEnabled } / ["-p"] { ast::UnaryPredicate::FileExistsAndIsFifo } / ["-r"] { ast::UnaryPredicate::FileExistsAndIsReadable } / ["-s"] { ast::UnaryPredicate::FileExistsAndIsNotZeroLength } / ["-t"] { ast::UnaryPredicate::FdIsOpenTerminal } / ["-u"] { ast::UnaryPredicate::FileExistsAndIsSetuid } / ["-v"] { ast::UnaryPredicate::ShellVariableIsSetAndAssigned } / ["-w"] { ast::UnaryPredicate::FileExistsAndIsWritable } / ["-x"] { ast::UnaryPredicate::FileExistsAndIsExecutable } / ["-z"] { ast::UnaryPredicate::StringHasZeroLength } / ["-G"] { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId } / ["-L"] { ast::UnaryPredicate::FileExistsAndIsSymlink } / ["-N"] { ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead } / ["-O"] { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId } / ["-R"] { ast::UnaryPredicate::ShellVariableIsSetAndNameRef } / ["-S"] { ast::UnaryPredicate::FileExistsAndIsSocket } rule binary_op() -> ast::BinaryPredicate = ["=="] { ast::BinaryPredicate::StringExactlyMatchesString } / ["-ef"] { ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers } / ["-eq"] { ast::BinaryPredicate::ArithmeticEqualTo } / ["-ge"] { ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo } / ["-gt"] { ast::BinaryPredicate::ArithmeticGreaterThan } / ["-le"] { ast::BinaryPredicate::ArithmeticLessThanOrEqualTo } / ["-lt"] { ast::BinaryPredicate::ArithmeticLessThan } / ["-ne"] { ast::BinaryPredicate::ArithmeticNotEqualTo } / ["-nt"] { ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot } / ["-ot"] { ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes } / ["="] { ast::BinaryPredicate::StringExactlyMatchesString } / ["!="] { ast::BinaryPredicate::StringDoesNotExactlyMatchString } / ["<"] { ast::BinaryPredicate::LeftSortsBeforeRight } / [">"] { ast::BinaryPredicate::LeftSortsAfterRight } rule end() = ![_] } } brush-parser-0.4.0/src/tokenizer.rs000064400000000000000000001633461046102023000154260ustar 00000000000000use std::borrow::Cow; use std::sync::Arc; use utf8_chars::BufReadCharsExt; use crate::{SourcePosition, SourceSpan}; #[derive(Clone, Debug)] pub(crate) enum TokenEndReason { /// End of input was reached. EndOfInput, /// An unescaped newline char was reached. UnescapedNewLine, /// Specified terminating char. SpecifiedTerminatingChar, /// A non-newline blank char was reached. NonNewLineBlank, /// A here-document's body is starting. HereDocumentBodyStart, /// A here-document's body was terminated. HereDocumentBodyEnd, /// A here-document's end tag was reached. HereDocumentEndTag, /// An operator was started. OperatorStart, /// An operator was terminated. OperatorEnd, /// Some other condition was reached. Other, } /// Compatibility alias for `SourceSpan`. pub type TokenLocation = SourceSpan; /// Represents a token extracted from a shell script. #[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 Token { /// An operator token. Operator(String, SourceSpan), /// A word token. Word(String, SourceSpan), } impl Token { /// Returns the string value of the token. pub fn to_str(&self) -> &str { match self { Self::Operator(s, _) => s, Self::Word(s, _) => s, } } /// Returns the location of the token in the source script. pub const fn location(&self) -> &SourceSpan { match self { Self::Operator(_, l) => l, Self::Word(_, l) => l, } } } #[cfg(feature = "diagnostics")] impl From<&Token> for miette::SourceSpan { fn from(token: &Token) -> Self { let start = token.location().start.as_ref(); Self::new(start.into(), token.location().length()) } } /// Encapsulates the result of tokenizing a shell script. #[derive(Clone, Debug)] pub(crate) struct TokenizeResult { /// Reason for tokenization ending. pub reason: TokenEndReason, /// The token that was extracted, if any. pub token: Option, } /// Represents an error that occurred during tokenization. #[derive(thiserror::Error, Debug)] pub enum TokenizerError { /// An unterminated escape sequence was encountered at the end of the input stream. #[error("unterminated escape sequence")] UnterminatedEscapeSequence, /// An unterminated single-quoted substring was encountered at the end of the input stream. #[error("unterminated single quote at {0}")] UnterminatedSingleQuote(SourcePosition), /// An unterminated ANSI C-quoted substring was encountered at the end of the input stream. #[error("unterminated ANSI C quote at {0}")] UnterminatedAnsiCQuote(SourcePosition), /// An unterminated double-quoted substring was encountered at the end of the input stream. #[error("unterminated double quote at {0}")] UnterminatedDoubleQuote(SourcePosition), /// An unterminated back-quoted substring was encountered at the end of the input stream. #[error("unterminated backquote near {0}")] UnterminatedBackquote(SourcePosition), /// An unterminated extended glob (extglob) pattern was encountered at the end of the input /// stream. #[error("unterminated extglob near {0}")] UnterminatedExtendedGlob(SourcePosition), /// An unterminated variable expression was encountered at the end of the input stream. #[error("unterminated variable expression")] UnterminatedVariable, /// An unterminated command substitiion was encountered at the end of the input stream. #[error("unterminated command substitution")] UnterminatedCommandSubstitution, /// An unterminated arithmetic or other expansion was encountered at the end of the input /// stream. #[error("unterminated expansion")] UnterminatedExpansion, /// An error occurred decoding UTF-8 characters in the input stream. #[error("failed to decode UTF-8 characters")] FailedDecoding, /// An I/O here tag was missing. #[error("missing here tag for here document body")] MissingHereTagForDocumentBody, /// The indicated I/O here tag was missing. #[error("missing here tag '{0}'")] MissingHereTag(String), /// An unterminated here document sequence was encountered at the end of the input stream. #[error("unterminated here document sequence; tag(s) [{0}] found at: [{1}]")] UnterminatedHereDocuments(String, String), /// An I/O error occurred while reading from the input stream. #[error("failed to read input")] ReadError(#[from] std::io::Error), } impl TokenizerError { /// Returns true if the error represents an error that could possibly be due /// to an incomplete input stream. pub const fn is_incomplete(&self) -> bool { matches!( self, Self::UnterminatedEscapeSequence | Self::UnterminatedAnsiCQuote(..) | Self::UnterminatedSingleQuote(..) | Self::UnterminatedDoubleQuote(..) | Self::UnterminatedBackquote(..) | Self::UnterminatedCommandSubstitution | Self::UnterminatedExpansion | Self::UnterminatedVariable | Self::UnterminatedExtendedGlob(..) | Self::UnterminatedHereDocuments(..) ) } } /// Encapsulates a sequence of tokens. #[derive(Debug)] pub(crate) struct Tokens<'a> { /// Sequence of tokens. pub tokens: &'a [Token], } #[derive(Clone, Debug)] enum QuoteMode { None, AnsiC(SourcePosition), Single(SourcePosition), Double(SourcePosition), } #[derive(Clone, Debug, Default)] enum HereState { /// In this state, we are not currently tracking any here-documents. #[default] None, /// In this state, we expect that the next token will be a here tag. NextTokenIsHereTag { remove_tabs: bool }, /// In this state, the *current* token is a here tag. CurrentTokenIsHereTag { remove_tabs: bool, operator_token_result: TokenizeResult, }, /// In this state, we expect that the *next line* will be the body of /// a here-document. NextLineIsHereDoc, /// In this state, we are in the set of lines that comprise 1 or more /// consecutive here-document bodies. InHereDocs, } #[derive(Clone, Debug)] struct HereTag { tag: String, tag_was_escaped_or_quoted: bool, remove_tabs: bool, position: SourcePosition, tokens: Vec, pending_tokens_after: Vec, } #[derive(Clone, Debug)] struct CrossTokenParseState { /// Cursor within the overall token stream; used for error reporting. cursor: SourcePosition, /// Current state of parsing here-documents. here_state: HereState, /// Ordered queue of here tags for which we're still looking for matching here-document bodies. current_here_tags: Vec, /// Tokens already tokenized that should be used first to serve requests for tokens. queued_tokens: Vec, /// Are we in an arithmetic expansion? arithmetic_expansion: bool, } /// Options controlling how the tokenizer operates. #[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct TokenizerOptions { /// Whether or not to enable extended globbing patterns (extglob). pub enable_extended_globbing: bool, /// Whether or not to operate in POSIX compliance mode. pub posix_mode: bool, /// Whether or not we're running in SH emulation mode. pub sh_mode: bool, } impl Default for TokenizerOptions { fn default() -> Self { Self { enable_extended_globbing: true, posix_mode: false, sh_mode: false, } } } /// A tokenizer for shell scripts. pub(crate) struct Tokenizer<'a, R: ?Sized + std::io::BufRead> { char_reader: std::iter::Peekable>, cross_state: CrossTokenParseState, options: TokenizerOptions, } /// Encapsulates the current token parsing state. #[derive(Clone, Debug)] struct TokenParseState { pub start_position: SourcePosition, pub token_so_far: String, pub token_is_operator: bool, pub in_escape: bool, pub quote_mode: QuoteMode, } impl TokenParseState { pub fn new(start_position: &SourcePosition) -> Self { Self { start_position: start_position.to_owned(), token_so_far: String::new(), token_is_operator: false, in_escape: false, quote_mode: QuoteMode::None, } } pub fn pop(&mut self, end_position: &SourcePosition) -> Token { let end = Arc::new(end_position.to_owned()); let token_location = SourceSpan { start: Arc::new(std::mem::take(&mut self.start_position)), end, }; let token = if std::mem::take(&mut self.token_is_operator) { Token::Operator(std::mem::take(&mut self.token_so_far), token_location) } else { Token::Word(std::mem::take(&mut self.token_so_far), token_location) }; end_position.clone_into(&mut self.start_position); self.in_escape = false; self.quote_mode = QuoteMode::None; token } pub const fn started_token(&self) -> bool { !self.token_so_far.is_empty() } pub fn append_char(&mut self, c: char) { self.token_so_far.push(c); } pub fn append_str(&mut self, s: &str) { self.token_so_far.push_str(s); } pub const fn unquoted(&self) -> bool { !self.in_escape && matches!(self.quote_mode, QuoteMode::None) } pub fn current_token(&self) -> &str { &self.token_so_far } pub fn is_specific_operator(&self, operator: &str) -> bool { self.token_is_operator && self.current_token() == operator } pub const fn in_operator(&self) -> bool { self.token_is_operator } fn is_newline(&self) -> bool { self.token_so_far == "\n" } fn replace_with_here_doc(&mut self, s: String) { self.token_so_far = s; } #[allow(clippy::too_many_lines)] pub fn delimit_current_token( &mut self, reason: TokenEndReason, cross_token_state: &mut CrossTokenParseState, ) -> Result, TokenizerError> { // If we don't have anything in the token, then don't yield an empty string token // *unless* it's the body of a here document. if !self.started_token() && !matches!(reason, TokenEndReason::HereDocumentBodyEnd) { return Ok(Some(TokenizeResult { reason, token: None, })); } // TODO(tokenizer): Make sure the here-tag meets criteria (and isn't a newline). let current_here_state = std::mem::take(&mut cross_token_state.here_state); match current_here_state { HereState::NextTokenIsHereTag { remove_tabs } => { // Don't yield the operator as a token yet. We need to make sure we collect // up everything we need for all the here-documents with tags on this line. let operator_token_result = TokenizeResult { reason, token: Some(self.pop(&cross_token_state.cursor)), }; cross_token_state.here_state = HereState::CurrentTokenIsHereTag { remove_tabs, operator_token_result, }; return Ok(None); } HereState::CurrentTokenIsHereTag { remove_tabs, operator_token_result, } => { if self.is_newline() { return Err(TokenizerError::MissingHereTag( self.current_token().to_owned(), )); } cross_token_state.here_state = HereState::NextLineIsHereDoc; // Include the trailing \n in the here tag so it's easier to check against. let tag = std::format!("{}\n", self.current_token().trim_ascii_start()); let tag_was_escaped_or_quoted = tag.contains(is_quoting_char); let tag_token_result = TokenizeResult { reason, token: Some(self.pop(&cross_token_state.cursor)), }; cross_token_state.current_here_tags.push(HereTag { tag, tag_was_escaped_or_quoted, remove_tabs, position: cross_token_state.cursor.clone(), tokens: vec![operator_token_result, tag_token_result], pending_tokens_after: vec![], }); return Ok(None); } HereState::NextLineIsHereDoc => { if self.is_newline() { cross_token_state.here_state = HereState::InHereDocs; } else { cross_token_state.here_state = HereState::NextLineIsHereDoc; } if let Some(last_here_tag) = cross_token_state.current_here_tags.last_mut() { let token = self.pop(&cross_token_state.cursor); let result = TokenizeResult { reason, token: Some(token), }; last_here_tag.pending_tokens_after.push(result); } else { return Err(TokenizerError::MissingHereTagForDocumentBody); } return Ok(None); } HereState::InHereDocs => { // We hit the end of the current here-document. let completed_here_tag = cross_token_state.current_here_tags.remove(0); // First queue the redirection operator and (start) here-tag. for here_token in completed_here_tag.tokens { cross_token_state.queued_tokens.push(here_token); } // Leave a hint that we are about to start a here-document. cross_token_state.queued_tokens.push(TokenizeResult { reason: TokenEndReason::HereDocumentBodyStart, token: None, }); // Then queue the body document we just finished. cross_token_state.queued_tokens.push(TokenizeResult { reason, token: Some(self.pop(&cross_token_state.cursor)), }); // Then queue up the (end) here-tag. let end_tag = if completed_here_tag.tag_was_escaped_or_quoted { unquote_str(&completed_here_tag.tag) } else { completed_here_tag.tag }; self.append_str(end_tag.trim_end_matches('\n')); cross_token_state.queued_tokens.push(TokenizeResult { reason: TokenEndReason::HereDocumentEndTag, token: Some(self.pop(&cross_token_state.cursor)), }); // Now we're ready to queue up any tokens that came between the completed // here tag and the next here tag (or newline after it if it was the last). for pending_token in completed_here_tag.pending_tokens_after { cross_token_state.queued_tokens.push(pending_token); } if cross_token_state.current_here_tags.is_empty() { cross_token_state.here_state = HereState::None; } else { cross_token_state.here_state = HereState::InHereDocs; } return Ok(None); } HereState::None => (), } let token = self.pop(&cross_token_state.cursor); let result = TokenizeResult { reason, token: Some(token), }; Ok(Some(result)) } } /// Break the given input shell script string into tokens, returning the tokens. /// /// # Arguments /// /// * `input` - The shell script to tokenize. pub fn tokenize_str(input: &str) -> Result, TokenizerError> { tokenize_str_with_options(input, &TokenizerOptions::default()) } /// Break the given input shell script string into tokens, returning the tokens. /// /// # Arguments /// /// * `input` - The shell script to tokenize. /// * `options` - Options controlling how the tokenizer operates. pub fn tokenize_str_with_options( input: &str, options: &TokenizerOptions, ) -> Result, TokenizerError> { uncached_tokenize_string(input.to_owned(), options.to_owned()) } #[cached::proc_macro::cached(name = "TOKENIZE_CACHE", size = 64, result = true)] fn uncached_tokenize_string( input: String, options: TokenizerOptions, ) -> Result, TokenizerError> { uncached_tokenize_str(input.as_str(), &options) } /// Break the given input shell script string into tokens, returning the tokens. /// No caching is performed. /// /// # Arguments /// /// * `input` - The shell script to tokenize. pub fn uncached_tokenize_str( input: &str, options: &TokenizerOptions, ) -> Result, TokenizerError> { let mut reader = std::io::BufReader::new(input.as_bytes()); let mut tokenizer = crate::tokenizer::Tokenizer::new(&mut reader, options); let mut tokens = vec![]; loop { match tokenizer.next_token()? { TokenizeResult { token: Some(token), .. } => tokens.push(token), TokenizeResult { reason: TokenEndReason::EndOfInput, .. } => break, _ => (), } } Ok(tokens) } impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> { pub fn new(reader: &'a mut R, options: &TokenizerOptions) -> Self { Tokenizer { options: options.clone(), char_reader: reader.chars().peekable(), cross_state: CrossTokenParseState { cursor: SourcePosition { index: 0, line: 1, column: 1, }, here_state: HereState::None, current_here_tags: vec![], queued_tokens: vec![], arithmetic_expansion: false, }, } } #[expect(clippy::unnecessary_wraps)] pub fn current_location(&self) -> Option { Some(self.cross_state.cursor.clone()) } fn next_char(&mut self) -> Result, TokenizerError> { let c = self .char_reader .next() .transpose() .map_err(TokenizerError::ReadError)?; if let Some(ch) = c { if ch == '\n' { self.cross_state.cursor.line += 1; self.cross_state.cursor.column = 1; } else { self.cross_state.cursor.column += 1; } self.cross_state.cursor.index += 1; } Ok(c) } fn consume_char(&mut self) -> Result<(), TokenizerError> { let _ = self.next_char()?; Ok(()) } fn peek_char(&mut self) -> Result, TokenizerError> { match self.char_reader.peek() { Some(result) => match result { Ok(c) => Ok(Some(*c)), Err(_) => Err(TokenizerError::FailedDecoding), }, None => Ok(None), } } pub fn next_token(&mut self) -> Result { self.next_token_until(None, false /* include space? */) } /// Consumes a nested construct (e.g., `$((...))` or `$[...]`), handling nested delimiters /// and here-documents. /// /// # Arguments /// /// * `state` - The current token parse state to append characters to. /// * `terminating_char` - The character that terminates the construct (e.g., `)` or `]`). /// * `nesting_open` - The character that increases nesting depth when encountered (e.g., `(` or /// `[`). /// * `initial_nesting` - The initial nesting count (e.g., 2 for `$((`, 1 for `$[`). fn consume_nested_construct( &mut self, state: &mut TokenParseState, terminating_char: char, nesting_open: &str, mut nesting_count: u32, ) -> Result<(), TokenizerError> { let mut pending_here_doc_tokens = vec![]; let mut drain_here_doc_tokens = false; loop { let cur_token = if drain_here_doc_tokens && !pending_here_doc_tokens.is_empty() { if pending_here_doc_tokens.len() == 1 { drain_here_doc_tokens = false; } pending_here_doc_tokens.remove(0) } else { let cur_token = self.next_token_until(Some(terminating_char), true)?; if matches!( cur_token.reason, TokenEndReason::HereDocumentBodyStart | TokenEndReason::HereDocumentBodyEnd | TokenEndReason::HereDocumentEndTag ) { pending_here_doc_tokens.push(cur_token); continue; } cur_token }; if matches!(cur_token.reason, TokenEndReason::UnescapedNewLine) && !pending_here_doc_tokens.is_empty() { pending_here_doc_tokens.push(cur_token); drain_here_doc_tokens = true; continue; } if let Some(cur_token_value) = cur_token.token { state.append_str(cur_token_value.to_str()); if matches!(cur_token_value, Token::Operator(o, _) if o == nesting_open) { nesting_count += 1; } } match cur_token.reason { TokenEndReason::HereDocumentBodyStart => { state.append_char('\n'); } TokenEndReason::NonNewLineBlank => state.append_char(' '), TokenEndReason::SpecifiedTerminatingChar => { nesting_count -= 1; if nesting_count == 0 { break; } state.append_char(self.next_char()?.unwrap()); } TokenEndReason::EndOfInput => { return Err(TokenizerError::UnterminatedExpansion); } _ => (), } } state.append_char(self.next_char()?.unwrap()); Ok(()) } /// Returns the next token from the input stream, optionally stopping early when a specified /// terminating character is encountered. /// /// # Arguments /// /// * `terminating_char` - An optional character that, if encountered, will stop the /// tokenization process and return the token up to that character. /// * `include_space` - If true, include spaces in the tokenization process. This is not /// typically the case, but can be helpful when needing to preserve the original source text /// embedded within a command substitution or similar construct. #[expect(clippy::cognitive_complexity)] #[expect(clippy::if_same_then_else)] #[expect(clippy::panic_in_result_fn)] #[expect(clippy::too_many_lines)] #[allow(clippy::unwrap_in_result)] fn next_token_until( &mut self, terminating_char: Option, include_space: bool, ) -> Result { let mut state = TokenParseState::new(&self.cross_state.cursor); let mut result: Option = None; while result.is_none() { // First satisfy token results from our queue. Once we exhaust the queue then // we'll look at the input stream. if !self.cross_state.queued_tokens.is_empty() { return Ok(self.cross_state.queued_tokens.remove(0)); } let next = self.peek_char()?; let c = next.unwrap_or('\0'); // When we hit the end of the input, then we're done with the current token (if there is // one). if next.is_none() { // TODO(tokenizer): Verify we're not waiting on some terminating character? // Verify we're out of all quotes. if state.in_escape { return Err(TokenizerError::UnterminatedEscapeSequence); } match state.quote_mode { QuoteMode::None => (), QuoteMode::AnsiC(pos) => { return Err(TokenizerError::UnterminatedAnsiCQuote(pos)); } QuoteMode::Single(pos) => { return Err(TokenizerError::UnterminatedSingleQuote(pos)); } QuoteMode::Double(pos) => { return Err(TokenizerError::UnterminatedDoubleQuote(pos)); } } // Verify we're not in a here document. if !matches!(self.cross_state.here_state, HereState::None) { if self.remove_here_end_tag(&mut state, &mut result, false)? { // If we hit end tag without a trailing newline, try to get next token. continue; } let tag_names = self .cross_state .current_here_tags .iter() .map(|tag| tag.tag.trim()) .collect::>() .join(", "); let tag_positions = self .cross_state .current_here_tags .iter() .map(|tag| std::format!("{}", tag.position)) .collect::>() .join(", "); return Err(TokenizerError::UnterminatedHereDocuments( tag_names, tag_positions, )); } result = state .delimit_current_token(TokenEndReason::EndOfInput, &mut self.cross_state)?; // // Handle being in a here document. // } else if matches!(self.cross_state.here_state, HereState::InHereDocs) { // // For now, just include the character in the current token. We also check // if there are leading tabs to be removed. // if !self.cross_state.current_here_tags.is_empty() && self.cross_state.current_here_tags[0].remove_tabs && (!state.started_token() || state.current_token().ends_with('\n')) && c == '\t' { // Consume it but don't include it. self.consume_char()?; } else { self.consume_char()?; state.append_char(c); // See if this was a newline character following the terminating here tag. if c == '\n' { self.remove_here_end_tag(&mut state, &mut result, true)?; } } // // Look for the specially specified terminating char. // } else if state.unquoted() && terminating_char == Some(c) { result = state.delimit_current_token( TokenEndReason::SpecifiedTerminatingChar, &mut self.cross_state, )?; } else if state.in_operator() { // // We're in an operator. See if this character continues an operator, or if it // must be a separate token (because it wouldn't make a prefix of an operator). // let mut hypothetical_token = state.current_token().to_owned(); hypothetical_token.push(c); if state.unquoted() && self.is_operator(hypothetical_token.as_ref()) { self.consume_char()?; state.append_char(c); } else { assert!(state.started_token()); // // N.B. If the completed operator indicates a here-document, then keep // track that the *next* token should be the here-tag. // if self.cross_state.arithmetic_expansion { // // We're in an arithmetic context; don't consider << and <<- // special. They're not here-docs, they're either a left-shift // operator or a left-shift operator followed by a unary // minus operator. // if state.is_specific_operator(")") && c == ')' { self.cross_state.arithmetic_expansion = false; } } else if state.is_specific_operator("<<") { self.cross_state.here_state = HereState::NextTokenIsHereTag { remove_tabs: false }; } else if state.is_specific_operator("<<-") { self.cross_state.here_state = HereState::NextTokenIsHereTag { remove_tabs: true }; } else if state.is_specific_operator("(") && c == '(' { self.cross_state.arithmetic_expansion = true; } let reason = if state.current_token() == "\n" { TokenEndReason::UnescapedNewLine } else { TokenEndReason::OperatorEnd }; result = state.delimit_current_token(reason, &mut self.cross_state)?; } // // See if this is a character that changes the current escaping/quoting state. // } else if does_char_newly_affect_quoting(&state, c) { if c == '\\' { // Consume the backslash ourselves so we can peek past it. self.consume_char()?; if matches!(self.peek_char()?, Some('\n')) { // Make sure the newline char gets consumed too. self.consume_char()?; // Make sure to include neither the backslash nor the newline character. } else { state.in_escape = true; state.append_char(c); } } else if c == '\'' { if state.token_so_far.ends_with('$') { state.quote_mode = QuoteMode::AnsiC(self.cross_state.cursor.clone()); } else { state.quote_mode = QuoteMode::Single(self.cross_state.cursor.clone()); } self.consume_char()?; state.append_char(c); } else if c == '\"' { state.quote_mode = QuoteMode::Double(self.cross_state.cursor.clone()); self.consume_char()?; state.append_char(c); } } // // Handle end of single-quote, double-quote, or ANSI-C quote. else if !state.in_escape && matches!( state.quote_mode, QuoteMode::Single(..) | QuoteMode::AnsiC(..) ) && c == '\'' { state.quote_mode = QuoteMode::None; self.consume_char()?; state.append_char(c); } else if !state.in_escape && matches!(state.quote_mode, QuoteMode::Double(..)) && c == '\"' { state.quote_mode = QuoteMode::None; self.consume_char()?; state.append_char(c); } // // Handle end of escape sequence. // TODO(tokenizer): Handle double-quote specific escape sequences. else if state.in_escape { state.in_escape = false; self.consume_char()?; state.append_char(c); } else if (state.unquoted() || (matches!(state.quote_mode, QuoteMode::Double(_)) && !state.in_escape)) && (c == '$' || c == '`') { // TODO(tokenizer): handle quoted $ or ` in a double quote if c == '$' { // Consume the '$' so we can peek beyond. self.consume_char()?; // Now peek beyond to see what we have. let char_after_dollar_sign = self.peek_char()?; match char_after_dollar_sign { Some('(') => { // Add the '$' we already consumed to the token. state.append_char('$'); // Consume the '(' and add it to the token. state.append_char(self.next_char()?.unwrap()); // Check to see if this is possibly an arithmetic expression // (i.e., one that starts with `$((`). let (initial_nesting, is_arithmetic) = if matches!(self.peek_char()?, Some('(')) { // Consume the second '(' and add it to the token. state.append_char(self.next_char()?.unwrap()); (2, true) } else { (1, false) }; if is_arithmetic { self.cross_state.arithmetic_expansion = true; } self.consume_nested_construct(&mut state, ')', "(", initial_nesting)?; if is_arithmetic { self.cross_state.arithmetic_expansion = false; } } Some('[') => { // Add the '$' we already consumed to the token. state.append_char('$'); // Consume the '[' and add it to the token. state.append_char(self.next_char()?.unwrap()); // Keep track that we're in an arithmetic expression, since // some text will be interpreted differently as a result. self.cross_state.arithmetic_expansion = true; self.consume_nested_construct(&mut state, ']', "[", 1)?; self.cross_state.arithmetic_expansion = false; } Some('{') => { // Add the '$' we already consumed to the token. state.append_char('$'); // Consume the '{' and add it to the token. state.append_char(self.next_char()?.unwrap()); let mut pending_here_doc_tokens = vec![]; let mut drain_here_doc_tokens = false; loop { let cur_token = if drain_here_doc_tokens && !pending_here_doc_tokens.is_empty() { if pending_here_doc_tokens.len() == 1 { drain_here_doc_tokens = false; } pending_here_doc_tokens.remove(0) } else { let cur_token = self.next_token_until( Some('}'), false, /* include space? */ )?; // See if this is a here-document-related token we need to hold // onto until after we've seen all the tokens that need to show // up before we get to the body. if matches!( cur_token.reason, TokenEndReason::HereDocumentBodyStart | TokenEndReason::HereDocumentBodyEnd | TokenEndReason::HereDocumentEndTag ) { pending_here_doc_tokens.push(cur_token); continue; } cur_token }; if matches!(cur_token.reason, TokenEndReason::UnescapedNewLine) && !pending_here_doc_tokens.is_empty() { pending_here_doc_tokens.push(cur_token); drain_here_doc_tokens = true; continue; } if let Some(cur_token_value) = cur_token.token { state.append_str(cur_token_value.to_str()); } match cur_token.reason { TokenEndReason::HereDocumentBodyStart => { state.append_char('\n'); } TokenEndReason::NonNewLineBlank => state.append_char(' '), TokenEndReason::SpecifiedTerminatingChar => { // We hit the end brace we were looking for but did not // yet consume it. Do so now. state.append_char(self.next_char()?.unwrap()); break; } TokenEndReason::EndOfInput => { return Err(TokenizerError::UnterminatedVariable); } _ => (), } } } _ => { // This is either a different character, or else the end of the string. // Either way, add the '$' we already consumed to the token. state.append_char('$'); } } } else { // We look for the terminating backquote. First disable normal consumption and // consume the starting backquote. let backquote_pos = self.cross_state.cursor.clone(); self.consume_char()?; // Add the opening backquote to the token. state.append_char(c); // Now continue until we see an unescaped backquote. let mut escaping_enabled = false; let mut done = false; while !done { // Read (and consume) the next char. let next_char_in_backquote = self.next_char()?; if let Some(cib) = next_char_in_backquote { // Include it in the token no matter what. state.append_char(cib); // Watch out for escaping. if !escaping_enabled && cib == '\\' { escaping_enabled = true; } else { // Look for an unescaped backquote to terminate. if !escaping_enabled && cib == '`' { done = true; } escaping_enabled = false; } } else { return Err(TokenizerError::UnterminatedBackquote(backquote_pos)); } } } } // // [Extension] // If extended globbing is enabled, the last consumed character is an // unquoted start of an extglob pattern, *and* if the current character // is an open parenthesis, then this begins an extglob pattern. else if c == '(' && self.options.enable_extended_globbing && state.unquoted() && !state.in_operator() && state .current_token() .ends_with(|x| Self::can_start_extglob(x)) { // Consume the '(' and append it. self.consume_char()?; state.append_char(c); let mut paren_depth = 1; let mut in_escape = false; // Keep consuming until we see the matching end ')'. while paren_depth > 0 { if let Some(extglob_char) = self.next_char()? { // Include it in the token. state.append_char(extglob_char); match extglob_char { _ if in_escape => in_escape = false, '\\' => in_escape = true, '(' => paren_depth += 1, ')' => paren_depth -= 1, _ => (), } } else { return Err(TokenizerError::UnterminatedExtendedGlob( self.cross_state.cursor.clone(), )); } } // // If the character *can* start an operator, then it will. // } else if state.unquoted() && Self::can_start_operator(c) { if state.started_token() { result = state.delimit_current_token( TokenEndReason::OperatorStart, &mut self.cross_state, )?; } else { state.token_is_operator = true; self.consume_char()?; state.append_char(c); } // // Whitespace gets discarded (and delimits tokens). // } else if state.unquoted() && is_blank(c) { if state.started_token() { result = state.delimit_current_token( TokenEndReason::NonNewLineBlank, &mut self.cross_state, )?; } else if include_space { state.append_char(c); } else { // Make sure we don't include this char in the token range. state.start_position.column += 1; state.start_position.index += 1; } self.consume_char()?; } // // N.B. We need to remember if we were recursively called in a variable // expansion expression; in that case we won't think a token was started but... // we'd be wrong. else if !state.token_is_operator && (state.started_token() || matches!(terminating_char, Some('}'))) { self.consume_char()?; state.append_char(c); } else if c == '#' { // Consume the '#'. self.consume_char()?; let mut done = false; while !done { done = match self.peek_char()? { Some('\n') => true, None => true, _ => { // Consume the peeked char; it's part of the comment. self.consume_char()?; false } }; } // Re-start loop as if the comment never happened. } else if state.started_token() { // In all other cases where we have an in-progress token, we delimit here. result = state.delimit_current_token(TokenEndReason::Other, &mut self.cross_state)?; } else { // If we got here, then we don't have a token in progress and we're not starting an // operator. Add the character to a new token. self.consume_char()?; state.append_char(c); } } let result = result.unwrap(); Ok(result) } fn remove_here_end_tag( &mut self, state: &mut TokenParseState, result: &mut Option, ends_with_newline: bool, ) -> Result { // Bail immediately if we don't even have a *starting* here tag. if self.cross_state.current_here_tags.is_empty() { return Ok(false); } let next_here_tag = &self.cross_state.current_here_tags[0]; let tag_str: Cow<'_, str> = if next_here_tag.tag_was_escaped_or_quoted { unquote_str(next_here_tag.tag.as_str()).into() } else { next_here_tag.tag.as_str().into() }; let tag_str = if !ends_with_newline { tag_str .strip_suffix('\n') .unwrap_or_else(|| tag_str.as_ref()) } else { tag_str.as_ref() }; if let Some(current_token_without_here_tag) = state.current_token().strip_suffix(tag_str) { // Make sure that was either the start of the here document, or there // was a newline between the preceding part // and the tag. if current_token_without_here_tag.is_empty() || current_token_without_here_tag.ends_with('\n') { state.replace_with_here_doc(current_token_without_here_tag.to_owned()); // Delimit the end of the here-document body. *result = state.delimit_current_token( TokenEndReason::HereDocumentBodyEnd, &mut self.cross_state, )?; return Ok(true); } } Ok(false) } const fn can_start_extglob(c: char) -> bool { matches!(c, '@' | '!' | '?' | '+' | '*') } const fn can_start_operator(c: char) -> bool { matches!(c, '&' | '(' | ')' | ';' | '\n' | '|' | '<' | '>') } fn is_operator(&self, s: &str) -> bool { // Handle non-POSIX operators. if !self.options.sh_mode && matches!(s, "<<<" | "&>" | "&>>" | ";;&" | ";&" | "|&") { return true; } matches!( s, "&" | "&&" | "(" | ")" | ";" | ";;" | "\n" | "|" | "||" | "<" | ">" | ">|" | "<<" | ">>" | "<&" | ">&" | "<<-" | "<>" ) } } impl Iterator for Tokenizer<'_, R> { type Item = Result; fn next(&mut self) -> Option { match self.next_token() { #[expect(clippy::manual_map)] Ok(result) => match result.token { Some(_) => Some(Ok(result)), None => None, }, Err(e) => Some(Err(e)), } } } const fn is_blank(c: char) -> bool { c == ' ' || c == '\t' } const fn does_char_newly_affect_quoting(state: &TokenParseState, c: char) -> bool { // If we're currently escaped, then nothing affects quoting. if state.in_escape { return false; } match state.quote_mode { // When we're in a double quote or ANSI-C quote, only a subset of escape // sequences are recognized. QuoteMode::Double(_) | QuoteMode::AnsiC(_) => { if c == '\\' { // TODO(tokenizer): handle backslash in double quote true } else { false } } // When we're in a single quote, nothing affects quoting. QuoteMode::Single(_) => false, // When we're not already in a quote, then we can straightforwardly look for a // quote mark or backslash. QuoteMode::None => is_quoting_char(c), } } const fn is_quoting_char(c: char) -> bool { matches!(c, '\\' | '\'' | '\"') } /// Return a string with all the quoting removed. /// /// # Arguments /// /// * `s` - The string to unquote. pub fn unquote_str(s: &str) -> String { let mut result = String::new(); let mut in_escape = false; for c in s.chars() { match c { c if in_escape => { result.push(c); in_escape = false; } '\\' => in_escape = true, c if is_quoting_char(c) => (), c => result.push(c), } } result } #[cfg(test)] mod tests { use super::*; use anyhow::Result; use insta::assert_ron_snapshot; use pretty_assertions::{assert_eq, assert_matches}; #[derive(serde::Serialize, serde::Deserialize)] struct TokenizerResult<'a> { input: &'a str, result: Vec, } fn test_tokenizer(input: &str) -> Result> { Ok(TokenizerResult { input, result: tokenize_str(input)?, }) } #[test] fn tokenize_empty() -> Result<()> { let tokens = tokenize_str("")?; assert_eq!(tokens.len(), 0); Ok(()) } #[test] fn tokenize_line_continuation() -> Result<()> { assert_ron_snapshot!(test_tokenizer( r"a\ bc" )?); Ok(()) } #[test] fn tokenize_operators() -> Result<()> { assert_ron_snapshot!(test_tokenizer("a>>b")?); Ok(()) } #[test] fn tokenize_comment() -> Result<()> { assert_ron_snapshot!(test_tokenizer( r"a #comment " )?); Ok(()) } #[test] fn tokenize_comment_at_eof() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r"a #comment")?); Ok(()) } #[test] fn tokenize_empty_here_doc() -> Result<()> { assert_ron_snapshot!(test_tokenizer( r"cat < Result<()> { assert_ron_snapshot!(test_tokenizer( r"cat < Result<()> { assert_ron_snapshot!(test_tokenizer( r"cat <<-HERE SOMETHING HERE " )?); Ok(()) } #[test] fn tokenize_here_doc_with_other_tokens() -> Result<()> { assert_ron_snapshot!(test_tokenizer( r"cat < Result<()> { assert_ron_snapshot!(test_tokenizer( r"cat < Result<()> { assert_ron_snapshot!(test_tokenizer( r"echo $(cat < Result<()> { assert_ron_snapshot!(test_tokenizer( r#"echo "$(cat < Result<()> { assert_ron_snapshot!(test_tokenizer( r#"echo "$(cat << HERE TEXT HERE )""# )?); Ok(()) } #[test] fn tokenize_complex_here_docs_in_command_substitution() -> Result<()> { assert_ron_snapshot!(test_tokenizer( r"echo $(cat < Result<()> { assert_ron_snapshot!(test_tokenizer(r"echo `echo hi`")?); Ok(()) } #[test] fn tokenize_backquote_with_escape() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r"echo `echo\`hi`")?); Ok(()) } #[test] fn tokenize_unterminated_backquote() { assert_matches!( tokenize_str("`"), Err(TokenizerError::UnterminatedBackquote(_)) ); } #[test] fn tokenize_unterminated_command_substitution() { // $( is consumed before the tokenizer knows whether it's $( or $((, // so it goes through consume_nested_construct and yields UnterminatedExpansion. assert_matches!( tokenize_str("$("), Err(TokenizerError::UnterminatedExpansion) ); } #[test] fn tokenize_unterminated_arithmetic_expansion() { assert_matches!( tokenize_str("$(("), Err(TokenizerError::UnterminatedExpansion) ); } #[test] fn tokenize_unterminated_legacy_arithmetic_expansion() { assert_matches!( tokenize_str("$["), Err(TokenizerError::UnterminatedExpansion) ); } #[test] fn tokenize_command_substitution() -> Result<()> { assert_ron_snapshot!(test_tokenizer("a$(echo hi)b c")?); Ok(()) } #[test] fn tokenize_command_substitution_with_subshell() -> Result<()> { assert_ron_snapshot!(test_tokenizer("$( (:) )")?); Ok(()) } #[test] fn tokenize_command_substitution_containing_extglob() -> Result<()> { assert_ron_snapshot!(test_tokenizer("echo $(echo !(x))")?); Ok(()) } #[test] fn tokenize_arithmetic_expression() -> Result<()> { assert_ron_snapshot!(test_tokenizer("a$((1+2))b c")?); Ok(()) } #[test] fn tokenize_arithmetic_expression_with_space() -> Result<()> { // N.B. The spacing comes out a bit odd, but it gets processed okay // by later stages. assert_ron_snapshot!(test_tokenizer("$(( 1 ))")?); Ok(()) } #[test] fn tokenize_arithmetic_expression_with_parens() -> Result<()> { assert_ron_snapshot!(test_tokenizer("$(( (0) ))")?); Ok(()) } #[test] fn tokenize_special_parameters() -> Result<()> { assert_ron_snapshot!(test_tokenizer("$$")?); assert_ron_snapshot!(test_tokenizer("$@")?); assert_ron_snapshot!(test_tokenizer("$!")?); assert_ron_snapshot!(test_tokenizer("$?")?); assert_ron_snapshot!(test_tokenizer("$*")?); Ok(()) } #[test] fn tokenize_unbraced_parameter_expansion() -> Result<()> { assert_ron_snapshot!(test_tokenizer("$x")?); assert_ron_snapshot!(test_tokenizer("a$x")?); Ok(()) } #[test] fn tokenize_unterminated_parameter_expansion() { assert_matches!( tokenize_str("${x"), Err(TokenizerError::UnterminatedVariable) ); } #[test] fn tokenize_braced_parameter_expansion() -> Result<()> { assert_ron_snapshot!(test_tokenizer("${x}")?); assert_ron_snapshot!(test_tokenizer("a${x}b")?); Ok(()) } #[test] fn tokenize_braced_parameter_expansion_with_escaping() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r"a${x\}}b")?); Ok(()) } #[test] fn tokenize_whitespace() -> Result<()> { assert_ron_snapshot!(test_tokenizer("1 2 3")?); Ok(()) } #[test] fn tokenize_escaped_whitespace() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r"1\ 2 3")?); Ok(()) } #[test] fn tokenize_single_quote() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r"x'a b'y")?); Ok(()) } #[test] fn tokenize_double_quote() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r#"x"a b"y"#)?); Ok(()) } #[test] fn tokenize_double_quoted_command_substitution() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r#"x"$(echo hi)"y"#)?); Ok(()) } #[test] fn tokenize_double_quoted_arithmetic_expression() -> Result<()> { assert_ron_snapshot!(test_tokenizer(r#"x"$((1+2))"y"#)?); Ok(()) } #[test] fn test_quote_removal() { assert_eq!(unquote_str(r#""hello""#), "hello"); assert_eq!(unquote_str(r"'hello'"), "hello"); assert_eq!(unquote_str(r#""hel\"lo""#), r#"hel"lo"#); assert_eq!(unquote_str(r"'hel\'lo'"), r"hel'lo"); } } brush-parser-0.4.0/src/word.rs000064400000000000000000001553341046102023000143650ustar 00000000000000//! Parser for shell words, used in expansion and other contexts. //! //! Implements support for: //! //! - Text quoting (single, double, ANSI C). //! - Escape sequences. //! - Tilde prefixes. //! - Parameter expansion expressions. //! - Command substitution expressions. //! - Arithmetic expansion expressions. use std::fmt::Debug; use std::fmt::Display; use crate::ParserOptions; use crate::SourceSpan; use crate::ast; use crate::error; /// Encapsulates a `WordPiece` together with its position in the string it came from. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub struct WordPieceWithSource { /// The word piece. pub piece: WordPiece, /// The start index of the piece in the source string. pub start_index: usize, /// The end index of the piece in the source string. pub end_index: usize, } /// Represents a piece of a word. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum WordPiece { /// A simple unquoted, unescaped string. Text(String), /// A string that is single-quoted. SingleQuotedText(String), /// A string that is ANSI-C quoted. AnsiCQuotedText(String), /// A sequence of pieces that are embedded in double quotes. DoubleQuotedSequence(Vec), /// Gettext enabled variant of [`WordPiece::DoubleQuotedSequence`]. GettextDoubleQuotedSequence(Vec), /// A tilde expansion. TildeExpansion(TildeExpr), /// A parameter expansion. ParameterExpansion(ParameterExpr), /// A command substitution. CommandSubstitution(String), /// A backquoted command substitution. BackquotedCommandSubstitution(String), /// An escape sequence. EscapeSequence(String), /// An arithmetic expression. ArithmeticExpression(ast::UnexpandedArithmeticExpr), } /// Represents an expandable tilde expression (e.g., ~). #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum TildeExpr { /// `~` Home, /// `~` UserHome(String), /// `~+` WorkingDir, /// `~-` OldWorkingDir, /// Represents a tilde expansion of the form `~+N`, referring to the Nth directory in /// the shell's directory stack, starting at the top of the stack. Note that the directory /// stack is expected to contains the current working directory as its topmost entry. NthDirFromTopOfDirStack { /// Index into the directory stack (zero-based: 0 is the top of the stack). n: usize, /// Whether the '+' prefix was explicitly used. plus_used: bool, }, /// Represents a tilde expansion of the form `~-N`, referring to the Nth directory in /// the shell's directory stack, starting at the bottom of the stack. NthDirFromBottomOfDirStack { /// Index into the directory stack (zero-based: 0 is the bottom of the stack). n: usize, }, } /// Type of a parameter test. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum ParameterTestType { /// Check for unset or null. UnsetOrNull, /// Check for unset. Unset, } /// A parameter, used in a parameter expansion. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum Parameter { /// A 0-indexed positional parameter. Positional(u32), /// A special parameter. Special(SpecialParameter), /// A named variable. Named(String), /// An index into a named variable. NamedWithIndex { /// Variable name. name: String, /// Index. index: String, }, /// A named array variable with all indices. NamedWithAllIndices { /// Variable name. name: String, /// Whether to concatenate the values. concatenate: bool, }, } impl Display for Parameter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Positional(n) => write!(f, "${n}"), Self::Special(s) => write!(f, "${s}"), Self::Named(name) => write!(f, "${{{name}}}"), Self::NamedWithIndex { name, index } => { write!(f, "${{{name}[{index}]}}") } Self::NamedWithAllIndices { name, concatenate } => { if *concatenate { write!(f, "${{{name}[*]}}") } else { write!(f, "${{{name}[@]}}") } } } } } /// A special parameter, used in a parameter expansion. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum SpecialParameter { /// All positional parameters. AllPositionalParameters { /// Whether to concatenate the values. concatenate: bool, }, /// The count of positional parameters. PositionalParameterCount, /// The last exit status in the shell. LastExitStatus, /// The current shell option flags. CurrentOptionFlags, /// The current shell process ID. ProcessId, /// The last background process ID managed by the shell. LastBackgroundProcessId, /// The name of the shell. ShellName, } impl Display for SpecialParameter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::AllPositionalParameters { concatenate } => { if *concatenate { write!(f, "*") } else { write!(f, "@") } } Self::PositionalParameterCount => write!(f, "#"), Self::LastExitStatus => write!(f, "?"), Self::CurrentOptionFlags => write!(f, "-"), Self::ProcessId => write!(f, "$"), Self::LastBackgroundProcessId => write!(f, "!"), Self::ShellName => write!(f, "0"), } } } /// A parameter expression, used in a parameter expansion. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum ParameterExpr { /// A parameter, with optional indirection. Parameter { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, }, /// Conditionally use default values. UseDefaultValues { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// The type of test to perform. test_type: ParameterTestType, /// Default value to conditionally use. default_value: Option, }, /// Conditionally assign default values. AssignDefaultValues { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// The type of test to perform. test_type: ParameterTestType, /// Default value to conditionally assign. default_value: Option, }, /// Indicate error if null or unset. IndicateErrorIfNullOrUnset { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// The type of test to perform. test_type: ParameterTestType, /// Error message to conditionally yield. error_message: Option, }, /// Conditionally use an alternative value. UseAlternativeValue { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// The type of test to perform. test_type: ParameterTestType, /// Alternative value to conditionally use. alternative_value: Option, }, /// Compute the length of the given parameter. ParameterLength { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, }, /// Remove the smallest suffix from the given string matching the given pattern. RemoveSmallestSuffixPattern { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Remove the largest suffix from the given string matching the given pattern. RemoveLargestSuffixPattern { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Remove the smallest prefix from the given string matching the given pattern. RemoveSmallestPrefixPattern { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Remove the largest prefix from the given string matching the given pattern. RemoveLargestPrefixPattern { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Extract a substring from the given parameter. Substring { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Arithmetic expression that will be expanded to compute the offset /// at which the substring should be extracted. offset: ast::UnexpandedArithmeticExpr, /// Optionally provides an arithmetic expression that will be expanded /// to compute the length of substring to be extracted; if left /// unspecified, the remainder of the string will be extracted. length: Option, }, /// Transform the given parameter. Transform { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Type of transformation to apply. op: ParameterTransformOp, }, /// Uppercase the first character of the given parameter. UppercaseFirstChar { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Uppercase the portion of the given parameter matching the given pattern. UppercasePattern { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Lowercase the first character of the given parameter. LowercaseFirstChar { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Lowercase the portion of the given parameter matching the given pattern. LowercasePattern { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Optionally provides a pattern to match. pattern: Option, }, /// Replace occurrences of the given pattern in the given parameter. ReplaceSubstring { /// The parameter. parameter: Parameter, /// Whether to treat the expanded parameter as an indirect /// reference, which should be subsequently dereferenced /// for the expansion. indirect: bool, /// Pattern to match. pattern: String, /// Replacement string. replacement: Option, /// Kind of match to perform. match_kind: SubstringMatchKind, }, /// Select variable names from the environment with a given prefix. VariableNames { /// The prefix to match. prefix: String, /// Whether to concatenate the results. concatenate: bool, }, /// Select member keys from the named array. MemberKeys { /// Name of the array variable. variable_name: String, /// Whether to concatenate the results. concatenate: bool, }, } /// Kind of substring match. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum SubstringMatchKind { /// Match the prefix of the string. Prefix, /// Match the suffix of the string. Suffix, /// Match the first occurrence in the string. FirstOccurrence, /// Match all instances in the string. Anywhere, } /// Kind of operation to apply to a parameter. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum ParameterTransformOp { /// Capitalizate initials. CapitalizeInitial, /// Expand escape sequences. ExpandEscapeSequences, /// Possibly quote with arrays expanded. PossiblyQuoteWithArraysExpanded { /// Whether or not to yield separate words. separate_words: bool, }, /// Apply prompt expansion. PromptExpand, /// Quote the parameter. Quoted, /// Translate to a format usable in an assignment/declaration. ToAssignmentLogic, /// Translate to the parameter's attribute flags. ToAttributeFlags, /// Translate to lowercase. ToLowerCase, /// Translate to uppercase. ToUpperCase, } /// Represents a sub-word that is either a brace expression or some other word text. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum BraceExpressionOrText { /// A brace expression. Expr(BraceExpression), /// Other word text. Text(String), } /// Represents a brace expression to be expanded. pub type BraceExpression = Vec; /// Member of a brace expression. #[derive(Clone, Debug)] #[cfg_attr( any(test, feature = "serde"), derive(PartialEq, Eq, serde::Serialize, serde::Deserialize) )] pub enum BraceExpressionMember { /// An inclusive numerical sequence. NumberSequence { /// Start of the sequence. start: i64, /// Inclusive end of the sequence. end: i64, /// Increment value. increment: i64, }, /// An inclusive character sequence. CharSequence { /// Start of the sequence. start: char, /// Inclusive end of the sequence. end: char, /// Increment value. increment: i64, }, /// Child text or expressions. Child(Vec), } /// Parse a word into its constituent pieces. /// /// # Arguments /// /// * `word` - The word to parse. /// * `options` - The parser options to use. pub fn parse( word: &str, options: &ParserOptions, ) -> Result, error::WordParseError> { cacheable_parse(word.to_owned(), options.to_owned()) } #[cached::proc_macro::cached(size = 64, result = true)] fn cacheable_parse( word: String, options: ParserOptions, ) -> Result, error::WordParseError> { tracing::debug!(target: "expansion", "Parsing word '{}'", word); let pieces = expansion_parser::unexpanded_word(word.as_str(), &options) .map_err(|err| error::WordParseError::Word(word.clone(), err.into()))?; tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces); Ok(pieces) } /// Parse a heredoc body, treating `"` and `'` as literal characters. /// /// # Arguments /// /// * `word` - The heredoc body to parse. /// * `options` - The parser options to use. pub fn parse_heredoc( word: &str, options: &ParserOptions, ) -> Result, error::WordParseError> { expansion_parser::unexpanded_heredoc_word(word, options) .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into())) } /// Parse the given word into a parameter expression. /// /// # Arguments /// /// * `word` - The word to parse. /// * `options` - The parser options to use. pub fn parse_parameter( word: &str, options: &ParserOptions, ) -> Result { expansion_parser::parameter(word, options) .map_err(|err| error::WordParseError::Parameter(word.to_owned(), err.into())) } /// Parse brace expansion from a given word . /// /// # Arguments /// /// * `word` - The word to parse. /// * `options` - The parser options to use. pub fn parse_brace_expansions( word: &str, options: &ParserOptions, ) -> Result>, error::WordParseError> { expansion_parser::brace_expansions(word, options) .map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err.into())) } pub(crate) fn parse_assignment_word( word: &str, ) -> Result> { expansion_parser::name_equals_scalar_value(word, &ParserOptions::default()) } pub(crate) fn parse_array_assignment( word: &str, elements: &[&String], ) -> Result { let (assignment_name, append) = expansion_parser::name_equals(word, &ParserOptions::default()) .map_err(|_| "not array assignment word")?; let elements = elements .iter() .map(|element| expansion_parser::literal_array_element(element, &ParserOptions::default())) .collect::, _>>() .map_err(|_| "invalid array element in literal")?; let elements_as_words = elements .into_iter() .map(|(key, value)| { ( key.map(|k| ast::Word::new(k.as_str())), ast::Word::new(value.as_str()), ) }) .collect(); Ok(ast::Assignment { name: assignment_name, value: ast::AssignmentValue::Array(elements_as_words), append, loc: SourceSpan::default(), }) } peg::parser! { grammar expansion_parser(parser_options: &ParserOptions) for str { // Helper rule that enables pegviz to be used to visualize debug peg traces. rule traced(e: rule) -> T = &(input:$([_]*) { #[cfg(feature = "debug-tracing")] println!("[PEG_INPUT_START]\n{input}\n[PEG_TRACE_START]"); }) e:e()? {? #[cfg(feature = "debug-tracing")] println!("[PEG_TRACE_STOP]"); e.ok_or("") } pub(crate) rule unexpanded_word() -> Vec = traced()>) rule word(stop_condition: rule) -> Vec = tilde:tilde_expr_prefix_with_source()? pieces:word_piece_with_source(, false /*in_command*/)* { let mut all_pieces = Vec::new(); if let Some(tilde) = tilde { all_pieces.push(tilde); } all_pieces.extend(pieces); all_pieces } // Takes a word as input. pub(crate) rule brace_expansions() -> Option> = pieces:(brace_expansion_piece()+) { Some(pieces) } / [_]* { None } // Returns either a complete brace expression (without any prefix or suffix), or a // non-brace-expression string. rule brace_expansion_piece(stop_condition: rule) -> BraceExpressionOrText = expr:brace_expr() { BraceExpressionOrText::Expr(expr) } / text:$(non_brace_expr_text()+) { BraceExpressionOrText::Text(text.to_owned()) } // Parses text that is not considered to contain a brace expression. rule non_brace_expr_text(stop_condition: rule) -> () = !"{" word_piece(<['{'] {} / stop_condition() {}>, false) {} / !brace_expr() !stop_condition() "{" {} // Parses a complete brace expression, with no prefix or suffix. pub(crate) rule brace_expr() -> BraceExpression = "{" inner:brace_expr_inner() "}" { inner } // Parses the text inside a complete brace expression; basically the complete brace // expression without the opening and closing brace characters. pub(crate) rule brace_expr_inner() -> BraceExpression = brace_text_list_expr() / seq:brace_sequence_expr() { vec![seq] } // Parses a list of brace expression members, including the separating commas; does // not include the opening and closing braces. pub(crate) rule brace_text_list_expr() -> BraceExpression = brace_text_list_member() **<2,> "," // Parses an element that can occur in a brace expression member list, not including the // terminating comma or closing brace. pub(crate) rule brace_text_list_member() -> BraceExpressionMember = // Matches an empty-string member, without consuming the comma or closing brace that terminates it. &[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } / // Matches a nested string that may include some combination of concatenated textual strings // and brace expressions. child_pieces:(brace_expansion_piece(<[',' | '}']>)+) { BraceExpressionMember::Child(child_pieces) } pub(crate) rule brace_sequence_expr() -> BraceExpressionMember = start:number() ".." end:number() increment:(".." n:number() { n })? { BraceExpressionMember::NumberSequence { start, end, increment: increment.unwrap_or(1) } } / start:character() ".." end:character() increment:(".." n:number() { n })? { BraceExpressionMember::CharSequence { start, end, increment: increment.unwrap_or(1) } } rule number() -> i64 = sign:number_sign()? n:$(['0'..='9']+) { let sign = sign.unwrap_or(1); let num: i64 = n.parse().unwrap(); num * sign } rule number_sign() -> i64 = ['-'] { -1 } / ['+'] { 1 } rule character() -> char = ['a'..='z' | 'A'..='Z'] pub(crate) rule is_arithmetic_word() = arithmetic_word() // N.B. We don't bother returning the word pieces, as all users of this rule // only try to extract the consumed input string and not the parse result. rule arithmetic_word(stop_condition: rule) = arithmetic_word_piece()* {} pub(crate) rule is_arithmetic_word_piece() = arithmetic_word_piece() // This rule matches an individual "piece" of an arithmetic expression. It needs to handle // matching nested parenthesized expressions as well. We stop consuming the input when // we reach the provided stop condition, which typically denotes the end of the containing // arithmetic expression. rule arithmetic_word_piece(stop_condition: rule) = // This branch matches a parenthesized piece; we consume the opening parenthesis and // delegate the rest to a helper rule. We don't worry about the stop condition passed // into us, because if we see an opening parenthesis then we *must* find its closing // partner. "(" arithmetic_word_plus_right_paren() {} / // This branch handles the case where we have an array element name with square brackets, // which may (legitimately) contain the stop condition. array_element_name() {} / // This branch matches any standard piece of a word, stopping as soon as we reach // either the overall stop condition *OR* an opening parenthesis. We add this latter // condition to ensure that *we* handle matching parentheses. !"(" word_piece()>, false /*in_command*/) {} // This is a helper rule that matches either the provided stop condition or an opening parenthesis. rule param_rule_or_open_paren(stop_condition: rule) -> () = stop_condition() {} / "(" {} // This rule matches an arithmetic word followed by a right parenthesis. It must consume the right parenthesis. rule arithmetic_word_plus_right_paren() = arithmetic_word(<[')']>) ")" rule word_piece_with_source(stop_condition: rule, in_command: bool) -> WordPieceWithSource = start_index:position!() piece:word_piece(, in_command) end_index:position!() { WordPieceWithSource { piece, start_index, end_index } } rule word_piece(stop_condition: rule, in_command: bool) -> WordPiece = // Rules that match quoted text. s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } / s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } / s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } / s:gettext_double_quoted_sequence() { WordPiece::GettextDoubleQuotedSequence(s) } / // Rules that match pieces starting with a dollar sign ('$'). dollar_sign_word_piece() / // Rules that match unquoted text that doesn't start with an unescaped dollar sign. normal_escape_sequence() / // Allow tilde expression to be matched as a word piece (for tilde-after-colon expansion) enabled_tilde_expr_after_colon() / // Finally, match unquoted literal text. unquoted_literal_text(, in_command) rule dollar_sign_word_piece() -> WordPiece = arithmetic_expansion() / legacy_arithmetic_expansion() / command_substitution() / parameter_expansion() rule double_quoted_word_piece() -> WordPiece = arithmetic_expansion() / legacy_arithmetic_expansion() / command_substitution() / parameter_expansion() / double_quoted_escape_sequence() / double_quoted_text() rule double_quoted_sequence() -> Vec = "\"" i:double_quoted_sequence_inner()* "\"" { i } rule gettext_double_quoted_sequence() -> Vec = "$\"" i:double_quoted_sequence_inner()* "\"" { i } rule double_quoted_sequence_inner() -> WordPieceWithSource = start_index:position!() piece:double_quoted_word_piece() end_index:position!() { WordPieceWithSource { piece, start_index, end_index } } rule single_quoted_literal_text() -> &'input str = "\'" inner:$([^'\'']*) "\'" { inner } rule ansi_c_quoted_text() -> &'input str = r"$'" inner:$((r"\\" / r"\'" / [^'\''])*) r"'" { inner } rule unquoted_literal_text(stop_condition: rule, in_command: bool) -> WordPiece = s:$(unquoted_literal_text_piece(, in_command)+) { WordPiece::Text(s.to_owned()) } // TODO(parser): Find a way to remove the special-case logic for extglob + subshell commands rule unquoted_literal_text_piece(stop_condition: rule, in_command: bool) = is_true(in_command) extglob_pattern() / is_true(in_command) subshell_command() / !stop_condition() !normal_escape_sequence() !enabled_tilde_expr_after_colon() [^'\'' | '\"' | '$' | '`'] {} rule enabled_tilde_expr_after_colon() -> WordPiece = tilde_exprs_after_colon_enabled() last_char_is_colon() piece:tilde_expression_piece() { piece } rule last_char_is_colon() = #{|input, pos| { if pos == 0 { // No preceding character - can't be preceded by ':' peg::RuleResult::Failed } else { // Check the byte directly (`:` is ASCII, single byte) if input.as_bytes()[pos - 1] == b':' { peg::RuleResult::Matched(pos, ()) } else { peg::RuleResult::Failed } } }} rule is_true(value: bool) = &[_] {? if value { Ok(()) } else { Err("not true") } } rule extglob_pattern() = ("@" / "!" / "?" / "+" / "*") "(" extglob_body_piece()* ")" {} rule extglob_body_piece() = word_piece(<[')']>, true /*in_command*/) {} rule subshell_command() = "(" command() ")" {} rule double_quoted_text() -> WordPiece = s:double_quote_body_text() { WordPiece::Text(s.to_owned()) } rule double_quote_body_text() -> &'input str = $((!double_quoted_escape_sequence() !dollar_sign_word_piece() [^'\"'])+) // Heredoc body parsing: like double-quoted content, but " and ' are literal characters. pub(crate) rule unexpanded_heredoc_word() -> Vec = traced()>) rule heredoc_word(stop_condition: rule) -> Vec = pieces:heredoc_word_piece_with_source()* { pieces } rule heredoc_word_piece_with_source(stop_condition: rule) -> WordPieceWithSource = !stop_condition() start_index:position!() piece:heredoc_word_piece() end_index:position!() { WordPieceWithSource { piece, start_index, end_index } } rule heredoc_word_piece() -> WordPiece = arithmetic_expansion() / legacy_arithmetic_expansion() / command_substitution() / parameter_expansion() / heredoc_escape_sequence() / heredoc_literal_text() rule heredoc_escape_sequence() -> WordPiece = s:$("\\" ['$' | '`' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) } rule heredoc_literal_text() -> WordPiece = s:$((!heredoc_escape_sequence() !dollar_sign_word_piece() [^'`'])+) { WordPiece::Text(s.to_owned()) } rule normal_escape_sequence() -> WordPiece = s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) } rule double_quoted_escape_sequence() -> WordPiece = s:$("\\" ['$' | '`' | '\"' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) } rule tilde_expr_prefix_with_source() -> WordPieceWithSource = start_index:position!() piece:tilde_expr_prefix() end_index:position!() { WordPieceWithSource { piece, start_index, end_index } } rule tilde_expr_prefix() -> WordPiece = tilde_exprs_at_word_start_enabled() piece:tilde_expression_piece() { piece } rule tilde_expr_after_colon() -> WordPiece = tilde_exprs_after_colon_enabled() piece:tilde_expression_piece() { piece } rule tilde_expression_piece() -> WordPiece = "~" expr:tilde_expression() { WordPiece::TildeExpansion(expr) } rule tilde_expression() -> TildeExpr = &tilde_terminator() { TildeExpr::Home } / "+" &tilde_terminator() { TildeExpr::WorkingDir } / plus:("+"?) n:$(['0'..='9']*) &tilde_terminator() { TildeExpr::NthDirFromTopOfDirStack { n: n.parse().unwrap(), plus_used: plus.is_some() } } / "-" &tilde_terminator() { TildeExpr::OldWorkingDir } / "-" n:$(['0'..='9']*) &tilde_terminator() { TildeExpr::NthDirFromBottomOfDirStack { n: n.parse().unwrap() } } / user:$(portable_filename_char()*) &tilde_terminator() { TildeExpr::UserHome(user.to_owned()) } rule tilde_terminator() = ['/' | ':' | ';' | '}'] / ![_] rule portable_filename_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '-'] // TODO(parser): Deal with fact that there may be a quoted word or escaped closing brace chars. // TODO(parser): Improve on how we handle a '$' not followed by a valid variable name or parameter. rule parameter_expansion() -> WordPiece = "${" e:parameter_expression() "}" { WordPiece::ParameterExpansion(e) } / "$" parameter:unbraced_parameter() { WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false }) } / "$" !['\''] { WordPiece::Text("$".to_owned()) } rule parameter_expression() -> ParameterExpr = indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? { ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value } } / indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? { ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value } } / indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? { ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message } } / indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? { ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value } } / "#" parameter:parameter() { ParameterExpr::ParameterLength { parameter, indirect: false } } / indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? { ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern } } / indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? { ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern } } / indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? { ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern } } / indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? { ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern } } / // N.B. The following case is for non-sh extensions. non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } / indirect:parameter_indirection() parameter:parameter() { ParameterExpr::Parameter { parameter, indirect } } rule parameter_test_type() -> ParameterTestType = colon:":"? { if colon.is_some() { ParameterTestType::UnsetOrNull } else { ParameterTestType::Unset } } rule non_posix_parameter_expression() -> ParameterExpr = "!" variable_name:variable_name() "[*]" { ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true } } / "!" variable_name:variable_name() "[@]" { ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false } } / indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? { ParameterExpr::Substring { parameter, indirect, offset, length } } / indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() { ParameterExpr::Transform { parameter, indirect, op } } / "!" prefix:variable_name() "*" { ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true } } / "!" prefix:variable_name() "@" { ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false } } / indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? { ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix } } / indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? { ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix } } / indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? { ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere } } / indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? { ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence } } / indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? { ParameterExpr::UppercasePattern { parameter, indirect, pattern } } / indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? { ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern } } / indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? { ParameterExpr::LowercasePattern { parameter, indirect, pattern } } / indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? { ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern } } rule parameter_indirection() -> bool = non_posix_extensions_enabled() "!" { true } / { false } rule non_posix_parameter_transformation_op() -> ParameterTransformOp = "U" { ParameterTransformOp::ToUpperCase } / "u" { ParameterTransformOp::CapitalizeInitial } / "L" { ParameterTransformOp::ToLowerCase } / "Q" { ParameterTransformOp::Quoted } / "E" { ParameterTransformOp::ExpandEscapeSequences } / "P" { ParameterTransformOp::PromptExpand } / "A" { ParameterTransformOp::ToAssignmentLogic } / "K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } / "a" { ParameterTransformOp::ToAttributeFlags } / "k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } } rule unbraced_parameter() -> Parameter = p:unbraced_positional_parameter() { Parameter::Positional(p) } / p:special_parameter() { Parameter::Special(p) } / p:variable_name() { Parameter::Named(p.to_owned()) } // N.B. The indexing syntax is not a standard sh-ism. pub(crate) rule parameter() -> Parameter = p:positional_parameter() { Parameter::Positional(p) } / p:special_parameter() { Parameter::Special(p) } / non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } / non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } / non_posix_extensions_enabled() p:variable_name() "[" index:array_index() "]" {? Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() }) } / p:variable_name() { Parameter::Named(p.to_owned()) } rule positional_parameter() -> u32 = n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) } rule unbraced_positional_parameter() -> u32 = n:$(['1'..='9']) {? n.parse().or(Err("u32")) } rule special_parameter() -> SpecialParameter = "@" { SpecialParameter::AllPositionalParameters { concatenate: false } } / "*" { SpecialParameter::AllPositionalParameters { concatenate: true } } / "#" { SpecialParameter::PositionalParameterCount } / "?" { SpecialParameter::LastExitStatus } / "-" { SpecialParameter::CurrentOptionFlags } / "$" { SpecialParameter::ProcessId } / "!" { SpecialParameter::LastBackgroundProcessId } / "0" { SpecialParameter::ShellName } rule variable_name() -> &'input str = $(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+) pub(crate) rule command_substitution() -> WordPiece = "$(" c:command() ")" { WordPiece::CommandSubstitution(c.to_owned()) } / "`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) } pub(crate) rule command() -> &'input str = $(command_piece()*) pub(crate) rule command_piece() -> () = word_piece(<[')']>, true /*in_command*/) {} / ([' ' | '\t'])+ {} / ['\'' | '`'] {} rule backquoted_command() -> String = chars:(backquoted_char()*) { chars.into_iter().collect() } rule backquoted_char() -> &'input str = "\\`" { "`" } / "\\\\" { "\\\\" } / s:$([^'`']) { s } rule arithmetic_expansion() -> WordPiece = "$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) } rule legacy_arithmetic_expansion() -> WordPiece = "$[" e:$(arithmetic_word(<"]">)) "]" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) } rule substring_offset() -> ast::UnexpandedArithmeticExpr = s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } } rule substring_length() -> ast::UnexpandedArithmeticExpr = s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } } rule parameter_replacement_str() -> String = "/" s:$(word(<['}']>)) { s.to_owned() } rule parameter_search_pattern() -> String = s:$(word(<['}' | '/']>)) { s.to_owned() } rule parameter_expression_word() -> String = s:$(word(<['}']>)) { s.to_owned() } rule extglob_enabled() -> () = &[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } } rule non_posix_extensions_enabled() -> () = &[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } } rule tilde_exprs_at_word_start_enabled() -> () = &[_] {? if parser_options.tilde_expansion_at_word_start { Ok(()) } else { Err("no tilde expansion at word start") } } rule tilde_exprs_after_colon_enabled() -> () = &[_] {? if parser_options.tilde_expansion_after_colon { Ok(()) } else { Err("no tilde expansion after colon") } } // Assignment rules. pub(crate) rule name_equals_scalar_value() -> ast::Assignment = nae:name_equals() value:assigned_scalar_value() { let (name, append) = nae; ast::Assignment { name, value, append, loc: SourceSpan::default() } } pub(crate) rule name_equals() -> (ast::AssignmentName, bool) = name:assignment_name() append:("+"?) "=" { (name, append.is_some()) } pub(crate) rule literal_array_element() -> (Option, String) = "[" inner:$((!"]" [_])*) "]=" value:$([_]*) { (Some(inner.to_owned()), value.to_owned()) } / value:$([_]+) { (None, value.to_owned()) } rule assignment_name() -> ast::AssignmentName = aen:array_element_name() { let (name, index) = aen; ast::AssignmentName::ArrayElementName(name.to_owned(), index.to_owned()) } / name:assigned_scalar_name() { ast::AssignmentName::VariableName(name.to_owned()) } rule array_element_name() -> (&'input str, &'input str) = name:assigned_scalar_name() "[" ai:array_index() "]" { (name, ai) } rule array_index() -> &'input str = $(arithmetic_word(<"]">)) rule assigned_scalar_name() -> &'input str = $(alpha_or_underscore() non_first_variable_char()*) rule non_first_variable_char() -> () = ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z'] {} rule alpha_or_underscore() -> () = ['_' | 'a'..='z' | 'A'..='Z'] {} rule assigned_scalar_value() -> ast::AssignmentValue = v:$([_]*) { ast::AssignmentValue::Scalar(ast::Word::from(v.to_owned())) } } } #[cfg(test)] #[allow(clippy::panic_in_result_fn)] mod tests { use super::*; use anyhow::Result; use insta::assert_ron_snapshot; use pretty_assertions::assert_matches; #[derive(serde::Serialize, serde::Deserialize)] struct ParseTestResults<'a> { input: &'a str, result: Vec, } fn test_parse(word: &str) -> Result> { let parsed = super::parse(word, &ParserOptions::default())?; Ok(ParseTestResults { input: word, result: parsed, }) } #[test] fn parse_ansi_c_quoted_text() -> Result<()> { assert_ron_snapshot!(test_parse(r"$'hi\nthere\t'")?); Ok(()) } #[test] fn parse_ansi_c_quoted_escape_seq() -> Result<()> { assert_ron_snapshot!(test_parse(r"$'\\'")?); Ok(()) } #[test] fn parse_tilde_after_colon() -> Result<()> { let opts = ParserOptions { tilde_expansion_after_colon: true, ..ParserOptions::default() }; let parsed = super::parse("a:~", &opts)?; // Should have: Text("a:"), TildeExpansion("") assert_eq!(parsed.len(), 2); assert_matches!(parsed[0].piece, WordPiece::Text(_)); assert_matches!(parsed[1].piece, WordPiece::TildeExpansion(_)); Ok(()) } #[test] fn parse_double_quoted_text() -> Result<()> { assert_ron_snapshot!(test_parse(r#""a ${b} c""#)?); Ok(()) } #[test] fn parse_gettext_double_quoted_text() -> Result<()> { assert_ron_snapshot!(test_parse(r#"$"a ${b} c""#)?); Ok(()) } #[test] fn parse_command_substitution() -> Result<()> { super::expansion_parser::command_piece("echo", &ParserOptions::default())?; super::expansion_parser::command_piece("hi", &ParserOptions::default())?; super::expansion_parser::command("echo hi", &ParserOptions::default())?; super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?; assert_ron_snapshot!(test_parse("$(echo hi)")?); Ok(()) } #[test] fn parse_command_substitution_with_embedded_quotes() -> Result<()> { super::expansion_parser::command_piece("echo", &ParserOptions::default())?; super::expansion_parser::command_piece(r#""hi""#, &ParserOptions::default())?; super::expansion_parser::command(r#"echo "hi""#, &ParserOptions::default())?; super::expansion_parser::command_substitution( r#"$(echo "hi")"#, &ParserOptions::default(), )?; assert_ron_snapshot!(test_parse(r#"$(echo "hi")"#)?); Ok(()) } #[test] fn parse_command_substitution_with_embedded_extglob() -> Result<()> { assert_ron_snapshot!(test_parse("$(echo !(x))")?); Ok(()) } #[test] fn parse_command_sub_with_unbalanced_single_quote() -> Result<()> { assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\nit's here\nEOF\n)\"")?); Ok(()) } #[test] fn parse_command_sub_with_unbalanced_backtick() -> Result<()> { assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\na ` b\nEOF\n)\"")?); Ok(()) } #[test] fn parse_command_sub_with_balanced_double_quotes() -> Result<()> { assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n\"hello\"\nEOF\n)\"")?); Ok(()) } #[test] fn parse_command_sub_with_balanced_single_quotes() -> Result<()> { assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n'hello'\nEOF\n)\"")?); Ok(()) } #[test] fn parse_command_sub_with_balanced_backticks() -> Result<()> { assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n`hello`\nEOF\n)\"")?); Ok(()) } #[test] fn parse_backquoted_command() -> Result<()> { assert_ron_snapshot!(test_parse("`echo hi`")?); Ok(()) } #[test] fn parse_backquoted_command_in_double_quotes() -> Result<()> { assert_ron_snapshot!(test_parse(r#""`echo hi`""#)?); Ok(()) } #[test] fn parse_extglob_with_embedded_parameter() -> Result<()> { assert_ron_snapshot!(test_parse("+([$var])")?); Ok(()) } #[test] fn parse_arithmetic_expansion() -> Result<()> { assert_ron_snapshot!(test_parse("$((0))")?); Ok(()) } #[test] fn parse_arithmetic_expansion_with_parens() -> Result<()> { assert_ron_snapshot!(test_parse("$((((1+2)*3)))")?); Ok(()) } #[test] fn test_arithmetic_word_parsing() { let options = ParserOptions::default(); assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok()); } #[test] fn test_arithmetic_word_piece_parsing() { let options = ParserOptions::default(); assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok()); assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err()); assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err()); assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err()); } #[test] fn test_brace_expansion_parsing() -> Result<()> { let options = ParserOptions::default(); let inputs = ["x{a,b}y", "{a,b{1,2}}"]; for input in inputs { assert_ron_snapshot!(super::parse_brace_expansions(input, &options)?.ok_or_else( || anyhow::anyhow!("Expected brace expansion to be parsed successfully") )?); } Ok(()) } #[test] fn parse_assignment_word() -> Result<()> { super::parse_assignment_word("x=3")?; super::parse_assignment_word("x=")?; super::parse_assignment_word("x[3]=a")?; super::parse_assignment_word("x[${y[3]}]=a")?; super::parse_assignment_word("x[y[3]]=a")?; Ok(()) } }