Repository: reubeno/brush
Pull request #407 (2025-03-12 20:54:06)

fix: honor COMP_WORDBREAKS in completion tokenization
Way back when, we naively used out own tokenization to split apart an input line before running through programmable completion. As best we can tell, though, we really just need to do a straightforward split by the delimiters defined in `COMP_WORDBREAKS`.

github-actions[bot]: ## Test Results
    2 files      9 suites   1m 34s ⏱️
  694 tests   694 ✅ 0 💤 0 ❌
1 374 runs  1 374 ✅ 0 💤 0 ❌

Results for commit 07e53041.

[test-results]:data:application/gzip;base64,H4sIAFP30WcC/13MQQ6DIBCF4asY1l2AoKO9TEMGJiFVaRBWpncvEFu1y+9/ydsYucmu7N60t4atycWKMcOkoKPzS6HKzkssW3/osSbE//R0r5z4L5B20yXYEHzYS0hL+RQS1K7v5yUdn9Wny+rzI/p5djGDcbCd5EqMhnplBwnGEsgBpCbEVpA2xFF0wN4fsyJkaAYBAAA=


Patch:
diff --git a/.cargo/config.toml b/.cargo/config.toml
index b7ba6ce..3518483 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -3,3 +3,11 @@ xtask = "run --package xtask --"
 
 [build]
 rustflags = ["-C", "target-cpu=native"]
+
+[target.wasm32-unknown-unknown]
+# Select a getrandom backend that will work for this target
+rustflags = ["--cfg", 'getrandom_backend="wasm_js"']
+
+[target.wasm32-wasip2]
+# Uninteresting flag to ensure this overrides the flags in [build]
+rustflags = ["--cfg", "x"]
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 48a2bc3..385c4b9 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -56,6 +56,14 @@ jobs:
             required_tools: "gcc-aarch64-linux-gnu"
             binary_name: "brush"
             extra_build_args: ""
+          # Build for wasm32 target on x86_64/linux host.
+          - host: "ubuntu-24.04"
+            target: "wasm32-unknown-unknown"
+            os: "unknown"
+            arch: "wasm32"
+            required_tools: ""
+            binary_name: "brush.wasm"
+            extra_build_args: "--no-default-features --features minimal"
           # Build for WASI-0.2 target on x86_64/linux host.
           - host: "ubuntu-24.04"
             target: "wasm32-wasip2"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41b8c6d..5c01e8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 All notable changes to this project will be documented in this file.
 
+## [0.2.16] - 2025-03-25
+
+### 🚀 Features
+
+- *(arithmetic)* Support explicit base#literal in arithmetic (#388)
+- Implement `command -p` (#402)
+
+### 🐛 Bug Fixes
+
+- Default PS1 and PS2 in interactive mode (#390)
+- *(builtins)* Implement command-less exec semantics with open fds (#384)
+- *(builtins)* Correct read handling of IFS/space (#385)
+- *(extglob)* Correct handling of extglobs with empty branches (#386)
+- Correct path tests on empty strings (#391)
+- Allow newline in empty array assignment (#405)
+- Improve panic handling output (#409)
+- *(regex)* Enable multiline mode for extended test regexes (#416)
+- Parse '#' as char only if inside a variable expansion (#418)
+
+### 📚 Documentation
+
+- Symlink licenses under crate dirs (#400)
+
+### 🧪 Testing
+
+- Add more linux distros to test matrix (#412)
+- Enable testing on nixos/nix container image (#413)
+
+### ⚙️ Miscellaneous Tasks
+
+- Upgrade cached crate (#398)
+- Rewrite dir diffing test code to avoid deps
+
+### Build
+
+- *(deps)* Bump the cargo group with 2 updates (#393)
+- *(deps)* Bump whoami from 1.5.2 to 1.6.0 in the cargo group (#423)
+- *(deps)* Bump the cargo group with 2 updates (#389)
+- *(deps)* Bump serde from 1.0.218 to 1.0.219 in the cargo group (#401)
+- *(deps)* Bump the cargo group with 2 updates (#410)
+
+<!-- generated by git-cliff -->
 ## [0.2.15] - 2025-02-03
 
 ### 🚀 Features
diff --git a/Cargo.lock b/Cargo.lock
index adb7884..6b57021 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -258,7 +258,7 @@ dependencies = [
 
 [[package]]
 name = "brush-core"
-version = "0.2.15"
+version = "0.2.16"
 dependencies = [
  "anyhow",
  "async-recursion",
@@ -272,6 +272,7 @@ dependencies = [
  "criterion",
  "fancy-regex",
  "futures",
+ "getrandom 0.3.2",
  "homedir",
  "hostname",
  "indexmap",
@@ -285,6 +286,7 @@ dependencies = [
  "thiserror 2.0.12",
  "tokio",
  "tracing",
+ "uuid",
  "uzers",
  "whoami",
 ]
@@ -304,12 +306,13 @@ dependencies = [
 
 [[package]]
 name = "brush-interactive"
-version = "0.2.15"
+version = "0.2.16"
 dependencies = [
  "async-trait",
  "brush-core",
  "brush-parser",
- "crossterm",
+ "crossterm 0.29.0",
+ "getrandom 0.3.2",
  "indexmap",
  "nu-ansi-term 0.50.1",
  "reedline",
@@ -320,7 +323,7 @@ dependencies = [
 
 [[package]]
 name = "brush-parser"
-version = "0.2.13"
+version = "0.2.14"
 dependencies = [
  "anyhow",
  "arbitrary",
@@ -338,7 +341,7 @@ dependencies = [
 
 [[package]]
 name = "brush-shell"
-version = "0.2.15"
+version = "0.2.16"
 dependencies = [
  "anyhow",
  "assert_cmd",
@@ -351,10 +354,11 @@ dependencies = [
  "clap",
  "colored",
  "const_format",
- "crossterm",
+ "crossterm 0.29.0",
  "descape",
  "diff",
  "expectrl",
+ "getrandom 0.3.2",
  "git-version",
  "glob",
  "human-panic",
@@ -368,6 +372,7 @@ dependencies = [
  "tokio",
  "tracing",
  "tracing-subscriber",
+ "uuid",
  "version-compare",
  "walkdir",
 ]
@@ -442,9 +447,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.2.16"
+version = "1.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
+checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
 dependencies = [
  "jobserver",
  "libc",
@@ -507,9 +512,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.32"
+version = "4.5.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
+checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -526,9 +531,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.32"
+version = "4.5.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
+checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
 dependencies = [
  "anstream",
  "anstyle",
@@ -620,6 +625,15 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "convert_case"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
@@ -724,6 +738,25 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "crossterm"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
+dependencies = [
+ "bitflags 2.9.0",
+ "crossterm_winapi",
+ "derive_more",
+ "document-features",
+ "mio",
+ "parking_lot",
+ "rustix 1.0.5",
+ "serde",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
 [[package]]
 name = "crossterm_winapi"
 version = "0.9.1"
@@ -741,9 +774,9 @@ checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
 
 [[package]]
 name = "darling"
-version = "0.20.10"
+version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
 dependencies = [
  "darling_core",
  "darling_macro",
@@ -751,9 +784,9 @@ dependencies = [
 
 [[package]]
 name = "darling_core"
-version = "0.20.10"
+version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
 dependencies = [
  "fnv",
  "ident_case",
@@ -765,9 +798,9 @@ dependencies = [
 
 [[package]]
 name = "darling_macro"
-version = "0.20.10"
+version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
 dependencies = [
  "darling_core",
  "quote",
@@ -814,6 +847,27 @@ dependencies = [
  "syn 2.0.100",
 ]
 
+[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
 [[package]]
 name = "descape"
 version = "2.0.3"
@@ -838,6 +892,15 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
+[[package]]
+name = "document-features"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
+dependencies = [
+ "litrs",
+]
+
 [[package]]
 name = "either"
 version = "1.15.0"
@@ -872,9 +935,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "errno"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
 dependencies = [
  "libc",
  "windows-sys 0.59.0",
@@ -915,7 +978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
 dependencies = [
  "cfg-if",
- "rustix 1.0.3",
+ "rustix 1.0.5",
  "windows-sys 0.59.0",
 ]
 
@@ -933,9 +996,9 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
+checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -1054,9 +1117,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi",
  "wasi 0.14.2+wasi-0.2.4",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -1200,16 +1265,17 @@ dependencies = [
 
 [[package]]
 name = "iana-time-zone"
-version = "0.1.61"
+version = "0.1.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
 dependencies = [
  "android_system_properties",
  "core-foundation-sys",
  "iana-time-zone-haiku",
  "js-sys",
+ "log",
  "wasm-bindgen",
- "windows-core 0.52.0",
+ "windows-core 0.61.0",
 ]
 
 [[package]]
@@ -1257,9 +1323,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
 
 [[package]]
 name = "indexmap"
-version = "2.8.0"
+version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
 dependencies = [
  "equivalent",
  "hashbrown 0.15.2",
@@ -1335,10 +1401,11 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
 [[package]]
 name = "jobserver"
-version = "0.1.32"
+version = "0.1.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
 dependencies = [
+ "getrandom 0.3.2",
  "libc",
 ]
 
@@ -1398,6 +1465,12 @@ version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
 
+[[package]]
+name = "litrs"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+
 [[package]]
 name = "lock_api"
 version = "0.4.12"
@@ -1410,9 +1483,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.26"
+version = "0.4.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
 
 [[package]]
 name = "memchr"
@@ -1440,9 +1513,9 @@ dependencies = [
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.5"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430"
 dependencies = [
  "adler2",
 ]
@@ -1550,9 +1623,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.21.1"
+version = "1.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 
 [[package]]
 name = "oorandom"
@@ -1712,7 +1785,7 @@ version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
 dependencies = [
- "zerocopy 0.8.23",
+ "zerocopy 0.8.24",
 ]
 
 [[package]]
@@ -1836,7 +1909,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
 dependencies = [
  "rand_chacha",
  "rand_core",
- "zerocopy 0.8.23",
+ "zerocopy 0.8.24",
 ]
 
 [[package]]
@@ -1880,9 +1953,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.10"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
+checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
 dependencies = [
  "bitflags 2.9.0",
 ]
@@ -1894,7 +1967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd4728ee71d2aa3a364ee64470d1aa64b3f0467b2d28b73df15259d005dec64a"
 dependencies = [
  "chrono",
- "crossterm",
+ "crossterm 0.28.1",
  "fd-lock",
  "itertools 0.13.0",
  "nu-ansi-term 0.50.1",
@@ -1972,9 +2045,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "1.0.3"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
+checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
 dependencies = [
  "bitflags 2.9.0",
  "errno",
@@ -2120,9 +2193,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.14.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
 
 [[package]]
 name = "stable_deref_trait"
@@ -2217,14 +2290,14 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.19.0"
+version = "3.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
+checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
 dependencies = [
  "fastrand",
  "getrandom 0.3.2",
  "once_cell",
- "rustix 1.0.3",
+ "rustix 1.0.5",
  "windows-sys 0.59.0",
 ]
 
@@ -2234,7 +2307,7 @@ version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
 dependencies = [
- "rustix 1.0.3",
+ "rustix 1.0.5",
  "windows-sys 0.59.0",
 ]
 
@@ -2296,9 +2369,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.40"
+version = "0.3.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
 dependencies = [
  "deranged",
  "itoa",
@@ -2317,9 +2390,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
 
 [[package]]
 name = "time-macros"
-version = "0.2.21"
+version = "0.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
 dependencies = [
  "num-conv",
  "time-core",
@@ -2337,9 +2410,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.44.1"
+version = "1.44.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
+checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
 dependencies = [
  "backtrace",
  "bytes",
@@ -2504,6 +2577,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
 dependencies = [
  "getrandom 0.3.2",
+ "js-sys",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -2663,9 +2738,9 @@ dependencies = [
 
 [[package]]
 name = "whoami"
-version = "1.5.2"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
+checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
 dependencies = [
  "redox_syscall",
  "wasite",
@@ -2753,12 +2828,25 @@ version = "0.57.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
 dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-result",
+ "windows-implement 0.57.0",
+ "windows-interface 0.57.0",
+ "windows-result 0.1.2",
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-core"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
+dependencies = [
+ "windows-implement 0.60.0",
+ "windows-interface 0.59.1",
+ "windows-link",
+ "windows-result 0.3.2",
+ "windows-strings",
+]
+
 [[package]]
 name = "windows-implement"
 version = "0.57.0"
@@ -2770,6 +2858,17 @@ dependencies = [
  "syn 2.0.100",
 ]
 
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
 [[package]]
 name = "windows-interface"
 version = "0.57.0"
@@ -2781,6 +2880,17 @@ dependencies = [
  "syn 2.0.100",
 ]
 
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
 [[package]]
 name = "windows-link"
 version = "0.1.1"
@@ -2796,6 +2906,24 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-result"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.52.0"
@@ -2972,11 +3100,11 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.23"
+version = "0.8.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
+checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
 dependencies = [
- "zerocopy-derive 0.8.23",
+ "zerocopy-derive 0.8.24",
 ]
 
 [[package]]
@@ -2992,9 +3120,9 @@ dependencies = [
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.23"
+version = "0.8.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
+checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/brush-core/Cargo.toml b/brush-core/Cargo.toml
index 7c70e0c..49130df 100644
--- a/brush-core/Cargo.toml
+++ b/brush-core/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "brush-core"
 description = "Reusable core of a POSIX/bash shell (used by brush-shell)"
-version = "0.2.15"
+version = "0.2.16"
 categories.workspace = true
 edition.workspace = true
 keywords.workspace = true
@@ -19,14 +19,14 @@ workspace = true
 [dependencies]
 async-recursion = "1.1.1"
 async-trait = "0.1.88"
-brush-parser = { version = "^0.2.13", path = "../brush-parser" }
+brush-parser = { version = "^0.2.14", path = "../brush-parser" }
 cached = "0.55.1"
 cfg-if = "1.0.0"
 chrono = "0.4.40"
 clap = { version = "4.5.21", features = ["derive", "wrap_help"] }
 fancy-regex = "0.14.0"
 futures = "0.3.31"
-indexmap = "2.7.1"
+indexmap = "2.9.0"
 itertools = "0.14.0"
 lazy_static = "1.5.0"
 rand = "0.9.0"
@@ -34,12 +34,12 @@ thiserror = "2.0.11"
 tracing = "0.1.41"
 
 [target.'cfg(target_family = "wasm")'.dependencies]
-tokio = { version = "1.44.1", features = ["io-util", "macros", "rt"] }
+tokio = { version = "1.44.2", features = ["io-util", "macros", "rt"] }
 
 [target.'cfg(any(windows, unix))'.dependencies]
 hostname = "0.4.0"
 os_pipe = { version = "1.2.1", features = ["io_safety"] }
-tokio = { version = "1.43.0", features = [
+tokio = { version = "1.44.2", features = [
     "io-util",
     "macros",
     "process",
@@ -50,7 +50,7 @@ tokio = { version = "1.43.0", features = [
 
 [target.'cfg(windows)'.dependencies]
 homedir = "0.3.4"
-whoami = "1.5.2"
+whoami = "1.6.0"
 
 [target.'cfg(unix)'.dependencies]
 command-fds = "0.3.0"
@@ -67,6 +67,10 @@ uzers = "0.12.1"
 [target.'cfg(target_os = "linux")'.dependencies]
 procfs = "0.17.0"
 
+[target.wasm32-unknown-unknown.dependencies]
+getrandom = { version = "0.3.2", features = ["wasm_js"] }
+uuid = { version = "1.16.0", features = ["js"] }
+
 [dev-dependencies]
 anyhow = "1.0.96"
 criterion = { version = "0.5.1", features = ["async_tokio", "html_reports"] }
diff --git a/brush-core/src/completion.rs b/brush-core/src/completion.rs
index 3b21c5f..d8a3a47 100644
--- a/brush-core/src/completion.rs
+++ b/brush-core/src/completion.rs
@@ -985,19 +985,15 @@ impl Config {
     }
 
     fn tokenize_input_for_completion(shell: &mut Shell, input: &str) -> Vec<brush_parser::Token> {
-        // Best-effort tokenization.
-        if let Ok(tokens) = brush_parser::tokenize_str_with_options(
-            input,
-            &(shell.parser_options().tokenizer_options()),
-        ) {
-            return tokens;
-        }
+        const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
 
-        // TODO: This isn't right; we need a better fallback, but this is better than nothing.
-        let default_delimiters = [
-            ' ', '\t', '\n', '"', '\'', '>', '<', '=', ';', '|', '&', '(', ':',
-        ];
-        simple_tokenize_by_delimiters(input, &default_delimiters)
+        let delimiter_str = shell
+            .get_env_str("COMP_WORDBREAKS")
+            .unwrap_or(FALLBACK.into());
+
+        let delimiters: Vec<_> = delimiter_str.chars().collect();
+
+        simple_tokenize_by_delimiters(input, delimiters.as_slice())
     }
 
     async fn get_completions_for_token(&self, shell: &mut Shell, context: Context<'_>) -> Answer {
diff --git a/brush-core/src/patterns.rs b/brush-core/src/patterns.rs
index ca88a02..220acb7 100644
--- a/brush-core/src/patterns.rs
+++ b/brush-core/src/patterns.rs
@@ -316,12 +316,6 @@ impl Pattern {
             regex_str.push('^');
         }
 
-        if self.multiline {
-            // Set option for multiline matching + set option for allowing '.' pattern to match
-            // newline.
-            regex_str.push_str("(?ms)");
-        }
-
         let mut current_pattern = String::new();
         for piece in &self.pieces {
             match piece {
@@ -365,7 +359,7 @@ impl Pattern {
 
         tracing::debug!(target: trace_categories::PATTERN, "pattern: '{self:?}' => regex: '{regex_str}'");
 
-        let re = regex::compile_regex(regex_str, self.case_insensitive)?;
+        let re = regex::compile_regex(regex_str, self.case_insensitive, self.multiline)?;
         Ok(re)
     }
 
diff --git a/brush-core/src/regex.rs b/brush-core/src/regex.rs
index 8c44ebc..508aaf1 100644
--- a/brush-core/src/regex.rs
+++ b/brush-core/src/regex.rs
@@ -29,6 +29,7 @@ type RegexWord = Vec<RegexPiece>;
 pub struct Regex {
     pieces: RegexWord,
     case_insensitive: bool,
+    multiline: bool,
 }
 
 impl From<RegexWord> for Regex {
@@ -36,6 +37,7 @@ impl From<RegexWord> for Regex {
         Self {
             pieces,
             case_insensitive: false,
+            multiline: false,
         }
     }
 }
@@ -51,6 +53,18 @@ impl Regex {
         self
     }
 
+    /// Enables (or disables) multiline support for this pattern.
+    /// This enables matching across lines as well as enables `.`
+    /// to match newline characters.
+    ///
+    /// # Arguments
+    ///
+    /// * `value` - The new multiline value.
+    pub fn set_multiline(mut self, value: bool) -> Self {
+        self.multiline = value;
+        self
+    }
+
     /// Computes if the regular expression matches the given string.
     ///
     /// # Arguments
@@ -64,7 +78,7 @@ impl Regex {
             .collect();
 
         // TODO: Evaluate how compatible the `fancy_regex` crate is with POSIX EREs.
-        let re = compile_regex(regex_pattern, self.case_insensitive)?;
+        let re = compile_regex(regex_pattern, self.case_insensitive, self.multiline)?;
 
         Ok(re.captures(value)?.map(|captures| {
             captures
@@ -79,10 +93,20 @@ impl Regex {
 pub(crate) fn compile_regex(
     regex_str: String,
     case_insensitive: bool,
+    multiline: bool,
 ) -> Result<fancy_regex::Regex, error::Error> {
     // Handle identified cases where a shell-supported regex isn't supported directly by
     // `fancy_regex` -- specifically, adding missing escape characters.
-    let regex_str = add_missing_escape_chars_to_regex(regex_str.as_str());
+    let mut regex_str = add_missing_escape_chars_to_regex(regex_str.as_str());
+
+    // Handle multiline enablement.
+    if multiline {
+        // The fancy_regex crate internally seems to have flags that can be used
+        // to enable multiline support, but they're not exposed via its
+        // RegexBuilder. We instead just prefix with the right flags.
+        let updated_str = std::format!("(?ms){regex_str}");
+        regex_str = updated_str.into();
+    }
 
     let mut builder = fancy_regex::RegexBuilder::new(regex_str.as_ref());
     builder.case_insensitive(case_insensitive);
diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs
index a0cd271..789f6ad 100644
--- a/brush-core/src/shell.rs
+++ b/brush-core/src/shell.rs
@@ -274,10 +274,13 @@ impl Shell {
         self.env.set_global("BASHOPTS", bashopts_var)?;
 
         // BASHPID
-        let mut bashpid_var =
-            ShellVariable::new(ShellValue::String(std::process::id().to_string()));
-        bashpid_var.treat_as_integer();
-        self.env.set_global("BASHPID", bashpid_var)?;
+        #[cfg(not(target_family = "wasm"))]
+        {
+            let mut bashpid_var =
+                ShellVariable::new(ShellValue::String(std::process::id().to_string()));
+            bashpid_var.treat_as_integer();
+            self.env.set_global("BASHPID", bashpid_var)?;
+        }
 
         // BASH_ALIASES
         self.env.set_global(
diff --git a/brush-interactive/Cargo.toml b/brush-interactive/Cargo.toml
index 6cb6339..c935f08 100644
--- a/brush-interactive/Cargo.toml
+++ b/brush-interactive/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "brush-interactive"
 description = "Interactive layer of brush-shell"
-version = "0.2.15"
+version = "0.2.16"
 authors.workspace = true
 categories.workspace = true
 edition.workspace = true
@@ -25,14 +25,17 @@ workspace = true
 
 [dependencies]
 async-trait = "0.1.88"
-brush-parser = { version = "^0.2.13", path = "../brush-parser" }
-brush-core = { version = "^0.2.15", path = "../brush-core" }
-crossterm = { version = "0.28.1", features = ["serde"], optional = true }
-indexmap = "2.7.1"
+brush-parser = { version = "^0.2.14", path = "../brush-parser" }
+brush-core = { version = "^0.2.16", path = "../brush-core" }
+crossterm = { version = "0.29.0", features = ["serde"], optional = true }
+indexmap = "2.9.0"
 nu-ansi-term = { version = "0.50.1", optional = true }
 reedline = { version = "0.39.0", optional = true }
 thiserror = "2.0.11"
 tracing = "0.1.41"
 
 [target.'cfg(any(windows, unix))'.dependencies]
-tokio = { version = "1.44.1", features = ["macros", "signal"] }
+tokio = { version = "1.44.2", features = ["macros", "signal"] }
+
+[target.wasm32-unknown-unknown.dependencies]
+getrandom = { version = "0.3.2", features = ["wasm_js"] }
diff --git a/brush-parser/Cargo.toml b/brush-parser/Cargo.toml
index 4784dbe..b30f816 100644
--- a/brush-parser/Cargo.toml
+++ b/brush-parser/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "brush-parser"
 description = "POSIX/bash shell tokenizer and parsers (used by brush-shell)"
-version = "0.2.13"
+version = "0.2.14"
 authors.workspace = true
 categories.workspace = true
 edition.workspace = true
diff --git a/brush-parser/src/tokenizer.rs b/brush-parser/src/tokenizer.rs
index 5d7d9df..a0c476c 100644
--- a/brush-parser/src/tokenizer.rs
+++ b/brush-parser/src/tokenizer.rs
@@ -1091,11 +1091,11 @@ impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> {
                 self.consume_char()?;
             }
             //
-            // N.B. We need to remember if we were recursively called, say in a command
-            // substitution; in that case we won't think a token was started but... we'd
-            // be wrong.
+            // 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() || terminating_char.is_some())
+                && (state.started_token() || matches!(terminating_char, Some('}')))
             {
                 self.consume_char()?;
                 state.append_char(c);
@@ -1115,7 +1115,6 @@ impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> {
                         }
                     };
                 }
-
                 // Re-start loop as if the comment never happened.
                 continue;
             //
diff --git a/brush-shell/Cargo.toml b/brush-shell/Cargo.toml
index b4f40ed..c5ae3bf 100644
--- a/brush-shell/Cargo.toml
+++ b/brush-shell/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "brush-shell"
 description = "Rust-implemented shell focused on POSIX and bash compatibility"
-version = "0.2.15"
+version = "0.2.16"
 authors.workspace = true
 categories.workspace = true
 edition.workspace = true
@@ -40,8 +40,8 @@ workspace = true
 
 [dependencies]
 async-trait = "0.1.88"
-brush-parser = { version = "^0.2.13", path = "../brush-parser" }
-brush-core = { version = "^0.2.15", path = "../brush-core" }
+brush-parser = { version = "^0.2.14", path = "../brush-parser" }
+brush-core = { version = "^0.2.16", path = "../brush-core" }
 cfg-if = "1.0.0"
 clap = { version = "4.5.21", features = ["derive", "env", "wrap_help"] }
 const_format = "0.2.34"
@@ -52,18 +52,22 @@ tracing-subscriber = "0.3.19"
 human-panic = "2.0.2"
 
 [target.'cfg(not(any(windows, unix)))'.dependencies]
-brush-interactive = { version = "^0.2.15", path = "../brush-interactive", features = [
+brush-interactive = { version = "^0.2.16", path = "../brush-interactive", features = [
     "minimal",
 ] }
-tokio = { version = "1.44.1", features = ["rt", "sync"] }
+tokio = { version = "1.44.2", features = ["rt", "sync"] }
 
 [target.'cfg(any(windows, unix))'.dependencies]
-brush-interactive = { version = "^0.2.15", path = "../brush-interactive", features = [
+brush-interactive = { version = "^0.2.16", path = "../brush-interactive", features = [
     "basic",
     "reedline",
 ] }
-crossterm = "0.28.1"
-tokio = { version = "1.43.0", features = ["rt", "rt-multi-thread", "sync"] }
+crossterm = "0.29.0"
+tokio = { version = "1.44.2", features = ["rt", "rt-multi-thread", "sync"] }
+
+[target.wasm32-unknown-unknown.dependencies]
+getrandom = { version = "0.3.2", features = ["wasm_js"] }
+uuid = { version = "1.16.0", features = ["js"] }
 
 [dev-dependencies]
 anyhow = "1.0.96"
diff --git a/brush-shell/src/main.rs b/brush-shell/src/main.rs
index fb07578..4418a49 100644
--- a/brush-shell/src/main.rs
+++ b/brush-shell/src/main.rs
@@ -277,7 +277,7 @@ fn get_default_input_backend() -> InputBackend {
     }
     #[cfg(not(any(windows, unix)))]
     {
-        InputBackend::Basic
+        InputBackend::Minimal
     }
 }
 
diff --git a/deny.toml b/deny.toml
index 460403a..b269ada 100644
--- a/deny.toml
+++ b/deny.toml
@@ -86,6 +86,7 @@ deny = []
 
 # Certain crates/versions that will be skipped when doing duplicate detection.
 skip = [
+    { crate = "crossterm" },
     { crate = "getrandom" },
     { crate = "hashbrown" },
     { crate = "itertools" },
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 6e14ca8..d07b399 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -20,7 +20,7 @@ anyhow = "1.0.96"
 assert_cmd = "2.0.16"
 lazy_static = "1.5.0"
 libfuzzer-sys = "0.4"
-tokio = { version = "1.44.1", features = ["rt"] }
+tokio = { version = "1.44.2", features = ["rt"] }
 
 [dependencies.brush-core]
 path = "../brush-core"
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 094e028..6c28cbf 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -13,7 +13,7 @@ rust-version.workspace = true
 
 [dependencies]
 anyhow = "1.0.96"
-brush-shell = { version = "^0.2.15", path = "../brush-shell" }
+brush-shell = { version = "^0.2.16", path = "../brush-shell" }
 clap = { version = "4.5.21", features = ["derive"] }
 clap_mangen = "0.2.26"
 clap-markdown = "0.1.4"


Test patch:
diff --git a/brush-core/src/extendedtests.rs b/brush-core/src/extendedtests.rs
index ba10429..3174c33 100644
--- a/brush-core/src/extendedtests.rs
+++ b/brush-core/src/extendedtests.rs
@@ -199,7 +199,9 @@ async fn apply_binary_predicate(
     match op {
         ast::BinaryPredicate::StringMatchesRegex => {
             let s = expansion::basic_expand_word(shell, params, left).await?;
-            let regex = expansion::basic_expand_regex(shell, params, right).await?;
+            let regex = expansion::basic_expand_regex(shell, params, right)
+                .await?
+                .set_multiline(true);
 
             if shell.options.print_commands_and_arguments {
                 shell
diff --git a/brush-shell/tests/cases/command_substitution.yaml b/brush-shell/tests/cases/command_substitution.yaml
new file mode 100644
index 0000000..6347298
--- /dev/null
+++ b/brush-shell/tests/cases/command_substitution.yaml
@@ -0,0 +1,73 @@
+name: "Command substitution"
+cases:
+  - name: "Ignore single quote in comment in command substitution"
+    stdin: |
+      var=$(
+        # I'm
+        echo "Batman"
+      )
+      echo $var
+
+  - name: "Ignore double quote in comment in command substitution"
+    stdin: |
+      var=$(
+        # This " is not being
+        echo "parsed"
+      )
+      echo $var
+
+  - name: "Ignore parantheses in comment in command substitution"
+    stdin: |
+      var=$(
+        # :(
+        echo "Sad"
+      )
+      echo $var
+
+  - name: "Ignore dollar in comment in command substitution"
+    stdin: |
+      var=$(
+        #               $
+        echo "Mr. Crabs ^"
+      )
+      echo $var
+
+  - name: "Positional parameter count not mistaken for comment"
+    stdin: |
+      echo $(echo $#)
+
+  - name: "Ignore single quote in comment in command substitution (backticks)"
+    stdin: |
+      var=`
+        # I'm
+        echo "Batman"
+      `
+      echo $var
+
+  - name: "Ignore double quote in comment in command substitution (backticks)"
+    stdin: |
+      var=`
+        # This " is not being
+        echo "parsed"
+      `
+      echo $var
+
+  - name: "Ignore parantheses in comment in command substitution (backticks)"
+    stdin: |
+      var=`
+        # :(
+        echo "Sad"
+      `
+      echo $var
+
+  - name: "Ignore dollar in comment in command substitution (backticks)"
+    stdin: |
+      var=`
+        #               $
+        echo "Mr. Crabs ^"
+      `
+      echo $var
+
+  - name: "Positional parameter count not mistaken for comment (backticks)"
+    stdin: |
+      echo `echo $#`
diff --git a/brush-shell/tests/cases/extended_tests.yaml b/brush-shell/tests/cases/extended_tests.yaml
index a233ce8..0016570 100644
--- a/brush-shell/tests/cases/extended_tests.yaml
+++ b/brush-shell/tests/cases/extended_tests.yaml
@@ -299,3 +299,7 @@ cases:
       [[ z =~ ^z{2,6}$ ]] && echo "1. Matches"
       [[ zzzz =~ ^z{2,6}$ ]] && echo "2. Matches"
       [[ zzzzzzzzz =~ ^z{2,6}$ ]] && echo "3. Matches"
+
+  - name: "Regex with newline"
+    stdin: |
+      [[ $'\n' =~ . ]] && echo "1. Matches"
diff --git a/brush-shell/tests/cases/list.yaml b/brush-shell/tests/cases/list.yaml
new file mode 100644
index 0000000..17dac4d
--- /dev/null
+++ b/brush-shell/tests/cases/list.yaml
@@ -0,0 +1,30 @@
+name: "List"
+cases:
+  - name: "Ignore single quote in comment in list"
+    stdin: |
+      (
+        # I'm
+        echo "Batman"
+      )
+
+  - name: "Ignore double quote in comment in list"
+    stdin: |
+      (
+        # This " is not being
+        echo "parsed"
+      )
+
+  - name: "Ignore parantheses in comment in list"
+    stdin: |
+      (
+        # :(
+        echo "Sad"
+      )
+
+  - name: "Ignore dollar in comment in list"
+    stdin: |
+      (
+        #               $
+        echo "Mr. Crabs ^"
+      )
+