Repository: reubeno/brush
Pull request #244 (2024-10-26 19:50:16)

feat: implement `|&` for function declarations
Closed: #242 
WIP. Adding `|&` for function declarations turns out to be quite straightforward. However `extended tests` require some move work

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `expand_one_string                   ` | `    3.41 μs` | `    3.41 μs` | `    0.00 μs` | `⚪  Unchanged` |
| `instantiate_shell                   ` | `   60.07 μs` | `   60.22 μs` | `    0.16 μs` | `⚪  Unchanged` |
| `instantiate_shell_with_init_scripts ` | `30283.29 μs` | `30189.36 μs` | `  -93.93 μs` | `⚪  Unchanged` |
| `parse_bash_completion               ` | ` 2795.34 μs` | ` 2787.00 μs` | `   -8.34 μs` | `⚪  Unchanged` |
| `parse_sample_script                 ` | `    4.38 μs` | `    4.24 μs` | `   -0.15 μs` | `🟢 -3.35%    ` |
| `run_echo_builtin_command            ` | `   88.97 μs` | `   90.29 μs` | `    1.32 μs` | `🟠 +1.48%    ` |
| `run_one_builtin_command             ` | `  107.32 μs` | `  110.27 μs` | `    2.95 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 1936.39 μs` | ` 1985.64 μs` | `   49.25 μs` | `🟠 +2.54%    ` |
| `run_one_external_command_directly   ` | ` 1005.51 μs` | ` 1008.38 μs` | `    2.87 μs` | `⚪  Unchanged` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/interp.rs | 🟢 90.13% | 🟢 90.34% | 🟢 0.21% |
| brush-core/src/openfiles.rs | 🟠 50% | 🟠 51.23% | 🟢 1.23% |
| brush-parser/src/parser.rs | 🟢 98.47% | 🟢 98.97% | 🟢 0.5% |
| **Overall Coverage** | **🟢 73.5%** | **🟢 73.6%** | **🟢 0.1%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `73.6%`
<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 94b9dd5..f76a7b8 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -277,6 +277,27 @@ jobs:
         if: matrix.rust-version == 'stable'
         run: cargo clippy --all-features --all-targets
 
+  # Check for unneeded dependencies.
+  check-deps:
+    name: "Check for unneeded dependencies"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up nightly rust toolchain
+        uses: dtolnay/rust-toolchain@master
+        with:
+          toolchain: nightly
+
+      - name: Install cargo-udeps
+        uses: taiki-e/install-action@v2
+        with:
+          tool: cargo-udeps
+
+      - name: Check for unused dependencies
+        run: cargo udeps --workspace --all-targets --all-features
+
   # Performance analysis of the code.
   benchmark:
     if: github.event_name == 'pull_request'
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
new file mode 100644
index 0000000..69d8f30
--- /dev/null
+++ b/.github/workflows/docs.yaml
@@ -0,0 +1,65 @@
+name: "Docs"
+on:
+  workflow_dispatch:
+  pull_request:
+  push:
+    branches:
+      - main
+
+env:
+  RUST_BACKTRACE: 1
+  CARGO_TERM_COLOR: always
+  CLICOLOR: 1
+  CLICOLOR_FORCE: 1
+
+jobs:
+  # Check links in docs
+  check-links:
+    name: "Check links"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout sources
+        uses: actions/checkout@v4
+
+      - name: Check repo-local links
+        uses: lycheeverse/lychee-action@v2
+        with:
+          args: ". --offline --verbose --no-progress"
+          fail: true
+
+  # Generate docs content
+  generate-docs:
+    name: "Generate usage docs"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up rust toolchain
+        uses: dtolnay/rust-toolchain@stable
+        with:
+          toolchain: stable
+
+      - name: Enable cargo cache
+        uses: Swatinem/rust-cache@v2
+
+      - name: Create dirs
+        run: mkdir -p ./md ./man
+
+      - name: Build markdown info
+        run: cargo xtask generate-markdown --out ./md/brush.md
+
+      - name: Upload markdown docs
+        uses: actions/upload-artifact@v4
+        with:
+          name: docs-markdown
+          path: md
+
+      - name: Build man pages
+        run: cargo xtask generate-man --output-dir ./man
+
+      - name: Upload man pages
+        uses: actions/upload-artifact@v4
+        with:
+          name: docs-man
+          path: man
diff --git a/Cargo.lock b/Cargo.lock
index d086b9f..461da76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -68,9 +68,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 [[package]]
 name = "anstream"
-version = "0.6.15"
+version = "0.6.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
+checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -83,43 +83,43 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.8"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.5"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.1"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
 dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.4"
+version = "3.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
 dependencies = [
  "anstyle",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anyhow"
-version = "1.0.90"
+version = "1.0.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
+checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
 
 [[package]]
 name = "arbitrary"
@@ -181,7 +181,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -192,7 +192,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -218,18 +218,18 @@ dependencies = [
 
 [[package]]
 name = "bit-set"
-version = "0.5.3"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
 dependencies = [
  "bit-vec",
 ]
 
 [[package]]
 name = "bit-vec"
-version = "0.6.3"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
 
 [[package]]
 name = "bitflags"
@@ -390,9 +390,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
 
 [[package]]
 name = "cached"
@@ -418,7 +418,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -435,9 +435,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.1.30"
+version = "1.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
+checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
 dependencies = [
  "jobserver",
  "libc",
@@ -537,7 +537,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -558,9 +558,9 @@ dependencies = [
 
 [[package]]
 name = "colorchoice"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
 name = "colored"
@@ -751,7 +751,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -762,7 +762,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
 dependencies = [
  "darling_core",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -802,7 +802,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -891,9 +891,9 @@ dependencies = [
 
 [[package]]
 name = "fancy-regex"
-version = "0.13.0"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
+checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
 dependencies = [
  "bit-set",
  "regex-automata",
@@ -1001,7 +1001,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1068,7 +1068,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1369,9 +1369,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libc"
-version = "0.2.160"
+version = "0.2.161"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
 
 [[package]]
 name = "libfuzzer-sys"
@@ -1638,9 +1638,9 @@ checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.14"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
 
 [[package]]
 name = "pin-utils"
@@ -1742,9 +1742,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.88"
+version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
 dependencies = [
  "unicode-ident",
 ]
@@ -1891,9 +1891,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.11.0"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1981,29 +1981,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
 name = "serde"
-version = "1.0.210"
+version = "1.0.213"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.210"
+version = "1.0.213"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.128"
+version = "1.0.132"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
 dependencies = [
  "itoa",
  "memchr",
@@ -2136,7 +2136,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2175,9 +2175,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.79"
+version = "2.0.85"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2224,22 +2224,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
 
 [[package]]
 name = "thiserror"
-version = "1.0.64"
+version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
+checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.64"
+version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
+checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2295,9 +2295,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.40.0"
+version = "1.41.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
+checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
 dependencies = [
  "backtrace",
  "bytes",
@@ -2317,7 +2317,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2372,7 +2372,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2565,7 +2565,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
  "wasm-bindgen-shared",
 ]
 
@@ -2587,7 +2587,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -2724,7 +2724,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2735,7 +2735,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2981,5 +2981,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
diff --git a/brush-core/Cargo.toml b/brush-core/Cargo.toml
index 0a0c98d..3de613e 100644
--- a/brush-core/Cargo.toml
+++ b/brush-core/Cargo.toml
@@ -17,28 +17,28 @@ bench = false
 workspace = true
 
 [dependencies]
-async-recursion = "1.1.0"
+async-recursion = "1.1.1"
 async-trait = "0.1.83"
 brush-parser = { version = "^0.2.9", path = "../brush-parser" }
-cached = "0.53.0"
+cached = "0.53.1"
 cfg-if = "1.0.0"
-clap = { version = "4.5.17", features = ["derive", "wrap_help"] }
-fancy-regex = "0.13.0"
+clap = { version = "4.5.20", features = ["derive", "wrap_help"] }
+fancy-regex = "0.14.0"
 futures = "0.3.31"
 indexmap = "2.6.0"
 itertools = "0.13.0"
 lazy_static = "1.5.0"
 rand = "0.8.5"
-thiserror = "1.0.64"
+thiserror = "1.0.65"
 tracing = "0.1.40"
 
 [target.'cfg(target_family = "wasm")'.dependencies]
-tokio = { version = "1.40.0", features = ["io-util", "macros", "rt"] }
+tokio = { version = "1.41.0", 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.40.0", features = [
+tokio = { version = "1.41.0", features = [
     "io-util",
     "macros",
     "process",
@@ -48,7 +48,7 @@ tokio = { version = "1.40.0", features = [
 ] }
 
 [target.'cfg(windows)'.dependencies]
-homedir = "0.3.3"
+homedir = "0.3.4"
 whoami = "1.5.2"
 
 [target.'cfg(unix)'.dependencies]
@@ -66,7 +66,7 @@ uzers = "0.12.1"
 procfs = "0.17.0"
 
 [dev-dependencies]
-anyhow = "1.0.90"
+anyhow = "1.0.91"
 criterion = { version = "0.5.1", features = ["async_tokio", "html_reports"] }
 
 [target.'cfg(unix)'.dev-dependencies]
diff --git a/brush-core/src/builtins/kill.rs b/brush-core/src/builtins/kill.rs
index f94b978..3a13410 100644
--- a/brush-core/src/builtins/kill.rs
+++ b/brush-core/src/builtins/kill.rs
@@ -1,6 +1,7 @@
 use clap::Parser;
 use std::io::Write;
 
+use crate::traps::TrapSignal;
 use crate::{builtins, commands, error};
 
 /// Signal a job or process.
@@ -37,15 +38,13 @@ impl builtins::Command for KillCommand {
         }
 
         if self.list_signals {
-            error::unimp("kill -l")
+            return print_signals(&context, self.args.as_ref());
         } else {
             if self.args.len() != 1 {
                 writeln!(context.stderr(), "{}: invalid usage", context.command_name)?;
                 return Ok(builtins::ExitCode::InvalidUsage);
             }
 
-            let exit_code = builtins::ExitCode::Success;
-
             let pid_or_job_spec = &self.args[0];
             if pid_or_job_spec.starts_with('%') {
                 // It's a job spec.
@@ -67,8 +66,56 @@ impl builtins::Command for KillCommand {
                 nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), nix::sys::signal::SIGKILL)
                     .map_err(|_errno| error::Error::FailedToSendSignal)?;
             }
+        }
+        Ok(builtins::ExitCode::Success)
+    }
+}
 
-            Ok(exit_code)
+fn print_signals(
+    context: &commands::ExecutionContext<'_>,
+    signals: &[String],
+) -> Result<builtins::ExitCode, error::Error> {
+    let mut exit_code = builtins::ExitCode::Success;
+    if !signals.is_empty() {
+        for s in signals {
+            // If the user gives us a code, we print the name; if they give a name, we print its
+            // code.
+            enum PrintSignal {
+                Name(&'static str),
+                Num(i32),
+            }
+
+            let signal = if let Ok(n) = s.parse::<i32>() {
+                // bash compatibility. `SIGHUP` -> `HUP`
+                TrapSignal::try_from(n).map(|s| {
+                    PrintSignal::Name(s.as_str().strip_prefix("SIG").unwrap_or(s.as_str()))
+                })
+            } else {
+                TrapSignal::try_from(s.as_str()).map(|sig| {
+                    i32::try_from(sig).map_or(PrintSignal::Name(sig.as_str()), PrintSignal::Num)
+                })
+            };
+
+            match signal {
+                Ok(PrintSignal::Num(n)) => {
+                    writeln!(context.stdout(), "{n}")?;
+                }
+                Ok(PrintSignal::Name(s)) => {
+                    writeln!(context.stdout(), "{s}")?;
+                }
+                Err(e) => {
+                    writeln!(context.stderr(), "{e}")?;
+                    exit_code = builtins::ExitCode::Custom(1);
+                }
+            }
         }
+    } else {
+        return crate::traps::format_signals(
+            context.stdout(),
+            TrapSignal::iterator().filter(|s| !matches!(s, TrapSignal::Exit)),
+        )
+        .map(|()| builtins::ExitCode::Success);
     }
+
+    Ok(exit_code)
 }
diff --git a/brush-core/src/builtins/trap.rs b/brush-core/src/builtins/trap.rs
index 1a9cc71..33cff63 100644
--- a/brush-core/src/builtins/trap.rs
+++ b/brush-core/src/builtins/trap.rs
@@ -1,7 +1,8 @@
 use clap::Parser;
 use std::io::Write;
 
-use crate::{builtins, commands, error, sys, traps};
+use crate::traps::TrapSignal;
+use crate::{builtins, commands, error};
 
 /// Manage signal traps.
 #[derive(Parser)]
@@ -23,13 +24,12 @@ impl builtins::Command for TrapCommand {
         mut context: commands::ExecutionContext<'_>,
     ) -> Result<builtins::ExitCode, crate::error::Error> {
         if self.list_signals {
-            Self::display_signals(&context)?;
-            Ok(builtins::ExitCode::Success)
+            crate::traps::format_signals(context.stdout(), TrapSignal::iterator())
+                .map(|()| builtins::ExitCode::Success)
         } else if self.print_trap_commands || self.args.is_empty() {
             if !self.args.is_empty() {
                 for signal_type in &self.args {
-                    let signal_type = parse_signal(signal_type)?;
-                    Self::display_handlers_for(&context, signal_type)?;
+                    Self::display_handlers_for(&context, signal_type.parse()?)?;
                 }
             } else {
                 Self::display_all_handlers(&context)?;
@@ -37,15 +37,14 @@ impl builtins::Command for TrapCommand {
             Ok(builtins::ExitCode::Success)
         } else if self.args.len() == 1 {
             let signal = self.args[0].as_str();
-            let signal_type = parse_signal(signal)?;
-            Self::remove_all_handlers(&mut context, signal_type);
+            Self::remove_all_handlers(&mut context, signal.parse()?);
             Ok(builtins::ExitCode::Success)
         } else {
             let handler = &self.args[0];
 
             let mut signal_types = vec![];
             for signal in &self.args[1..] {
-                signal_types.push(parse_signal(signal)?);
+                signal_types.push(signal.parse()?);
             }
 
             Self::register_handler(&mut context, signal_types, handler.as_str());
@@ -56,16 +55,6 @@ impl builtins::Command for TrapCommand {
 
 #[allow(unused_variables)]
 impl TrapCommand {
-    #[allow(clippy::unnecessary_wraps)]
-    fn display_signals(context: &commands::ExecutionContext<'_>) -> Result<(), error::Error> {
-        #[cfg(unix)]
-        for signal in nix::sys::signal::Signal::iterator() {
-            writeln!(context.stdout(), "{}: {signal}", signal as i32)?;
-        }
-
-        Ok(())
-    }
-
     fn display_all_handlers(context: &commands::ExecutionContext<'_>) -> Result<(), error::Error> {
         for signal in context.shell.traps.handlers.keys() {
             Self::display_handlers_for(context, *signal)?;
@@ -75,7 +64,7 @@ impl TrapCommand {
 
     fn display_handlers_for(
         context: &commands::ExecutionContext<'_>,
-        signal_type: traps::TrapSignal,
+        signal_type: TrapSignal,
     ) -> Result<(), error::Error> {
         if let Some(handler) = context.shell.traps.handlers.get(&signal_type) {
             writeln!(context.stdout(), "trap -- '{handler}' {signal_type}")?;
@@ -85,14 +74,14 @@ impl TrapCommand {
 
     fn remove_all_handlers(
         context: &mut crate::commands::ExecutionContext<'_>,
-        signal: traps::TrapSignal,
+        signal: TrapSignal,
     ) {
         context.shell.traps.remove_handlers(signal);
     }
 
     fn register_handler(
         context: &mut crate::commands::ExecutionContext<'_>,
-        signals: Vec<traps::TrapSignal>,
+        signals: Vec<TrapSignal>,
         handler: &str,
     ) {
         for signal in signals {
@@ -103,26 +92,3 @@ impl TrapCommand {
         }
     }
 }
-
-fn parse_signal(signal: &str) -> Result<traps::TrapSignal, error::Error> {
-    if signal.chars().all(|c| c.is_ascii_digit()) {
-        let digits = signal
-            .parse::<i32>()
-            .map_err(|_| error::Error::InvalidSignal)?;
-
-        sys::signal::parse_numeric_signal(digits)
-    } else {
-        let mut signal_to_parse = signal.to_ascii_uppercase();
-
-        if !signal_to_parse.starts_with("SIG") {
-            signal_to_parse.insert_str(0, "SIG");
-        }
-
-        match signal_to_parse {
-            s if s == "SIGDEBUG" => Ok(traps::TrapSignal::Debug),
-            s if s == "SIGERR" => Ok(traps::TrapSignal::Err),
-            s if s == "SIGEXIT" => Ok(traps::TrapSignal::Exit),
-            s => sys::signal::parse_os_signal_name(s.as_str()),
-        }
-    }
-}
diff --git a/brush-core/src/completion.rs b/brush-core/src/completion.rs
index 450a661..5a73cee 100644
--- a/brush-core/src/completion.rs
+++ b/brush-core/src/completion.rs
@@ -487,10 +487,9 @@ impl Spec {
                     }
                 }
                 CompleteAction::Signal => {
-                    for signal in traps::TrapSignal::all_values() {
-                        let signal_str = signal.to_string();
-                        if signal_str.starts_with(token) {
-                            candidates.insert(signal_str);
+                    for signal in traps::TrapSignal::iterator() {
+                        if signal.as_str().starts_with(token) {
+                            candidates.insert(signal.as_str().to_string());
                         }
                     }
                 }
diff --git a/brush-core/src/error.rs b/brush-core/src/error.rs
index d2e890d..946e082 100644
--- a/brush-core/src/error.rs
+++ b/brush-core/src/error.rs
@@ -148,8 +148,8 @@ pub enum Error {
     ThreadingError(#[from] tokio::task::JoinError),
 
     /// An invalid signal was referenced.
-    #[error("invalid signal")]
-    InvalidSignal,
+    #[error("{0}: invalid signal specification")]
+    InvalidSignal(String),
 
     /// A system error occurred.
     #[cfg(unix)]
diff --git a/brush-core/src/sys/stubs/signal.rs b/brush-core/src/sys/stubs/signal.rs
index 22c8932..6b816cf 100644
--- a/brush-core/src/sys/stubs/signal.rs
+++ b/brush-core/src/sys/stubs/signal.rs
@@ -1,13 +1,5 @@
 use crate::{error, sys, traps};
 
-pub(crate) fn parse_numeric_signal(_signal: i32) -> Result<traps::TrapSignal, error::Error> {
-    Err(error::Error::InvalidSignal)
-}
-
-pub(crate) fn parse_os_signal_name(_signal: &str) -> Result<traps::TrapSignal, error::Error> {
-    Err(error::Error::InvalidSignal)
-}
-
 pub(crate) fn continue_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
     error::unimp("continue process")
 }
diff --git a/brush-core/src/sys/unix/signal.rs b/brush-core/src/sys/unix/signal.rs
index 281f2f5..67cb1a6 100644
--- a/brush-core/src/sys/unix/signal.rs
+++ b/brush-core/src/sys/unix/signal.rs
@@ -1,18 +1,4 @@
-use std::str::FromStr;
-
-use crate::{error, sys, traps};
-
-pub(crate) fn parse_numeric_signal(signal: i32) -> Result<traps::TrapSignal, error::Error> {
-    Ok(traps::TrapSignal::Signal(
-        nix::sys::signal::Signal::try_from(signal).map_err(|_| error::Error::InvalidSignal)?,
-    ))
-}
-
-pub(crate) fn parse_os_signal_name(signal: &str) -> Result<traps::TrapSignal, error::Error> {
-    Ok(traps::TrapSignal::Signal(
-        nix::sys::signal::Signal::from_str(signal).map_err(|_| error::Error::InvalidSignal)?,
-    ))
-}
+use crate::{error, sys};
 
 pub(crate) fn continue_process(pid: sys::process::ProcessId) -> Result<(), error::Error> {
     #[allow(clippy::cast_possible_wrap)]
diff --git a/brush-core/src/traps.rs b/brush-core/src/traps.rs
index f4004ca..25729f2 100644
--- a/brush-core/src/traps.rs
+++ b/brush-core/src/traps.rs
@@ -1,5 +1,10 @@
+use std::str::FromStr;
 use std::{collections::HashMap, fmt::Display};
 
+use itertools::Itertools as _;
+
+use crate::error;
+
 /// Type of signal that can be trapped in the shell.
 #[derive(Clone, Copy, Eq, Hash, PartialEq)]
 pub enum TrapSignal {
@@ -16,28 +21,128 @@ pub enum TrapSignal {
 
 impl Display for TrapSignal {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(self.as_str())
+    }
+}
+
+impl TrapSignal {
+    /// Returns all possible values of [`TrapSignal`].
+    pub fn iterator() -> impl Iterator<Item = TrapSignal> {
+        const SIGNALS: &[TrapSignal] = &[TrapSignal::Debug, TrapSignal::Err, TrapSignal::Exit];
+        let iter = SIGNALS.iter().copied();
+
+        #[cfg(unix)]
+        let iter = itertools::chain!(
+            iter,
+            nix::sys::signal::Signal::iterator().map(TrapSignal::Signal)
+        );
+
+        iter
+    }
+
+    /// Converts [`TrapSignal`] into its corresponding signal name as a [`&'static str`](str)
+    pub const fn as_str(self) -> &'static str {
         match self {
             #[cfg(unix)]
-            TrapSignal::Signal(s) => s.fmt(f),
-            TrapSignal::Debug => write!(f, "DEBUG"),
-            TrapSignal::Err => write!(f, "ERR"),
-            TrapSignal::Exit => write!(f, "EXIT"),
+            TrapSignal::Signal(s) => s.as_str(),
+            TrapSignal::Debug => "DEBUG",
+            TrapSignal::Err => "ERR",
+            TrapSignal::Exit => "EXIT",
         }
     }
 }
 
-impl TrapSignal {
-    /// Returns all possible values of `TrapSignal`.
-    #[allow(unused_mut)]
-    pub fn all_values() -> Vec<TrapSignal> {
-        let mut signals = vec![TrapSignal::Debug, TrapSignal::Err, TrapSignal::Exit];
+/// Formats [`Iterator<Item = TrapSignal>`](TrapSignal)  to the provided writer.
+///
+/// # Arguments
+///
+/// * `f` - Any type that implements [`std::io::Write`].
+/// * `it` - An iterator over the signals that will be formatted into the `f`.
+pub fn format_signals(
+    mut f: impl std::io::Write,
+    it: impl Iterator<Item = TrapSignal>,
+) -> Result<(), error::Error> {
+    let it = it
+        .filter_map(|s| i32::try_from(s).ok().map(|n| (s, n)))
+        .sorted_by(|a, b| Ord::cmp(&a.1, &b.1))
+        .format_with("\n", |s, f| f(&format_args!("{}) {}", s.1, s.0)));
+    write!(f, "{it}")?;
+    Ok(())
+}
 
-        #[cfg(unix)]
-        for signal in nix::sys::signal::Signal::iterator() {
-            signals.push(TrapSignal::Signal(signal));
+// implement s.parse::<TrapSignal>()
+impl FromStr for TrapSignal {
+    type Err = error::Error;
+    fn from_str(s: &str) -> Result<Self, <TrapSignal as FromStr>::Err> {
+        if let Ok(n) = s.parse::<i32>() {
+            TrapSignal::try_from(n)
+        } else {
+            TrapSignal::try_from(s)
         }
+    }
+}
+
+// from a signal number
+impl TryFrom<i32> for TrapSignal {
+    type Error = error::Error;
+    fn try_from(value: i32) -> Result<Self, Self::Error> {
+        // NOTE: DEBUG and ERR are real-time signals, defined based on NSIG or SIGRTMAX (is not
+        // available on bsd-like systems),
+        // and don't have persistent numbers across platforms, so we skip them here.
+        Ok(match value {
+            0 => TrapSignal::Exit,
+            #[cfg(unix)]
+            value => TrapSignal::Signal(
+                nix::sys::signal::Signal::try_from(value)
+                    .map_err(|_| error::Error::InvalidSignal(value.to_string()))?,
+            ),
+            #[cfg(not(unix))]
+            _ => return Err(error::Error::InvalidSignal(value.to_string())),
+        })
+    }
+}
+
+// from a signal name
+impl TryFrom<&str> for TrapSignal {
+    type Error = error::Error;
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        #[allow(unused_mut)] // on not unix platforms
+        let mut s = value.to_ascii_uppercase();
 
-        signals
+        Ok(match s.as_str() {
+            "DEBUG" => TrapSignal::Debug,
+            "ERR" => TrapSignal::Err,
+            "EXIT" => TrapSignal::Exit,
+
+            #[cfg(unix)]
+            _ => {
+                // Bash compatibility:
+                // support for signal names without the `SIG` prefix, for example `HUP` -> `SIGHUP`
+                if !s.starts_with("SIG") {
+                    s.insert_str(0, "SIG");
+                }
+                nix::sys::signal::Signal::from_str(s.as_str())
+                    .map(TrapSignal::Signal)
+                    .map_err(|_| error::Error::InvalidSignal(value.into()))?
+            }
+            #[cfg(not(unix))]
+            _ => return Err(error::Error::InvalidSignal(value.into())),
+        })
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct DoesntHaveANumber;
+
+impl TryFrom<TrapSignal> for i32 {
+    type Error = DoesntHaveANumber;
+    fn try_from(value: TrapSignal) -> Result<Self, Self::Error> {
+        Ok(match value {
+            #[cfg(unix)]
+            TrapSignal::Signal(s) => s as i32,
+            TrapSignal::Exit => 0,
+            _ => return Err(DoesntHaveANumber),
+        })
     }
 }
 
diff --git a/brush-interactive/Cargo.toml b/brush-interactive/Cargo.toml
index 3653884..9832a4a 100644
--- a/brush-interactive/Cargo.toml
+++ b/brush-interactive/Cargo.toml
@@ -29,8 +29,8 @@ brush-core = { version = "^0.2.11", path = "../brush-core" }
 indexmap = "2.6.0"
 nu-ansi-term = { version = "0.50.1", optional = true }
 reedline = { version = "0.36.0", optional = true }
-thiserror = "1.0.64"
+thiserror = "1.0.65"
 tracing = "0.1.40"
 
 [target.'cfg(any(windows, unix))'.dependencies]
-tokio = { version = "1.40.0", features = ["macros", "signal"] }
+tokio = { version = "1.41.0", features = ["macros", "signal"] }
diff --git a/brush-parser/Cargo.toml b/brush-parser/Cargo.toml
index c84d0a3..46bacb2 100644
--- a/brush-parser/Cargo.toml
+++ b/brush-parser/Cargo.toml
@@ -19,15 +19,15 @@ fuzz-testing = ["dep:arbitrary"]
 
 [dependencies]
 arbitrary = { version = "1.3.2", optional = true, features = ["derive"] }
-cached = "0.53.0"
+cached = "0.53.1"
 indenter = "0.3.3"
 peg = "0.8.4"
-thiserror = "1.0.64"
+thiserror = "1.0.65"
 tracing = "0.1.40"
 utf8-chars = "3.0.5"
 
 [dev-dependencies]
-anyhow = "1.0.90"
+anyhow = "1.0.91"
 assert_matches = "1.5.0"
 criterion = { version = "0.5.1", features = ["html_reports"] }
 
diff --git a/brush-parser/src/parser.rs b/brush-parser/src/parser.rs
index 5979ce5..8a86d0e 100644
--- a/brush-parser/src/parser.rs
+++ b/brush-parser/src/parser.rs
@@ -847,27 +847,30 @@ fn add_pipe_extension_redirection(c: &mut ast::Command) -> Result<(), &'static s
         ast::IoFileRedirectKind::DuplicateOutput,
         ast::IoFileRedirectTarget::Fd(1),
     );
+
+    fn add_to_redirect_list(l: &mut Option<ast::RedirectList>, r: ast::IoRedirect) {
+        if let Some(l) = l {
+            l.0.push(r);
+        } else {
+            let v = vec![r];
+            *l = Some(ast::RedirectList(v));
+        }
+    }
+
     match c {
         ast::Command::Simple(c) => {
             let r = ast::CommandPrefixOrSuffixItem::IoRedirect(r);
-            if let Some(suffix) = &mut c.suffix {
-                suffix.0.push(r);
-            } else {
-                let v = vec![r];
-                c.suffix = Some(ast::CommandSuffix(v));
-            }
-        }
-        ast::Command::Compound(_, l) => {
-            if let Some(r_list) = l {
-                r_list.0.push(r);
+            if let Some(l) = &mut c.suffix {
+                l.0.push(r);
             } else {
-                let v = vec![r];
-                *l = Some(ast::RedirectList(v));
+                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"),
-        ast::Command::Function(_) => return Err("|& unimplemented for functions"),
     };
+
     Ok(())
 }
 
@@ -987,4 +990,35 @@ esac\
         }
         Ok(())
     }
+
+    #[test]
+    fn parse_function_with_pipe_redirection() -> Result<()> {
+        let inputs = [r"foo() { echo 1; } 2>&1 | cat", r"foo() { echo 1; } |& cat"];
+
+        for input in inputs {
+            let tokens = tokenize_str(input)?;
+            let seq = super::token_parser::pipe_sequence(
+                &Tokens {
+                    tokens: tokens.as_slice(),
+                },
+                &ParserOptions::default(),
+                &SourceInfo::default(),
+            )?;
+            assert_eq!(seq.len(), 2);
+            assert_matches!(seq[0], ast::Command::Function(..));
+            if let ast::Command::Function(f) = &seq[0] {
+                let l = &f.body.1;
+                assert!(l.is_some());
+                assert_matches!(
+                    l.as_ref().unwrap().0[0],
+                    ast::IoRedirect::File(
+                        Some(2),
+                        ast::IoFileRedirectKind::DuplicateOutput,
+                        ast::IoFileRedirectTarget::Fd(1)
+                    )
+                )
+            }
+        }
+        Ok(())
+    }
 }
diff --git a/brush-shell/Cargo.toml b/brush-shell/Cargo.toml
index 8dbd52b..cad0fd0 100644
--- a/brush-shell/Cargo.toml
+++ b/brush-shell/Cargo.toml
@@ -42,7 +42,7 @@ async-trait = "0.1.83"
 brush-parser = { version = "^0.2.9", path = "../brush-parser" }
 brush-core = { version = "^0.2.11", path = "../brush-core" }
 cfg-if = "1.0.0"
-clap = { version = "4.5.17", features = ["derive", "env", "wrap_help"] }
+clap = { version = "4.5.20", features = ["derive", "env", "wrap_help"] }
 const_format = "0.2.33"
 git-version = "0.3.9"
 lazy_static = "1.5.0"
@@ -54,18 +54,18 @@ human-panic = "2.0.2"
 brush-interactive = { version = "^0.2.11", path = "../brush-interactive", features = [
     "basic",
 ] }
-tokio = { version = "1.40.0", features = ["rt", "sync"] }
+tokio = { version = "1.41.0", features = ["rt", "sync"] }
 
 [target.'cfg(any(windows, unix))'.dependencies]
 brush-interactive = { version = "^0.2.11", path = "../brush-interactive", features = [
     "reedline",
 ] }
-tokio = { version = "1.40.0", features = ["rt", "rt-multi-thread", "sync"] }
+tokio = { version = "1.41.0", features = ["rt", "rt-multi-thread", "sync"] }
 
 [dev-dependencies]
-anyhow = "1.0.90"
-assert_cmd = "2.0.15"
-assert_fs = "1.1.1"
+anyhow = "1.0.91"
+assert_cmd = "2.0.16"
+assert_fs = "1.1.2"
 colored = "2.1.0"
 descape = "2.0.3"
 diff = "0.1.13"
@@ -75,8 +75,8 @@ glob = "0.3.1"
 indent = "0.1.1"
 junit-report = "0.8.3"
 pathdiff = "0.2.2"
-regex = "1.11.0"
-serde = { version = "1.0.210", features = ["derive"] }
+regex = "1.11.1"
+serde = { version = "1.0.213", features = ["derive"] }
 serde_yaml = "0.9.34"
 strip-ansi-escapes = "0.2.0"
 version-compare = "0.2.0"
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 05f8452..55bcbe6 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -16,11 +16,11 @@ rust-version.workspace = true
 cargo-fuzz = true
 
 [dependencies]
-anyhow = "1.0.90"
-assert_cmd = "2.0.15"
+anyhow = "1.0.91"
+assert_cmd = "2.0.16"
 lazy_static = "1.5.0"
 libfuzzer-sys = "0.4"
-tokio = { version = "1.40.0", features = ["rt"] }
+tokio = { version = "1.41.0", features = ["rt"] }
 
 [dependencies.brush-core]
 path = "../brush-core"
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index c5d474a..bacd06b 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -12,8 +12,8 @@ repository.workspace = true
 rust-version.workspace = true
 
 [dependencies]
-anyhow = "1.0.90"
+anyhow = "1.0.91"
 brush-shell = { version = "^0.2.11", path = "../brush-shell" }
-clap = { version = "4.5.17", features = ["derive"] }
+clap = { version = "4.5.20", features = ["derive"] }
 clap_mangen = "0.2.24"
 clap-markdown = "0.1.4"


Test patch:
diff --git a/brush-shell/tests/cases/builtins/kill.yaml b/brush-shell/tests/cases/builtins/kill.yaml
new file mode 100644
index 0000000..3645ab6
--- /dev/null
+++ b/brush-shell/tests/cases/builtins/kill.yaml
@@ -0,0 +1,17 @@
+name: "Builtins: kill"
+cases:
+  - name: "kill -l"
+    ignore_stderr: true
+    stdin: |
+      for i in $(seq 1 31); do kill -l $i; done
+      # limit the number of signals to 31. Realtime signals are not implemented yet.
+      for i in $(kill -l | sed -e "s/[[:digit:]]*)//g"); do echo $i; done | head -31
+      # invalid option
+      kill -l 9999
+      kill -l HUP
+      kill -l iNt
+      kill -l int
+      kill -l SIGHUP
+      kill -l EXIT
+
+
diff --git a/brush-shell/tests/cases/pipeline.yaml b/brush-shell/tests/cases/pipeline.yaml
index 6b7a39d..1d9f347 100644
--- a/brush-shell/tests/cases/pipeline.yaml
+++ b/brush-shell/tests/cases/pipeline.yaml
@@ -41,3 +41,4 @@ cases:
     stdin: |
       echo -e "hello" |& wc -l
       cat dfdfgdfgdf |& wc -l
+      foo() { cat dfgdfg; } |& wc -l