Repository: reubeno/brush
Pull request #330 (2025-01-19 19:22:10)

chore: test updates
* Add a few new tests to track recently reported issues; mark them as being known to fail for now.
* Slightly regroup the pattern tests, dividing them into filename expansion using patterns vs. pattern matching.
* Add a test case for tilde-expansion in assignments.
* Add a `launch.json` for more easily debugging brush in VS Code.
* Add a sample VHS-style transcript for generating scripted screen caps.

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `clone_shell_object                  ` | `   16.44 μs` | `   16.40 μs` | `   -0.03 μs` | `⚪  Unchanged` |
| `eval_arithmetic                     ` | `    0.18 μs` | `    0.20 μs` | `    0.01 μs` | `🟠 +5.95%    ` |
| `expand_one_string                   ` | `    1.52 μs` | `    1.65 μs` | `    0.13 μs` | `🟠 +8.44%    ` |
| `for_loop                            ` | `   32.09 μs` | `   32.76 μs` | `    0.67 μs` | `⚪  Unchanged` |
| `function_call                       ` | `    3.16 μs` | `    3.34 μs` | `    0.18 μs` | `🟠 +5.57%    ` |
| `instantiate_shell                   ` | `   47.43 μs` | `   47.40 μs` | `   -0.03 μs` | `⚪  Unchanged` |
| `instantiate_shell_with_init_scripts ` | `22149.65 μs` | `20897.97 μs` | `-1251.68 μs` | `🟢 -5.65%    ` |
| `parse_bash_completion               ` | ` 1658.71 μs` | ` 1649.87 μs` | `   -8.84 μs` | `⚪  Unchanged` |
| `parse_sample_script                 ` | `    1.73 μs` | `    1.72 μs` | `   -0.02 μs` | `⚪  Unchanged` |
| `run_echo_builtin_command            ` | `   16.44 μs` | `   16.41 μs` | `   -0.03 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 1591.01 μs` | ` 2721.71 μs` | ` 1130.70 μs` | `🟠 +71.07%   ` |
| `tokenize_sample_script              ` | `    2.90 μs` | `    2.89 μs` | `   -0.01 μs` | `⚪  Unchanged` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/namedoptions.rs | 🟠 54.81% | 🟠 55.29% | 🟢 0.48% |
| **Overall Coverage** | **🟢 74.6%** | **🟢 74.6%** | **⚪ 0%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `74.6%`
<!-- Sticky Pull Request Comment -->
github-actions[bot]: ## Test Results
    2 files      9 suites   1m 20s ⏱️
  597 tests   597 ✅ 0 💤 0 ❌
1 180 runs  1 180 ✅ 0 💤 0 ❌

Results for commit aa2ff7df.

[test-results]:data:application/gzip;base64,H4sIAGxVjWcC/13MTQ7CIBCG4as0rF0UKn9ephmBSYhtMRRWjXeXEtHW5fNN5t0I+smt5NaxS0fW7FOFLrA5QvJhKVR9cbmk/ca1bBrXbMz/9PDPMn0/RgQ/nQYXY4ifJeZlb1KqmlrzNP2a1Ydk9bFowjz7VEAAGKK0eLeUG+OE4G6QaLiQSK3CoQd91SAYeb0BFjy29QYBAAA=


Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..a59ae86
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,65 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "lldb",
+            "request": "launch",
+            "name": "Debug executable 'brush'",
+            "cargo": {
+                "args": [
+                    "build",
+                    "--bin=brush",
+                    "--package=brush-shell"
+                ],
+                "filter": {
+                    "name": "brush",
+                    "kind": "bin"
+                }
+            },
+            "args": [
+                "--enable-highlighting"
+            ],
+            "cwd": "${workspaceFolder}"
+        },
+        {
+            "type": "lldb",
+            "request": "launch",
+            "name": "Debug integration test 'brush-compat-tests'",
+            "cargo": {
+                "args": [
+                    "test",
+                    "--no-run",
+                    "--test=brush-compat-tests",
+                    "--package=brush-shell"
+                ],
+                "filter": {
+                    "name": "brush-compat-tests",
+                    "kind": "test"
+                }
+            },
+            "args": [],
+            "cwd": "${workspaceFolder}"
+        },
+        {
+            "type": "lldb",
+            "request": "launch",
+            "name": "Debug executable 'xtask'",
+            "cargo": {
+                "args": [
+                    "build",
+                    "--bin=xtask",
+                    "--package=xtask"
+                ],
+                "filter": {
+                    "name": "xtask",
+                    "kind": "bin"
+                }
+            },
+            "args": [],
+            "cwd": "${workspaceFolder}"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 9ab6af5..b7c8f86 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,11 +116,12 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.6"
+version = "3.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
 dependencies = [
  "anstyle",
+ "once_cell",
  "windows-sys 0.59.0",
 ]
 
@@ -248,9 +249,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.7.0"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
+checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
 dependencies = [
  "serde",
 ]
@@ -447,9 +448,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.2.9"
+version = "1.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
+checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
 dependencies = [
  "jobserver",
  "libc",
@@ -718,7 +719,7 @@ version = "0.28.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
  "crossterm_winapi",
  "mio",
  "parking_lot",
@@ -1130,7 +1131,7 @@ version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
  "ignore",
  "walkdir",
 ]
@@ -1430,9 +1431,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.22"
+version = "0.4.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
 
 [[package]]
 name = "memchr"
@@ -1460,9 +1461,9 @@ dependencies = [
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
+checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
 dependencies = [
  "adler2",
 ]
@@ -1498,7 +1499,7 @@ version = "0.27.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
  "cfg-if",
  "libc",
 ]
@@ -1509,7 +1510,7 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
  "cfg-if",
  "cfg_aliases",
  "libc",
@@ -1793,7 +1794,7 @@ version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
  "chrono",
  "flate2",
  "hex",
@@ -1807,7 +1808,7 @@ version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
  "chrono",
  "hex",
 ]
@@ -1904,7 +1905,7 @@ version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
 ]
 
 [[package]]
@@ -1983,7 +1984,7 @@ version = "0.38.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
 dependencies = [
- "bitflags 2.7.0",
+ "bitflags 2.8.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -2039,9 +2040,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.135"
+version = "1.0.136"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
+checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2"
 dependencies = [
  "itoa",
  "memchr",
@@ -2145,9 +2146,9 @@ checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb"
 
 [[package]]
 name = "strip-ansi-escapes"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa"
+checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
 dependencies = [
  "vte",
 ]
@@ -2179,9 +2180,9 @@ dependencies = [
 
 [[package]]
 name = "symbolic-common"
-version = "12.13.1"
+version = "12.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf08b42a6f9469bd8584daee39a1352c8133ccabc5151ccccb15896ef047d99a"
+checksum = "8150eae9699e3c73a3e6431dc1f80d87748797c0457336af23e94c1de619ed24"
 dependencies = [
  "debugid",
  "memmap2",
@@ -2191,9 +2192,9 @@ dependencies = [
 
 [[package]]
 name = "symbolic-demangle"
-version = "12.13.1"
+version = "12.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f73b5a5bd4da72720c45756a2d11edf110116b87f998bda59b97be8c2c7cf1"
+checksum = "95f4a9846f7a8933b6d198c022faa2c9bd89e1a970bed9d9a98d25708bf8de17"
 dependencies = [
  "cpp_demangle",
  "rustc-demangle",
@@ -2516,9 +2517,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.11.1"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
+checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
 dependencies = [
  "getrandom",
 ]
@@ -2535,9 +2536,9 @@ dependencies = [
 
 [[package]]
 name = "valuable"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
 
 [[package]]
 name = "version-compare"
@@ -2553,22 +2554,11 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
 [[package]]
 name = "vte"
-version = "0.11.1"
+version = "0.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
+checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
 dependencies = [
- "utf8parse",
- "vte_generate_state_changes",
-]
-
-[[package]]
-name = "vte_generate_state_changes"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
-dependencies = [
- "proc-macro2",
- "quote",
+ "memchr",
 ]
 
 [[package]]
diff --git a/README.md b/README.md
index c9eac90..9669768 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
 `brush` (**B**o(u)rn(e) **RU**sty **SH**ell) is a [POSIX-](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html) and [bash-](https://www.gnu.org/software/bash/)compatible shell,
 implemented in Rust. It's built and tested on Linux and macOS, with experimental support on Windows. (Its Linux build is fully supported running on Windows via WSL.)
 
-![screenshot](./docs/extras/brush-screenshot.png)
+![screenshot](https://github.com/user-attachments/assets/0e64d1b9-7e4e-43be-8593-6c1b9607ac52)
 
 `brush` is functional for interactive use as a daily driver! It can execute most `sh` and `bash` scripts we've
 encountered. Known limitations are tracked with filed issues. Out of an abundance of caution, 
@@ -54,8 +54,8 @@ installed on your system, then you can also author a `~/.brushrc` file.
 
 There are some known gaps in compatibility. Most notably:
 
-* **Honoring all `set` and `shopt` options (e.g., `set -e`).**
-  The `set` builtin is implemented, as is `set -x` and some other options, but many of the options aren't fully implemented. `set -e`, for example, will execute but its semantics aren't applied across execution.
+* **Honoring the full semantics of all `set` and `shopt` options.**
+  The `set` builtin is implemented, as is `set -x` and many frequently used options, but a number of options aren't fully implemented. `set -e`, for example, will execute but its semantics aren't applied across execution.
 
 * **Anything tagged with a `TODO` comment or where `error::unimp()` is used to return a "not implemented" error**.
   These aren't all tracked with GitHub issues right now, but there's a number of these scattered throughout the code base. Some are indicative of missing functionality that may be straightforward to implement; others may be more complicated.
@@ -64,7 +64,7 @@ If you feel so inclined, we'd love contributions toward any of the above, with b
 
 ## Testing strategy
 
-This project is primarily tested by comparing its behavior with other existing shells, leveraging the latter as test oracles. The integration tests implemented in this repo include [450+ test cases](brush-shell/tests/cases) run on both this shell and an oracle, comparing standard output and exit codes.
+This project is primarily tested by comparing its behavior with other existing shells, leveraging the latter as test oracles. The integration tests implemented in this repo include [475+ test cases](brush-shell/tests/cases) run on both this shell and an oracle, comparing standard output and exit codes.
 
 For more details, please consult the [reference documentation on integration testing](docs/reference/integration-testing.md).
 
diff --git a/docs/demos/demo.tape b/docs/demos/demo.tape
new file mode 100644
index 0000000..e41e288
--- /dev/null
+++ b/docs/demos/demo.tape
@@ -0,0 +1,146 @@
+# Works with https://github.com/charmbracelet/vhs
+
+Output sample.gif
+
+Set FontFamily "CaskaydiaMono Nerd Font Mono"
+Set FontSize 20
+Set Theme "Monokai Pro"
+Set Width 1600
+Set Height 600
+Set CursorBlink false
+
+# Setup environment to launch brush in
+Env HISTFILE ""
+Env PS1 '$0$ '
+
+# Launch brush and set up bash-completion
+Hide
+Type `brush --enable-highlighting --norc --noprofile`
+Enter
+Type `source /usr/share/bash-completion/bash_completion && clear`
+Enter
+Show
+
+# Enable starship
+Type `# Let's start with a better prompt. starship to the rescue!`
+Sleep 0.8s
+Enter
+Type `eval "$(starship init bash)"`
+Sleep 0.8s
+Enter
+Sleep 1s
+
+# git describe
+Type `git d`
+Tab
+Sleep 1.3s
+Enter
+Sleep 0.4s
+Type `--l`
+Sleep 0.4s
+Tab
+Sleep 0.8s
+Type `brush-she`
+Sleep 0.5s
+Tab
+Sleep 0.2s
+Right
+Sleep 0.2s
+Right
+Sleep 0.8s
+Enter
+Sleep 0.8s
+Enter
+Sleep 1s
+
+# vim
+Type `vim Ca`
+Sleep 0.5s
+Tab
+Sleep 0.8s
+Right
+Sleep 0.5s
+Enter
+Sleep 1s
+Enter
+
+Type `1G`
+Type `i`
+Enter
+Up
+Type `# Let's try suspending vim...`
+Enter
+Escape
+Sleep 1s
+Ctrl+Z
+Sleep 0.7s
+
+Type `# Yep, it's suspended.`
+Sleep 0.4s
+Type ` Let's bring it back.`
+Enter
+Sleep 0.8s
+
+Type `fg`
+Sleep 0.4s
+Enter
+Sleep 0.6s
+
+Type `:q!`
+Sleep 0.3s
+Enter
+Sleep 0.5s
+Ctrl+L
+Sleep 0.2s
+
+Type `# Let's properly greet the world.`
+Enter
+Sleep 0.4s
+
+# Figure out version
+Type `verline=$(help | head -n1)`
+Sleep 0.2s
+Enter
+Sleep 0.3s
+Type `[[ "${verline}" =~ ^.*version\ ([[:digit:]\.]+).*$ ]] && ver=${BASH_REMATCH[1]}`
+Sleep 0.4s
+Enter
+Sleep 0.2s
+Type `declare -p ver`
+Enter
+Sleep 0.4s
+
+# Declare function
+Type `function greet() {`
+Enter
+Type `  echo "Hello from brush ${ver}!"`
+Enter
+Type `}`
+Sleep 1s
+Enter
+Type `type greet`
+Sleep 0.4s
+Enter
+Sleep 1s
+
+# Use function
+Ctrl+L
+Type `for ((i = 0; i < 5; i++)); do greet; done`
+Sleep 1s
+Enter
+Sleep 0.8s
+
+Type `# Surely we can make that more colorful.`
+Enter
+Sleep 1s
+
+# Now with lolcat
+Type `for `
+Sleep 1s
+Right
+Sleep 0.8s
+Type ` | lolcat -F 0.3 -S 12`
+Sleep 0.6s
+Enter
+
+Sleep 3s


Test patch:
diff --git a/brush-shell/tests/cases/assignments.yaml b/brush-shell/tests/cases/assignments.yaml
index d48b5ce..2c9c841 100644
--- a/brush-shell/tests/cases/assignments.yaml
+++ b/brush-shell/tests/cases/assignments.yaml
@@ -24,3 +24,14 @@ cases:
     stdin: |
       x=y=z
       echo "x: ${x}"
+
+  - name: "Assignment with tilde expansion"
+    known_failure: true
+    stdin: |
+      HOME=/some/dir
+
+      var=~/file1.txt
+      echo "~/file1.txt: ${var}"
+
+      var=~/file1.txt:~/file2.txt
+      echo "~/file1.txt:~/file2.txt: ${var}"
diff --git a/brush-shell/tests/cases/builtins/compgen.yaml b/brush-shell/tests/cases/builtins/compgen.yaml
index a838a8d..27851ef 100644
--- a/brush-shell/tests/cases/builtins/compgen.yaml
+++ b/brush-shell/tests/cases/builtins/compgen.yaml
@@ -27,6 +27,19 @@ cases:
 
       compgen -A file some | sort
 
+  - name: "compgen -A file with dot files"
+    stdin: |
+      touch .file
+      mkdir .dir
+
+      echo "[without dotglob]"
+      shopt -u dotglob
+      compgen -A file | sort
+
+      echo "[with dotglob]"
+      shopt -s dotglob
+      compgen -A file | sort
+
   - name: "compgen -A function"
     stdin: |
       myfunc() {
diff --git a/brush-shell/tests/cases/patterns.yaml b/brush-shell/tests/cases/patterns/filename_expansion.yaml
similarity index 54%
rename from brush-shell/tests/cases/patterns.yaml
rename to brush-shell/tests/cases/patterns/filename_expansion.yaml
index 8a75a44..16a457a 100644
--- a/brush-shell/tests/cases/patterns.yaml
+++ b/brush-shell/tests/cases/patterns/filename_expansion.yaml
@@ -1,4 +1,4 @@
-name: "Patterns"
+name: "Filename expansion"
 cases:
   - name: "Single file expansion"
     stdin: "echo file1.txt"
@@ -77,7 +77,9 @@ cases:
   - name: "Expansion with tilde"
     stdin: |
       HOME=/some/dir
+
       echo ~/file1.txt
+      echo ~/file1.txt:~/file1.txt
 
   - name: "Expansion with dots"
     stdin: |
@@ -94,84 +96,6 @@ cases:
       - path: "file*.txt"
     stdin: "echo file\\*.txt"
 
-  - name: "Basic pattern matching"
-    stdin: |
-      test_pattern() {
-        local str="$1"
-        local pattern="$2"
-
-        if [[ "${str}" == ${pattern} ]]; then
-          echo "Matched: ${str} using ${pattern}"
-        else
-          echo "No match: ${str} using ${pattern}"
-        fi
-      }
-
-      test_pattern "abc" "a*"
-      test_pattern "abc" "b*"
-
-      test_pattern "?bc" "abc"
-      test_pattern "?bc" "aabc"
-
-      test_pattern "ac" "[ab]c"
-      test_pattern "bc" "[ab]c"
-      test_pattern "cc" "[ab]c"
-
-      test_pattern "ad" "[a-c]d"
-      test_pattern "bd" "[a-c]d"
-      test_pattern "dd" "[a-c]d"
-
-      test_pattern "1" "[[:alpha:]]"
-      test_pattern "a" "[[:alpha:]]"
-
-      test_pattern "a/b" "*b"
-      test_pattern "a/b" "a/b"
-      test_pattern "a/b" "*/*"
-
-  - name: "Extglob pattern matching"
-    stdin: |
-      shopt -s extglob
-
-      test_pattern() {
-        local str="$1"
-        local pattern="$2"
-
-        if [[ "${str}" == ${pattern} ]]; then
-          echo "Matched: ${str} using ${pattern}"
-        else
-          echo "No match: ${str} using ${pattern}"
-        fi
-      }
-
-      test_pattern "aabc" "!(a*)"
-      test_pattern "abc" "!(a*)"
-      test_pattern "def" "!(a*)"
-
-      test_pattern "a.foo.tar.gz" "a.!(foo|bar).tar.gz"
-      test_pattern "a.bar.tar.gz" "a.!(foo|bar).tar.gz"
-      test_pattern "a.baz.tar.gz" "a.!(foo|bar).tar.gz"
-      test_pattern "a.tar.tar.gz" "a.!(foo|bar).tar.gz"
-      test_pattern "a..tar.gz" "a.!(foo|bar).tar.gz"
-      test_pattern "a.tar.gz" "a.!(foo|bar).tar.gz"
-
-      test_pattern "abc" "@(abc|def)"
-      test_pattern "def" "@(abc|def)"
-      test_pattern "ghi" "@(abc|def)"
-
-      test_pattern "abc" "ab?(c)"
-      test_pattern "ab" "ab?(c)"
-      test_pattern "abd" "ab?(c)"
-
-      test_pattern "" "*(ab|ac)"
-      test_pattern "ab" "*(ab|ac)"
-      test_pattern "abab" "*(ab|ac)"
-      test_pattern "ad" "*(ab|ac)"
-
-      test_pattern "" "+(ab|ac)"
-      test_pattern "ab" "+(ab|ac)"
-      test_pattern "abab" "+(ab|ac)"
-      test_pattern "ad" "+(ab|ac)"
-
   - name: "Pathname expansion: extglob disabled"
     ignore_stderr: true
     test_files:
@@ -272,56 +196,28 @@ cases:
       echo "subdir"/*.txt
       echo "test.*"
 
-  - name: "Patterns: quoting"
+  - name: "Pathname expansion: dot files (no dotglob)"
+    known_failure: true # https://github.com/reubeno/brush/issues/328
     stdin: |
-      [[ "abc" == "a"* ]] && echo "1. Matched"
-      [[ "abc" == a"*" ]] && echo "2. Matched"
-      [[ "abc" == "a*" ]] && echo "3. Matched"
+      touch .file
+      touch .dir
 
-  - name: "Patterns: escaped special characters"
-    stdin: |
-      myfunc() {
-        if [[ $1 == \\* ]]; then
-          echo "Matched: '$1'"
-        else
-          echo "Did *not* match: '$1'"
-        fi
-      }
-
-      myfunc abc
-      myfunc "*"
-
-  - name: "Pattern matching: character ranges"
-    stdin: |
-      [[ "x" == [a-z] ]] && echo "1. Matched"
-      [[ "x" == [0-9] ]] && echo "2. Matched"
-      [[ "-" == [---] ]] && echo "3. Matched"
+      shopt -u dotglob
+      echo "*   : " *
+      echo "*i* : " *i*
+      echo "./* : " ./*
+      echo ".*  : " .*
+      echo "./.*: " ./.*
 
-  - name: "Pattern matching: character sets"
-    stdin: |
-      [[ "x" == [abc]  ]] && echo "1. Matched"
-      [[ "x" == [xyz]  ]] && echo "2. Matched"
-      [[ "x" == [^xyz] ]] && echo "3. Matched"
-      [[ "x" == [!xyz] ]] && echo "4. Matched"
-      [[ "(" == [\(]   ]] && echo "5. Matched"
-      [[ "+" == [+-]   ]] && echo "6. Matched"
-
-  - name: "Pattern matching: character classes"
+  - name: "Pathname expansion: dot files (with dotglob)"
+    skip: true # TODO: passes on *some* platforms but not others (due to . and ..); needs investigation
     stdin: |
-      [[ "1" == [[:digit:]] ]] && echo "1. Matched"
-      [[ "1" == [[:alpha:]] ]] && echo "2. Matched"
+      touch .file
+      touch .dir
 
-  - name: "Pattern matching: case sensitivity"
-    stdin: |
-      shopt -u nocasematch
-      [[ "abc" == "ABC" ]]     && echo "1. Matched"
-      [[ "abc" == "[A-Z]BC" ]] && echo "2. Matched"
-
-      shopt -s nocasematch
-      [[ "abc" == "ABC" ]]     && echo "3. Matched"
-      [[ "abc" == "[A-Z]BC" ]] && echo "4. Matched"
-
-  - name: "Pattern matching: stars in negative extglobs"
-    stdin: |
-      shopt -s extglob
-      [[ 'd' == !(*d)d ]] && echo "1. Matched"
+      shopt -s dotglob
+      echo "*   : " *
+      echo "*i* : " *i*
+      echo "./* : " ./*
+      echo ".*  : " .*
+      echo "./.*: " ./.*
diff --git a/brush-shell/tests/cases/patterns/patterns.yaml b/brush-shell/tests/cases/patterns/patterns.yaml
new file mode 100644
index 0000000..aed27d7
--- /dev/null
+++ b/brush-shell/tests/cases/patterns/patterns.yaml
@@ -0,0 +1,138 @@
+name: "Patterns"
+cases:
+  - name: "Expansion with escaped characters"
+    test_files:
+      - path: "file*.txt"
+    stdin: "echo file\\*.txt"
+
+  - name: "Basic pattern matching"
+    stdin: |
+      test_pattern() {
+        local str="$1"
+        local pattern="$2"
+
+        if [[ "${str}" == ${pattern} ]]; then
+          echo "Matched: ${str} using ${pattern}"
+        else
+          echo "No match: ${str} using ${pattern}"
+        fi
+      }
+
+      test_pattern "abc" "a*"
+      test_pattern "abc" "b*"
+
+      test_pattern "?bc" "abc"
+      test_pattern "?bc" "aabc"
+
+      test_pattern "ac" "[ab]c"
+      test_pattern "bc" "[ab]c"
+      test_pattern "cc" "[ab]c"
+
+      test_pattern "ad" "[a-c]d"
+      test_pattern "bd" "[a-c]d"
+      test_pattern "dd" "[a-c]d"
+
+      test_pattern "1" "[[:alpha:]]"
+      test_pattern "a" "[[:alpha:]]"
+
+      test_pattern "a/b" "*b"
+      test_pattern "a/b" "a/b"
+      test_pattern "a/b" "*/*"
+
+  - name: "Extglob pattern matching"
+    stdin: |
+      shopt -s extglob
+
+      test_pattern() {
+        local str="$1"
+        local pattern="$2"
+
+        if [[ "${str}" == ${pattern} ]]; then
+          echo "Matched: ${str} using ${pattern}"
+        else
+          echo "No match: ${str} using ${pattern}"
+        fi
+      }
+
+      test_pattern "aabc" "!(a*)"
+      test_pattern "abc" "!(a*)"
+      test_pattern "def" "!(a*)"
+
+      test_pattern "a.foo.tar.gz" "a.!(foo|bar).tar.gz"
+      test_pattern "a.bar.tar.gz" "a.!(foo|bar).tar.gz"
+      test_pattern "a.baz.tar.gz" "a.!(foo|bar).tar.gz"
+      test_pattern "a.tar.tar.gz" "a.!(foo|bar).tar.gz"
+      test_pattern "a..tar.gz" "a.!(foo|bar).tar.gz"
+      test_pattern "a.tar.gz" "a.!(foo|bar).tar.gz"
+
+      test_pattern "abc" "@(abc|def)"
+      test_pattern "def" "@(abc|def)"
+      test_pattern "ghi" "@(abc|def)"
+
+      test_pattern "abc" "ab?(c)"
+      test_pattern "ab" "ab?(c)"
+      test_pattern "abd" "ab?(c)"
+
+      test_pattern "" "*(ab|ac)"
+      test_pattern "ab" "*(ab|ac)"
+      test_pattern "abab" "*(ab|ac)"
+      test_pattern "ad" "*(ab|ac)"
+
+      test_pattern "" "+(ab|ac)"
+      test_pattern "ab" "+(ab|ac)"
+      test_pattern "abab" "+(ab|ac)"
+      test_pattern "ad" "+(ab|ac)"
+
+  - name: "Patterns: quoting"
+    stdin: |
+      [[ "abc" == "a"* ]] && echo "1. Matched"
+      [[ "abc" == a"*" ]] && echo "2. Matched"
+      [[ "abc" == "a*" ]] && echo "3. Matched"
+
+  - name: "Patterns: escaped special characters"
+    stdin: |
+      myfunc() {
+        if [[ $1 == \\* ]]; then
+          echo "Matched: '$1'"
+        else
+          echo "Did *not* match: '$1'"
+        fi
+      }
+
+      myfunc abc
+      myfunc "*"
+
+  - name: "Pattern matching: character ranges"
+    stdin: |
+      [[ "x" == [a-z] ]] && echo "1. Matched"
+      [[ "x" == [0-9] ]] && echo "2. Matched"
+      [[ "-" == [---] ]] && echo "3. Matched"
+
+  - name: "Pattern matching: character sets"
+    stdin: |
+      [[ "x" == [abc]  ]] && echo "1. Matched"
+      [[ "x" == [xyz]  ]] && echo "2. Matched"
+      [[ "x" == [^xyz] ]] && echo "3. Matched"
+      [[ "x" == [!xyz] ]] && echo "4. Matched"
+      [[ "(" == [\(]   ]] && echo "5. Matched"
+      [[ "+" == [+-]   ]] && echo "6. Matched"
+
+  - name: "Pattern matching: character classes"
+    stdin: |
+      [[ "1" == [[:digit:]] ]] && echo "1. Matched"
+      [[ "1" == [[:alpha:]] ]] && echo "2. Matched"
+
+  - name: "Pattern matching: case sensitivity"
+    stdin: |
+      shopt -u nocasematch
+      [[ "abc" == "ABC" ]]     && echo "1. Matched"
+      [[ "abc" == "[A-Z]BC" ]] && echo "2. Matched"
+
+      shopt -s nocasematch
+      [[ "abc" == "ABC" ]]     && echo "3. Matched"
+      [[ "abc" == "[A-Z]BC" ]] && echo "4. Matched"
+
+  - name: "Pattern matching: stars in negative extglobs"
+    stdin: |
+      shopt -s extglob
+      [[ 'd' == !(*d)d ]] && echo "1. Matched"
diff --git a/brush-shell/tests/cases/word_expansion.yaml b/brush-shell/tests/cases/word_expansion.yaml
index dfff71d..5e5361d 100644
--- a/brush-shell/tests/cases/word_expansion.yaml
+++ b/brush-shell/tests/cases/word_expansion.yaml
@@ -66,6 +66,12 @@ cases:
       x=$(false) && echo "1. Made it past false"
       y=$(true) && echo "2. Made it past true"
 
+  - name: "Command substitution with exec"
+    known_failure: true
+    stdin: |
+      x=$(exec echo hi)
+      echo "x: $x"
+
   - name: "Backtick command substitution"
     stdin: |
       echo `echo hi`