From c8d5ada5284aad93441eb27f67bb35e0a81399e9 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 1 Feb 2026 19:29:36 +0100 Subject: [PATCH] chore: bump version to 10.0.0 --- Cargo.lock | 48 +- Cargo.toml | 5 +- bun.lock | 14 +- crates/brush-builtins-vendored/Cargo.lock | 2269 +++++++++++++++++ crates/brush-builtins-vendored/Cargo.toml | 324 +++ crates/brush-builtins-vendored/LICENSE | 21 + crates/brush-builtins-vendored/README.md | 187 ++ crates/brush-builtins-vendored/src/alias.rs | 53 + crates/brush-builtins-vendored/src/bg.rs | 47 + crates/brush-builtins-vendored/src/bind.rs | 393 +++ crates/brush-builtins-vendored/src/break_.rs | 34 + .../brush-builtins-vendored/src/brushinfo.rs | 137 + crates/brush-builtins-vendored/src/builder.rs | 18 + .../brush-builtins-vendored/src/builtin_.rs | 43 + crates/brush-builtins-vendored/src/cd.rs | 97 + crates/brush-builtins-vendored/src/colon.rs | 27 + crates/brush-builtins-vendored/src/command.rs | 166 ++ .../brush-builtins-vendored/src/complete.rs | 660 +++++ .../brush-builtins-vendored/src/continue_.rs | 34 + crates/brush-builtins-vendored/src/declare.rs | 632 +++++ crates/brush-builtins-vendored/src/dirs.rs | 101 + crates/brush-builtins-vendored/src/dot.rs | 34 + crates/brush-builtins-vendored/src/echo.rs | 81 + crates/brush-builtins-vendored/src/enable.rs | 96 + crates/brush-builtins-vendored/src/eval.rs | 36 + crates/brush-builtins-vendored/src/exec.rs | 66 + crates/brush-builtins-vendored/src/exit.rs | 31 + crates/brush-builtins-vendored/src/export.rs | 157 ++ crates/brush-builtins-vendored/src/factory.rs | 203 ++ crates/brush-builtins-vendored/src/false_.rs | 18 + crates/brush-builtins-vendored/src/fc.rs | 299 +++ crates/brush-builtins-vendored/src/fg.rs | 70 + crates/brush-builtins-vendored/src/getopts.rs | 224 ++ crates/brush-builtins-vendored/src/hash.rs | 101 + crates/brush-builtins-vendored/src/help.rs | 138 + crates/brush-builtins-vendored/src/history.rs | 246 ++ crates/brush-builtins-vendored/src/jobs.rs | 83 + crates/brush-builtins-vendored/src/kill.rs | 179 ++ crates/brush-builtins-vendored/src/let_.rs | 41 + crates/brush-builtins-vendored/src/lib.rs | 157 ++ crates/brush-builtins-vendored/src/mapfile.rs | 155 ++ crates/brush-builtins-vendored/src/popd.rs | 36 + crates/brush-builtins-vendored/src/printf.rs | 184 ++ crates/brush-builtins-vendored/src/pushd.rs | 45 + crates/brush-builtins-vendored/src/pwd.rs | 40 + crates/brush-builtins-vendored/src/read.rs | 396 +++ crates/brush-builtins-vendored/src/return_.rs | 40 + crates/brush-builtins-vendored/src/set.rs | 434 ++++ crates/brush-builtins-vendored/src/shift.rs | 36 + crates/brush-builtins-vendored/src/shopt.rs | 153 ++ crates/brush-builtins-vendored/src/suspend.rs | 34 + crates/brush-builtins-vendored/src/test.rs | 53 + crates/brush-builtins-vendored/src/times.rs | 36 + crates/brush-builtins-vendored/src/trap.rs | 102 + crates/brush-builtins-vendored/src/true_.rs | 18 + crates/brush-builtins-vendored/src/type_.rs | 217 ++ crates/brush-builtins-vendored/src/ulimit.rs | 500 ++++ crates/brush-builtins-vendored/src/umask.rs | 97 + crates/brush-builtins-vendored/src/unalias.rs | 44 + crates/brush-builtins-vendored/src/unimp.rs | 35 + crates/brush-builtins-vendored/src/unset.rs | 122 + crates/brush-builtins-vendored/src/wait.rs | 56 + crates/brush-core-vendored/src/processes.rs | 2 +- crates/pi-natives/Cargo.toml | 2 +- packages/agent/package.json | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/package.json | 2 +- packages/natives/package.json | 2 +- packages/stats/package.json | 2 +- packages/tui/package.json | 2 +- packages/utils/package.json | 2 +- rustfmt.toml | 2 +- 73 files changed, 10398 insertions(+), 27 deletions(-) create mode 100644 crates/brush-builtins-vendored/Cargo.lock create mode 100644 crates/brush-builtins-vendored/Cargo.toml create mode 100644 crates/brush-builtins-vendored/LICENSE create mode 100644 crates/brush-builtins-vendored/README.md create mode 100644 crates/brush-builtins-vendored/src/alias.rs create mode 100644 crates/brush-builtins-vendored/src/bg.rs create mode 100644 crates/brush-builtins-vendored/src/bind.rs create mode 100644 crates/brush-builtins-vendored/src/break_.rs create mode 100644 crates/brush-builtins-vendored/src/brushinfo.rs create mode 100644 crates/brush-builtins-vendored/src/builder.rs create mode 100644 crates/brush-builtins-vendored/src/builtin_.rs create mode 100644 crates/brush-builtins-vendored/src/cd.rs create mode 100644 crates/brush-builtins-vendored/src/colon.rs create mode 100644 crates/brush-builtins-vendored/src/command.rs create mode 100644 crates/brush-builtins-vendored/src/complete.rs create mode 100644 crates/brush-builtins-vendored/src/continue_.rs create mode 100644 crates/brush-builtins-vendored/src/declare.rs create mode 100644 crates/brush-builtins-vendored/src/dirs.rs create mode 100644 crates/brush-builtins-vendored/src/dot.rs create mode 100644 crates/brush-builtins-vendored/src/echo.rs create mode 100644 crates/brush-builtins-vendored/src/enable.rs create mode 100644 crates/brush-builtins-vendored/src/eval.rs create mode 100644 crates/brush-builtins-vendored/src/exec.rs create mode 100644 crates/brush-builtins-vendored/src/exit.rs create mode 100644 crates/brush-builtins-vendored/src/export.rs create mode 100644 crates/brush-builtins-vendored/src/factory.rs create mode 100644 crates/brush-builtins-vendored/src/false_.rs create mode 100644 crates/brush-builtins-vendored/src/fc.rs create mode 100644 crates/brush-builtins-vendored/src/fg.rs create mode 100644 crates/brush-builtins-vendored/src/getopts.rs create mode 100644 crates/brush-builtins-vendored/src/hash.rs create mode 100644 crates/brush-builtins-vendored/src/help.rs create mode 100644 crates/brush-builtins-vendored/src/history.rs create mode 100644 crates/brush-builtins-vendored/src/jobs.rs create mode 100644 crates/brush-builtins-vendored/src/kill.rs create mode 100644 crates/brush-builtins-vendored/src/let_.rs create mode 100644 crates/brush-builtins-vendored/src/lib.rs create mode 100644 crates/brush-builtins-vendored/src/mapfile.rs create mode 100644 crates/brush-builtins-vendored/src/popd.rs create mode 100644 crates/brush-builtins-vendored/src/printf.rs create mode 100644 crates/brush-builtins-vendored/src/pushd.rs create mode 100644 crates/brush-builtins-vendored/src/pwd.rs create mode 100644 crates/brush-builtins-vendored/src/read.rs create mode 100644 crates/brush-builtins-vendored/src/return_.rs create mode 100644 crates/brush-builtins-vendored/src/set.rs create mode 100644 crates/brush-builtins-vendored/src/shift.rs create mode 100644 crates/brush-builtins-vendored/src/shopt.rs create mode 100644 crates/brush-builtins-vendored/src/suspend.rs create mode 100644 crates/brush-builtins-vendored/src/test.rs create mode 100644 crates/brush-builtins-vendored/src/times.rs create mode 100644 crates/brush-builtins-vendored/src/trap.rs create mode 100644 crates/brush-builtins-vendored/src/true_.rs create mode 100644 crates/brush-builtins-vendored/src/type_.rs create mode 100644 crates/brush-builtins-vendored/src/ulimit.rs create mode 100644 crates/brush-builtins-vendored/src/umask.rs create mode 100644 crates/brush-builtins-vendored/src/unalias.rs create mode 100644 crates/brush-builtins-vendored/src/unimp.rs create mode 100644 crates/brush-builtins-vendored/src/unset.rs create mode 100644 crates/brush-builtins-vendored/src/wait.rs diff --git a/Cargo.lock b/Cargo.lock index 50d4bfdcd..68de04a94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,7 +233,7 @@ version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365" dependencies = [ - "darling", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -245,8 +245,6 @@ dependencies = [ [[package]] name = "brush-builtins" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "454219f460ee4308ae5b8befc47936b4ef7ac3a94347052270e59b6c6cfe14ec" dependencies = [ "brush-core", "brush-parser", @@ -375,7 +373,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn", @@ -571,8 +569,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -589,13 +597,37 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] @@ -1899,7 +1931,7 @@ dependencies = [ [[package]] name = "pi-natives" -version = "9.8.0" +version = "10.0.0" dependencies = [ "arboard", "brush-builtins", diff --git a/Cargo.toml b/Cargo.toml index fc5f98dbd..fc6054707 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] members = ["crates/*"] -exclude = ["crates/brush-core-vendored"] +exclude = ["crates/brush-core-vendored", "crates/brush-builtins-vendored"] resolver = "3" [workspace.package] -version = "9.8.0" +version = "10.0.0" edition = "2024" license = "MIT" authors = ["Can Boluk"] @@ -12,6 +12,7 @@ repository = "https://github.com/can1357/oh-my-pi" [patch.crates-io] brush-core = { path = "crates/brush-core-vendored" } +brush-builtins = { path = "crates/brush-builtins-vendored" } [profile.release] opt-level = 3 diff --git a/bun.lock b/bun.lock index 7bb9b2833..6e4e7913a 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/agent": { "name": "@oh-my-pi/pi-agent-core", - "version": "9.8.0", + "version": "10.0.0", "dependencies": { "@oh-my-pi/pi-ai": "workspace:*", "@oh-my-pi/pi-tui": "workspace:*", @@ -35,7 +35,7 @@ }, "packages/ai": { "name": "@oh-my-pi/pi-ai", - "version": "9.8.0", + "version": "10.0.0", "bin": { "pi-ai": "./src/cli.ts", }, @@ -65,7 +65,7 @@ }, "packages/coding-agent": { "name": "@oh-my-pi/pi-coding-agent", - "version": "9.8.0", + "version": "10.0.0", "bin": { "omp": "src/cli.ts", }, @@ -100,7 +100,7 @@ }, "packages/natives": { "name": "@oh-my-pi/pi-natives", - "version": "9.8.0", + "version": "10.0.0", "dependencies": { "@oh-my-pi/pi-utils": "workspace:*", }, @@ -128,7 +128,7 @@ }, "packages/stats": { "name": "@oh-my-pi/omp-stats", - "version": "9.8.0", + "version": "10.0.0", "bin": { "omp-stats": "./src/index.ts", }, @@ -148,7 +148,7 @@ }, "packages/tui": { "name": "@oh-my-pi/pi-tui", - "version": "9.8.0", + "version": "10.0.0", "dependencies": { "@oh-my-pi/pi-natives": "workspace:*", "@types/mime-types": "^3.0.1", @@ -163,7 +163,7 @@ }, "packages/utils": { "name": "@oh-my-pi/pi-utils", - "version": "9.8.0", + "version": "10.0.0", "dependencies": { "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", diff --git a/crates/brush-builtins-vendored/Cargo.lock b/crates/brush-builtins-vendored/Cargo.lock new file mode 100644 index 000000000..83232ba4d --- /dev/null +++ b/crates/brush-builtins-vendored/Cargo.lock @@ -0,0 +1,2269 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "archery" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" +dependencies = [ + "triomphe", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bigdecimal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "brush-builtins" +version = "0.1.0" +dependencies = [ + "anyhow", + "brush-core", + "brush-parser", + "cfg-if", + "chrono", + "clap", + "fancy-regex", + "futures", + "itertools", + "nix 0.30.1", + "pretty_assertions", + "procfs", + "rlimit", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tracing", + "uucore", +] + +[[package]] +name = "brush-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3ad2f2d4eb45ef7f11e74bc1a0816c8f3dce298072352c6b3b41a2c4bfdb74" +dependencies = [ + "async-recursion", + "async-trait", + "bon", + "brush-parser", + "cached", + "cfg-if", + "chrono", + "clap", + "command-fds", + "fancy-regex", + "futures", + "getrandom", + "homedir", + "hostname", + "indexmap", + "itertools", + "nix 0.30.1", + "normalize-path", + "rand 0.9.2", + "rpds", + "strum", + "strum_macros", + "terminfo", + "thiserror", + "tokio", + "tracing", + "uuid", + "uzers", + "whoami", +] + +[[package]] +name = "brush-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7367124d4f38fdcd65f4b815bda7caeb3de377b9cd95ffa1b23627989c93718" +dependencies = [ + "bon", + "cached", + "indenter", + "peg", + "thiserror", + "tracing", + "utf8-chars", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cached" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.15.5", + "once_cell", + "thiserror", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + +[[package]] +name = "cc" +version = "1.2.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "command-fds" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd" +dependencies = [ + "nix 0.30.1", + "thiserror", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "homedir" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" +dependencies = [ + "cfg-if", + "nix 0.29.0", + "widestring", + "windows", +] + +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.1.3", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03e2fcaefecdf05619f3d6f91740e79ab969b4dd54f77cbf546b1d0d28e3147" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "normalize-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "os_display" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "peg" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" +dependencies = [ + "bitflags", + "chrono", + "flate2", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags", + "chrono", + "hex", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rlimit" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +dependencies = [ + "libc", +] + +[[package]] +name = "rpds" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e75f485e819d4d3015e6c0d55d02a4fd3db47c1993d9e603e0361fba2bffb34" +dependencies = [ + "archery", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "self_cell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8-chars" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f598f797138b219a4560b4e9c53c255e872e267c9e3fdcc75aa59a2a90953bcd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uucore" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2003164a38a7f39da1de103a70fa66b745f572f0045ec261481539516c0a8a0e" +dependencies = [ + "bigdecimal", + "bstr", + "clap", + "fluent", + "fluent-bundle", + "fluent-syntax", + "glob", + "icu_locale", + "itertools", + "nix 0.30.1", + "num-traits", + "number_prefix", + "os_display", + "phf 0.13.1", + "procfs", + "thiserror", + "unic-langid", + "uucore_procs", + "wild", +] + +[[package]] +name = "uucore_procs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76f0308f7810d915246a39748e7f5d64e43e6bb9d6c8107224f9d741aefc375" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uzers" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df81ff504e7d82ad53e95ed1ad5b72103c11253f39238bcc0235b90768a97dd" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "wild" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" +dependencies = [ + "glob", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "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.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crates/brush-builtins-vendored/Cargo.toml b/crates/brush-builtins-vendored/Cargo.toml new file mode 100644 index 000000000..0b7b5f377 --- /dev/null +++ b/crates/brush-builtins-vendored/Cargo.toml @@ -0,0 +1,324 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.87.0" +name = "brush-builtins" +version = "0.1.0" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Builtins for a POSIX/bash shell (used by brush-shell)" +readme = "README.md" +keywords = [ + "cli", + "shell", + "sh", + "bash", + "script", +] +categories = [ + "command-line-utilities", + "development-tools", +] +license = "MIT" +repository = "https://github.com/reubeno/brush" + +[features] +"builtin.alias" = [] +"builtin.bg" = [] +"builtin.bind" = [] +"builtin.break" = [] +"builtin.brushinfo" = [] +"builtin.builtin" = [] +"builtin.cd" = [] +"builtin.colon" = [] +"builtin.command" = [] +"builtin.compgen" = [] +"builtin.complete" = [] +"builtin.compopt" = [] +"builtin.continue" = [] +"builtin.declare" = [] +"builtin.dirs" = [] +"builtin.dot" = [] +"builtin.echo" = [] +"builtin.enable" = [] +"builtin.eval" = [] +"builtin.exec" = [] +"builtin.exit" = [] +"builtin.export" = [] +"builtin.false" = [] +"builtin.fc" = [] +"builtin.fg" = [] +"builtin.getopts" = [] +"builtin.hash" = [] +"builtin.help" = [] +"builtin.history" = [] +"builtin.jobs" = [] +"builtin.kill" = [] +"builtin.let" = [] +"builtin.mapfile" = [] +"builtin.popd" = [] +"builtin.printf" = [] +"builtin.pushd" = [] +"builtin.pwd" = [] +"builtin.read" = [] +"builtin.return" = [] +"builtin.set" = [] +"builtin.shift" = [] +"builtin.shopt" = [] +"builtin.suspend" = [] +"builtin.test" = [] +"builtin.times" = [] +"builtin.trap" = [] +"builtin.true" = [] +"builtin.type" = [] +"builtin.ulimit" = [] +"builtin.umask" = [] +"builtin.unalias" = [] +"builtin.unset" = [] +"builtin.wait" = [] +default = [ + "builtin.alias", + "builtin.bg", + "builtin.bind", + "builtin.break", + "builtin.brushinfo", + "builtin.builtin", + "builtin.cd", + "builtin.colon", + "builtin.command", + "builtin.compgen", + "builtin.compopt", + "builtin.complete", + "builtin.continue", + "builtin.declare", + "builtin.dirs", + "builtin.dot", + "builtin.echo", + "builtin.enable", + "builtin.eval", + "builtin.exec", + "builtin.exit", + "builtin.export", + "builtin.false", + "builtin.fc", + "builtin.fg", + "builtin.getopts", + "builtin.hash", + "builtin.help", + "builtin.history", + "builtin.jobs", + "builtin.kill", + "builtin.let", + "builtin.mapfile", + "builtin.popd", + "builtin.printf", + "builtin.pushd", + "builtin.pwd", + "builtin.read", + "builtin.return", + "builtin.set", + "builtin.shift", + "builtin.shopt", + "builtin.suspend", + "builtin.test", + "builtin.times", + "builtin.trap", + "builtin.true", + "builtin.type", + "builtin.ulimit", + "builtin.umask", + "builtin.unalias", + "builtin.unset", + "builtin.wait", +] + +[lib] +name = "brush_builtins" +path = "src/lib.rs" +bench = false + +[dependencies.brush-core] +version = "^0.4.0" + +[dependencies.brush-parser] +version = "^0.3.0" + +[dependencies.cfg-if] +version = "1.0.4" + +[dependencies.chrono] +version = "0.4.42" + +[dependencies.clap] +version = "4.5.51" +features = [ + "derive", + "wrap_help", +] + +[dependencies.fancy-regex] +version = "0.16.2" + +[dependencies.futures] +version = "0.3.31" + +[dependencies.itertools] +version = "0.14.0" + +[dependencies.strum] +version = "0.27.2" + +[dependencies.strum_macros] +version = "0.27.2" + +[dependencies.thiserror] +version = "2.0.17" + +[dependencies.tracing] +version = "0.1.41" + +[dev-dependencies.anyhow] +version = "1.0.100" + +[dev-dependencies.pretty_assertions] +version = "1.4.1" +features = ["unstable"] + +[target."cfg(any(unix, windows))".dependencies.tokio] +version = "1.48.0" +features = [ + "io-util", + "macros", + "process", + "rt", + "rt-multi-thread", + "signal", + "sync", +] + +[target."cfg(any(unix, windows))".dependencies.uucore] +version = "0.4.0" +features = ["format"] +default-features = false + +[target.'cfg(target_family = "wasm")'.dependencies.tokio] +version = "1.48.0" +features = [ + "io-util", + "macros", + "rt", +] + +[target.'cfg(target_os = "linux")'.dependencies.procfs] +version = "0.18.0" + +[target."cfg(unix)".dependencies.nix] +version = "0.30.1" +features = [ + "fs", + "process", + "resource", + "signal", + "term", + "user", +] + +[target."cfg(unix)".dependencies.rlimit] +version = "0.10.2" + +[lints.clippy] +bool_to_int_with_if = "allow" +cognitive_complexity = "allow" +collapsible_else_if = "allow" +collapsible_if = "allow" +expect_used = "deny" +format_push_string = "deny" +if_not_else = "allow" +if_same_then_else = "allow" +match_same_arms = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +multiple_crate_versions = "allow" +must_use_candidate = "allow" +option_if_let_else = "allow" +panic = "deny" +panic_in_result_fn = "deny" +redundant_closure_for_method_calls = "allow" +redundant_else = "allow" +redundant_pub_crate = "allow" +result_large_err = "allow" +similar_names = "allow" +string_slice = "deny" +struct_excessive_bools = "allow" +todo = "deny" +undocumented_unsafe_blocks = "deny" +unwrap_in_result = "deny" + +[lints.clippy.all] +level = "deny" +priority = -1 + +[lints.clippy.cargo] +level = "deny" +priority = -1 + +[lints.clippy.nursery] +level = "deny" +priority = -1 + +[lints.clippy.pedantic] +level = "deny" +priority = -1 + +[lints.clippy.perf] +level = "deny" +priority = -1 + +[lints.rust] +unnameable_types = "deny" +unsafe_op_in_unsafe_fn = "deny" +unused_attributes = "deny" +unused_lifetimes = "deny" +unused_macro_rules = "deny" + +[lints.rust.future_incompatible] +level = "deny" +priority = 0 + +[lints.rust.missing_docs] +level = "deny" +priority = 0 + +[lints.rust.nonstandard_style] +level = "deny" +priority = 0 + +[lints.rust.rust_2018_idioms] +level = "deny" +priority = -1 + +[lints.rust.unknown_lints] +level = "allow" +priority = -100 + +[lints.rust.warnings] +level = "deny" +priority = 0 + +[lints.rustdoc.all] +level = "deny" +priority = -1 diff --git a/crates/brush-builtins-vendored/LICENSE b/crates/brush-builtins-vendored/LICENSE new file mode 100644 index 000000000..3d3cb7866 --- /dev/null +++ b/crates/brush-builtins-vendored/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 reuben olinsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/brush-builtins-vendored/README.md b/crates/brush-builtins-vendored/README.md new file mode 100644 index 000000000..c8881e4af --- /dev/null +++ b/crates/brush-builtins-vendored/README.md @@ -0,0 +1,187 @@ +
+ +
+ +
+ + +

+ + + + + + + + + + +
+ + + + + Packaging status + + + Dependency status + + + Discord invite + +

+ + + + +

+ +
+ +`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. At its core is an embeddable shell interpreter published for reuse +in other Rust projects. It's built and tested on Linux, macOS, and WSL. Native Windows +support is experimental. + +

+ +

+ +`brush` is functional for interactive use as a daily driver! It executes most `sh` and `bash` scripts we've +encountered. Known limitations are tracked with filed issues. Out of an abundance of caution, +we wouldn't recommend using it yet in _production_ scenarios in case it doesn't behave identically +to your existing stable shell. (If you do find any behavioral differences, though, please report them with an +issue!) + +Contributions and feedback of all kinds are welcome! For more guidance, please consult our +[contribution guidelines](CONTRIBUTING.md). For more technical details, please consult the +[documentation](docs/README.md) in this repo. + +This project was originally borne out of curiosity and a desire to learn. We're doing our best to keep that +attitude :). + +
+ +## 📝 License + +Available for use and distribution under the [MIT license](LICENSE). + +## ⌨️ Installation + +_When you run `brush`, it should look exactly as `bash` does on your system: it processes your `.bashrc` and +other standard configuration. If you'd like to distinguish the look of `brush` from the other shells +on your system, you may author a `~/.brushrc` file._ + +
+🚀 Installing prebuilt binaries from GitHub + +We publish prebuilt binaries of `brush` for Linux (x86_64, aarch64) and macOS (aarch64) to GitHub for official [releases](https://github.com/reubeno/brush/releases). You can manually download and extract the `brush` binary from one of the archives published there, or otherwise use the GitHub CLI to download it, e.g.: + +```bash +gh release download --repo reubeno/brush --pattern "brush-x86_64-unknown-linux-gnu.*" +``` + +After downloading the archive for your platform, you may verify its authenticity using the [GitHub CLI](https://cli.github.com/), e.g.: + +```bash +gh attestation verify brush-x86_64-unknown-linux-gnu.tar.gz --repo reubeno/brush +``` +
+ +
+🚀 Installing prebuilt binaries via `cargo binstall` + +You may use [cargo binstall](https://github.com/cargo-bins/cargo-binstall) to install pre-built `brush` binaries. Once you've installed `cargo-binstall` you can run: + +```bash +cargo binstall brush-shell +``` + +
+ +
+ 🔨 Installing from sources + +To build from sources, first install a working (and recent) `rust` toolchain; we recommend installing it via [`rustup`](https://rustup.rs/). Then run: + +```bash +cargo install --locked brush-shell +``` +
+ +
+🐧 Installing using Nix + +If you are a Nix user, you can use the registered version: + +```bash +nix run 'github:NixOS/nixpkgs/nixpkgs-unstable#brush' -- --version +``` +
+ +
+🐧 Installing on Arch Linux + +Arch Linux users can install `brush` from the official [extra repository](https://archlinux.org/packages/extra/x86_64/brush/): + +```bash +pacman -S brush +``` +
+ +
+🍺 Installing using Homebrew + +Homebrew users can install using [the `brush` formula](https://formulae.brew.sh/formula/brush): + +```bash +brew install brush +``` +
+ +## 👥 Community + +`brush` has a community Discord server, available [here](https://discord.gg/kPRgC9j3Tj). + +## 🔍 Known limitations + +There are some known gaps in compatibility. Most notably: + +* **Some `set` and `shopt` options.** + The `set` builtin is implemented, as is `set -x` and many frequently used `set`/`shopt` options, but a number aren't fully implemented. For example, `set -e` will execute but its semantics aren't applied across execution. + +If you're interested, we'd love contributions to improve compatibility, broaden test coverage, or really any other opportunities you can find to help us make this project better. + +## 🧪 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 [850+ 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). + +## 🙏 Credits + +There's a long list of OSS crates whose shoulders this project rests on. Notably, the following crates are directly relied on for major portions of shell functionality: + +* [`reedline`](https://github.com/nushell/reedline) - for readline-like input and interactive usage +* [`clap`](https://github.com/clap-rs/clap) - command-line parsing, used both by the top-level brush CLI as well as built-in commands +* [`fancy-regex`](https://github.com/fancy-regex/fancy-regex) - relied on for everything regex +* [`tokio`](https://github.com/tokio-rs/tokio) - async, well, everything +* [`nix` rust crate](https://github.com/nix-rust/nix) - higher-level APIs for Unix/POSIX system APIs + +For testing, performance benchmarking, and other important engineering support, we use and love: + +* [`pprof-rs`](https://github.com/tikv/pprof-rs) - for sampling-based CPU profiling +* [`criterion.rs`](https://github.com/bheisler/criterion.rs) - for statistics-based benchmarking +* [`bash-completion`](https://github.com/scop/bash-completion) - for its completion test suite and general completion support! + +## 🔗 Links: other shell implementations + +There are a number of other POSIX-ish shells implemented in a non-C/C++ implementation language. Some inspirational examples include: + +* [`nushell`](https://www.nushell.sh/) - modern Rust-implemented shell (which also provides the `reedline` crate we use!) +* [`rusty_bash`](https://github.com/shellgei/rusty_bash) +* [`mvdan/sh`](https://github.com/mvdan/sh) +* [`Oils`](https://github.com/oils-for-unix/oils) +* [`fish`](https://fishshell.com) ([as of 4.0](https://fishshell.com/blog/rustport/)) + +We're sure there are plenty more; we're happy to include links to them as well. diff --git a/crates/brush-builtins-vendored/src/alias.rs b/crates/brush-builtins-vendored/src/alias.rs new file mode 100644 index 000000000..8023f3ae2 --- /dev/null +++ b/crates/brush-builtins-vendored/src/alias.rs @@ -0,0 +1,53 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins}; + +/// Manage aliases within the shell. +#[derive(Parser)] +pub(crate) struct AliasCommand { + /// Print all defined aliases in a reusable format. + #[arg(short = 'p')] + print: bool, + + /// List of aliases to display or update. + #[arg(name = "name[=value]")] + aliases: Vec, +} + +impl builtins::Command for AliasCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut exit_code = ExecutionResult::success(); + + if self.print || self.aliases.is_empty() { + for (name, value) in &context.shell.aliases { + writeln!(context.stdout(), "alias {name}='{value}'")?; + } + } else { + for alias in &self.aliases { + if let Some((name, unexpanded_value)) = alias.split_once('=') { + context + .shell + .aliases + .insert(name.to_owned(), unexpanded_value.to_owned()); + } else if let Some(value) = context.shell.aliases.get(alias) { + writeln!(context.stdout(), "alias {alias}='{value}'")?; + } else { + writeln!( + context.stderr(), + "{}: {alias}: not found", + context.command_name + )?; + exit_code = ExecutionResult::general_error(); + } + } + } + + Ok(exit_code) + } +} diff --git a/crates/brush-builtins-vendored/src/bg.rs b/crates/brush-builtins-vendored/src/bg.rs new file mode 100644 index 000000000..325ce309c --- /dev/null +++ b/crates/brush-builtins-vendored/src/bg.rs @@ -0,0 +1,47 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins}; + +/// Moves a job to run in the background. +#[derive(Parser)] +pub(crate) struct BgCommand { + /// List of job specs to move to background. + job_specs: Vec, +} + +impl builtins::Command for BgCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut exit_code = ExecutionResult::success(); + + if !self.job_specs.is_empty() { + for job_spec in &self.job_specs { + if let Some(job) = context.shell.jobs.resolve_job_spec(job_spec) { + job.move_to_background()?; + } else { + writeln!( + context.stderr(), + "{}: {}: no such job", + context.command_name, + job_spec + )?; + exit_code = ExecutionResult::general_error(); + } + } + } else { + if let Some(job) = context.shell.jobs.current_job_mut() { + job.move_to_background()?; + } else { + writeln!(context.stderr(), "{}: no current job", context.command_name)?; + exit_code = ExecutionResult::general_error(); + } + } + + Ok(exit_code) + } +} diff --git a/crates/brush-builtins-vendored/src/bind.rs b/crates/brush-builtins-vendored/src/bind.rs new file mode 100644 index 000000000..a2c99fa4e --- /dev/null +++ b/crates/brush-builtins-vendored/src/bind.rs @@ -0,0 +1,393 @@ +use clap::{Parser, ValueEnum}; +use std::{io::Write, str::FromStr as _, sync::Arc}; +use strum::IntoEnumIterator; +use tokio::sync::Mutex; + +use brush_core::{ + ExecutionExitCode, ExecutionResult, builtins, error, interfaces, sys, trace_categories, +}; + +/// Identifier for a keymap +#[derive(Clone, ValueEnum)] +enum BindKeyMap { + #[clap(name = "emacs-standard", alias = "emacs")] + EmacsStandard, + #[clap(name = "emacs-meta")] + EmacsMeta, + #[clap(name = "emacs-ctlx")] + EmacsCtlx, + #[clap(name = "vi-command", aliases = &["vi", "vi-move"])] + ViCommand, + #[clap(name = "vi-insert")] + ViInsert, +} + +impl BindKeyMap { + const fn is_vi(&self) -> bool { + matches!(self, Self::ViCommand | Self::ViInsert) + } + + #[expect(dead_code)] + const fn is_emacs(&self) -> bool { + matches!( + self, + Self::EmacsStandard | Self::EmacsMeta | Self::EmacsCtlx + ) + } +} + +/// Inspect and modify key bindings and other input configuration. +#[derive(Parser)] +pub(crate) struct BindCommand { + /// Name of key map to use. + #[arg(short = 'm')] + keymap: Option, + /// List functions. + #[arg(short = 'l')] + list_funcs: bool, + /// List functions and bindings. + #[arg(short = 'P')] + list_funcs_and_bindings: bool, + /// List functions and bindings in a format suitable for use as input. + #[arg(short = 'p')] + list_funcs_and_bindings_reusable: bool, + /// List key sequences that invoke macros. + #[arg(short = 'S')] + list_key_seqs_that_invoke_macros: bool, + /// List key sequences that invoke macros in a format suitable for use as input. + #[arg(short = 's')] + list_key_seqs_that_invoke_macros_reusable: bool, + /// List variables. + #[arg(short = 'V')] + list_vars: bool, + /// List variables in a format suitable for use as input. + #[arg(short = 'v')] + list_vars_reusable: bool, + /// Find the keys bound to the given named function. + #[arg(short = 'q', value_name = "FUNC_NAME")] + query_func_bindings: Option, + /// Remove all bindings for the given named function. + #[arg(short = 'u', value_name = "FUNC_NAME")] + remove_func_bindings: Option, + /// Remove the binding for the given key sequence. + #[arg(short = 'r', value_name = "KEY_SEQ")] + remove_key_seq_binding: Option, + /// Import bindings from the given file. + #[arg(short = 'f', value_name = "PATH")] + bindings_file: Option, + /// Bind key sequence to command. + #[arg(short = 'x', value_name = "BINDING")] + key_seq_bindings: Vec, + /// List key sequence bindings. + #[arg(short = 'X')] + list_key_seq_bindings: bool, + /// Key sequence binding to readline function or command. + key_sequence: Option, +} + +const BIND_FEATURE_ISSUE_ID: u32 = 380; + +impl builtins::Command for BindCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if let Some(key_bindings) = context.shell.key_bindings() { + Ok(self.execute_impl(key_bindings, &context).await?) + } else { + writeln!( + context.stderr(), + "bind: key bindings not supported in this config" + )?; + + Ok(ExecutionExitCode::Unimplemented.into()) + } + } +} + +impl BindCommand { + async fn execute_impl( + &self, + bindings: &Arc>, + context: &brush_core::ExecutionContext<'_>, + ) -> Result { + let mut bindings = bindings.lock().await; + + if self.list_funcs { + for func in interfaces::InputFunction::iter() { + writeln!(context.stdout(), "{func}")?; + } + } + + if self.list_funcs_and_bindings { + for (seq, action) in &bindings.get_current() { + writeln!(context.stdout(), "{action} can be found on {seq}")?; + } + + return error::unimp_with_issue("bind -P", BIND_FEATURE_ISSUE_ID); + } + + if self.list_funcs_and_bindings_reusable { + return error::unimp_with_issue("bind -p", BIND_FEATURE_ISSUE_ID); + } + + if self.list_key_seqs_that_invoke_macros { + return error::unimp_with_issue("bind -S", BIND_FEATURE_ISSUE_ID); + } + + if self.list_key_seqs_that_invoke_macros_reusable { + return error::unimp_with_issue("bind -s", BIND_FEATURE_ISSUE_ID); + } + + if self.list_vars { + return error::unimp_with_issue("bind -V", BIND_FEATURE_ISSUE_ID); + } + + if self.list_vars_reusable { + // For now we'll just display a few items and show defaults. + writeln!(context.stdout(), "set mark-directories on")?; + writeln!(context.stdout(), "set mark-symlinked-directories off")?; + } + + if self.query_func_bindings.is_some() { + return error::unimp_with_issue("bind -q", BIND_FEATURE_ISSUE_ID); + } + + if self.remove_func_bindings.is_some() { + return error::unimp_with_issue("bind -u", BIND_FEATURE_ISSUE_ID); + } + + if self.remove_key_seq_binding.is_some() { + return error::unimp_with_issue("bind -r", BIND_FEATURE_ISSUE_ID); + } + + if self.bindings_file.is_some() { + return error::unimp_with_issue("bind -f", BIND_FEATURE_ISSUE_ID); + } + + if self.list_key_seq_bindings { + return error::unimp_with_issue("bind -X", BIND_FEATURE_ISSUE_ID); + } + + if !self.key_seq_bindings.is_empty() { + if self.keymap.as_ref().is_some_and(|k| k.is_vi()) { + // NOTE(vi): Quietly ignore since we don't support vi mode. + return Ok(ExecutionResult::success()); + } + + for key_seq_and_command in &self.key_seq_bindings { + let (key_seq, command) = parse_key_sequence_and_shell_command(key_seq_and_command)?; + bind_key_sequence_to_shell_cmd(&mut *bindings, key_seq, command)?; + } + } + + if let Some(key_sequence) = &self.key_sequence { + if self.keymap.as_ref().is_some_and(|k| k.is_vi()) { + // NOTE(vi): Quietly ignore since we don't support vi mode. + return Ok(ExecutionResult::success()); + } + + let (key_seq, target) = parse_key_sequence_and_readline_target(key_sequence.as_str())?; + bind_key_sequence_to_readline_target(&mut *bindings, key_seq, target)?; + } + + drop(bindings); + + Ok(ExecutionResult::success()) + } +} + +fn parse_key_sequence_and_shell_command( + input: &str, +) -> Result<(interfaces::KeySequence, String), brush_core::Error> { + tracing::debug!(target: trace_categories::INPUT, + "parsing key binding entry: '{input}'" + ); + + // First trim any whitespace. + let input = input.trim(); + + // This should be something of the form: + // "KEY-SEQUENCE": SHELL-COMMAND + let binding = brush_parser::readline_binding::parse_key_sequence_shell_cmd_binding(input)?; + let strokes = key_sequence_to_abstract_strokes(&binding.seq)?; + + Ok((interfaces::KeySequence { strokes }, binding.shell_cmd)) +} + +#[derive(Debug)] +#[allow(dead_code, reason = "not all variants implemented yet")] +enum BindableReadlineTarget { + Function(brush_core::interfaces::InputFunction), + Command(String), +} + +fn parse_key_sequence_and_readline_target( + input: &str, +) -> Result<(interfaces::KeySequence, BindableReadlineTarget), brush_core::Error> { + tracing::debug!(target: trace_categories::INPUT, + "parsing key binding entry: '{input}'" + ); + + // First trim any whitespace. + let input = input.trim(); + + // This should be of one of these forms: + // "KEY-SEQUENCE":function-name + // "KEY-SEQUENCE":readline-command + let binding = brush_parser::readline_binding::parse_key_sequence_readline_binding(input)?; + let strokes = key_sequence_to_abstract_strokes(&binding.seq)?; + + match binding.target { + brush_parser::readline_binding::ReadlineTarget::Function(func_name) => { + let func = parse_readline_function(func_name.as_str())?; + Ok(( + interfaces::KeySequence { strokes }, + BindableReadlineTarget::Function(func), + )) + } + brush_parser::readline_binding::ReadlineTarget::Command(cmd) => Ok(( + interfaces::KeySequence { strokes }, + BindableReadlineTarget::Command(cmd), + )), + } +} + +fn bind_key_sequence_to_shell_cmd( + bindings: &mut dyn interfaces::KeyBindings, + key_sequence: interfaces::KeySequence, + command: String, +) -> Result<(), brush_core::Error> { + tracing::debug!(target: trace_categories::INPUT, + "binding key sequence: '{key_sequence}' => command '{command}'" + ); + + bindings.bind(key_sequence, interfaces::KeyAction::ShellCommand(command))?; + + Ok(()) +} + +fn bind_key_sequence_to_readline_target( + bindings: &mut dyn interfaces::KeyBindings, + key_sequence: interfaces::KeySequence, + target: BindableReadlineTarget, +) -> Result<(), brush_core::Error> { + match target { + BindableReadlineTarget::Function(func) => { + tracing::debug!(target: trace_categories::INPUT, + "binding key sequence: '{key_sequence}' => readline function '{func}'" + ); + + if matches!(func, interfaces::InputFunction::ViEditingMode) { + // NOTE(vi): We don't support vi mode; silently ignore. + return Ok(()); + } + + bindings.bind(key_sequence, interfaces::KeyAction::DoInputFunction(func))?; + Ok(()) + } + BindableReadlineTarget::Command(cmd_macro) => { + tracing::debug!(target: trace_categories::INPUT, + "binding key sequence: '{key_sequence}' => readline macro '{cmd_macro}'" + ); + + error::unimp("binding key sequence to readline macro") + } + } +} + +fn key_sequence_to_abstract_strokes( + seq: &brush_parser::readline_binding::KeySequence, +) -> Result, brush_core::Error> { + let phys_strokes = brush_parser::readline_binding::key_sequence_to_strokes(seq)?; + + // Lift from key codes to abstract keys. + let mut abstract_strokes = vec![]; + for mut phys_stroke in phys_strokes { + let mut key = sys::input::try_get_key_from_key_code(phys_stroke.key_code.as_slice()); + + // If we couldn't interpret it directly but we see it starts with the escape character, + // try to see if we can parse it as an Alt+ sequence. + if key.is_none() && phys_stroke.key_code.len() > 1 && phys_stroke.key_code[0] == b'\x1b' { + key = sys::input::try_get_key_from_key_code(&phys_stroke.key_code[1..]); + if key.is_some() { + phys_stroke.meta = true; + } + } + + let Some(key) = key else { + return Err(error::ErrorKind::UnhandledKeyCode(phys_stroke.key_code).into()); + }; + + abstract_strokes.push(interfaces::KeyStroke { + alt: phys_stroke.meta, + control: phys_stroke.control, + shift: false, + key, + }); + } + + Ok(abstract_strokes) +} + +fn parse_readline_function( + func_name: &str, +) -> Result { + interfaces::InputFunction::from_str(func_name).map_err(|_err| { + brush_core::ErrorKind::UnknownKeyBindingFunction(func_name.to_owned()).into() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::{assert_eq, assert_matches}; + + #[test] + fn parse_example_key_sequence_and_readline_func() { + let (key_seq, target) = + parse_key_sequence_and_readline_target(r#""\C-a":beginning-of-line"#).unwrap(); + + assert_eq!( + key_seq, + interfaces::KeySequence { + strokes: vec![interfaces::KeyStroke { + alt: false, + control: true, + shift: false, + key: interfaces::Key::Character('a'), + }], + } + ); + + assert_matches!( + target, + BindableReadlineTarget::Function(interfaces::InputFunction::BeginningOfLine) + ); + } + + #[test] + fn parse_escape_char_key_binding() { + let (key_seq, target) = + parse_key_sequence_and_readline_target(r#""\er":transpose-chars"#).unwrap(); + + assert_eq!( + key_seq, + interfaces::KeySequence { + strokes: vec![interfaces::KeyStroke { + alt: true, + control: false, + shift: false, + key: interfaces::Key::Character('r'), + }], + } + ); + + assert_matches!( + target, + BindableReadlineTarget::Function(interfaces::InputFunction::TransposeChars) + ); + } +} diff --git a/crates/brush-builtins-vendored/src/break_.rs b/crates/brush-builtins-vendored/src/break_.rs new file mode 100644 index 000000000..2525db6f3 --- /dev/null +++ b/crates/brush-builtins-vendored/src/break_.rs @@ -0,0 +1,34 @@ +use clap::Parser; + +use brush_core::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, builtins}; + +/// Breaks out of a control-flow loop. +#[derive(Parser)] +pub(crate) struct BreakCommand { + /// If specified, indicates which nested loop to break out of. + #[clap(default_value_t = 1)] + which_loop: i8, +} + +impl builtins::Command for BreakCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + _context: brush_core::ExecutionContext<'_>, + ) -> Result { + // If specified, which_loop needs to be positive. + if self.which_loop <= 0 { + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + let mut result = ExecutionResult::success(); + + result.next_control_flow = ExecutionControlFlow::BreakLoop { + #[expect(clippy::cast_sign_loss)] + levels: (self.which_loop - 1) as usize, + }; + + Ok(result) + } +} diff --git a/crates/brush-builtins-vendored/src/brushinfo.rs b/crates/brush-builtins-vendored/src/brushinfo.rs new file mode 100644 index 000000000..1f07674d8 --- /dev/null +++ b/crates/brush-builtins-vendored/src/brushinfo.rs @@ -0,0 +1,137 @@ +use std::io::Write; + +use clap::{Parser, Subcommand}; + +use brush_core::{ExecutionResult, builtins, sys}; + +/// Change the current working directory. +#[derive(Parser)] +pub(crate) struct BrushInfoCommand { + #[clap(subcommand)] + command_group: CommandGroup, +} + +#[derive(Subcommand)] +enum CommandGroup { + #[clap(subcommand)] + Process(ProcessCommand), + #[clap(subcommand)] + Complete(CompleteCommand), +} + +/// Commands for configuring tracing events. +#[expect(clippy::enum_variant_names)] +#[derive(Subcommand)] +enum ProcessCommand { + /// Display process ID. + #[clap(name = "pid")] + ShowProcessId, + /// Display process group ID. + #[clap(name = "pgid")] + ShowProcessGroupId, + /// Display foreground process ID. + #[clap(name = "fgpid")] + ShowForegroundProcessId, + /// Display parent process ID. + #[clap(name = "ppid")] + ShowParentProcessId, +} + +/// Commands for generating completions. +#[derive(Subcommand)] +enum CompleteCommand { + /// Generate completions for an input line. + #[clap(name = "line")] + Line { + /// The 0-indexed cursor position for generation. + #[arg(long = "cursor", short = 'c')] + cursor_index: Option, + + /// The input line to generate completions for. + line: String, + }, +} + +impl builtins::Command for BrushInfoCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + mut context: brush_core::ExecutionContext<'_>, + ) -> Result { + self.command_group.execute(&mut context).await + } +} + +impl CommandGroup { + async fn execute( + &self, + context: &mut brush_core::ExecutionContext<'_>, + ) -> Result { + match self { + Self::Process(process) => process.execute(context), + Self::Complete(complete) => complete.execute(context).await, + } + } +} + +impl ProcessCommand { + fn execute( + &self, + context: &brush_core::ExecutionContext<'_>, + ) -> Result { + match self { + Self::ShowProcessId => { + writeln!(context.stdout(), "{}", std::process::id())?; + Ok(ExecutionResult::success()) + } + Self::ShowProcessGroupId => { + if let Some(pgid) = sys::terminal::get_process_group_id() { + writeln!(context.stdout(), "{pgid}")?; + Ok(ExecutionResult::success()) + } else { + writeln!(context.stderr(), "failed to get process group ID")?; + Ok(ExecutionResult::general_error()) + } + } + Self::ShowForegroundProcessId => { + if let Some(pid) = sys::terminal::get_foreground_pid() { + writeln!(context.stdout(), "{pid}")?; + Ok(ExecutionResult::success()) + } else { + writeln!(context.stderr(), "failed to get foreground process ID")?; + Ok(ExecutionResult::general_error()) + } + } + Self::ShowParentProcessId => { + if let Some(pid) = sys::terminal::get_parent_process_id() { + writeln!(context.stdout(), "{pid}")?; + Ok(ExecutionResult::success()) + } else { + writeln!(context.stderr(), "failed to get parent process ID")?; + Ok(ExecutionResult::general_error()) + } + } + } + } +} + +impl CompleteCommand { + async fn execute( + &self, + context: &mut brush_core::ExecutionContext<'_>, + ) -> Result { + match self { + Self::Line { cursor_index, line } => { + let completions = context + .shell + .complete(line, cursor_index.unwrap_or(line.len())) + .await?; + for candidate in completions.candidates { + writeln!(context.stdout(), "{candidate}")?; + } + Ok(ExecutionResult::success()) + } + } + } +} diff --git a/crates/brush-builtins-vendored/src/builder.rs b/crates/brush-builtins-vendored/src/builder.rs new file mode 100644 index 000000000..c8f643b68 --- /dev/null +++ b/crates/brush-builtins-vendored/src/builder.rs @@ -0,0 +1,18 @@ +use crate::BuiltinSet; + +/// Extension trait that simplifies adding default builtins to a shell builder. +pub trait ShellBuilderExt { + /// Add default builtins to the shell being built. + /// + /// # Arguments + /// + /// * `set` - The well-known set of built-ins to add. + #[must_use] + fn default_builtins(self, set: BuiltinSet) -> Self; +} + +impl ShellBuilderExt for brush_core::ShellBuilder { + fn default_builtins(self, set: BuiltinSet) -> Self { + self.builtins(crate::default_builtins(set)) + } +} diff --git a/crates/brush-builtins-vendored/src/builtin_.rs b/crates/brush-builtins-vendored/src/builtin_.rs new file mode 100644 index 000000000..f7c2b56d9 --- /dev/null +++ b/crates/brush-builtins-vendored/src/builtin_.rs @@ -0,0 +1,43 @@ +use clap::Parser; + +use brush_core::{ExecutionResult, builtins}; + +/// Directly invokes a built-in, without going through typical search order. +#[derive(Default, Parser)] +pub(crate) struct BuiltinCommand { + #[clap(skip)] + args: Vec, +} + +impl builtins::DeclarationCommand for BuiltinCommand { + fn set_declarations(&mut self, args: Vec) { + self.args = args; + } +} + +impl builtins::Command for BuiltinCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + mut context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.args.is_empty() { + return Ok(ExecutionResult::success()); + } + + let args: Vec<_> = self.args.iter().skip(1).cloned().collect(); + if args.is_empty() { + return Ok(ExecutionResult::success()); + } + + let builtin_name = args[0].to_string(); + + if let Some(builtin) = context.shell.builtins().get(&builtin_name) { + context.command_name = builtin_name; + (builtin.execute_func)(context, args).await + } else { + Err(brush_core::ErrorKind::BuiltinNotFound(builtin_name).into()) + } + } +} diff --git a/crates/brush-builtins-vendored/src/cd.rs b/crates/brush-builtins-vendored/src/cd.rs new file mode 100644 index 000000000..c9131e793 --- /dev/null +++ b/crates/brush-builtins-vendored/src/cd.rs @@ -0,0 +1,97 @@ +use std::io::Write; +use std::path::PathBuf; + +use clap::Parser; + +use brush_core::{ExecutionResult, builtins, error}; + +/// Change the current shell working directory. +#[derive(Parser)] +pub(crate) struct CdCommand { + /// Force following symlinks. + #[arg(short = 'L', overrides_with = "use_physical_dir")] + force_follow_symlinks: bool, + + /// Use physical dir structure without following symlinks. + #[arg(short = 'P', overrides_with = "force_follow_symlinks")] + use_physical_dir: bool, + + /// Exit with non zero exit status if current working directory resolution fails. + #[arg(short = 'e')] + exit_on_failed_cwd_resolution: bool, + + /// Show file with extended attributes as a dir with extended + /// attributes. + #[arg(short = '@')] + file_with_xattr_as_dir: bool, + + /// By default it is the value of the HOME shell variable. If `TARGET_DIR` is "-", it is + /// converted to $OLDPWD. + target_dir: Option, +} + +impl builtins::Command for CdCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + // TODO: implement 'cd -@' + if self.file_with_xattr_as_dir { + return error::unimp("cd -@"); + } + + let mut should_print = false; + let mut target_dir = if let Some(target_dir) = &self.target_dir { + // `cd -', equivalent to `cd $OLDPWD' + if target_dir.as_os_str() == "-" { + should_print = true; + if let Some(oldpwd) = context.shell.env_str("OLDPWD") { + PathBuf::from(oldpwd.to_string()) + } else { + writeln!(context.stderr(), "OLDPWD not set")?; + return Ok(ExecutionResult::general_error()); + } + } else { + // TODO: remove clone, and use temporary lifetime extension after rust 1.75 + target_dir.clone() + } + // `cd' without arguments is equivalent to `cd $HOME' + } else { + if let Some(home_var) = context.shell.env_str("HOME") { + PathBuf::from(home_var.to_string()) + } else { + writeln!(context.stderr(), "HOME not set")?; + return Ok(ExecutionResult::general_error()); + } + }; + + if self.use_physical_dir + || context + .shell + .options + .do_not_resolve_symlinks_when_changing_dir + { + // -e is only relevant in physical mode. + if self.exit_on_failed_cwd_resolution { + return error::unimp("cd -e"); + } + + target_dir = context.shell.absolute_path(target_dir).canonicalize()?; + } + + context.shell.set_working_dir(&target_dir)?; + + // Bash compatibility + // https://www.gnu.org/software/bash/manual/bash.html#index-cd + // If a non-empty directory name from CDPATH is used, or if '-' is the first argument, and + // the directory change is successful, the absolute pathname of the new working + // directory is written to the standard output. + if should_print { + writeln!(context.stdout(), "{}", target_dir.display())?; + } + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/colon.rs b/crates/brush-builtins-vendored/src/colon.rs new file mode 100644 index 000000000..4d0544373 --- /dev/null +++ b/crates/brush-builtins-vendored/src/colon.rs @@ -0,0 +1,27 @@ +use brush_core::{ExecutionResult, builtins, error}; + +/// No-op command. +pub(crate) struct ColonCommand {} + +impl builtins::SimpleCommand for ColonCommand { + fn get_content( + _name: &str, + content_type: builtins::ContentType, + ) -> Result { + match content_type { + builtins::ContentType::DetailedHelp => { + Ok("Null command; always returns success.".into()) + } + builtins::ContentType::ShortUsage => Ok(":: :".into()), + builtins::ContentType::ShortDescription => Ok(": - Null command".into()), + builtins::ContentType::ManPage => error::unimp("man page not yet implemented"), + } + } + + fn execute, S: AsRef>( + _context: brush_core::ExecutionContext<'_>, + _args: I, + ) -> Result { + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/command.rs b/crates/brush-builtins-vendored/src/command.rs new file mode 100644 index 000000000..7d77c266d --- /dev/null +++ b/crates/brush-builtins-vendored/src/command.rs @@ -0,0 +1,166 @@ +use clap::Parser; +use std::{fmt::Display, io::Write, path::Path}; + +use brush_core::{ + ExecutionResult, ExecutionSpawnResult, builtins, commands, pathsearch, + sys::{self, fs::PathExt}, +}; + +/// Directly invokes an external command, without going through typical search order. +#[derive(Parser)] +pub(crate) struct CommandCommand { + /// Use default PATH value. + #[arg(short = 'p')] + use_default_path: bool, + + /// Display a short description of the command. + #[arg(short = 'v')] + print_description: bool, + + /// Display a more verbose description of the command. + #[arg(short = 'V')] + print_verbose_description: bool, + + /// Command and arguments. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + command_and_args: Vec, +} + +impl CommandCommand { + fn command(&self) -> Option<&String> { + self.command_and_args.first() + } +} + +impl builtins::Command for CommandCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + // Silently exit if no command was provided. + if let Some(command_name) = self.command() { + if self.print_description || self.print_verbose_description { + if let Some(found_cmd) = Self::try_find_command( + context.shell, + command_name.as_str(), + self.use_default_path, + ) { + if self.print_description { + writeln!(context.stdout(), "{found_cmd}")?; + } else { + match found_cmd { + FoundCommand::Builtin(_name) => { + writeln!(context.stdout(), "{command_name} is a shell builtin")?; + } + FoundCommand::External(path) => { + writeln!(context.stdout(), "{command_name} is {path}")?; + } + } + } + Ok(ExecutionResult::success()) + } else { + if self.print_verbose_description { + writeln!(context.stderr(), "command: {command_name}: not found")?; + } + Ok(ExecutionResult::general_error()) + } + } else { + self.execute_command(context, command_name, self.use_default_path) + .await + } + } else { + Ok(ExecutionResult::success()) + } + } +} + +enum FoundCommand { + Builtin(String), + External(String), +} + +impl Display for FoundCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Builtin(name) => write!(f, "{name}"), + Self::External(path) => write!(f, "{path}"), + } + } +} + +impl CommandCommand { + fn try_find_command( + shell: &mut brush_core::Shell, + command_name: &str, + use_default_path: bool, + ) -> Option { + // Look in path. + if command_name.contains(std::path::MAIN_SEPARATOR) { + let candidate_path = shell.absolute_path(Path::new(command_name)); + if candidate_path.executable() { + Some(FoundCommand::External( + candidate_path.to_string_lossy().to_string(), + )) + } else { + None + } + } else { + if let Some(builtin_cmd) = shell.builtins().get(command_name) { + if !builtin_cmd.disabled { + return Some(FoundCommand::Builtin(command_name.to_owned())); + } + } + + if use_default_path { + let dirs = sys::fs::get_default_standard_utils_paths(); + + pathsearch::search_for_executable(dirs.iter().map(String::as_str), command_name) + .next() + .map(|path| FoundCommand::External(path.to_string_lossy().to_string())) + } else { + shell + .find_first_executable_in_path_using_cache(command_name) + .map(|path| FoundCommand::External(path.to_string_lossy().to_string())) + } + } + } + + async fn execute_command( + &self, + mut context: brush_core::ExecutionContext<'_>, + command_name: &str, + use_default_path: bool, + ) -> Result { + command_name.clone_into(&mut context.command_name); + let command_and_args = self.command_and_args.iter().map(|arg| arg.into()).collect(); + + let path_dirs = if use_default_path { + Some(sys::fs::get_default_standard_utils_paths()) + } else { + None + }; + + // We do not have an existing process group to place this into. + let mut pgid = None; + let cancel_token = context.cancel_token(); + + match commands::execute( + context, + &mut pgid, + command_and_args, + false, /* use functions? */ + path_dirs, + ) + .await? + { + ExecutionSpawnResult::StartedProcess(mut child) => { + // TODO: jobs: review this logic + let wait_result = child.wait(cancel_token).await?; + Ok(ExecutionResult::from(wait_result)) + } + ExecutionSpawnResult::Completed(result) => Ok(result), + } + } +} diff --git a/crates/brush-builtins-vendored/src/complete.rs b/crates/brush-builtins-vendored/src/complete.rs new file mode 100644 index 000000000..e51c5f46d --- /dev/null +++ b/crates/brush-builtins-vendored/src/complete.rs @@ -0,0 +1,660 @@ +use clap::Parser; +use std::collections::HashMap; +use std::fmt::Write as _; +use std::io::Write; + +use brush_core::completion::{self, CompleteAction, CompleteOption, Spec}; +use brush_core::{ExecutionExitCode, ExecutionResult, builtins, error, escape}; + +#[derive(Parser)] +struct CommonCompleteCommandArgs { + /// Options governing the behavior of completions. + #[arg(short = 'o')] + options: Vec, + + /// Actions to apply to generate completions. + #[arg(short = 'A')] + actions: Vec, + + /// File glob pattern to be expanded to generate completions. + #[arg(short = 'G', allow_hyphen_values = true, value_name = "GLOB")] + glob_pattern: Option, + + /// List of words that will be considered as completions. + #[arg(short = 'W', allow_hyphen_values = true)] + word_list: Option, + + /// Name of a shell function to invoke to generate completions. + #[arg(short = 'F', allow_hyphen_values = true, value_name = "FUNC_NAME")] + function_name: Option, + + /// Command to execute to generate completions. + #[arg(short = 'C', allow_hyphen_values = true)] + command: Option, + + /// Pattern used as filter for completions. + #[arg(short = 'X', allow_hyphen_values = true, value_name = "PATTERN")] + filter_pattern: Option, + + /// Prefix pattern used as filter for completions. + #[arg(short = 'P', allow_hyphen_values = true)] + prefix: Option, + + /// Suffix pattern used as filter for completions. + #[arg(short = 'S', allow_hyphen_values = true)] + suffix: Option, + + /// Complete with valid aliases. + #[arg(short = 'a')] + action_alias: bool, + + /// Complete with names of shell builtins. + #[arg(short = 'b')] + action_builtin: bool, + + /// Complete with names of executable commands. + #[arg(short = 'c')] + action_command: bool, + + /// Complete with directory names. + #[arg(short = 'd')] + action_directory: bool, + + /// Complete with names of exported shell variables. + #[arg(short = 'e')] + action_exported: bool, + + /// Complete with filenames. + #[arg(short = 'f')] + action_file: bool, + + /// Complete with valid user groups. + #[arg(short = 'g')] + action_group: bool, + + /// Complete with job specs. + #[arg(short = 'j')] + action_job: bool, + + /// Complete with keywords. + #[arg(short = 'k')] + action_keyword: bool, + + /// Complete with names of system services. + #[arg(short = 's')] + action_service: bool, + + /// Complete with valid usernames. + #[arg(short = 'u')] + action_user: bool, + + /// Complete with names of shell variables. + #[arg(short = 'v')] + action_variable: bool, +} + +impl CommonCompleteCommandArgs { + fn create_spec(&self, extglob_enabled: bool) -> completion::Spec { + let filter_pattern_excludes; + let filter_pattern = if let Some(filter_pattern) = self.filter_pattern.as_ref() { + // If the pattern starts with a '!' that's not the start of an extglob pattern, + // then we invert. + if let Some(remaining_pattern) = filter_pattern.strip_prefix('!') { + if !extglob_enabled || !remaining_pattern.starts_with('(') { + filter_pattern_excludes = false; + Some(remaining_pattern.to_owned()) + } else { + filter_pattern_excludes = true; + Some(filter_pattern.to_owned()) + } + } else { + filter_pattern_excludes = true; + Some(filter_pattern.clone()) + } + } else { + filter_pattern_excludes = false; + None + }; + + let mut spec = completion::Spec { + options: completion::GenerationOptions::default(), + actions: self.resolve_actions(), + glob_pattern: self.glob_pattern.clone(), + word_list: self.word_list.clone(), + function_name: self.function_name.clone(), + command: self.command.clone(), + filter_pattern, + filter_pattern_excludes, + prefix: self.prefix.clone(), + suffix: self.suffix.clone(), + }; + + for option in &self.options { + match option { + CompleteOption::BashDefault => spec.options.bash_default = true, + CompleteOption::Default => spec.options.default = true, + CompleteOption::DirNames => spec.options.dir_names = true, + CompleteOption::FileNames => spec.options.file_names = true, + CompleteOption::NoQuote => spec.options.no_quote = true, + CompleteOption::NoSort => spec.options.no_sort = true, + CompleteOption::NoSpace => spec.options.no_space = true, + CompleteOption::PlusDirs => spec.options.plus_dirs = true, + } + } + + spec + } + + fn resolve_actions(&self) -> Vec { + let mut actions = self.actions.clone(); + + if self.action_alias { + actions.push(CompleteAction::Alias); + } + if self.action_builtin { + actions.push(CompleteAction::Builtin); + } + if self.action_command { + actions.push(CompleteAction::Command); + } + if self.action_directory { + actions.push(CompleteAction::Directory); + } + if self.action_exported { + actions.push(CompleteAction::Export); + } + if self.action_file { + actions.push(CompleteAction::File); + } + if self.action_group { + actions.push(CompleteAction::Group); + } + if self.action_job { + actions.push(CompleteAction::Job); + } + if self.action_keyword { + actions.push(CompleteAction::Keyword); + } + if self.action_service { + actions.push(CompleteAction::Service); + } + if self.action_user { + actions.push(CompleteAction::User); + } + if self.action_variable { + actions.push(CompleteAction::Variable); + } + + actions + } +} + +/// Configure programmable command completion. +#[derive(Parser)] +pub(crate) struct CompleteCommand { + /// Display registered completion settings. + #[arg(short = 'p')] + print: bool, + + /// Remove the completion settings associated with the given command. + #[arg(short = 'r')] + remove: bool, + + /// Apply these settings to the default completion scenario. + #[arg(short = 'D')] + use_as_default: bool, + + /// Apply these settings to completion of empty lines. + #[arg(short = 'E')] + use_for_empty_line: bool, + + /// Apply these settings to completion of the initial word of the input line. + #[arg(short = 'I')] + use_for_initial_word: bool, + + #[clap(flatten)] + common_args: CommonCompleteCommandArgs, + + names: Vec, +} + +impl builtins::Command for CompleteCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + mut context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut result = ExecutionResult::success(); + + // If -D, -E, or -I are specified, then any names provided are ignored. + if self.use_as_default + || self.use_for_empty_line + || self.use_for_initial_word + || self.names.is_empty() + { + self.process_global(&mut context)?; + } else { + for name in &self.names { + if !self.try_process_for_command(&mut context, name.as_str())? { + result = ExecutionResult::general_error(); + } + } + } + + Ok(result) + } +} + +impl CompleteCommand { + fn process_global( + &self, + context: &mut brush_core::ExecutionContext<'_>, + ) -> Result<(), brush_core::Error> { + // These are processed in an intentional order. + let special_option_name; + let target_spec = if self.use_as_default { + special_option_name = "-D"; + Some(&mut context.shell.completion_config.default) + } else if self.use_for_empty_line { + special_option_name = "-E"; + Some(&mut context.shell.completion_config.empty_line) + } else if self.use_for_initial_word { + special_option_name = "-I"; + Some(&mut context.shell.completion_config.initial_word) + } else { + special_option_name = ""; + None + }; + + // Treat 'complete' with no options the same as 'complete -p'. + if self.print || (!self.remove && target_spec.is_none()) { + if let Some(target_spec) = target_spec { + if let Some(existing_spec) = target_spec { + let existing_spec = existing_spec.clone(); + Self::display_spec(context, Some(special_option_name), None, &existing_spec)?; + } else { + return error::unimp("special spec not found"); + } + } else { + for (command_name, spec) in context.shell.completion_config.iter() { + Self::display_spec(context, None, Some(command_name.as_str()), spec)?; + } + } + } else if self.remove { + if let Some(target_spec) = target_spec { + let mut new_spec = None; + std::mem::swap(&mut new_spec, target_spec); + } else { + context.shell.completion_config.clear(); + } + } else { + if let Some(target_spec) = target_spec { + let mut new_spec = Some( + self.common_args + .create_spec(context.shell.options.extended_globbing), + ); + std::mem::swap(&mut new_spec, target_spec); + } else { + return error::unimp("set unspecified spec"); + } + } + + Ok(()) + } + + fn try_display_spec_for_command( + context: &brush_core::ExecutionContext<'_>, + name: &str, + ) -> Result { + if let Some(spec) = context.shell.completion_config.get(name) { + Self::display_spec(context, None, Some(name), spec)?; + Ok(true) + } else { + writeln!(context.stderr(), "no completion found for command")?; + Ok(false) + } + } + + #[expect(clippy::too_many_lines)] + fn display_spec( + context: &brush_core::ExecutionContext<'_>, + special_name: Option<&str>, + command_name: Option<&str>, + spec: &Spec, + ) -> Result<(), brush_core::Error> { + let mut s = String::from("complete"); + + if let Some(special_name) = special_name { + s.push(' '); + s.push_str(special_name); + } + + for action in &spec.actions { + s.push(' '); + + let action_str = match action { + CompleteAction::Alias => "-a", + CompleteAction::ArrayVar => "-A arrayvar", + CompleteAction::Binding => "-A binding", + CompleteAction::Builtin => "-b", + CompleteAction::Command => "-c", + CompleteAction::Directory => "-d", + CompleteAction::Disabled => "-A disabled", + CompleteAction::Enabled => "-A enabled", + CompleteAction::Export => "-e", + CompleteAction::File => "-f", + CompleteAction::Function => "-A function", + CompleteAction::Group => "-g", + CompleteAction::HelpTopic => "-A helptopic", + CompleteAction::HostName => "-A hostname", + CompleteAction::Job => "-j", + CompleteAction::Keyword => "-k", + CompleteAction::Running => "-A running", + CompleteAction::Service => "-s", + CompleteAction::SetOpt => "-A setopt", + CompleteAction::ShOpt => "-A shopt", + CompleteAction::Signal => "-A signal", + CompleteAction::Stopped => "-A stopped", + CompleteAction::User => "-u", + CompleteAction::Variable => "-v", + }; + + s.push_str(action_str); + } + + if spec.options.bash_default { + s.push_str(" -o bashdefault"); + } + if spec.options.default { + s.push_str(" -o default"); + } + if spec.options.dir_names { + s.push_str(" -o dirnames"); + } + if spec.options.file_names { + s.push_str(" -o filenames"); + } + if spec.options.no_quote { + s.push_str(" -o noquote"); + } + if spec.options.no_sort { + s.push_str(" -o nosort"); + } + if spec.options.no_space { + s.push_str(" -o nospace"); + } + if spec.options.plus_dirs { + s.push_str(" -o plusdirs"); + } + + if let Some(glob_pattern) = &spec.glob_pattern { + write!( + s, + " -G {}", + escape::force_quote(glob_pattern, escape::QuoteMode::SingleQuote) + )?; + } + if let Some(word_list) = &spec.word_list { + write!( + s, + " -W {}", + escape::force_quote(word_list, escape::QuoteMode::SingleQuote) + )?; + } + if let Some(function_name) = &spec.function_name { + write!(s, " -F {function_name}")?; + } + if let Some(command) = &spec.command { + write!( + s, + " -C {}", + escape::force_quote(command, escape::QuoteMode::SingleQuote) + )?; + } + if let Some(filter_pattern) = &spec.filter_pattern { + write!( + s, + " -X {}", + escape::force_quote(filter_pattern, escape::QuoteMode::SingleQuote) + )?; + } + if let Some(prefix) = &spec.prefix { + write!( + s, + " -P {}", + escape::force_quote(prefix, escape::QuoteMode::SingleQuote) + )?; + } + if let Some(suffix) = &spec.suffix { + write!( + s, + " -S {}", + escape::force_quote(suffix, escape::QuoteMode::SingleQuote) + )?; + } + + if let Some(command_name) = command_name { + s.push(' '); + s.push_str(command_name); + } + + writeln!(context.stdout(), "{s}")?; + + Ok(()) + } + + fn try_process_for_command( + &self, + context: &mut brush_core::ExecutionContext<'_>, + name: &str, + ) -> Result { + if self.print { + return Self::try_display_spec_for_command(context, name); + } else if self.remove { + let mut result = context.shell.completion_config.remove(name); + + if !result { + if context.shell.options.interactive { + writeln!(context.stderr(), "complete: {name}: not found")?; + } else { + // For some reason, this is not supposed to be treated as a failure + // in non-interactive execution. + result = true; + } + } + + return Ok(result); + } + + let config = self + .common_args + .create_spec(context.shell.options.extended_globbing); + + context.shell.completion_config.set(name, config); + + Ok(true) + } +} + +/// Generate command completions. +#[derive(Parser)] +pub(crate) struct CompGenCommand { + #[clap(flatten)] + common_args: CommonCompleteCommandArgs, + + // N.B. The word can only start with a hyphen if it's after a --. + word: Option, +} + +impl builtins::Command for CompGenCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut spec = self + .common_args + .create_spec(context.shell.options.extended_globbing); + spec.options.no_sort = true; + + let token_to_complete = self.word.as_deref().unwrap_or_default(); + + // We unquote the token-to-be-completed before passing it to the completion system. + let unquoted_token = brush_parser::unquote_str(token_to_complete); + + let completion_context = completion::Context { + token_to_complete: unquoted_token.as_str(), + preceding_token: None, + command_name: None, + token_index: 0, + tokens: &[&brush_parser::Token::Word( + token_to_complete.to_owned(), + brush_parser::TokenLocation::default(), + )], + input_line: token_to_complete, + cursor_index: token_to_complete.len(), + }; + + let result = spec + .get_completions(context.shell, &completion_context) + .await?; + + match result { + completion::Answer::Candidates(candidates, _options) => { + // We are expected to return 1 if there are no candidates, even if no errors + // occurred along the way. + if candidates.is_empty() { + return Ok(ExecutionResult::general_error()); + } + + for candidate in candidates { + writeln!(context.stdout(), "{candidate}")?; + } + } + completion::Answer::RestartCompletionProcess => { + return error::unimp("restart completion"); + } + } + + Ok(ExecutionResult::success()) + } +} + +/// Set programmable command completion options. +#[derive(Parser)] +pub(crate) struct CompOptCommand { + /// Update the default completion settings. + #[arg(short = 'D')] + update_default: bool, + + /// Update the completion settings for empty lines. + #[arg(short = 'E')] + update_empty: bool, + + /// Update the completion settings for the initial word of the input line. + #[arg(short = 'I')] + update_initial_word: bool, + + /// Enable the specified option for selected completion scenarios. + #[arg(short = 'o', value_name = "OPT")] + enabled_options: Vec, + #[arg(long = concat!("+o"), hide = true)] + disabled_options: Vec, + + /// If specified, scopes updates to completions of the named commands. + names: Vec, +} + +impl builtins::Command for CompOptCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut options = HashMap::new(); + for option in &self.disabled_options { + options.insert(option.clone(), false); + } + for option in &self.enabled_options { + options.insert(option.clone(), true); + } + + if !self.names.is_empty() { + if self.update_default || self.update_empty || self.update_initial_word { + writeln!( + context.stderr(), + "compopt: cannot specify names with -D, -E, or -I" + )?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + for name in &self.names { + let spec = context.shell.completion_config.get_or_add_mut(name); + Self::set_options_for_spec(spec, &options); + } + } else if self.update_default { + if let Some(spec) = &mut context.shell.completion_config.default { + Self::set_options_for_spec(spec, &options); + } else { + let mut spec = Spec::default(); + Self::set_options_for_spec(&mut spec, &options); + context.shell.completion_config.default = Some(spec); + } + } else if self.update_empty { + if let Some(spec) = &mut context.shell.completion_config.empty_line { + Self::set_options_for_spec(spec, &options); + } else { + let mut spec = Spec::default(); + Self::set_options_for_spec(&mut spec, &options); + context.shell.completion_config.empty_line = Some(spec); + } + } else if self.update_initial_word { + if let Some(spec) = &mut context.shell.completion_config.initial_word { + Self::set_options_for_spec(spec, &options); + } else { + let mut spec = Spec::default(); + Self::set_options_for_spec(&mut spec, &options); + context.shell.completion_config.initial_word = Some(spec); + } + } else { + // If we got here, then we need to apply to any completion actively in-flight. + if let Some(in_flight_options) = context + .shell + .completion_config + .current_completion_options + .as_mut() + { + Self::set_options(in_flight_options, &options); + } + } + + Ok(ExecutionResult::success()) + } +} + +impl CompOptCommand { + fn set_options_for_spec(spec: &mut Spec, options: &HashMap) { + Self::set_options(&mut spec.options, options); + } + + fn set_options( + target_options: &mut completion::GenerationOptions, + options: &HashMap, + ) { + for (option, value) in options { + match option { + CompleteOption::BashDefault => target_options.bash_default = *value, + CompleteOption::Default => target_options.default = *value, + CompleteOption::DirNames => target_options.dir_names = *value, + CompleteOption::FileNames => target_options.file_names = *value, + CompleteOption::NoQuote => target_options.no_quote = *value, + CompleteOption::NoSort => target_options.no_sort = *value, + CompleteOption::NoSpace => target_options.no_space = *value, + CompleteOption::PlusDirs => target_options.plus_dirs = *value, + } + } + } +} diff --git a/crates/brush-builtins-vendored/src/continue_.rs b/crates/brush-builtins-vendored/src/continue_.rs new file mode 100644 index 000000000..2ff0590e4 --- /dev/null +++ b/crates/brush-builtins-vendored/src/continue_.rs @@ -0,0 +1,34 @@ +use clap::Parser; + +use brush_core::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, builtins}; + +/// Continue to the next iteration of a control-flow loop. +#[derive(Parser)] +pub(crate) struct ContinueCommand { + /// If specified, indicates which nested loop to continue to the next iteration of. + #[clap(default_value_t = 1)] + which_loop: i8, +} + +impl builtins::Command for ContinueCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + _context: brush_core::ExecutionContext<'_>, + ) -> Result { + // If specified, which_loop needs to be positive. + if self.which_loop <= 0 { + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + let mut result = ExecutionResult::success(); + + result.next_control_flow = ExecutionControlFlow::ContinueLoop { + #[expect(clippy::cast_sign_loss)] + levels: (self.which_loop - 1) as usize, + }; + + Ok(result) + } +} diff --git a/crates/brush-builtins-vendored/src/declare.rs b/crates/brush-builtins-vendored/src/declare.rs new file mode 100644 index 000000000..c19e79d6b --- /dev/null +++ b/crates/brush-builtins-vendored/src/declare.rs @@ -0,0 +1,632 @@ +use clap::Parser; +use itertools::Itertools; +use std::{io::Write, sync::LazyLock}; + +use brush_core::{ + ErrorKind, ExecutionResult, builtins, + env::{self, EnvironmentLookup, EnvironmentScope}, + error, + variables::{ + self, ArrayLiteral, ShellValue, ShellValueLiteral, ShellValueUnsetType, ShellVariable, + ShellVariableUpdateTransform, + }, +}; + +crate::minus_or_plus_flag_arg!( + MakeIndexedArrayFlag, + 'a', + "Make the variable an indexed array." +); +crate::minus_or_plus_flag_arg!( + MakeAssociativeArrayFlag, + 'A', + "Make the variable an associative array." +); +crate::minus_or_plus_flag_arg!( + CapitalizeValueOnAssignmentFlag, + 'c', + "Enable capitalize-on-assignment for the variable." +); +crate::minus_or_plus_flag_arg!(MakeIntegerFlag, 'i', "Mark the variable as integer-typed"); +crate::minus_or_plus_flag_arg!( + LowercaseValueOnAssignmentFlag, + 'l', + "Enable lowercase-on-assignment for the variable." +); +crate::minus_or_plus_flag_arg!( + MakeNameRefFlag, + 'n', + "Mark the variable as a name reference" +); +crate::minus_or_plus_flag_arg!(MakeReadonlyFlag, 'r', "Mark the variable as read-only."); +crate::minus_or_plus_flag_arg!(MakeTracedFlag, 't', "Enable tracing for the variable."); +crate::minus_or_plus_flag_arg!( + UppercaseValueOnAssignmentFlag, + 'u', + "Enable uppercase-on-assignment for the variable." +); +crate::minus_or_plus_flag_arg!(MakeExportedFlag, 'x', "Mark the variable for export."); + +/// Display or update variables and their attributes. +#[derive(Parser)] +#[clap(override_usage = "declare [OPTIONS] [DECLARATIONS]...")] +pub(crate) struct DeclareCommand { + /// Constrain to function names or definitions. + #[arg(short = 'f')] + function_names_or_defs_only: bool, + + /// Constrain to function names only. + #[arg(short = 'F')] + function_names_only: bool, + + /// Create global variable, if applicable. + #[arg(short = 'g')] + create_global: bool, + + /// When creating a local variable that shadows another variable of the same name, + /// then initialize it with the contents and attributes of the variable being shadowed. + #[arg(short = 'I')] + locals_inherit_from_prev_scope: bool, + + /// Display each item's attributes and values. + #[arg(short = 'p')] + print: bool, + + // + // Attribute options + #[clap(flatten)] // -a + make_indexed_array: MakeIndexedArrayFlag, + #[clap(flatten)] // -A + make_associative_array: MakeAssociativeArrayFlag, + #[clap(flatten)] // -c + capitalize_value_on_assignment: CapitalizeValueOnAssignmentFlag, + #[clap(flatten)] // -i + make_integer: MakeIntegerFlag, + #[clap(flatten)] // -l + lowercase_value_on_assignment: LowercaseValueOnAssignmentFlag, + #[clap(flatten)] // -n + make_nameref: MakeNameRefFlag, + #[clap(flatten)] // -r + make_readonly: MakeReadonlyFlag, + #[clap(flatten)] // -t + make_traced: MakeTracedFlag, + #[clap(flatten)] // -u + uppercase_value_on_assignment: UppercaseValueOnAssignmentFlag, + #[clap(flatten)] // -x + make_exported: MakeExportedFlag, + + // + // Declarations + // + // N.B. These are skipped by clap, but filled in by the BuiltinDeclarationCommand trait. + #[clap(skip)] + declarations: Vec, +} + +#[derive(Clone, Copy)] +enum DeclareVerb { + Declare, + Local, + Readonly, +} + +impl builtins::DeclarationCommand for DeclareCommand { + fn set_declarations(&mut self, declarations: Vec) { + self.declarations = declarations; + } +} + +impl builtins::Command for DeclareCommand { + fn takes_plus_options() -> bool { + true + } + + type Error = brush_core::Error; + + async fn execute( + &self, + mut context: brush_core::ExecutionContext<'_>, + ) -> Result { + let verb = match context.command_name.as_str() { + "local" => DeclareVerb::Local, + "readonly" => DeclareVerb::Readonly, + _ => DeclareVerb::Declare, + }; + + if matches!(verb, DeclareVerb::Local) && !context.shell.in_function() { + writeln!(context.stderr(), "can only be used in a function")?; + return Ok(ExecutionResult::general_error()); + } + + if self.locals_inherit_from_prev_scope { + return error::unimp("declare -I"); + } + + let mut result = ExecutionResult::success(); + if !self.declarations.is_empty() { + for declaration in &self.declarations { + if self.print && !matches!(verb, DeclareVerb::Readonly) { + if !self.try_display_declaration(&context, declaration, verb)? { + result = ExecutionResult::general_error(); + } + } else { + if !self.process_declaration(&mut context, declaration, verb)? { + result = ExecutionResult::general_error(); + } + } + } + } else { + // Display matching declarations from the variable environment. + if !self.function_names_only && !self.function_names_or_defs_only { + self.display_matching_env_declarations(&context, verb)?; + } + + // Do the same for functions. + if !matches!(verb, DeclareVerb::Local | DeclareVerb::Readonly) + && (!self.print || self.function_names_only || self.function_names_or_defs_only) + { + self.display_matching_functions(&context)?; + } + } + + Ok(result) + } +} + +impl DeclareCommand { + fn try_display_declaration( + &self, + context: &brush_core::ExecutionContext<'_>, + declaration: &brush_core::CommandArg, + verb: DeclareVerb, + ) -> Result { + let name = match declaration { + brush_core::CommandArg::String(s) => s, + brush_core::CommandArg::Assignment(_) => { + writeln!(context.stderr(), "declare: {declaration}: not found")?; + return Ok(false); + } + }; + + let lookup = if matches!(verb, DeclareVerb::Local) { + EnvironmentLookup::OnlyInCurrentLocal + } else { + EnvironmentLookup::Anywhere + }; + + if self.function_names_only || self.function_names_or_defs_only { + if let Some(func_registration) = context.shell.funcs().get(name) { + if self.function_names_only { + if self.print { + writeln!(context.stdout(), "declare -f {name}")?; + } else { + writeln!(context.stdout(), "{name}")?; + } + } else { + writeln!(context.stdout(), "{}", func_registration.definition())?; + } + Ok(true) + } else { + // For some reason, bash does not print an error message in this case. + Ok(false) + } + } else if let Some(variable) = context.shell.env.get_using_policy(name, lookup) { + let mut cs = variable.attribute_flags(context.shell); + if cs.is_empty() { + cs.push('-'); + } + + let resolved_value = variable.resolve_value(context.shell); + let separator_str = if matches!(resolved_value, ShellValue::Unset(_)) { + "" + } else { + "=" + }; + + writeln!( + context.stdout(), + "declare -{cs} {name}{separator_str}{}", + resolved_value.format(variables::FormatStyle::DeclarePrint, context.shell)? + )?; + + Ok(true) + } else { + writeln!(context.stderr(), "declare: {name}: not found")?; + Ok(false) + } + } + + fn process_declaration( + &self, + context: &mut brush_core::ExecutionContext<'_>, + declaration: &brush_core::CommandArg, + verb: DeclareVerb, + ) -> Result { + let create_var_local = matches!(verb, DeclareVerb::Local) + || (context.shell.in_function() && !self.create_global); + + if self.function_names_or_defs_only || self.function_names_only { + return self.try_display_declaration(context, declaration, verb); + } + + // Extract the variable name and the initial value being assigned (if any). + let (name, assigned_index, initial_value, name_is_array) = + Self::declaration_to_name_and_value(declaration)?; + + // Special-case: `local -` + if name == "-" && matches!(verb, DeclareVerb::Local) { + // TODO: `local -` allows shadowing the current `set` options (i.e., $-), with + // subsequent updates getting discarded when the current local scope is popped. + tracing::warn!("not yet implemented: local -"); + return Ok(true); + } + + // Make sure it's a valid name. + if !env::valid_variable_name(name.as_str()) { + writeln!( + context.stderr(), + "{}: {name}: not a valid variable name", + context.command_name + )?; + return Ok(false); + } + + // Figure out where we should look. + let lookup = if create_var_local { + EnvironmentLookup::OnlyInCurrentLocal + } else { + EnvironmentLookup::Anywhere + }; + + // Look up the variable. + if let Some(var) = context + .shell + .env + .get_mut_using_policy(name.as_str(), lookup) + { + if self.make_associative_array.is_some() { + var.convert_to_associative_array()?; + } + if self.make_indexed_array.is_some() { + var.convert_to_indexed_array()?; + } + + self.apply_attributes_before_update(var)?; + + if let Some(initial_value) = initial_value { + // We append if the declaration included an explicit index. + var.assign(initial_value, assigned_index.is_some())?; + } + + self.apply_attributes_after_update(var, verb)?; + } else { + let unset_type = if self.make_indexed_array.is_some() { + ShellValueUnsetType::IndexedArray + } else if self.make_associative_array.is_some() { + ShellValueUnsetType::AssociativeArray + } else if name_is_array { + ShellValueUnsetType::IndexedArray + } else { + ShellValueUnsetType::Untyped + }; + + let mut var = ShellVariable::new(ShellValue::Unset(unset_type)); + + self.apply_attributes_before_update(&mut var)?; + + if let Some(initial_value) = initial_value { + var.assign(initial_value, false)?; + } + + if context.shell.options.export_variables_on_modification && !var.value().is_array() { + var.export(); + } + + self.apply_attributes_after_update(&mut var, verb)?; + + let scope = if create_var_local { + EnvironmentScope::Local + } else { + EnvironmentScope::Global + }; + + context.shell.env.add(name, var, scope)?; + } + + Ok(true) + } + + #[allow(clippy::unwrap_in_result)] + fn declaration_to_name_and_value( + declaration: &brush_core::CommandArg, + ) -> Result<(String, Option, Option, bool), brush_core::Error> { + let name; + let assigned_index; + let initial_value; + let name_is_array; + + match declaration { + brush_core::CommandArg::String(s) => { + // We need to handle the case of someone invoking `declare array[index]`. + // In such case, we ignore the index and treat it as a declaration of + // the array. + static ARRAY_AND_INDEX_RE: LazyLock = + LazyLock::new(|| fancy_regex::Regex::new(r"^(.*?)\[(.*?)\]$").unwrap()); + if let Some(captures) = ARRAY_AND_INDEX_RE.captures(s)? { + name = captures.get(1).unwrap().as_str().to_owned(); + assigned_index = Some(captures.get(2).unwrap().as_str().to_owned()); + name_is_array = true; + } else { + name = s.clone(); + assigned_index = None; + name_is_array = false; + } + initial_value = None; + } + brush_core::CommandArg::Assignment(assignment) => { + match &assignment.name { + brush_parser::ast::AssignmentName::VariableName(var_name) => { + name = var_name.to_owned(); + assigned_index = None; + } + brush_parser::ast::AssignmentName::ArrayElementName(var_name, index) => { + if matches!( + assignment.value, + brush_parser::ast::AssignmentValue::Array(_) + ) { + return Err(ErrorKind::AssigningListToArrayMember.into()); + } + + name = var_name.to_owned(); + assigned_index = Some(index.to_owned()); + } + } + + match &assignment.value { + brush_parser::ast::AssignmentValue::Scalar(s) => { + if let Some(index) = &assigned_index { + initial_value = Some(ShellValueLiteral::Array(ArrayLiteral(vec![( + Some(index.to_owned()), + s.value.clone(), + )]))); + name_is_array = true; + } else { + initial_value = Some(ShellValueLiteral::Scalar(s.value.clone())); + name_is_array = false; + } + } + brush_parser::ast::AssignmentValue::Array(a) => { + initial_value = Some(ShellValueLiteral::Array(ArrayLiteral( + a.iter() + .map(|(i, v)| { + (i.as_ref().map(|w| w.value.clone()), v.value.clone()) + }) + .collect(), + ))); + name_is_array = true; + } + } + } + } + + Ok((name, assigned_index, initial_value, name_is_array)) + } + + fn display_matching_env_declarations( + &self, + context: &brush_core::ExecutionContext<'_>, + verb: DeclareVerb, + ) -> Result<(), brush_core::Error> { + // + // Dump all declarations. Use attribute flags to filter which variables are dumped. + // + + // We start by excluding all variables that are not enumerable. + #[expect(clippy::type_complexity)] + let mut filters: Vec bool>> = + vec![Box::new(|(_, v)| v.is_enumerable())]; + + // Add filters depending on verb. + if matches!(verb, DeclareVerb::Readonly) { + filters.push(Box::new(|(_, v)| v.is_readonly())); + } + + // Add filters depending on attribute flags. + if let Some(value) = self.make_indexed_array.to_bool() { + filters.push(Box::new(move |(_, v)| { + matches!(v.value(), ShellValue::IndexedArray(_)) == value + })); + } + if let Some(value) = self.make_associative_array.to_bool() { + filters.push(Box::new(move |(_, v)| { + matches!(v.value(), ShellValue::AssociativeArray(_)) == value + })); + } + if let Some(value) = self.make_integer.to_bool() { + filters.push(Box::new(move |(_, v)| v.is_treated_as_integer() == value)); + } + if let Some(value) = self.capitalize_value_on_assignment.to_bool() { + filters.push(Box::new(move |(_, v)| { + matches!( + v.get_update_transform(), + ShellVariableUpdateTransform::Capitalize + ) == value + })); + } + if let Some(value) = self.lowercase_value_on_assignment.to_bool() { + filters.push(Box::new(move |(_, v)| { + matches!( + v.get_update_transform(), + ShellVariableUpdateTransform::Lowercase + ) == value + })); + } + if let Some(value) = self.make_nameref.to_bool() { + filters.push(Box::new(move |(_, v)| v.is_treated_as_nameref() == value)); + } + if let Some(value) = self.make_readonly.to_bool() { + filters.push(Box::new(move |(_, v)| v.is_readonly() == value)); + } + if let Some(value) = self.make_readonly.to_bool() { + filters.push(Box::new(move |(_, v)| v.is_trace_enabled() == value)); + } + if let Some(value) = self.uppercase_value_on_assignment.to_bool() { + filters.push(Box::new(move |(_, v)| { + matches!( + v.get_update_transform(), + ShellVariableUpdateTransform::Uppercase + ) == value + })); + } + if let Some(value) = self.make_exported.to_bool() { + filters.push(Box::new(move |(_, v)| v.is_exported() == value)); + } + + let iter_policy = if matches!(verb, DeclareVerb::Local) { + EnvironmentLookup::OnlyInCurrentLocal + } else { + EnvironmentLookup::Anywhere + }; + + // Iterate through an ordered list of all matching declarations tracked in the + // environment. + for (name, variable) in context + .shell + .env + .iter_using_policy(iter_policy) + .filter(|pair| filters.iter().all(|f| f(*pair))) + .sorted_by_key(|v| v.0) + { + if self.print { + let mut cs = variable.attribute_flags(context.shell); + if cs.is_empty() { + cs.push('-'); + } + + let separator_str = if matches!(variable.value(), ShellValue::Unset(_)) { + "" + } else { + "=" + }; + + writeln!( + context.stdout(), + "declare -{cs} {name}{separator_str}{}", + variable + .value() + .format(variables::FormatStyle::DeclarePrint, context.shell)? + )?; + } else { + writeln!( + context.stdout(), + "{name}={}", + variable + .value() + .format(variables::FormatStyle::Basic, context.shell)? + )?; + } + } + + Ok(()) + } + + fn display_matching_functions( + &self, + context: &brush_core::ExecutionContext<'_>, + ) -> Result<(), brush_core::Error> { + for (name, registration) in context.shell.funcs().iter().sorted_by_key(|v| v.0) { + if self.function_names_only { + writeln!(context.stdout(), "declare -f {name}")?; + } else { + writeln!(context.stdout(), "{}", registration.definition())?; + } + } + + Ok(()) + } + + #[expect(clippy::unnecessary_wraps)] + const fn apply_attributes_before_update( + &self, + var: &mut ShellVariable, + ) -> Result<(), brush_core::Error> { + if let Some(value) = self.make_integer.to_bool() { + if value { + var.treat_as_integer(); + } else { + var.unset_treat_as_integer(); + } + } + if let Some(value) = self.capitalize_value_on_assignment.to_bool() { + if value { + var.set_update_transform(ShellVariableUpdateTransform::Capitalize); + } else if matches!( + var.get_update_transform(), + ShellVariableUpdateTransform::Capitalize + ) { + var.set_update_transform(ShellVariableUpdateTransform::None); + } + } + if let Some(value) = self.lowercase_value_on_assignment.to_bool() { + if value { + var.set_update_transform(ShellVariableUpdateTransform::Lowercase); + } else if matches!( + var.get_update_transform(), + ShellVariableUpdateTransform::Lowercase + ) { + var.set_update_transform(ShellVariableUpdateTransform::None); + } + } + if let Some(value) = self.make_nameref.to_bool() { + if value { + var.treat_as_nameref(); + } else { + var.unset_treat_as_nameref(); + } + } + if let Some(value) = self.make_traced.to_bool() { + if value { + var.enable_trace(); + } else { + var.disable_trace(); + } + } + if let Some(value) = self.uppercase_value_on_assignment.to_bool() { + if value { + var.set_update_transform(ShellVariableUpdateTransform::Uppercase); + } else if matches!( + var.get_update_transform(), + ShellVariableUpdateTransform::Uppercase + ) { + var.set_update_transform(ShellVariableUpdateTransform::None); + } + } + if let Some(value) = self.make_exported.to_bool() { + if value { + var.export(); + } else { + var.unexport(); + } + } + + Ok(()) + } + + fn apply_attributes_after_update( + &self, + var: &mut ShellVariable, + verb: DeclareVerb, + ) -> Result<(), brush_core::Error> { + if matches!(verb, DeclareVerb::Readonly) { + var.set_readonly(); + } else if let Some(value) = self.make_readonly.to_bool() { + if value { + var.set_readonly(); + } else { + var.unset_readonly()?; + } + } + + Ok(()) + } +} diff --git a/crates/brush-builtins-vendored/src/dirs.rs b/crates/brush-builtins-vendored/src/dirs.rs new file mode 100644 index 000000000..7e18ffc37 --- /dev/null +++ b/crates/brush-builtins-vendored/src/dirs.rs @@ -0,0 +1,101 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins}; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum DirError { + /// Directory stack is empty. + #[error("directory stack is empty")] + DirStackEmpty, + + /// A shell error occurred. + #[error(transparent)] + ShellError(#[from] brush_core::Error), +} + +impl From<&DirError> for brush_core::ExecutionExitCode { + fn from(value: &DirError) -> Self { + match value { + DirError::DirStackEmpty => Self::GeneralError, + DirError::ShellError(e) => e.into(), + } + } +} + +impl brush_core::BuiltinError for DirError {} + +/// Manage the current directory stack. +#[derive(Default, Parser)] +pub(crate) struct DirsCommand { + /// Clear the directory stack. + #[arg(short = 'c')] + clear: bool, + + /// Don't tilde-shorten paths. + #[arg(short = 'l')] + tilde_long: bool, + + /// Print one directory per line instead of all on one line. + #[arg(short = 'p')] + print_one_per_line: bool, + + /// Print one directory per line with its index. + #[arg(short = 'v')] + print_one_per_line_with_index: bool, + // + // TODO: implement +N and -N +} + +impl builtins::Command for DirsCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.clear { + context.shell.directory_stack.clear(); + } else { + let dirs = vec![context.shell.working_dir()] + .into_iter() + .chain( + context + .shell + .directory_stack + .iter() + .rev() + .map(|p| p.as_path()), + ) + .collect::>(); + + let one_per_line = self.print_one_per_line || self.print_one_per_line_with_index; + + for (i, dir) in dirs.iter().enumerate() { + if !one_per_line && i > 0 { + write!(context.stdout(), " ")?; + } + + if self.print_one_per_line_with_index { + write!(context.stdout(), "{i:2} ")?; + } + + let mut dir_str = dir.to_string_lossy().to_string(); + + if !self.tilde_long { + dir_str = context.shell.tilde_shorten(dir_str); + } + + write!(context.stdout(), "{dir_str}")?; + + if one_per_line || i == dirs.len() - 1 { + writeln!(context.stdout())?; + } + } + + return Ok(ExecutionResult::success()); + } + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/dot.rs b/crates/brush-builtins-vendored/src/dot.rs new file mode 100644 index 000000000..251f68c40 --- /dev/null +++ b/crates/brush-builtins-vendored/src/dot.rs @@ -0,0 +1,34 @@ +use std::path::Path; + +use brush_core::builtins; +use clap::Parser; + +/// Evaluate the provided script in the current shell environment. +#[derive(Parser)] +pub(crate) struct DotCommand { + /// Path to the script to evaluate. + script_path: String, + + /// Any arguments to be passed as positional parameters to the script. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + script_args: Vec, +} + +impl builtins::Command for DotCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + // TODO: Handle trap inheritance. + context + .shell + .source_script( + Path::new(&self.script_path), + self.script_args.iter(), + &context.params, + ) + .await + } +} diff --git a/crates/brush-builtins-vendored/src/echo.rs b/crates/brush-builtins-vendored/src/echo.rs new file mode 100644 index 000000000..41a097e2e --- /dev/null +++ b/crates/brush-builtins-vendored/src/echo.rs @@ -0,0 +1,81 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins, escape}; + +/// Echo text to standard output. +#[derive(Parser)] +#[clap(disable_help_flag = true, disable_version_flag = true)] +pub(crate) struct EchoCommand { + /// Suppress the trailing newline from the output. + #[arg(short = 'n')] + no_trailing_newline: bool, + + /// Interpret backslash escapes in the provided text. + #[arg(short = 'e')] + interpret_backslash_escapes: bool, + + /// Do not interpret backslash escapes in the provided text. + #[arg(short = 'E')] + no_interpret_backslash_escapes: bool, + + /// Tokens to echo to standard output. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, +} + +impl builtins::Command for EchoCommand { + type Error = brush_core::Error; + + /// Override the default [`builtins::Command::new`] function to handle clap's limitation related + /// to `--`. See [`builtins::parse_known`] for more information + /// TODO: we can safely remove this after the issue is resolved + fn new(args: I) -> Result + where + I: IntoIterator, + { + let (mut this, rest_args) = brush_core::builtins::try_parse_known::(args)?; + if let Some(args) = rest_args { + this.args.extend(args); + } + Ok(this) + } + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut trailing_newline = !self.no_trailing_newline; + let mut s; + if self.interpret_backslash_escapes { + s = String::new(); + for (i, arg) in self.args.iter().enumerate() { + if i > 0 { + s.push(' '); + } + + let (expanded_arg, keep_going) = escape::expand_backslash_escapes( + arg.as_str(), + escape::EscapeExpansionMode::EchoBuiltin, + )?; + s.push_str(&String::from_utf8_lossy(expanded_arg.as_slice())); + + if !keep_going { + trailing_newline = false; + break; + } + } + } else { + s = self.args.join(" "); + } + + if trailing_newline { + s.push('\n'); + } + + write!(context.stdout(), "{s}")?; + context.stdout().flush()?; + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/enable.rs b/crates/brush-builtins-vendored/src/enable.rs new file mode 100644 index 000000000..9687e3d1c --- /dev/null +++ b/crates/brush-builtins-vendored/src/enable.rs @@ -0,0 +1,96 @@ +use brush_core::ExecutionResult; +use clap::Parser; +use itertools::Itertools; +use std::io::Write; + +use brush_core::builtins; +use brush_core::error; + +/// Enable, disable, or display built-in commands. +#[derive(Parser)] +pub(crate) struct EnableCommand { + /// Print a list of built-in commands. + #[arg(short = 'a')] + print_list: bool, + + /// Disables the specified built-in commands. + #[arg(short = 'n')] + disable: bool, + + /// Print a list of built-in commands with reusable output. + #[arg(short = 'p')] + print_reusably: bool, + + /// Only operate on special built-in commands. + #[arg(short = 's')] + special_only: bool, + + /// Path to a shared object from which built-in commands will be loaded. + #[arg(short = 'f', value_name = "PATH")] + shared_object_path: Option, + + /// Remove the built-in commands loaded from the indicated object path. + #[arg(short = 'd')] + remove_loaded_builtin: bool, + + /// Names of built-in commands to operate on. + names: Vec, +} + +impl builtins::Command for EnableCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut result = ExecutionResult::success(); + + if self.shared_object_path.is_some() { + return error::unimp("enable -f"); + } + if self.remove_loaded_builtin { + return error::unimp("enable -d"); + } + + if !self.names.is_empty() { + for name in &self.names { + if let Some(builtin) = context.shell.builtin_mut(name) { + builtin.disabled = self.disable; + } else { + writeln!(context.stderr(), "{name}: not a shell builtin")?; + result = ExecutionResult::general_error(); + } + } + } else { + let builtins: Vec<_> = context + .shell + .builtins() + .iter() + .sorted_by_key(|(name, _reg)| *name) + .collect(); + + for (builtin_name, builtin) in builtins { + if self.disable { + if !builtin.disabled { + continue; + } + } else if self.print_list { + if builtin.disabled { + continue; + } + } + + if self.special_only && !builtin.special_builtin { + continue; + } + + let prefix = if builtin.disabled { "-n " } else { "" }; + + writeln!(context.stdout(), "enable {prefix}{builtin_name}")?; + } + } + + Ok(result) + } +} diff --git a/crates/brush-builtins-vendored/src/eval.rs b/crates/brush-builtins-vendored/src/eval.rs new file mode 100644 index 000000000..d15be4932 --- /dev/null +++ b/crates/brush-builtins-vendored/src/eval.rs @@ -0,0 +1,36 @@ +use brush_core::{ExecutionResult, builtins}; +use clap::Parser; + +/// Evaluate the given string as script. +#[derive(Parser)] +pub(crate) struct EvalCommand { + /// The script to evaluate. + #[clap(allow_hyphen_values = true)] + args: Vec, +} + +impl builtins::Command for EvalCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if !self.args.is_empty() { + let args_concatenated = self.args.join(" "); + + tracing::debug!("Applying eval to: {:?}", args_concatenated); + + // Return the direct result of running the string; we intentionally + // pass through the result and honor its requested control flow. eval + // executes in the current environment, so all control flow (return, + // exit, break, continue) should propagate. + context + .shell + .run_string(args_concatenated, &context.params) + .await + } else { + Ok(ExecutionResult::success()) + } + } +} diff --git a/crates/brush-builtins-vendored/src/exec.rs b/crates/brush-builtins-vendored/src/exec.rs new file mode 100644 index 000000000..058d4251f --- /dev/null +++ b/crates/brush-builtins-vendored/src/exec.rs @@ -0,0 +1,66 @@ +use clap::Parser; +use std::{borrow::Cow, os::unix::process::CommandExt}; + +use brush_core::{ErrorKind, ExecutionExitCode, ExecutionResult, builtins, commands}; + +/// Exec the provided command. +#[derive(Parser)] +pub(crate) struct ExecCommand { + /// Pass given name as zeroth argument to command. + #[arg(short = 'a', value_name = "NAME")] + name_for_argv0: Option, + + /// Exec command with an empty environment. + #[arg(short = 'c')] + empty_environment: bool, + + /// Exec command as a login shell. + #[arg(short = 'l')] + exec_as_login: bool, + + /// Command and args. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, +} + +impl builtins::Command for ExecCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.args.is_empty() { + // When no arguments are present, then there's nothing for us to execute -- but we need + // to ensure that any redirections setup for this builtin get applied to the calling + // shell instance. + #[allow(clippy::needless_collect)] + let fds: Vec<_> = context.iter_fds().collect(); + + context.shell.replace_open_files(fds.into_iter()); + return Ok(ExecutionResult::success()); + } + + let mut argv0 = Cow::Borrowed(self.name_for_argv0.as_ref().unwrap_or(&self.args[0])); + + if self.exec_as_login { + argv0 = Cow::Owned(std::format!("-{argv0}")); + } + + let mut cmd = commands::compose_std_command( + &context, + &self.args[0], + argv0.as_str(), + &self.args[1..], + self.empty_environment, + )?; + + let exec_error = cmd.exec(); + + if exec_error.kind() == std::io::ErrorKind::NotFound { + Ok(ExecutionExitCode::NotFound.into()) + } else { + Err(ErrorKind::from(exec_error).into()) + } + } +} diff --git a/crates/brush-builtins-vendored/src/exit.rs b/crates/brush-builtins-vendored/src/exit.rs new file mode 100644 index 000000000..1cb736d98 --- /dev/null +++ b/crates/brush-builtins-vendored/src/exit.rs @@ -0,0 +1,31 @@ +use clap::Parser; + +use brush_core::{ExecutionControlFlow, ExecutionResult, builtins}; + +/// Exit the shell. +#[derive(Parser)] +pub(crate) struct ExitCommand { + /// The exit code to return. + code: Option, +} + +impl builtins::Command for ExitCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + #[expect(clippy::cast_sign_loss)] + let code_8bit = if let Some(code_32bit) = &self.code { + (code_32bit & 0xFF) as u8 + } else { + context.shell.last_result() + }; + + let mut result = ExecutionResult::new(code_8bit); + result.next_control_flow = ExecutionControlFlow::ExitShell; + + Ok(result) + } +} diff --git a/crates/brush-builtins-vendored/src/export.rs b/crates/brush-builtins-vendored/src/export.rs new file mode 100644 index 000000000..dc9385592 --- /dev/null +++ b/crates/brush-builtins-vendored/src/export.rs @@ -0,0 +1,157 @@ +use clap::Parser; +use itertools::Itertools; +use std::io::Write; + +use brush_core::{ + ExecutionExitCode, ExecutionResult, builtins, + env::{EnvironmentLookup, EnvironmentScope}, + variables, +}; + +/// Add or update exported shell variables. +#[derive(Parser)] +pub(crate) struct ExportCommand { + /// Names are treated as function names. + #[arg(short = 'f')] + names_are_functions: bool, + + /// Un-export the names. + #[arg(short = 'n')] + unexport: bool, + + /// Display all exported names. + #[arg(short = 'p')] + display_exported_names: bool, + + // + // Declarations + // + // N.B. These are skipped by clap, but filled in by the BuiltinDeclarationCommand trait. + #[clap(skip)] + declarations: Vec, +} + +impl builtins::DeclarationCommand for ExportCommand { + fn set_declarations(&mut self, declarations: Vec) { + self.declarations = declarations; + } +} + +impl builtins::Command for ExportCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + mut context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.declarations.is_empty() { + display_all_exported_vars(&context)?; + return Ok(ExecutionResult::success()); + } + + let mut result = ExecutionResult::success(); + for decl in &self.declarations { + let current_result = self.process_decl(&mut context, decl)?; + if !current_result.is_success() { + result = current_result; + } + } + + Ok(result) + } +} + +impl ExportCommand { + fn process_decl( + &self, + context: &mut brush_core::ExecutionContext<'_>, + decl: &brush_core::CommandArg, + ) -> Result { + match decl { + brush_core::CommandArg::String(s) => { + // See if this is supposed to be a function name. + if self.names_are_functions { + // Try to find the function already present; if we find it, then mark it + // exported. + if let Some(func) = context.shell.func_mut(s) { + if self.unexport { + func.unexport(); + } else { + func.export(); + } + } else { + writeln!(context.stderr(), "{s}: not a function")?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } + // Try to find the variable already present; if we find it, then mark it + // exported. + else if let Some((_, variable)) = context.shell.env.get_mut(s) { + if self.unexport { + variable.unexport(); + } else { + variable.export(); + } + } + } + brush_core::CommandArg::Assignment(assignment) => { + let name = match &assignment.name { + brush_parser::ast::AssignmentName::VariableName(name) => name, + brush_parser::ast::AssignmentName::ArrayElementName(_, _) => { + writeln!(context.stderr(), "not a valid variable name")?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + }; + + let value = match &assignment.value { + brush_parser::ast::AssignmentValue::Scalar(s) => { + variables::ShellValueLiteral::Scalar(s.flatten()) + } + brush_parser::ast::AssignmentValue::Array(a) => { + variables::ShellValueLiteral::Array(variables::ArrayLiteral( + a.iter() + .map(|(k, v)| (k.as_ref().map(|k| k.flatten()), v.flatten())) + .collect(), + )) + } + }; + + // Update the variable with the provided value and then mark it exported. + context.shell.env.update_or_add( + name, + value, + |var| { + if self.unexport { + var.unexport(); + } else { + var.export(); + } + Ok(()) + }, + EnvironmentLookup::Anywhere, + EnvironmentScope::Global, + )?; + } + } + + Ok(ExecutionResult::success()) + } +} + +fn display_all_exported_vars( + context: &brush_core::ExecutionContext<'_>, +) -> Result<(), brush_core::Error> { + // Enumerate variables, sorted by key. + for (name, variable) in context.shell.env.iter().sorted_by_key(|v| v.0) { + if variable.is_exported() { + let value = variable.value().try_get_cow_str(context.shell); + if let Some(value) = value { + writeln!(context.stdout(), "declare -x {name}=\"{value}\"")?; + } else { + writeln!(context.stdout(), "declare -x {name}")?; + } + } + } + + Ok(()) +} diff --git a/crates/brush-builtins-vendored/src/factory.rs b/crates/brush-builtins-vendored/src/factory.rs new file mode 100644 index 000000000..10de99429 --- /dev/null +++ b/crates/brush-builtins-vendored/src/factory.rs @@ -0,0 +1,203 @@ +use std::collections::HashMap; + +#[allow(clippy::wildcard_imports)] +use super::*; + +#[allow(unused_imports, reason = "not all builtins are used in all configs")] +use brush_core::builtins::{self, builtin, decl_builtin, raw_arg_builtin, simple_builtin}; + +/// Identifies well-known sets of builtins. +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum BuiltinSet { + /// Identifies builtins appropriate for POSIX `sh` compatibility. + ShMode, + /// Identifies builtins appropriate for a more full-featured `bash`-compatible shell. + BashMode, +} + +/// Returns the default set of built-in commands. +/// +/// # Arguments +/// +/// * `set` - The set of built-ins to return. +#[allow(clippy::too_many_lines)] +pub fn default_builtins(set: BuiltinSet) -> HashMap { + let mut m = HashMap::::new(); + + // + // POSIX special builtins + // + // N.B. There seems to be some inconsistency as to whether 'times' + // should be a special built-in. + // + + #[cfg(feature = "builtin.break")] + m.insert("break".into(), builtin::().special()); + #[cfg(feature = "builtin.colon")] + m.insert( + ":".into(), + simple_builtin::().special(), + ); + #[cfg(feature = "builtin.continue")] + m.insert( + "continue".into(), + builtin::().special(), + ); + #[cfg(feature = "builtin.dot")] + m.insert(".".into(), builtin::().special()); + #[cfg(feature = "builtin.eval")] + m.insert("eval".into(), builtin::().special()); + #[cfg(all(feature = "builtin.exec", unix))] + m.insert("exec".into(), builtin::().special()); + #[cfg(feature = "builtin.exit")] + m.insert("exit".into(), builtin::().special()); + #[cfg(feature = "builtin.export")] + m.insert( + "export".into(), + decl_builtin::().special(), + ); + #[cfg(feature = "builtin.return")] + m.insert( + "return".into(), + builtin::().special(), + ); + #[cfg(feature = "builtin.set")] + m.insert("set".into(), builtin::().special()); + #[cfg(feature = "builtin.shift")] + m.insert("shift".into(), builtin::().special()); + #[cfg(feature = "builtin.trap")] + m.insert("trap".into(), builtin::().special()); + #[cfg(feature = "builtin.unset")] + m.insert("unset".into(), builtin::().special()); + + #[cfg(feature = "builtin.declare")] + m.insert( + "readonly".into(), + decl_builtin::().special(), + ); + #[cfg(feature = "builtin.times")] + m.insert("times".into(), builtin::().special()); + + // + // Non-special builtins + // + + #[cfg(feature = "builtin.alias")] + m.insert("alias".into(), builtin::()); // TODO: should be exec_declaration_builtin + #[cfg(feature = "builtin.bg")] + m.insert("bg".into(), builtin::()); + #[cfg(feature = "builtin.cd")] + m.insert("cd".into(), builtin::()); + #[cfg(feature = "builtin.command")] + m.insert("command".into(), builtin::()); + #[cfg(feature = "builtin.false")] + m.insert("false".into(), builtin::()); + #[cfg(feature = "builtin.fg")] + m.insert("fg".into(), builtin::()); + #[cfg(feature = "builtin.getopts")] + m.insert("getopts".into(), builtin::()); + #[cfg(feature = "builtin.hash")] + m.insert("hash".into(), builtin::()); + #[cfg(feature = "builtin.help")] + m.insert("help".into(), builtin::()); + #[cfg(feature = "builtin.jobs")] + m.insert("jobs".into(), builtin::()); + #[cfg(all(feature = "builtin.kill", unix))] + m.insert("kill".into(), builtin::()); + #[cfg(feature = "builtin.declare")] + m.insert("local".into(), decl_builtin::()); + #[cfg(feature = "builtin.pwd")] + m.insert("pwd".into(), builtin::()); + #[cfg(feature = "builtin.read")] + m.insert("read".into(), builtin::()); + #[cfg(feature = "builtin.true")] + m.insert("true".into(), builtin::()); + #[cfg(feature = "builtin.type")] + m.insert("type".into(), builtin::()); + #[cfg(all(feature = "builtin.ulimit", unix))] + m.insert("ulimit".into(), builtin::()); + #[cfg(all(feature = "builtin.umask", unix))] + m.insert("umask".into(), builtin::()); + #[cfg(feature = "builtin.unalias")] + m.insert("unalias".into(), builtin::()); + #[cfg(feature = "builtin.wait")] + m.insert("wait".into(), builtin::()); + + #[cfg(feature = "builtin.fc")] + m.insert("fc".into(), builtin::()); + + if matches!(set, BuiltinSet::BashMode) { + #[cfg(feature = "builtin.builtin")] + m.insert( + "builtin".into(), + raw_arg_builtin::(), + ); + #[cfg(feature = "builtin.declare")] + m.insert("declare".into(), decl_builtin::()); + #[cfg(feature = "builtin.echo")] + m.insert("echo".into(), builtin::()); + #[cfg(feature = "builtin.enable")] + m.insert("enable".into(), builtin::()); + #[cfg(feature = "builtin.let")] + m.insert("let".into(), builtin::()); + #[cfg(feature = "builtin.mapfile")] + m.insert("mapfile".into(), builtin::()); + #[cfg(feature = "builtin.mapfile")] + m.insert("readarray".into(), builtin::()); + #[cfg(all(feature = "builtin.printf", any(unix, windows)))] + m.insert("printf".into(), builtin::()); + #[cfg(feature = "builtin.shopt")] + m.insert("shopt".into(), builtin::()); + #[cfg(feature = "builtin.dot")] + m.insert("source".into(), builtin::().special()); + #[cfg(all(feature = "builtin.suspend", unix))] + m.insert("suspend".into(), builtin::()); + #[cfg(feature = "builtin.test")] + m.insert("test".into(), builtin::()); + #[cfg(feature = "builtin.test")] + m.insert("[".into(), builtin::()); + #[cfg(feature = "builtin.declare")] + m.insert("typeset".into(), decl_builtin::()); + + // Completion builtins + #[cfg(feature = "builtin.complete")] + m.insert("complete".into(), builtin::()); + #[cfg(feature = "builtin.compgen")] + m.insert("compgen".into(), builtin::()); + #[cfg(feature = "builtin.compopt")] + m.insert("compopt".into(), builtin::()); + + // Dir stack builtins + #[cfg(feature = "builtin.dirs")] + m.insert("dirs".into(), builtin::()); + #[cfg(feature = "builtin.popd")] + m.insert("popd".into(), builtin::()); + #[cfg(feature = "builtin.pushd")] + m.insert("pushd".into(), builtin::()); + + // Input configuration builtins + #[cfg(feature = "builtin.bind")] + m.insert("bind".into(), builtin::()); + + // History + #[cfg(feature = "builtin.history")] + m.insert("history".into(), builtin::()); + + // TODO: implement caller builtin + m.insert("caller".into(), builtin::()); + + // TODO: implement disown builtin + m.insert("disown".into(), builtin::()); + + // TODO: implement logout builtin + m.insert("logout".into(), builtin::()); + } + + // + // Brush-specific builtins. + // + #[cfg(feature = "builtin.brushinfo")] + m.insert("brushinfo".into(), builtin::()); + + m +} diff --git a/crates/brush-builtins-vendored/src/false_.rs b/crates/brush-builtins-vendored/src/false_.rs new file mode 100644 index 000000000..628e8ec2b --- /dev/null +++ b/crates/brush-builtins-vendored/src/false_.rs @@ -0,0 +1,18 @@ +use clap::Parser; + +use brush_core::{ExecutionResult, builtins}; + +/// Return a non-zero exit code. +#[derive(Parser)] +pub(crate) struct FalseCommand {} + +impl builtins::Command for FalseCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + _context: brush_core::ExecutionContext<'_>, + ) -> Result { + Ok(ExecutionResult::general_error()) + } +} diff --git a/crates/brush-builtins-vendored/src/fc.rs b/crates/brush-builtins-vendored/src/fc.rs new file mode 100644 index 000000000..8c0a38aa1 --- /dev/null +++ b/crates/brush-builtins-vendored/src/fc.rs @@ -0,0 +1,299 @@ +use brush_core::{ExecutionResult, builtins, error, history}; +use clap::Parser; +use std::io::Write; + +/// Process command history list. +#[derive(Parser)] +pub(crate) struct FcCommand { + /// List commands instead of editing them. + #[arg(short = 'l')] + list: bool, + + /// Suppress line numbers when listing. + #[arg(short = 'n', requires = "list")] + no_line_numbers: bool, + + /// Reverse the order of commands. + #[arg(short = 'r')] + reverse: bool, + + /// Re-execute command after substitution (old=new format). + #[arg(short = 's')] + substitute: bool, + + /// Editor to use (only relevant when not listing or substituting). + #[arg(short = 'e', value_name = "ENAME")] + editor: Option, + + /// First command in range (number or string prefix). + #[arg(value_name = "FIRST", allow_hyphen_values = true)] + first: Option, + + /// Last command in range (number or string prefix). + #[arg(value_name = "LAST", allow_hyphen_values = true)] + last: Option, +} + +impl builtins::Command for FcCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.substitute { + return self.do_execute(context).await; + } + + if self.list { + return self.do_list(&context); + } + + error::unimp("fc editor mode is not yet implemented") + } +} + +impl FcCommand { + fn do_list( + &self, + context: &brush_core::ExecutionContext<'_>, + ) -> Result { + let history = context + .shell + .history() + .ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?; + + let (first_idx, last_idx, reverse) = self.resolve_range(history)?; + + // Determine the order of iteration + let indices: Vec = if reverse { + (first_idx..=last_idx).rev().collect() + } else { + (first_idx..=last_idx).collect() + }; + + for idx in indices { + if let Some(item) = history.get(idx) { + if self.no_line_numbers { + // With -n, bash still outputs a tab before the command + writeln!(context.stdout(), "\t {}", item.command_line)?; + } else { + // Match bash's fc format: number, tab, command + writeln!(context.stdout(), "{}\t {}", idx + 1, item.command_line)?; + } + } + } + + Ok(ExecutionResult::success()) + } + + async fn do_execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let history = context + .shell + .history() + .ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?; + + // Parse the first argument for pattern=replacement + let (pattern, replacement) = self + .first + .as_ref() + .and_then(|s| s.split_once('=')) + .map_or((None, None), |(p, r)| (Some(p), Some(r))); + + // Determine which command to re-execute + let cmd_spec = if pattern.is_some() { + // If we have a pattern, the command spec is in 'last' if present + self.last.as_deref() + } else { + // Otherwise, it's in 'first' + self.first.as_deref() + }; + + // Find the command + let cmd_line = if let Some(spec) = cmd_spec { + Self::find_command_by_specifier(history, spec)? + } else { + // No spec means use the previous command (excluding the fc command itself) + let effective_count = effective_history_count(history); + history + .get(effective_count.saturating_sub(1)) + .map(|item| item.command_line.clone()) + .ok_or_else(|| brush_core::Error::from(error::ErrorKind::HistoryItemNotFound))? + }; + + // Apply substitution if present + let final_cmd = if let (Some(pat), Some(rep)) = (pattern, replacement) { + cmd_line.replace(pat, rep) + } else { + cmd_line + }; + + // Echo the command to stderr. + writeln!(context.stderr(), "{final_cmd}")?; + + // Remove the fc command from history before executing the substituted command + // This matches bash behavior where the fc command is replaced by the executed command + let history_mut = context + .shell + .history_mut() + .ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?; + history_mut.remove_nth_item(history_mut.count().saturating_sub(1)); + + // Execute the command + let result = context + .shell + .run_string(final_cmd.clone(), &context.params) + .await?; + + // Add the executed command to history. + context.shell.add_to_history(&final_cmd)?; + + Ok(result) + } + + fn resolve_range( + &self, + history: &history::History, + ) -> Result<(usize, usize, bool), brush_core::Error> { + let effective_count = effective_history_count(history); + let max_idx = effective_count.saturating_sub(1); + + // Resolve first index + let first_idx = self + .first + .as_ref() + .map(|s| Self::resolve_position(history, s)) + .transpose()? + .unwrap_or_else(|| { + if self.list { + effective_count.saturating_sub(16) // Default for listing: -16 + } else { + max_idx // Default for editing: previous command + } + }); + + // Resolve last index (default depends on mode and first_idx) + let default_last = if self.list { max_idx } else { first_idx }; + let last_idx = self + .last + .as_ref() + .map(|s| Self::resolve_position(history, s)) + .transpose()? + .unwrap_or(default_last); + + // If first > last, swap them and indicate reversal + let (first_idx, last_idx, force_reverse) = if first_idx > last_idx { + (last_idx, first_idx, true) + } else { + (first_idx, last_idx, false) + }; + + // Clamp both indices to valid range + Ok(( + first_idx.min(max_idx), + last_idx.min(max_idx), + force_reverse || self.reverse, + )) + } + + /// Resolves a position specifier (number or string prefix) to a history index. + /// NOTE: The returned index may still be out of range if the history is empty. + /// + /// # Arguments + /// + /// * `history` - The history to resolve against. + /// * `spec` - The position specifier (number or string prefix). + fn resolve_position( + history: &history::History, + spec: &str, + ) -> Result { + // Try to parse it as a number. If it's not parseable, then we need to assume + // it's a string prefix we need to search for. + let Ok(num) = spec.parse::() else { + // Not a number, treat as string prefix + return Self::find_command_by_prefix(history, spec); + }; + + let effective_count = effective_history_count(history); + + #[expect(clippy::cast_sign_loss)] + #[expect(clippy::cast_possible_truncation)] + let result = match num.cmp(&0) { + std::cmp::Ordering::Equal => { + // 0 means -1 for listing (relative to effective count) + effective_count.saturating_sub(1) + } + std::cmp::Ordering::Greater => { + // Positive: 1-based index + let idx = (num - 1) as usize; + if idx < effective_count { + idx + } else { + // Out of range - use 0 (first item) + 0 + } + } + std::cmp::Ordering::Less => { + // Negative: offset from end (relative to effective count) + let offset = (-num) as usize; + effective_count.saturating_sub(offset) + } + }; + + Ok(result) + } + + /// Finds the command matching the given specifier (number or string prefix). Returns + /// the command line. Returns an error if no such command can be found in the history. + /// + /// # Arguments + /// + /// * `history` - The history to search. + /// * `spec` - The position spec + fn find_command_by_specifier( + history: &history::History, + spec: &str, + ) -> Result { + let idx = Self::resolve_position(history, spec)?; + history + .get(idx) + .map(|item| item.command_line.clone()) + .ok_or_else(|| brush_core::Error::from(error::ErrorKind::HistoryItemNotFound)) + } + + /// Finds the most recent command starting with the given prefix. Returns + /// the index of the command in the history. Returns an error if no such + /// command can be found in the history. + /// + /// # Arguments + /// + /// * `history` - The history to search. + /// * `prefix` - The command prefix to search for. + fn find_command_by_prefix( + history: &history::History, + prefix: &str, + ) -> Result { + // Search backwards for a command starting with the prefix (excluding fc command itself) + let effective_count = effective_history_count(history); + + for idx in (0..effective_count).rev() { + if let Some(item) = history.get(idx) { + if item.command_line.starts_with(prefix) { + return Ok(idx); + } + } + } + + Err(brush_core::Error::from( + error::ErrorKind::HistoryItemNotFound, + )) + } +} + +/// Returns the effective history count (excluding the fc command itself). +fn effective_history_count(history: &history::History) -> usize { + history.count().saturating_sub(1) +} diff --git a/crates/brush-builtins-vendored/src/fg.rs b/crates/brush-builtins-vendored/src/fg.rs new file mode 100644 index 000000000..898245326 --- /dev/null +++ b/crates/brush-builtins-vendored/src/fg.rs @@ -0,0 +1,70 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins, jobs, sys}; + +/// Move a specified job to the foreground. +#[derive(Parser)] +pub(crate) struct FgCommand { + /// Job spec for the job to move to the foreground; if not specified, the current job is moved. + job_spec: Option, +} + +impl builtins::Command for FgCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut stderr = context.stdout(); + + if let Some(job_spec) = &self.job_spec { + if let Some(job) = context.shell.jobs.resolve_job_spec(job_spec) { + job.move_to_foreground()?; + writeln!(stderr, "{}", job.command_line)?; + + let result = job.wait().await?; + if context.shell.options.interactive { + sys::terminal::move_self_to_foreground()?; + } + + if matches!(job.state, jobs::JobState::Stopped) { + // N.B. We use the '\r' to overwrite any ^Z output. + let formatted = job.to_string(); + writeln!(context.stderr(), "\r{formatted}")?; + } + + Ok(result) + } else { + writeln!( + stderr, + "{}: {}: no such job", + job_spec, context.command_name + )?; + Ok(ExecutionResult::general_error()) + } + } else { + if let Some(job) = context.shell.jobs.current_job_mut() { + job.move_to_foreground()?; + writeln!(stderr, "{}", job.command_line)?; + + let result = job.wait().await?; + if context.shell.options.interactive { + sys::terminal::move_self_to_foreground()?; + } + + if matches!(job.state, jobs::JobState::Stopped) { + // N.B. We use the '\r' to overwrite any ^Z output. + let formatted = job.to_string(); + writeln!(context.stderr(), "\r{formatted}")?; + } + + Ok(result) + } else { + writeln!(stderr, "{}: no current job", context.command_name)?; + Ok(ExecutionResult::general_error()) + } + } + } +} diff --git a/crates/brush-builtins-vendored/src/getopts.rs b/crates/brush-builtins-vendored/src/getopts.rs new file mode 100644 index 000000000..147c1dd06 --- /dev/null +++ b/crates/brush-builtins-vendored/src/getopts.rs @@ -0,0 +1,224 @@ +use std::{borrow::Cow, collections::HashMap, io::Write}; + +use clap::Parser; + +use brush_core::{ExecutionExitCode, ExecutionResult, builtins, variables}; + +/// Parse command options. +#[derive(Parser)] +pub(crate) struct GetOptsCommand { + /// Specification for options + options_string: String, + + /// Name of variable to receive next option + variable_name: String, + + /// Arguments to parse + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, +} + +const VAR_GETOPTS_NEXT_CHAR_INDEX: &str = "__GETOPTS_NEXT_CHAR"; + +impl builtins::Command for GetOptsCommand { + type Error = brush_core::Error; + + /// Override the default [`builtins::Command::new`] function to handle clap's limitation related + /// to `--`. See [`builtins::parse_known`] for more information + /// TODO: we can safely remove this after the issue is resolved + fn new(args: I) -> Result + where + I: IntoIterator, + { + let (mut this, rest_args) = brush_core::builtins::try_parse_known::(args)?; + if let Some(args) = rest_args { + this.args.extend(args); + } + Ok(this) + } + + #[expect(clippy::too_many_lines)] + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut args = HashMap::::new(); + let mut treat_unknown_options_as_failure = true; + + // Build the args map + let mut last_char = None; + for c in self.options_string.chars() { + if c == ':' { + if let Some(last_char) = last_char { + args.insert(last_char, true); + continue; + } else if args.is_empty() { + // This is the first character of the options string. + // Its presence indicates a request for unknown + // options *not* to be treated as failures. + treat_unknown_options_as_failure = false; + } else { + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } + + args.insert(c, false); + last_char = Some(c); + } + + // If unset, assume OPTIND is 1. + let mut next_index: usize = context + .shell + .env_str("OPTIND") + .unwrap_or(Cow::Borrowed("1")) + .parse()?; + + if next_index < 1 { + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + let mut new_optarg = None; + let new_optind; + let mut variable_value; + let exit_code; + + // See if there are any args left to parse. + let mut next_index_zero_based = next_index - 1; + if next_index_zero_based < self.args.len() { + let arg = self.args[next_index_zero_based].as_str(); + + // See if this is an option. + if arg.starts_with('-') && arg != "--" { + // Figure out how far into this option we are. + const DEFAULT_NEXT_CHAR_INDEX: usize = 1; + let next_char_index = context + .shell + .env_str(VAR_GETOPTS_NEXT_CHAR_INDEX) + .map_or(DEFAULT_NEXT_CHAR_INDEX, |s| { + s.parse().unwrap_or(DEFAULT_NEXT_CHAR_INDEX) + }); + + // Find the char. + let c = arg.chars().nth(next_char_index).unwrap(); + let is_last_char_in_option = next_char_index == arg.len() - 1; + + // Look up the char. + let mut is_error = false; + if let Some(takes_arg) = args.get(&c) { + variable_value = String::from(c); + + if *takes_arg { + // If the option takes a value but it's not the last option in this + // argument, then this is an error. + if is_last_char_in_option { + next_index += 1; + next_index_zero_based += 1; + + if next_index_zero_based >= self.args.len() { + return Ok(ExecutionResult::general_error()); + } + + new_optarg = Some(self.args[next_index_zero_based].clone()); + } else { + is_error = true; + } + } else { + new_optarg = None; + } + } else { + // Set to satisfy compiler. + variable_value = String::from("?"); + + is_error = true; + } + + if is_error { + // Unknown option; set variable to '?' and OPTARG to the unknown option (sans + // hyphen). + variable_value = String::from("?"); + if !treat_unknown_options_as_failure { + new_optarg = Some(String::from(c)); + } else { + new_optarg = None; + } + + // TODO: honor OPTERR=0 indicating suppression of error messages + if treat_unknown_options_as_failure { + writeln!(context.stderr(), "getopts: illegal option -- {c}")?; + } + } + + if is_last_char_in_option { + // We're done with this argument, so unset the internal char index variable + // and request an update to OPTIND. + new_optind = next_index + 1; + context.shell.env.unset(VAR_GETOPTS_NEXT_CHAR_INDEX)?; + } else { + // We have more to go in this argument, so update the internal char index + // and request that OPTIND not be updated. + new_optind = next_index; + context.shell.env.update_or_add( + VAR_GETOPTS_NEXT_CHAR_INDEX, + variables::ShellValueLiteral::Scalar((next_char_index + 1).to_string()), + |_| Ok(()), + brush_core::env::EnvironmentLookup::Anywhere, + brush_core::env::EnvironmentScope::Global, + )?; + } + + exit_code = ExecutionResult::success(); + } else { + variable_value = String::from("?"); + new_optarg = None; + + // If it was "--" we encountered, then skip past it. + if arg == "--" { + new_optind = next_index + 1; + } else { + new_optind = next_index; + } + + // Note that we're done parsing options. + exit_code = ExecutionResult::general_error(); + } + } else { + variable_value = String::from("?"); + new_optarg = None; + new_optind = next_index; + exit_code = ExecutionResult::general_error(); + } + + // Update variable value. + context.shell.env.update_or_add( + self.variable_name.as_str(), + variables::ShellValueLiteral::Scalar(variable_value), + |_| Ok(()), + brush_core::env::EnvironmentLookup::Anywhere, + brush_core::env::EnvironmentScope::Global, + )?; + + // Update OPTARG + if let Some(new_optarg) = new_optarg { + context.shell.env.update_or_add( + "OPTARG", + variables::ShellValueLiteral::Scalar(new_optarg), + |_| Ok(()), + brush_core::env::EnvironmentLookup::Anywhere, + brush_core::env::EnvironmentScope::Global, + )?; + } else { + let _ = context.shell.env.unset("OPTARG")?; + } + + // Update OPTIND + context.shell.env.update_or_add( + "OPTIND", + variables::ShellValueLiteral::Scalar(new_optind.to_string()), + |_| Ok(()), + brush_core::env::EnvironmentLookup::Anywhere, + brush_core::env::EnvironmentScope::Global, + )?; + + Ok(exit_code) + } +} diff --git a/crates/brush-builtins-vendored/src/hash.rs b/crates/brush-builtins-vendored/src/hash.rs new file mode 100644 index 000000000..fb7a82b74 --- /dev/null +++ b/crates/brush-builtins-vendored/src/hash.rs @@ -0,0 +1,101 @@ +use clap::Parser; +use std::{io::Write, path::PathBuf}; + +use brush_core::{ExecutionResult, builtins}; + +#[derive(Parser)] +pub(crate) struct HashCommand { + /// Remove entries associated with the given names. + #[arg(short = 'd')] + remove: bool, + + /// Display paths in a format usable for input. + #[arg(short = 'l')] + display_as_usable_input: bool, + + /// The path to associate with the names. + #[arg(short = 'p', value_name = "PATH")] + path_to_use: Option, + + /// Remove all entries. + #[arg(short = 'r')] + remove_all: bool, + + /// Display the paths associated with the names. + #[arg(short = 't')] + display_paths: bool, + + /// Names to process. + names: Vec, +} + +impl builtins::Command for HashCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut result = ExecutionResult::success(); + + if self.remove_all { + context.shell.program_location_cache.reset(); + } else if self.remove { + for name in &self.names { + if !context.shell.program_location_cache.unset(name) { + writeln!(context.stderr(), "{name}: not found")?; + result = ExecutionResult::general_error(); + } + } + } else if self.display_paths { + for name in &self.names { + if let Some(path) = context.shell.program_location_cache.get(name) { + if self.display_as_usable_input { + writeln!( + context.stdout(), + "builtin hash -p {} {name}", + path.to_string_lossy() + )?; + } else { + let mut prefix = String::new(); + + if self.names.len() > 1 { + prefix.push_str(name.as_str()); + prefix.push('\t'); + } + + writeln!( + context.stdout(), + "{prefix}{}", + path.to_string_lossy().as_ref() + )?; + } + } else { + writeln!(context.stderr(), "{name}: not found")?; + result = ExecutionResult::general_error(); + } + } + } else if let Some(path) = &self.path_to_use { + for name in &self.names { + context.shell.program_location_cache.set(name, path.clone()); + } + } else { + for name in &self.names { + // Remove from the cache if already hashed. + let _ = context.shell.program_location_cache.unset(name); + + // Hash the path. + if context + .shell + .find_first_executable_in_path_using_cache(name) + .is_none() + { + writeln!(context.stderr(), "{name}: not found")?; + result = ExecutionResult::general_error(); + } + } + } + + Ok(result) + } +} diff --git a/crates/brush-builtins-vendored/src/help.rs b/crates/brush-builtins-vendored/src/help.rs new file mode 100644 index 000000000..42771c950 --- /dev/null +++ b/crates/brush-builtins-vendored/src/help.rs @@ -0,0 +1,138 @@ +use brush_core::{ExecutionResult, builtins}; +use clap::Parser; +use itertools::Itertools; +use std::io::Write; + +/// Display command help. +#[derive(Parser)] +pub(crate) struct HelpCommand { + /// Display a short description for the commands. + #[arg(short = 'd')] + short_description: bool, + + /// Display a man-style page of documentation for the commands. + #[arg(short = 'm')] + man_page_style: bool, + + /// Display a short usage summary for the commands. + #[arg(short = 's')] + short_usage: bool, + + /// Patterns of topics to display help for. + topic_patterns: Vec, +} + +impl builtins::Command for HelpCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.topic_patterns.is_empty() { + Self::display_general_help(&context)?; + } else { + for topic_pattern in &self.topic_patterns { + self.display_help_for_topic_pattern(&context, topic_pattern)?; + } + } + + Ok(ExecutionResult::success()) + } +} + +impl HelpCommand { + fn display_general_help( + context: &brush_core::ExecutionContext<'_>, + ) -> Result<(), brush_core::Error> { + const COLUMN_COUNT: usize = 3; + + if let Some(display_str) = context.shell.product_display_str() { + writeln!(context.stdout(), "{display_str}\n")?; + } + + writeln!( + context.stdout(), + "The following commands are implemented as shell built-ins:" + )?; + + let builtins = get_builtins_sorted_by_name(context); + let items_per_column = builtins.len().div_ceil(COLUMN_COUNT); + + for i in 0..items_per_column { + for j in 0..COLUMN_COUNT { + if let Some((name, builtin)) = builtins.get(i + j * items_per_column) { + let prefix = if builtin.disabled { "*" } else { " " }; + write!(context.stdout(), " {prefix}{name:<20}")?; // adjust 20 to the desired + // column width + } + } + writeln!(context.stdout())?; + } + + Ok(()) + } + + fn display_help_for_topic_pattern( + &self, + context: &brush_core::ExecutionContext<'_>, + topic_pattern: &str, + ) -> Result<(), brush_core::Error> { + let pattern = brush_core::patterns::Pattern::from(topic_pattern) + .set_extended_globbing(context.shell.options.extended_globbing) + .set_case_insensitive(context.shell.options.case_insensitive_pathname_expansion); + + let mut found_count = 0; + for (builtin_name, builtin_registration) in get_builtins_sorted_by_name(context) { + if pattern.exactly_matches(builtin_name.as_str())? { + self.display_help_for_builtin( + context, + builtin_name.as_str(), + builtin_registration, + )?; + found_count += 1; + } + } + + if found_count == 0 { + writeln!(context.stderr(), "No help topics match '{topic_pattern}'")?; + } + + Ok(()) + } + + fn display_help_for_builtin( + &self, + context: &brush_core::ExecutionContext<'_>, + name: &str, + registration: &builtins::Registration, + ) -> Result<(), brush_core::Error> { + let content_type = if self.short_description { + builtins::ContentType::ShortDescription + } else if self.man_page_style { + builtins::ContentType::ManPage + } else if self.short_usage { + builtins::ContentType::ShortUsage + } else { + builtins::ContentType::DetailedHelp + }; + + let content = (registration.content_func)(name, content_type)?; + + write!(context.stdout(), "{content}")?; + context.stdout().flush()?; + + Ok(()) + } +} + +fn get_builtins_sorted_by_name<'a>( + context: &'a brush_core::ExecutionContext<'_>, +) -> Vec<(&'a String, &'a builtins::Registration)> { + context + .shell + .builtins() + .iter() + .sorted_by_key(|(name, _)| *name) + .collect() +} diff --git a/crates/brush-builtins-vendored/src/history.rs b/crates/brush-builtins-vendored/src/history.rs new file mode 100644 index 000000000..3e9799928 --- /dev/null +++ b/crates/brush-builtins-vendored/src/history.rs @@ -0,0 +1,246 @@ +use brush_core::{ExecutionExitCode, ExecutionResult, builtins, error, history}; +use clap::Parser; +use std::{io::Write, path::PathBuf}; + +/// Query or manipulate the shell's command history. +// TODO: Evaluate which of the options conflict with each other. +#[derive(Parser)] +#[expect(clippy::option_option)] +pub(crate) struct HistoryCommand { + /// Clears all history. + #[arg(short = 'c')] + clear_history: bool, + + /// Deletes the history entry at the given offset. Positive offsets are relative to the + /// beginning of the history, while negative offsets are relative to the end of the history. + #[arg(short = 'd', value_name = "OFFSET")] + delete_offset: Option, + + /// Appends the history from the current session to the history file. + #[arg(short = 'a', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")] + append_session_to_file: Option>, + + /// Appends any remaining history from the history file to the current session. + #[arg(short = 'n', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")] + append_rest_of_file_to_session: Option>, + + /// Appends the history from the history file to the current session. + #[arg(short = 'r', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")] + append_file_to_session: Option>, + + /// Replaces the history file with the current session history. + #[arg(short = 'w', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")] + write_session_to_file: Option>, + + /// History-expands positional arguments and displays them. + #[arg(short = 'p', num_args = 0.., value_name = "ARG")] + expand_args: Option>, + + /// Appends positional arguments as an entry in the current session. + #[arg(short = 's', num_args = 0.., value_name = "ARG")] + append_args_to_session: Option>, + + /// Arguments. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, +} + +struct HistoryConfig { + default_history_file_path: Option, + time_format: Option, +} + +impl builtins::Command for HistoryCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + // Retrieve the shell's history config while we still can. + let config = HistoryConfig { + default_history_file_path: context.shell.history_file_path(), + time_format: context.shell.history_time_format(), + }; + + let stdout = context.stdout(); + let stderr = context.stderr(); + + if let Some(history) = context.shell.history_mut() { + self.execute_with_history(history, config, stdout, stderr) + } else { + Err(brush_core::ErrorKind::HistoryNotEnabled.into()) + } + } +} + +impl HistoryCommand { + #[expect(clippy::cast_possible_wrap)] + #[expect(clippy::cast_possible_truncation)] + #[expect(clippy::cast_sign_loss)] + fn execute_with_history( + &self, + history: &mut history::History, + config: HistoryConfig, + stdout: impl Write, + mut stderr: impl Write, + ) -> Result { + if self.clear_history { + history.clear()?; + } + + if let Some(offset) = self.delete_offset { + if offset == 0 { + writeln!(stderr, "cannot delete history item at offset 0")?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + if offset > 0 { + // Convert to 0-based index. + let index = (offset - 1) as usize; + if !history.remove_nth_item(index) { + writeln!(stderr, "index past end of history")?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } else { + let count = history.count() as i64; + let index = count + offset; + if index < 0 { + writeln!(stderr, "index before beginning of history")?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + let _ = history.remove_nth_item(index as usize); + } + + return Ok(ExecutionResult::success()); + } + + if let Some(append_option) = &self.append_session_to_file { + if let Some(file_path) = get_effective_history_file_path( + config.default_history_file_path, + append_option.as_ref(), + ) { + history.flush( + file_path, + true, /*append?*/ + true, /*unsaved items only*/ + config.time_format.is_some(), /*write timestamps?*/ + )?; + } + + return Ok(ExecutionResult::success()); + } + + if self.append_rest_of_file_to_session.is_some() { + return error::unimp("history -n is not yet implemented"); + } + + if self.append_file_to_session.is_some() { + return error::unimp("history -r is not yet implemented"); + } + + if let Some(write_option) = &self.write_session_to_file { + if let Some(file_path) = get_effective_history_file_path( + config.default_history_file_path, + write_option.as_ref(), + ) { + history.flush( + file_path, + false, /*append?*/ + false, /*unsaved items only?*/ + config.time_format.is_some(), /*write timestamps?*/ + )?; + } + + return Ok(ExecutionResult::success()); + } + + if self.expand_args.is_some() { + return error::unimp("history -p is not yet implemented"); + } + + if let Some(args) = &self.append_args_to_session { + history.add(history::Item::new(args.join(" ")))?; + return Ok(ExecutionResult::success()); + } + + let max_entries: Option = if let Some(arg) = self.args.first() { + Some(arg.parse()?) + } else { + None + }; + + display_history(history, &config, max_entries, stdout, stderr)?; + + Ok(ExecutionResult::success()) + } +} + +fn display_history( + history: &history::History, + config: &HistoryConfig, + max_entries: Option, + mut stdout: impl Write, + _stderr: impl Write, +) -> Result<(), brush_core::Error> { + let item_count = history.count(); + let skip_count = item_count - max_entries.unwrap_or(item_count); + + for (i, item) in history.iter().skip(skip_count).enumerate() { + let mut formatted_timestamp = String::new(); + + if let Some(timestamp) = item.timestamp { + let local_timestamp = timestamp.with_timezone(&chrono::Local); + if let Some(time_format) = &config.time_format { + let fmt_items = chrono::format::StrftimeItems::new(time_format); + formatted_timestamp = local_timestamp.format_with_items(fmt_items).to_string(); + } + } + + // Output format is something like: + // 1 echo hello world + std::writeln!( + stdout, + "{:>5} {formatted_timestamp}{}", + skip_count + i + 1, + item.command_line + )?; + } + + Ok(()) +} + +fn get_effective_history_file_path( + default_history_file_path: Option, + option: Option<&String>, +) -> Option { + option.map_or_else( + || default_history_file_path, + |file_path| Some(PathBuf::from(file_path)), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use pretty_assertions::{assert_eq, assert_matches}; + + #[test] + fn test_parse_dash_a() -> Result<()> { + let cmd = HistoryCommand::try_parse_from(["history", "5"])?; + assert_matches!(cmd.append_session_to_file, None); + + let cmd = HistoryCommand::try_parse_from(["history", "-a"])?; + assert_matches!(cmd.append_session_to_file, Some(None)); + + let cmd = HistoryCommand::try_parse_from(["history", "-a", "token"])?; + assert_eq!( + cmd.append_session_to_file, + Some(Some(String::from("token"))) + ); + + Ok(()) + } +} diff --git a/crates/brush-builtins-vendored/src/jobs.rs b/crates/brush-builtins-vendored/src/jobs.rs new file mode 100644 index 000000000..6b9263d99 --- /dev/null +++ b/crates/brush-builtins-vendored/src/jobs.rs @@ -0,0 +1,83 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins, error, jobs}; + +/// Manage jobs. +#[derive(Parser)] +pub(crate) struct JobsCommand { + /// Also show process IDs. + #[arg(short = 'l')] + also_show_pids: bool, + + /// List only jobs that have changed status since the last notification. + #[arg(short = 'n')] + list_changed_only: bool, + + /// Show only process IDs. + #[arg(short = 'p')] + show_pids_only: bool, + + /// Show only running jobs. + #[arg(short = 'r')] + running_jobs_only: bool, + + /// Show only stopped jobs. + #[arg(short = 's')] + stopped_jobs_only: bool, + + /// Job specs to list. + // TODO: Add -x option + job_specs: Vec, +} + +impl builtins::Command for JobsCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.also_show_pids { + return error::unimp("jobs -l"); + } + if self.list_changed_only { + return error::unimp("jobs -n"); + } + + if self.job_specs.is_empty() { + for job in &context.shell.jobs.jobs { + self.display_job(&context, job)?; + } + } else { + return error::unimp("jobs with job specs"); + } + + Ok(ExecutionResult::success()) + } +} + +impl JobsCommand { + fn display_job( + &self, + context: &brush_core::ExecutionContext<'_>, + job: &jobs::Job, + ) -> Result<(), brush_core::Error> { + if self.running_jobs_only && !matches!(job.state, jobs::JobState::Running) { + return Ok(()); + } + if self.stopped_jobs_only && !matches!(job.state, jobs::JobState::Stopped) { + return Ok(()); + } + + if self.show_pids_only { + if let Some(pid) = job.representative_pid() { + writeln!(context.stdout(), "{pid}")?; + } + } else { + writeln!(context.stdout(), "{job}")?; + } + + Ok(()) + } +} diff --git a/crates/brush-builtins-vendored/src/kill.rs b/crates/brush-builtins-vendored/src/kill.rs new file mode 100644 index 000000000..683b71d44 --- /dev/null +++ b/crates/brush-builtins-vendored/src/kill.rs @@ -0,0 +1,179 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::traps::TrapSignal; +use brush_core::{ExecutionExitCode, ExecutionResult, builtins, sys}; + +/// Signal a job or process. +#[derive(Parser)] +pub(crate) struct KillCommand { + /// Name of the signal to send. + #[arg(short = 's', value_name = "SIG_NAME")] + signal_name: Option, + + /// Number of the signal to send. + #[arg(short = 'n', value_name = "SIG_NUM")] + signal_number: Option, + + // + // TODO: implement -sigspec syntax + /// List known signal names. + #[arg(short = 'l', short_alias = 'L')] + list_signals: bool, + + // Interpretation of these depends on whether -l is present. + #[arg(allow_hyphen_values = true)] + args: Vec, +} + +impl builtins::Command for KillCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + // Default signal is SIGKILL. + let mut trap_signal = TrapSignal::Signal(nix::sys::signal::Signal::SIGKILL); + + // Try parsing the signal name (if specified). + if let Some(signal_name) = &self.signal_name { + if let Ok(parsed_trap_signal) = TrapSignal::try_from(signal_name.as_str()) { + trap_signal = parsed_trap_signal; + } else { + writeln!( + context.stderr(), + "{}: invalid signal name: {}", + context.command_name, + signal_name + )?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } + + // Try parsing the signal number (if specified). + if let Some(signal_number) = &self.signal_number { + #[expect(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_wrap)] + if let Ok(parsed_trap_signal) = TrapSignal::try_from(*signal_number as i32) { + trap_signal = parsed_trap_signal; + } else { + writeln!( + context.stderr(), + "{}: invalid signal number: {}", + context.command_name, + signal_number + )?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } + + // Look through the remaining args for a pid/job spec or a -sigspec style option. + let mut pid_or_job_spec = None; + for arg in &self.args { + // See if this is -sigspec syntax. + if let Some(possible_sigspec) = arg.strip_prefix("-") { + // See if this is -sigspec syntax. + if let Ok(parsed_trap_signal) = TrapSignal::try_from(possible_sigspec) { + trap_signal = parsed_trap_signal; + } else { + writeln!( + context.stderr(), + "{}: invalid signal name", + context.command_name + )?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } else if pid_or_job_spec.is_none() { + pid_or_job_spec = Some(arg); + } else { + writeln!( + context.stderr(), + "{}: too many jobs or processes specified", + context.command_name + )?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } + + if self.list_signals { + return print_signals(&context, self.args.as_ref()); + } else { + if pid_or_job_spec.is_none() { + writeln!(context.stderr(), "{}: invalid usage", context.command_name)?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + let pid_or_job_spec = pid_or_job_spec.unwrap(); + if pid_or_job_spec.starts_with('%') { + // It's a job spec. + if let Some(job) = context.shell.jobs.resolve_job_spec(pid_or_job_spec) { + job.kill(trap_signal)?; + } else { + writeln!( + context.stderr(), + "{}: {}: no such job", + context.command_name, + pid_or_job_spec + )?; + return Ok(ExecutionResult::general_error()); + } + } else { + let pid = pid_or_job_spec.parse::()?; + + // It's a pid. + sys::signal::kill_process(pid, trap_signal)?; + } + } + Ok(ExecutionResult::success()) + } +} + +fn print_signals( + context: &brush_core::ExecutionContext<'_>, + signals: &[String], +) -> Result { + let mut exit_code = ExecutionResult::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::() { + // 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 = ExecutionResult::general_error(); + } + } + } + } else { + return brush_core::traps::format_signals( + context.stdout(), + TrapSignal::iterator().filter(|s| !matches!(s, TrapSignal::Exit)), + ) + .map(|()| ExecutionResult::success()); + } + + Ok(exit_code) +} diff --git a/crates/brush-builtins-vendored/src/let_.rs b/crates/brush-builtins-vendored/src/let_.rs new file mode 100644 index 000000000..ddc89a650 --- /dev/null +++ b/crates/brush-builtins-vendored/src/let_.rs @@ -0,0 +1,41 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionExitCode, ExecutionResult, arithmetic::Evaluatable, builtins}; + +/// Evaluate arithmetic expressions. +#[derive(Parser)] +pub(crate) struct LetCommand { + /// Arithmetic expressions to evaluate. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + exprs: Vec, +} + +impl builtins::Command for LetCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut result = ExecutionExitCode::InvalidUsage.into(); + + if self.exprs.is_empty() { + writeln!(context.stderr(), "missing expression")?; + return Ok(result); + } + + for expr in &self.exprs { + let parsed = brush_parser::arithmetic::parse(expr.as_str())?; + let evaluated = parsed.eval(context.shell)?; + + if evaluated == 0 { + result = ExecutionResult::general_error(); + } else { + result = ExecutionResult::success(); + } + } + + Ok(result) + } +} diff --git a/crates/brush-builtins-vendored/src/lib.rs b/crates/brush-builtins-vendored/src/lib.rs new file mode 100644 index 000000000..ba13e812d --- /dev/null +++ b/crates/brush-builtins-vendored/src/lib.rs @@ -0,0 +1,157 @@ +//! Standard builtins. + +#[cfg(feature = "builtin.alias")] +mod alias; +#[cfg(feature = "builtin.bg")] +mod bg; +#[cfg(feature = "builtin.bind")] +mod bind; +#[cfg(feature = "builtin.break")] +mod break_; +#[cfg(feature = "builtin.brushinfo")] +mod brushinfo; +#[cfg(feature = "builtin.builtin")] +mod builtin_; +#[cfg(feature = "builtin.cd")] +mod cd; +#[cfg(feature = "builtin.colon")] +mod colon; +#[cfg(feature = "builtin.command")] +mod command; +#[cfg(any( + feature = "builtin.complete", + feature = "builtin.compgen", + feature = "builtin.compopt" +))] +mod complete; +#[cfg(feature = "builtin.continue")] +mod continue_; +#[cfg(feature = "builtin.declare")] +mod declare; +#[cfg(feature = "builtin.dirs")] +mod dirs; +#[cfg(feature = "builtin.dot")] +mod dot; +#[cfg(feature = "builtin.echo")] +mod echo; +#[cfg(feature = "builtin.enable")] +mod enable; +#[cfg(feature = "builtin.eval")] +mod eval; +#[cfg(all(feature = "builtin.exec", unix))] +mod exec; +#[cfg(feature = "builtin.exit")] +mod exit; +#[cfg(feature = "builtin.export")] +mod export; +#[cfg(feature = "builtin.false")] +mod false_; +#[cfg(feature = "builtin.fc")] +mod fc; +#[cfg(feature = "builtin.fg")] +mod fg; +#[cfg(feature = "builtin.getopts")] +mod getopts; +#[cfg(feature = "builtin.hash")] +mod hash; +#[cfg(feature = "builtin.help")] +mod help; +#[cfg(feature = "builtin.history")] +mod history; +#[cfg(feature = "builtin.jobs")] +mod jobs; +#[cfg(all(feature = "builtin.kill", unix))] +mod kill; +#[cfg(feature = "builtin.let")] +mod let_; +#[cfg(feature = "builtin.mapfile")] +mod mapfile; +#[cfg(feature = "builtin.popd")] +mod popd; +#[cfg(all(feature = "builtin.printf", any(unix, windows)))] +mod printf; +#[cfg(feature = "builtin.pushd")] +mod pushd; +#[cfg(feature = "builtin.pwd")] +mod pwd; +#[cfg(feature = "builtin.read")] +mod read; +#[cfg(feature = "builtin.return")] +mod return_; +#[cfg(feature = "builtin.set")] +mod set; +#[cfg(feature = "builtin.shift")] +mod shift; +#[cfg(feature = "builtin.shopt")] +mod shopt; +#[cfg(all(feature = "builtin.suspend", unix))] +mod suspend; +#[cfg(feature = "builtin.test")] +mod test; +#[cfg(feature = "builtin.times")] +mod times; +#[cfg(feature = "builtin.trap")] +mod trap; +#[cfg(feature = "builtin.true")] +mod true_; +#[cfg(feature = "builtin.type")] +mod type_; +#[cfg(all(feature = "builtin.ulimit", unix))] +mod ulimit; +#[cfg(all(feature = "builtin.umask", unix))] +mod umask; +#[cfg(feature = "builtin.unalias")] +mod unalias; +#[cfg(feature = "builtin.unset")] +mod unset; +#[cfg(feature = "builtin.wait")] +mod wait; + +mod builder; +mod factory; +mod unimp; + +pub use builder::ShellBuilderExt; +pub use factory::{BuiltinSet, default_builtins}; + +/// Macro to define a struct that represents a shell built-in flag argument that can be +/// enabled or disabled by specifying an option with a leading '+' or '-' character. +/// +/// # Arguments +/// +/// - `$struct_name` - The identifier to be used for the struct to define. +/// - `$flag_char` - The character to use as the flag. +/// - `$desc` - The string description of the flag. +#[macro_export] +macro_rules! minus_or_plus_flag_arg { + ($struct_name:ident, $flag_char:literal, $desc:literal) => { + #[derive(clap::Parser)] + pub(crate) struct $struct_name { + #[arg(short = $flag_char, name = concat!(stringify!($struct_name), "_enable"), action = clap::ArgAction::SetTrue, help = $desc)] + _enable: bool, + #[arg(long = concat!("+", $flag_char), name = concat!(stringify!($struct_name), "_disable"), action = clap::ArgAction::SetTrue, hide = true)] + _disable: bool, + } + + impl From<$struct_name> for Option { + fn from(value: $struct_name) -> Self { + value.to_bool() + } + } + + impl $struct_name { + #[allow(dead_code, reason = "may not be used in all macro instantiations")] + pub const fn is_some(&self) -> bool { + self._enable || self._disable + } + + pub const fn to_bool(&self) -> Option { + match (self._enable, self._disable) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => None, + } + } + } + }; +} diff --git a/crates/brush-builtins-vendored/src/mapfile.rs b/crates/brush-builtins-vendored/src/mapfile.rs new file mode 100644 index 000000000..20eae2b9a --- /dev/null +++ b/crates/brush-builtins-vendored/src/mapfile.rs @@ -0,0 +1,155 @@ +use std::io::Read; + +use clap::Parser; + +use brush_core::{ErrorKind, ExecutionResult, builtins, env, error, variables}; + +/// Inspect and modify key bindings and other input configuration. +#[derive(Parser)] +pub(crate) struct MapFileCommand { + /// Delimiter to use (defaults to newline). + #[arg(short = 'd', default_value = "\n")] + delimiter: String, + + /// Maximum number of entries to read (0 means no limit). + #[arg(short = 'n', default_value_t = 0)] + max_count: i64, + + /// Index into array at which to start assignment. + #[arg(short = 'O', default_value_t = 0)] + origin: i64, + + /// Number of initial entries to skip. + #[arg(short = 's', default_value_t = 0, value_parser = clap::value_parser!(i64).range(0..))] + skip_count: i64, + + /// Whether or not to remove the delimiter from each read line. + #[arg(short = 't')] + remove_delimiter: bool, + + /// File descriptor to read from (defaults to stdin). + #[arg(short = 'u', default_value_t = 0)] + fd: brush_core::ShellFd, + + /// Name of function to call for each group of lines. + #[arg(short = 'C')] + callback: Option, + + /// Number of lines to pass the callback for each group. + #[arg(short = 'c', default_value_t = 5000, value_parser = clap::value_parser!(i64).range(1..))] + callback_group_size: i64, + + /// Name of array to read into. + #[arg(default_value = "MAPFILE")] + array_var_name: String, +} + +impl builtins::Command for MapFileCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.origin != 0 { + // This will require merging into a potentially already-existing array. + return error::unimp("mapfile -O is not yet implemented"); + } + + if self.callback_group_size != 5000 || self.callback.is_some() { + return error::unimp("mapfile -C/-c is not yet implemented"); + } + + let input_file = context + .try_fd(self.fd) + .ok_or_else(|| ErrorKind::BadFileDescriptor(self.fd))?; + + // Read! + let results = self.read_entries(input_file)?; + + // Assign! + context.shell.env.update_or_add( + &self.array_var_name, + variables::ShellValueLiteral::Array(results), + |_| Ok(()), + env::EnvironmentLookup::Anywhere, + env::EnvironmentScope::Global, + )?; + + Ok(ExecutionResult::success()) + } +} + +impl MapFileCommand { + fn read_entries( + &self, + mut input_file: brush_core::openfiles::OpenFile, + ) -> Result { + let _term_mode = setup_terminal_settings(&input_file)?; + + let mut entries = vec![]; + let mut read_count = 0; + let max_count = self.max_count.try_into()?; + let delimiter = self.delimiter.chars().next().unwrap_or('\n') as u8; + + let mut buf = [0u8; 1]; + + while max_count == 0 || entries.len() < max_count { + let mut line = vec![]; + let mut saw_delimiter = false; + + loop { + match input_file.read(&mut buf) { + Ok(0) => break, // End of input + Ok(1) if buf[0] == b'\x03' => break, // Ctrl+C + Ok(1) if buf[0] == b'\x04' && line.is_empty() => break, // Ctrl+D + Ok(1) => { + let byte = buf[0]; + line.push(byte); + if byte == delimiter { + saw_delimiter = true; + break; + } + } + Ok(_) => unreachable!("input can only be 0, 1, or error"), + Err(e) => return Err(e.into()), + } + } + + if line.is_empty() && !saw_delimiter { + break; + } + + if read_count < self.skip_count { + read_count += 1; + continue; + } + + if self.remove_delimiter && line.ends_with(&[delimiter]) { + line.pop(); + } + + let line_str = String::from_utf8_lossy(&line).to_string(); + + entries.push((None, line_str)); + } + + Ok(variables::ArrayLiteral(entries)) + } +} + +fn setup_terminal_settings( + file: &brush_core::openfiles::OpenFile, +) -> Result, brush_core::Error> { + let mode = brush_core::terminal::AutoModeGuard::new(file.to_owned()).ok(); + if let Some(mode) = &mode { + let config = brush_core::terminal::Settings::builder() + .line_input(false) + .interrupt_signals(false) + .build(); + + mode.apply_settings(&config)?; + } + + Ok(mode) +} diff --git a/crates/brush-builtins-vendored/src/popd.rs b/crates/brush-builtins-vendored/src/popd.rs new file mode 100644 index 000000000..1f712efe6 --- /dev/null +++ b/crates/brush-builtins-vendored/src/popd.rs @@ -0,0 +1,36 @@ +use clap::Parser; + +use brush_core::{ExecutionResult, builtins}; + +/// Pop a path from the current directory stack. +#[derive(Parser)] +pub(crate) struct PopdCommand { + /// Pop the path without changing the current working directory. + #[clap(short = 'n')] + no_directory_change: bool, + // + // TODO: implement +N and -N +} + +impl builtins::Command for PopdCommand { + type Error = crate::dirs::DirError; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if let Some(popped) = context.shell.directory_stack.pop() { + if !self.no_directory_change { + context.shell.set_working_dir(&popped)?; + } + + // Display dirs. + let dirs_cmd = crate::dirs::DirsCommand::default(); + dirs_cmd.execute(context).await?; + + Ok(ExecutionResult::success()) + } else { + Err(crate::dirs::DirError::DirStackEmpty) + } + } +} diff --git a/crates/brush-builtins-vendored/src/printf.rs b/crates/brush-builtins-vendored/src/printf.rs new file mode 100644 index 000000000..b5b6ccd36 --- /dev/null +++ b/crates/brush-builtins-vendored/src/printf.rs @@ -0,0 +1,184 @@ +use clap::Parser; +use std::{io::Write, ops::ControlFlow}; +use uucore::format; + +use brush_core::{Error, ErrorKind, ExecutionResult, builtins, escape, expansion}; + +/// Format a string. +#[derive(Parser)] +#[clap(disable_help_flag = true, disable_version_flag = true)] +pub(crate) struct PrintfCommand { + /// If specified, the output of the command is assigned to this variable. + #[arg(short = 'v')] + output_variable: Option, + + /// Format string + arguments to the format string. + #[arg(trailing_var_arg = true, required = true, allow_hyphen_values = true)] + format_and_args: Vec, +} + +impl builtins::Command for PrintfCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if let Some(variable_name) = &self.output_variable { + // Format to a u8 vector. + let mut result: Vec = vec![]; + format(self.format_and_args.as_slice(), &mut result)?; + + // Convert to a string. + let result_str = String::from_utf8(result).map_err(|_| { + brush_core::ErrorKind::PrintfInvalidUsage("invalid UTF-8 output".into()) + })?; + + // Assign to the selected variable. + expansion::assign_to_named_parameter( + context.shell, + &context.params, + variable_name, + result_str, + ) + .await?; + } else { + format(self.format_and_args.as_slice(), context.stdout())?; + context.stdout().flush()?; + } + + Ok(ExecutionResult::success()) + } +} + +fn format(format_and_args: &[String], writer: impl Write) -> Result<(), brush_core::Error> { + match format_and_args { + // Special-case invocation of printf with %q-based format string from bash-completion. + // It has hard-coded expectation of backslash-style escaping instead of quoting. + [fmt, arg] if fmt == "%q" => format_special_case_for_percent_q(None, arg, writer), + [fmt, arg] if fmt == "~%q" => format_special_case_for_percent_q(Some("~"), arg, writer), + // Handle format string with arguments using uucore + [fmt, args @ ..] => format_via_uucore(fmt, args.iter(), writer), + // Handle case with no format string (we shouldn't be able to get here since clap will + // fail parsing when the format string is missing) + [] => Err(ErrorKind::PrintfInvalidUsage("missing operand".into()).into()), + } +} + +fn format_special_case_for_percent_q( + prefix: Option<&str>, + arg: &str, + mut writer: impl Write, +) -> Result<(), brush_core::Error> { + let mut result = escape::quote_if_needed(arg, escape::QuoteMode::BackslashEscape).to_string(); + + if let Some(prefix) = prefix { + result.insert_str(0, prefix); + } + + write!(writer, "{result}")?; + + Ok(()) +} + +fn format_via_uucore( + format_string: &str, + args: impl Iterator>, + mut writer: impl Write, +) -> Result<(), brush_core::Error> { + // Convert string arguments to FormatArgument::Unparsed + let format_args: Vec<_> = args + .map(|s| format::FormatArgument::Unparsed(s.as_ref().to_string().into())) + .collect(); + + // Parse format string once. + let format_items = parse_format_string(format_string)?; + + // Wrap the format arguments. + let mut format_args_wrapper = format::FormatArguments::new(&format_args); + + // Keep going until we've exhausted all format arguments. Also make sure to run at least once + // even if there's no format arguments. + while format_args.is_empty() || !format_args_wrapper.is_exhausted() { + // Process all format items, in order. We'll bail when we're told to stop. + for item in &format_items { + let control_flow = item + .write(&mut writer, &mut format_args_wrapper) + .map_err(|e| { + Error::from(ErrorKind::PrintfInvalidUsage(std::format!( + "printf formatting error: {e}" + ))) + })?; + + if control_flow == ControlFlow::Break(()) { + break; + } + } + + // Start next batch if not exhausted + if !format_args_wrapper.is_exhausted() { + format_args_wrapper.start_next_batch(); + } + + if format_args.is_empty() { + break; + } + } + + Ok(()) +} + +fn parse_format_string( + format_string: &str, +) -> Result>, brush_core::Error> { + let format_items: Result, _> = + format::parse_spec_and_escape(format_string.as_bytes()).collect(); + + // Observe any errors we encountered along the way. + let format_items = format_items + .map_err(|e| ErrorKind::PrintfInvalidUsage(format!("printf parsing error: {e}")))?; + + Ok(format_items) +} + +#[cfg(test)] +#[expect(clippy::panic_in_result_fn)] +mod tests { + use super::*; + use anyhow::Result; + + fn sprintf_via_uucore( + format_string: &str, + args: impl Iterator>, + ) -> Result { + let mut result = vec![]; + format_via_uucore(format_string, args, &mut result)?; + + Ok(String::from_utf8(result)?) + } + + #[test] + fn test_basic_sprintf() -> Result<()> { + assert_eq!(sprintf_via_uucore("%s", std::iter::once(&"xyz"))?, "xyz"); + assert_eq!(sprintf_via_uucore(r"%d\n", std::iter::once(&"1"))?, "1\n"); + + Ok(()) + } + + #[test] + fn test_sprintf_without_args() -> Result<()> { + let empty: [&str; 0] = []; + + assert_eq!(sprintf_via_uucore("xyz", empty.iter())?, "xyz"); + assert_eq!(sprintf_via_uucore("%s|", empty.iter())?, "|"); + + Ok(()) + } + + #[test] + fn test_sprintf_with_cycles() -> Result<()> { + assert_eq!(sprintf_via_uucore("%s|", ["x", "y"].iter())?, "x|y|"); + + Ok(()) + } +} diff --git a/crates/brush-builtins-vendored/src/pushd.rs b/crates/brush-builtins-vendored/src/pushd.rs new file mode 100644 index 000000000..f47e64e2b --- /dev/null +++ b/crates/brush-builtins-vendored/src/pushd.rs @@ -0,0 +1,45 @@ +use clap::Parser; + +use brush_core::{ExecutionResult, builtins}; + +/// Push a path onto the current directory stack. +#[derive(Parser)] +pub(crate) struct PushdCommand { + /// Push the path without changing the current working directory. + #[clap(short = 'n')] + no_directory_change: bool, + + /// Directory to push on the directory stack. + dir: String, + // + // TODO: implement +N and -N +} + +impl builtins::Command for PushdCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.no_directory_change { + context + .shell + .directory_stack + .push(std::path::PathBuf::from(&self.dir)); + } else { + let prev_working_dir = context.shell.working_dir().to_path_buf(); + + let dir = std::path::Path::new(&self.dir); + context.shell.set_working_dir(dir)?; + + context.shell.directory_stack.push(prev_working_dir); + } + + // Display dirs. + let dirs_cmd = crate::dirs::DirsCommand::default(); + dirs_cmd.execute(context).await?; + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/pwd.rs b/crates/brush-builtins-vendored/src/pwd.rs new file mode 100644 index 000000000..56d0bd593 --- /dev/null +++ b/crates/brush-builtins-vendored/src/pwd.rs @@ -0,0 +1,40 @@ +use brush_core::{ExecutionResult, builtins}; +use clap::Parser; +use std::{borrow::Cow, io::Write, path::Path}; + +/// Display the current working directory. +#[derive(Parser)] +pub(crate) struct PwdCommand { + /// Print the physical directory without any symlinks. + #[arg(short = 'P', overrides_with = "allow_symlinks")] + physical: bool, + + /// Print $PWD if it names the current working directory. + #[arg(short = 'L', overrides_with = "physical")] + allow_symlinks: bool, +} + +impl builtins::Command for PwdCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut cwd: Cow<'_, Path> = context.shell.working_dir().into(); + + let should_canonicalize = self.physical + || context + .shell + .options + .do_not_resolve_symlinks_when_changing_dir; + + if should_canonicalize { + cwd = cwd.canonicalize()?.into(); + } + + writeln!(context.stdout(), "{}", cwd.to_string_lossy())?; + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/read.rs b/crates/brush-builtins-vendored/src/read.rs new file mode 100644 index 000000000..8dc2ea48f --- /dev/null +++ b/crates/brush-builtins-vendored/src/read.rs @@ -0,0 +1,396 @@ +use clap::Parser; +use itertools::Itertools; +use std::collections::VecDeque; + +use brush_core::{ErrorKind, builtins, env, error, variables}; + +use std::io::Read; + +/// Parse standard input. +#[derive(Parser)] +pub(crate) struct ReadCommand { + /// Optionally, name of an array variable to receive read words + /// of input. + #[clap(short = 'a', value_name = "VAR_NAME")] + array_variable: Option, + + /// Optionally, a delimiter to use other than a newline character. + #[clap(short = 'd')] + delimiter: Option, + + /// Use readline-like input. + #[clap(short = 'e')] + use_readline: bool, + + /// Provide text to use as initial input for readline. + #[clap(short = 'i', value_name = "STR")] + initial_text: Option, + + /// Read only the first N characters or until a specified + /// delimiter is reached, whichever happens first. + #[clap(short = 'n', value_name = "COUNT")] + return_after_n_chars: Option, + + /// Read exactly N characters, ignoring any specified delimiter. + #[clap(short = 'N', value_name = "COUNT")] + return_after_n_chars_no_delimiter: Option, + + /// Prompt to display before reading. + #[clap(short = 'p')] + prompt: Option, + + /// Read input in raw mode; no escape sequences. + #[clap(short = 'r')] + raw_mode: bool, + + /// Do not echo input. + #[clap(short = 's')] + silent: bool, + + /// Specify timeout in seconds; fail if the timeout elapses before + /// input is completed. + #[clap(short = 't', value_name = "SECONDS")] + timeout_in_seconds: Option, + + /// File descriptor to read from instead of stdin. + #[clap(short = 'u', name = "FD")] + fd_num_to_read: Option, + + /// Optionally, names of variables to receive read input. + variable_names: Vec, +} + +impl builtins::Command for ReadCommand { + type Error = brush_core::Error; + + #[allow(clippy::too_many_lines)] + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.use_readline { + return error::unimp("read -e"); + } + if self.initial_text.is_some() { + return error::unimp("read -i"); + } + if self.raw_mode { + tracing::debug!("read -r is not implemented"); + } + if self.timeout_in_seconds.is_some() { + return error::unimp_with_issue("read -t", 227); + } + + // Find the input stream to use. + let input_stream = if let Some(fd_num) = self.fd_num_to_read { + let fd_num = brush_core::ShellFd::from(fd_num); + context + .try_fd(fd_num) + .ok_or_else(|| ErrorKind::BadFileDescriptor(fd_num))? + } else { + context + .try_fd(brush_core::openfiles::OpenFiles::STDIN_FD) + .unwrap() + }; + + // Retrieve effective value of IFS for splitting. + let ifs = context.shell.ifs(); + + let input_line = self.read_line(input_stream, context.stdout())?; + let result = if input_line.is_some() { + brush_core::ExecutionResult::success() + } else { + brush_core::ExecutionResult::general_error() + }; + + // If -a was specified, then place the fields as elements into the array. + if let Some(array_variable) = &self.array_variable { + let literal_fields = if let Some(input_line) = input_line { + let fields: VecDeque<_> = split_line_by_ifs( + ifs.as_ref(), + input_line.as_str(), + None, /* max_fields */ + ); + + fields.into_iter().map(|f| (None, f)).collect() + } else { + vec![] + }; + + context.shell.env.update_or_add( + array_variable, + variables::ShellValueLiteral::Array(variables::ArrayLiteral(literal_fields)), + |_| Ok(()), + env::EnvironmentLookup::Anywhere, + env::EnvironmentScope::Global, + )?; + } else if !self.variable_names.is_empty() { + let mut fields: VecDeque<_> = if let Some(input_line) = input_line { + split_line_by_ifs( + ifs.as_ref(), + input_line.as_str(), + /* max_fields */ Some(self.variable_names.len()), + ) + } else { + VecDeque::new() + }; + + for (i, name) in self.variable_names.iter().enumerate() { + if fields.is_empty() { + // Ensure the var is empty. + context.shell.env.update_or_add( + name, + variables::ShellValueLiteral::Scalar(String::new()), + |_| Ok(()), + env::EnvironmentLookup::Anywhere, + env::EnvironmentScope::Global, + )?; + continue; + } + + let last = i == self.variable_names.len() - 1; + if !last { + let next_field = fields.pop_front().unwrap(); + context.shell.env.update_or_add( + name, + variables::ShellValueLiteral::Scalar(next_field), + |_| Ok(()), + env::EnvironmentLookup::Anywhere, + env::EnvironmentScope::Global, + )?; + } else { + let remaining_fields = fields.into_iter().join(" "); + context.shell.env.update_or_add( + name, + variables::ShellValueLiteral::Scalar(remaining_fields), + |_| Ok(()), + env::EnvironmentLookup::Anywhere, + env::EnvironmentScope::Global, + )?; + break; + } + } + } else { + let input_line = input_line.unwrap_or_default(); + + // If no variable names were specified, then place everything into the + // REPLY variable. + context.shell.env.update_or_add( + "REPLY", + variables::ShellValueLiteral::Scalar(input_line), + |_| Ok(()), + env::EnvironmentLookup::Anywhere, + env::EnvironmentScope::Global, + )?; + } + + Ok(result) + } +} + +enum ReadTermination { + Delimiter, + EndOfInput, + CtrlC, + Limit, +} + +impl ReadCommand { + fn read_line( + &self, + mut input_file: brush_core::openfiles::OpenFile, + mut output_file: impl std::io::Write, + ) -> Result, brush_core::Error> { + let _term_mode = self.setup_terminal_settings(&input_file)?; + + let delimiter = if self.return_after_n_chars_no_delimiter.is_some() { + None + } else if let Some(delimiter_str) = &self.delimiter { + // If the delimiter string is empty, then the docs indicate we need to use + // the NUL character as the actual delimiter. + if delimiter_str.is_empty() { + Some('\0') + } else { + // In other cases, use the first character in the string as the delimiter. + delimiter_str.chars().next() + } + } else { + Some('\n') + }; + + let char_limit = self + .return_after_n_chars_no_delimiter + .or(self.return_after_n_chars); + + if let Some(prompt) = &self.prompt { + write!(output_file, "{prompt}")?; + output_file.flush()?; + } + + let mut line = String::new(); + let mut buffer = [0; 1]; // 1-byte buffer + + let reason = loop { + // TODO: Figure out how to restore terminal settings on error? + let n = input_file.read(&mut buffer)?; + if n == 0 { + break ReadTermination::EndOfInput; // EOF reached. + } + + let ch = buffer[0] as char; + + // Check for Ctrl+C. + if ch == '\x03' { + break ReadTermination::CtrlC; + } else if ch == '\x04' { + // Ctrl+D is EOF. + break ReadTermination::EndOfInput; + } + + // Check for a delimiter that indicates end-of-input. + if let Some(delimiter) = delimiter { + if ch == delimiter { + break ReadTermination::Delimiter; + } + } + + // Ignore other control characters without including them in the input. + if ch.is_ascii_control() && !ch.is_ascii_whitespace() { + continue; + } + + line.push(ch); + + // Check to see if we've hit a character limit. + if let Some(char_limit) = char_limit { + if line.len() >= char_limit { + break ReadTermination::Limit; + } + } + }; + + match reason { + ReadTermination::EndOfInput => { + if line.is_empty() { + Ok(None) + } else { + Ok(Some(line)) + } + } + ReadTermination::CtrlC => { + // Discard the input and return. + Ok(None) + } + ReadTermination::Delimiter | ReadTermination::Limit => Ok(Some(line)), + } + } + + fn setup_terminal_settings( + &self, + file: &brush_core::openfiles::OpenFile, + ) -> Result, brush_core::Error> { + let mode = brush_core::terminal::AutoModeGuard::new(file.to_owned()).ok(); + if let Some(mode) = &mode { + let config = brush_core::terminal::Settings::builder() + .line_input(false) + .interrupt_signals(false) + .echo_input(!self.silent) + .build(); + + mode.apply_settings(&config)?; + } + + Ok(mode) + } +} + +fn split_line_by_ifs(ifs: &str, line: &str, max_fields: Option) -> VecDeque { + // Separate out the chars to split by. + let ifs_chars = ifs.chars().collect::>(); + + // Compute which IFS characters are one of the default 3 whitespace chars. + let ifs_space_chars = ifs_chars + .iter() + .copied() + .filter(|c| *c == ' ' || *c == '\t' || *c == '\n') + .collect::>(); + + // First, trim from the prefix and suffix of the string any matching chars that are + // *both* whitespace and present in the IFS string. + let trimmed_line = line.trim_matches(ifs_space_chars.as_slice()); + if trimmed_line.is_empty() { + return VecDeque::new(); + } + + let max_fields = max_fields.unwrap_or(usize::MAX); + + // Now, iterate through the string, manually splitting it by the shell's rules for + // honoring IFS. We implement this by hand because we need to ensure that any + // IFS character is a valid delimiter, *but* consecutive adjacent IFS characters + // are considered a single delimiter if and only if they are one of the 3 default + // whitespace characters (i.e., ' ', '\t', or '\n'). + let mut fields = VecDeque::new(); + let mut current_field = String::new(); + let mut skipping_ifs_whitespace = false; + + for c in trimmed_line.chars() { + if skipping_ifs_whitespace { + if ifs_space_chars.contains(&c) { + continue; + } + + skipping_ifs_whitespace = false; + } + + if fields.len() + 1 < max_fields && ifs_chars.contains(&c) { + fields.push_back(current_field); + current_field = String::new(); + skipping_ifs_whitespace = ifs_space_chars.contains(&c); + } else { + current_field.push(c); + } + } + + fields.push_back(current_field); + + fields +} + +#[cfg(test)] +mod tests { + use itertools::assert_equal; + + use super::*; + + #[test] + fn test_split_line_by_ifs() { + let result = split_line_by_ifs(",", "a,b,c", None); + assert_equal(result, VecDeque::from(vec!["a", "b", "c"])); + } + + #[test] + fn test_split_line_by_ifs_leading_or_trailing_space() { + // Test with leading or trailing space. + let result = split_line_by_ifs(" ", " a b c ", None); + assert_equal(result, VecDeque::from(vec!["a", "b", "c"])); + } + + #[test] + fn test_split_line_by_ifs_extra_interior_space() { + // Test with leading or trailing space. + let result = split_line_by_ifs(" ", "a b c", None); + assert_equal(result, VecDeque::from(vec!["a", "b", "c"])); + } + + #[test] + fn test_split_line_by_ifs_leading_non_space_delimiter() { + let result = split_line_by_ifs(",", ",a,b,c", None); + assert_equal(result, VecDeque::from(vec!["", "a", "b", "c"])); + } + + #[test] + fn test_split_line_by_ifs_trailing_non_space_delimiter() { + let result = split_line_by_ifs(",", "a,b,c,", None); + assert_equal(result, VecDeque::from(vec!["a", "b", "c", ""])); + } +} diff --git a/crates/brush-builtins-vendored/src/return_.rs b/crates/brush-builtins-vendored/src/return_.rs new file mode 100644 index 000000000..edd723bd8 --- /dev/null +++ b/crates/brush-builtins-vendored/src/return_.rs @@ -0,0 +1,40 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, builtins}; + +/// Return from the current function. +#[derive(Parser)] +pub(crate) struct ReturnCommand { + /// The exit code to return. + code: Option, +} + +impl builtins::Command for ReturnCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + #[expect(clippy::cast_sign_loss)] + let code_8bit = if let Some(code_32bit) = &self.code { + (code_32bit & 0xFF) as u8 + } else { + context.shell.last_result() + }; + + if context.shell.in_function() || context.shell.in_sourced_script() { + let mut result = ExecutionResult::new(code_8bit); + result.next_control_flow = ExecutionControlFlow::ReturnFromFunctionOrScript; + + Ok(result) + } else { + writeln!( + context.stderr(), + "return: can only be used in a function or sourced script" + )?; + Ok(ExecutionExitCode::InvalidUsage.into()) + } + } +} diff --git a/crates/brush-builtins-vendored/src/set.rs b/crates/brush-builtins-vendored/src/set.rs new file mode 100644 index 000000000..662ab9fc0 --- /dev/null +++ b/crates/brush-builtins-vendored/src/set.rs @@ -0,0 +1,434 @@ +use std::collections::HashMap; +use std::io::Write; + +use clap::Parser; +use itertools::Itertools; + +use brush_core::{ExecutionExitCode, ExecutionResult, builtins, variables}; + +crate::minus_or_plus_flag_arg!( + ExportVariablesOnModification, + 'a', + "Export variables on modification" +); +crate::minus_or_plus_flag_arg!( + NotifyJobTerminationImmediately, + 'b', + "Notify job termination immediately" +); +crate::minus_or_plus_flag_arg!( + ExitOnNonzeroCommandExit, + 'e', + "Exit on nonzero command exit" +); +crate::minus_or_plus_flag_arg!(DisableFilenameGlobbing, 'f', "Disable filename globbing"); +crate::minus_or_plus_flag_arg!(RememberCommandLocations, 'h', "Remember command locations"); +crate::minus_or_plus_flag_arg!( + PlaceAllAssignmentArgsInCommandEnv, + 'k', + "Place all assignment args in command environment" +); +crate::minus_or_plus_flag_arg!(EnableJobControl, 'm', "Enable job control"); +crate::minus_or_plus_flag_arg!(DoNotExecuteCommands, 'n', "Do not execute commands"); +crate::minus_or_plus_flag_arg!(RealEffectiveUidMismatch, 'p', "Real effective UID mismatch"); +crate::minus_or_plus_flag_arg!(ExitAfterOneCommand, 't', "Exit after one command"); +crate::minus_or_plus_flag_arg!( + TreatUnsetVariablesAsError, + 'u', + "Treat unset variables as error" +); +crate::minus_or_plus_flag_arg!(PrintShellInputLines, 'v', "Print shell input lines"); +crate::minus_or_plus_flag_arg!( + PrintCommandsAndArguments, + 'x', + "Print commands and arguments" +); +crate::minus_or_plus_flag_arg!(PerformBraceExpansion, 'B', "Perform brace expansion"); +crate::minus_or_plus_flag_arg!( + DisallowOverwritingRegularFilesViaOutputRedirection, + 'C', + "Disallow overwriting regular files via output redirection" +); +crate::minus_or_plus_flag_arg!( + ShellFunctionsInheritErrTrap, + 'E', + "Shell functions inherit ERR trap" +); +crate::minus_or_plus_flag_arg!( + EnableBangStyleHistorySubstitution, + 'H', + "Enable bang style history substitution" +); +crate::minus_or_plus_flag_arg!( + DoNotResolveSymlinksWhenChangingDir, + 'P', + "Do not resolve symlinks when changing dir" +); +crate::minus_or_plus_flag_arg!( + ShellFunctionsInheritDebugAndReturnTraps, + 'T', + "Shell functions inherit DEBUG and RETURN traps" +); + +#[derive(clap::Parser)] +pub(crate) struct SetOption { + #[arg(short = 'o', name = "setopt_enable", num_args=0..=1, value_name = "OPT")] + enable: Option>, + #[arg(long = concat!("+o"), name = "setopt_disable", hide = true, num_args=0..=1)] + disable: Option>, +} + +/// Manage set-based shell options. +#[derive(Parser)] +#[clap(disable_help_flag = true)] +pub(crate) struct SetCommand { + /// Display help for this command. + #[clap(long, action = clap::ArgAction::HelpLong)] + help: Option, + + #[clap(flatten)] + export_variables_on_modification: ExportVariablesOnModification, + #[clap(flatten)] + notify_job_termination_immediately: NotifyJobTerminationImmediately, + #[clap(flatten)] + exit_on_nonzero_command_exit: ExitOnNonzeroCommandExit, + #[clap(flatten)] + disable_filename_globbing: DisableFilenameGlobbing, + #[clap(flatten)] + remember_command_locations: RememberCommandLocations, + #[clap(flatten)] + place_all_assignment_args_in_command_env: PlaceAllAssignmentArgsInCommandEnv, + #[clap(flatten)] + enable_job_control: EnableJobControl, + #[clap(flatten)] + do_not_execute_commands: DoNotExecuteCommands, + #[clap(flatten)] + real_effective_uid_mismatch: RealEffectiveUidMismatch, + #[clap(flatten)] + exit_after_one_command: ExitAfterOneCommand, + #[clap(flatten)] + treat_unset_variables_as_error: TreatUnsetVariablesAsError, + #[clap(flatten)] + print_shell_input_lines: PrintShellInputLines, + #[clap(flatten)] + print_commands_and_arguments: PrintCommandsAndArguments, + #[clap(flatten)] + perform_brace_expansion: PerformBraceExpansion, + #[clap(flatten)] + disallow_overwriting_regular_files_via_output_redirection: + DisallowOverwritingRegularFilesViaOutputRedirection, + #[clap(flatten)] + shell_functions_inherit_err_trap: ShellFunctionsInheritErrTrap, + #[clap(flatten)] + enable_bang_style_history_substitution: EnableBangStyleHistorySubstitution, + #[clap(flatten)] + do_not_resolve_symlinks_when_changing_dir: DoNotResolveSymlinksWhenChangingDir, + #[clap(flatten)] + shell_functions_inherit_debug_and_return_traps: ShellFunctionsInheritDebugAndReturnTraps, + + #[clap(flatten)] + set_option: SetOption, + + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + positional_args: Vec, +} + +impl builtins::Command for SetCommand { + fn takes_plus_options() -> bool { + true + } + + /// Override the default [`builtins::Command::new`] function to handle clap's limitation related + /// to `--`. See [`builtins::parse_known`] for more information + /// TODO: we can safely remove this after the issue is resolved + fn new(args: I) -> Result + where + I: IntoIterator, + { + // + // TODO: This is getting pretty messy; we need to see how to avoid this -- handling from + // leaking into too many commands' custom parsing. + // + + // Apply the same workaround from the default implementation of Command::new to handle '+' + // args. + let mut updated_args = vec![]; + let mut now_parsing_positional_args = false; + let mut next_arg_is_option_value = false; + for (i, arg) in args.into_iter().enumerate() { + if now_parsing_positional_args || next_arg_is_option_value { + updated_args.push(arg); + + next_arg_is_option_value = false; + continue; + } + + if arg == "-" || arg == "--" || (i > 0 && !arg.starts_with(['-', '+'])) { + now_parsing_positional_args = true; + } + + if let Some(plus_options) = arg.strip_prefix("+") { + next_arg_is_option_value = plus_options.ends_with('o'); + for c in plus_options.chars() { + updated_args.push(format!("--+{c}")); + } + } else { + next_arg_is_option_value = arg.starts_with('-') && arg.ends_with('o'); + updated_args.push(arg); + } + } + + let (mut this, rest_args) = brush_core::builtins::try_parse_known::(updated_args)?; + if let Some(args) = rest_args { + this.positional_args.extend(args); + } + Ok(this) + } + + type Error = brush_core::Error; + + #[expect(clippy::too_many_lines)] + #[allow(clippy::useless_let_if_seq)] + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut result = ExecutionResult::success(); + + let mut saw_option = false; + + if let Some(value) = self.print_commands_and_arguments.to_bool() { + context.shell.options.print_commands_and_arguments = value; + saw_option = true; + } + + if let Some(value) = self.export_variables_on_modification.to_bool() { + context.shell.options.export_variables_on_modification = value; + saw_option = true; + } + + if let Some(value) = self.notify_job_termination_immediately.to_bool() { + context.shell.options.notify_job_termination_immediately = value; + saw_option = true; + } + + if let Some(value) = self.exit_on_nonzero_command_exit.to_bool() { + context.shell.options.exit_on_nonzero_command_exit = value; + saw_option = true; + } + + if let Some(value) = self.disable_filename_globbing.to_bool() { + context.shell.options.disable_filename_globbing = value; + saw_option = true; + } + + if let Some(value) = self.remember_command_locations.to_bool() { + context.shell.options.remember_command_locations = value; + saw_option = true; + } + + if let Some(value) = self.place_all_assignment_args_in_command_env.to_bool() { + context + .shell + .options + .place_all_assignment_args_in_command_env = value; + saw_option = true; + } + + if let Some(value) = self.enable_job_control.to_bool() { + context.shell.options.enable_job_control = value; + saw_option = true; + } + + if let Some(value) = self.do_not_execute_commands.to_bool() { + context.shell.options.do_not_execute_commands = value; + saw_option = true; + } + + if let Some(value) = self.real_effective_uid_mismatch.to_bool() { + context.shell.options.real_effective_uid_mismatch = value; + saw_option = true; + } + + if let Some(value) = self.exit_after_one_command.to_bool() { + context.shell.options.exit_after_one_command = value; + saw_option = true; + } + + if let Some(value) = self.treat_unset_variables_as_error.to_bool() { + context.shell.options.treat_unset_variables_as_error = value; + saw_option = true; + } + + if let Some(value) = self.print_shell_input_lines.to_bool() { + context.shell.options.print_shell_input_lines = value; + saw_option = true; + } + + if let Some(value) = self.print_commands_and_arguments.to_bool() { + context.shell.options.print_commands_and_arguments = value; + saw_option = true; + } + + if let Some(value) = self.perform_brace_expansion.to_bool() { + context.shell.options.perform_brace_expansion = value; + saw_option = true; + } + + if let Some(value) = self + .disallow_overwriting_regular_files_via_output_redirection + .to_bool() + { + context + .shell + .options + .disallow_overwriting_regular_files_via_output_redirection = value; + saw_option = true; + } + + if let Some(value) = self.shell_functions_inherit_err_trap.to_bool() { + context.shell.options.shell_functions_inherit_err_trap = value; + saw_option = true; + } + + if let Some(value) = self.enable_bang_style_history_substitution.to_bool() { + context.shell.options.enable_bang_style_history_substitution = value; + saw_option = true; + } + + if let Some(value) = self.do_not_resolve_symlinks_when_changing_dir.to_bool() { + context + .shell + .options + .do_not_resolve_symlinks_when_changing_dir = value; + saw_option = true; + } + + if let Some(value) = self + .shell_functions_inherit_debug_and_return_traps + .to_bool() + { + context + .shell + .options + .shell_functions_inherit_debug_and_return_traps = value; + saw_option = true; + } + + let mut named_options: HashMap = HashMap::new(); + if let Some(option_names) = &self.set_option.disable { + saw_option = true; + if option_names.is_empty() { + for option in brush_core::namedoptions::options( + brush_core::namedoptions::ShellOptionKind::SetO, + ) + .iter() + .sorted_by_key(|option| option.name) + { + let option_value = option.definition.get(&context.shell.options); + let option_value_str = if option_value { "-o" } else { "+o" }; + writeln!(context.stdout(), "set {option_value_str} {}", option.name)?; + } + } else { + for option_name in option_names { + named_options.insert(option_name.to_owned(), false); + } + } + } + if let Some(option_names) = &self.set_option.enable { + saw_option = true; + if option_names.is_empty() { + for option in brush_core::namedoptions::options( + brush_core::namedoptions::ShellOptionKind::SetO, + ) + .iter() + .sorted_by_key(|option| option.name) + { + let option_value = option.definition.get(&context.shell.options); + let option_value_str = if option_value { "on" } else { "off" }; + writeln!(context.stdout(), "{:15}\t{option_value_str}", option.name)?; + } + } else { + for option_name in option_names { + named_options.insert(option_name.to_owned(), true); + } + } + } + + for (option_name, value) in named_options { + if let Some(option_def) = + brush_core::namedoptions::options(brush_core::namedoptions::ShellOptionKind::SetO) + .get(option_name.as_str()) + { + option_def.set(&mut context.shell.options, value); + } else { + result = ExecutionExitCode::InvalidUsage.into(); + } + } + + let skip = match self.positional_args.first() { + Some(x) if x == "-" => { + if self.positional_args.len() > 1 { + context.shell.positional_parameters.clear(); + } + 1 + } + Some(x) if x == "--" => { + context.shell.positional_parameters.clear(); + 1 + } + Some(_) => { + context.shell.positional_parameters.clear(); + 0 + } + None => 0, + }; + + for arg in self.positional_args.iter().skip(skip) { + context.shell.positional_parameters.push(arg.to_owned()); + } + + saw_option = saw_option || !self.positional_args.is_empty(); + + // If we *still* haven't seen any options, then we need to display all variables and + // functions. + if !saw_option { + display_all(&context)?; + } + + Ok(result) + } +} + +fn display_all(context: &brush_core::ExecutionContext<'_>) -> Result<(), brush_core::Error> { + // Display variables. + for (name, var) in context.shell.env.iter().sorted_by_key(|v| v.0) { + if !var.is_enumerable() { + continue; + } + + // TODO: For now, skip all dynamic variables. The current behavior + // of bash is not quite clear. We've empirically found that some + // special variables don't get displayed until they're observed + // at least once. + if matches!(var.value(), variables::ShellValue::Dynamic { .. }) { + continue; + } + + writeln!( + context.stdout(), + "{name}={}", + var.value() + .format(variables::FormatStyle::Basic, context.shell)?, + )?; + } + + // Display functions... unless we're in posix compliance mode. + if !context.shell.options.posix_mode { + for (_name, registration) in context.shell.funcs().iter().sorted_by_key(|v| v.0) { + writeln!(context.stdout(), "{}", registration.definition())?; + } + } + + Ok(()) +} diff --git a/crates/brush-builtins-vendored/src/shift.rs b/crates/brush-builtins-vendored/src/shift.rs new file mode 100644 index 000000000..6fcde5c94 --- /dev/null +++ b/crates/brush-builtins-vendored/src/shift.rs @@ -0,0 +1,36 @@ +use clap::Parser; + +use brush_core::{ExecutionExitCode, ExecutionResult, builtins}; + +/// Shift positional arguments. +#[derive(Parser)] +pub(crate) struct ShiftCommand { + /// Number of positions to shift the arguments by (defaults to 1). + n: Option, +} + +impl builtins::Command for ShiftCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let n = self.n.unwrap_or(1); + + if n < 0 { + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + #[expect(clippy::cast_sign_loss)] + let n = n as usize; + + if n > context.shell.positional_parameters.len() { + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + context.shell.positional_parameters.drain(0..n); + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/shopt.rs b/crates/brush-builtins-vendored/src/shopt.rs new file mode 100644 index 000000000..5fe82eef4 --- /dev/null +++ b/crates/brush-builtins-vendored/src/shopt.rs @@ -0,0 +1,153 @@ +use clap::Parser; +use itertools::Itertools; +use std::io::Write; + +use brush_core::{ExecutionExitCode, ExecutionResult, builtins}; + +/// Manage shopt-style options. +#[derive(Parser)] +pub(crate) struct ShoptCommand { + /// Manage set -o options. + #[arg(short = 'o')] + set_o_names_only: bool, + + /// Print options' current values. + #[arg(short = 'p')] + print: bool, + + /// Suppress typical output. + #[arg(short = 'q')] + quiet: bool, + + /// Set the specified options. + #[arg(short = 's')] + set: bool, + + /// Unset the specified options. + #[arg(short = 'u')] + unset: bool, + + /// Names of options to operate on. + options: Vec, +} + +impl builtins::Command for ShoptCommand { + type Error = brush_core::Error; + + #[allow(clippy::too_many_lines)] + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.set && self.unset { + writeln!( + context.stderr(), + "cannot set and unset shell options simultaneously" + )?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + if self.options.is_empty() { + if self.quiet { + return Ok(ExecutionResult::success()); + } + + // Enumerate all options of the selected type. + let options = if self.set_o_names_only { + brush_core::namedoptions::options(brush_core::namedoptions::ShellOptionKind::SetO) + .iter() + .sorted_by_key(|opt| opt.name) + } else { + brush_core::namedoptions::options(brush_core::namedoptions::ShellOptionKind::Shopt) + .iter() + .sorted_by_key(|opt| opt.name) + }; + + for option in options { + let option_value = option.definition.get(&context.shell.options); + if self.set && !option_value { + continue; + } + if self.unset && option_value { + continue; + } + + if self.print { + if self.set_o_names_only { + let option_value_str = if option_value { "-o" } else { "+o" }; + writeln!(context.stdout(), "set {option_value_str} {}", option.name)?; + } else { + let option_value_str = if option_value { "-s" } else { "-u" }; + writeln!(context.stdout(), "shopt {option_value_str} {}", option.name)?; + } + } else { + let option_value_str = if option_value { "on" } else { "off" }; + writeln!(context.stdout(), "{:15}\t{option_value_str}", option.name)?; + } + } + + Ok(ExecutionResult::success()) + } else { + let mut return_value = ExecutionResult::success(); + + // Enumerate only the specified options. + for option_name in &self.options { + let option_definition = if self.set_o_names_only { + brush_core::namedoptions::options( + brush_core::namedoptions::ShellOptionKind::SetO, + ) + .get(option_name.as_str()) + } else { + brush_core::namedoptions::options( + brush_core::namedoptions::ShellOptionKind::Shopt, + ) + .get(option_name.as_str()) + }; + + if let Some(option_definition) = option_definition { + if self.set { + option_definition.set(&mut context.shell.options, true); + } else if self.unset { + option_definition.set(&mut context.shell.options, false); + } else { + let option_value = option_definition.get(&context.shell.options); + if !option_value { + return_value = ExecutionResult::general_error(); + } + + if !self.quiet { + if self.print { + if self.set_o_names_only { + let option_value_str = if option_value { "-o" } else { "+o" }; + writeln!( + context.stdout(), + "set {option_value_str} {option_name}" + )?; + } else { + let option_value_str = if option_value { "-s" } else { "-u" }; + writeln!( + context.stdout(), + "shopt {option_value_str} {option_name}" + )?; + } + } else { + let option_value_str = if option_value { "on" } else { "off" }; + writeln!(context.stdout(), "{option_name:20}\t{option_value_str}")?; + } + } + } + } else { + writeln!( + context.stderr(), + "{}: {}: invalid shell option name", + context.command_name, + option_name + )?; + return_value = ExecutionResult::general_error(); + } + } + + Ok(return_value) + } + } +} diff --git a/crates/brush-builtins-vendored/src/suspend.rs b/crates/brush-builtins-vendored/src/suspend.rs new file mode 100644 index 000000000..5e31d7342 --- /dev/null +++ b/crates/brush-builtins-vendored/src/suspend.rs @@ -0,0 +1,34 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionExitCode, ExecutionResult, builtins}; + +/// Suspend the shell. +#[derive(Parser)] +pub(crate) struct SuspendCommand { + /// Force suspend login shells. + #[arg(short = 'f')] + force: bool, +} + +impl builtins::Command for SuspendCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if context.shell.options.login_shell && !self.force { + writeln!(context.stderr(), "login shell cannot be suspended")?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + + #[expect(clippy::cast_possible_wrap)] + brush_core::sys::signal::kill_process( + std::process::id() as i32, + brush_core::traps::TrapSignal::Signal(nix::sys::signal::SIGSTOP), + )?; + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/test.rs b/crates/brush-builtins-vendored/src/test.rs new file mode 100644 index 000000000..321cbaccb --- /dev/null +++ b/crates/brush-builtins-vendored/src/test.rs @@ -0,0 +1,53 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ + ErrorKind, ExecutionExitCode, ExecutionParameters, ExecutionResult, Shell, builtins, tests, +}; + +/// Evaluate test expression. +#[derive(Parser)] +#[clap(disable_help_flag = true, disable_version_flag = true)] +pub(crate) struct TestCommand { + #[clap(allow_hyphen_values = true)] + args: Vec, +} + +impl builtins::Command for TestCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut args = self.args.as_slice(); + + if context.command_name == "[" { + match args.last() { + Some(s) if s == "]" => (), + None | Some(_) => { + writeln!(context.stderr(), "[: missing ']'")?; + return Ok(ExecutionExitCode::InvalidUsage.into()); + } + } + + args = &args[0..args.len() - 1]; + } + + if execute_test(context.shell, &context.params, args)? { + Ok(ExecutionResult::success()) + } else { + Ok(ExecutionResult::general_error()) + } + } +} + +fn execute_test( + shell: &mut Shell, + params: &ExecutionParameters, + args: &[String], +) -> Result { + let test_command = + brush_parser::test_command::parse(args).map_err(ErrorKind::TestCommandParseError)?; + tests::eval_expr(&test_command, shell, params) +} diff --git a/crates/brush-builtins-vendored/src/times.rs b/crates/brush-builtins-vendored/src/times.rs new file mode 100644 index 000000000..5342b035b --- /dev/null +++ b/crates/brush-builtins-vendored/src/times.rs @@ -0,0 +1,36 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins, timing}; + +/// Report on usage time. +#[derive(Parser)] +pub(crate) struct TimesCommand {} + +impl builtins::Command for TimesCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let (self_user, self_system) = brush_core::sys::resource::get_self_user_and_system_time()?; + writeln!( + context.stdout(), + "{} {}", + timing::format_duration_non_posixly(&self_user), + timing::format_duration_non_posixly(&self_system), + )?; + + let (children_user, children_system) = + brush_core::sys::resource::get_children_user_and_system_time()?; + writeln!( + context.stdout(), + "{} {}", + timing::format_duration_non_posixly(&children_user), + timing::format_duration_non_posixly(&children_system), + )?; + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/trap.rs b/crates/brush-builtins-vendored/src/trap.rs new file mode 100644 index 000000000..b18fbe269 --- /dev/null +++ b/crates/brush-builtins-vendored/src/trap.rs @@ -0,0 +1,102 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::traps::TrapSignal; +use brush_core::{ExecutionResult, builtins}; + +/// Manage signal traps. +#[derive(Parser)] +pub(crate) struct TrapCommand { + /// List all signal names. + #[arg(short = 'l')] + list_signals: bool, + + /// Print registered trap commands. + #[arg(short = 'p')] + print_trap_commands: bool, + + args: Vec, +} + +impl builtins::Command for TrapCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + mut context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.list_signals { + brush_core::traps::format_signals(context.stdout(), TrapSignal::iterator()) + .map(|()| ExecutionResult::success()) + } else if self.print_trap_commands || self.args.is_empty() { + if !self.args.is_empty() { + for signal_type in &self.args { + Self::display_handlers_for(&context, signal_type.parse()?)?; + } + } else { + Self::display_all_handlers(&context)?; + } + Ok(ExecutionResult::success()) + } else if self.args.len() == 1 { + // When only a single argument is given, it is assumed to be a signal name + // and an indication to remove the handlers for that signal. + let signal = self.args[0].as_str(); + Self::remove_all_handlers(&mut context, signal.parse()?); + Ok(ExecutionResult::success()) + } else if self.args[0] == "-" { + // Alternatively, "-" as the first argument indicates that the next + // argument is a signal name and we need to remove the handlers for that signal. + let signal = self.args[1].as_str(); + Self::remove_all_handlers(&mut context, signal.parse()?); + Ok(ExecutionResult::success()) + } else { + let handler = &self.args[0]; + + let mut signal_types = vec![]; + for signal in &self.args[1..] { + signal_types.push(signal.parse()?); + } + + Self::register_handler(&mut context, signal_types, handler.as_str()); + Ok(ExecutionResult::success()) + } + } +} + +impl TrapCommand { + fn display_all_handlers( + context: &brush_core::ExecutionContext<'_>, + ) -> Result<(), brush_core::Error> { + for (signal, _) in context.shell.traps.iter_handlers() { + Self::display_handlers_for(context, signal)?; + } + Ok(()) + } + + fn display_handlers_for( + context: &brush_core::ExecutionContext<'_>, + signal_type: TrapSignal, + ) -> Result<(), brush_core::Error> { + if let Some(handler) = context.shell.traps.get_handler(signal_type) { + writeln!(context.stdout(), "trap -- '{handler}' {signal_type}")?; + } + Ok(()) + } + + fn remove_all_handlers(context: &mut brush_core::ExecutionContext<'_>, signal: TrapSignal) { + context.shell.traps.remove_handlers(signal); + } + + fn register_handler( + context: &mut brush_core::ExecutionContext<'_>, + signals: Vec, + handler: &str, + ) { + for signal in signals { + context + .shell + .traps + .register_handler(signal, handler.to_owned()); + } + } +} diff --git a/crates/brush-builtins-vendored/src/true_.rs b/crates/brush-builtins-vendored/src/true_.rs new file mode 100644 index 000000000..4dff01372 --- /dev/null +++ b/crates/brush-builtins-vendored/src/true_.rs @@ -0,0 +1,18 @@ +use clap::Parser; + +use brush_core::{ExecutionResult, builtins}; + +/// Return 0. +#[derive(Parser)] +pub(crate) struct TrueCommand {} + +impl builtins::Command for TrueCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + _context: brush_core::ExecutionContext<'_>, + ) -> Result { + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-builtins-vendored/src/type_.rs b/crates/brush-builtins-vendored/src/type_.rs new file mode 100644 index 000000000..dbfba2006 --- /dev/null +++ b/crates/brush-builtins-vendored/src/type_.rs @@ -0,0 +1,217 @@ +use std::io::Write; +use std::path::{Path, PathBuf}; + +use brush_parser::ast; +use clap::Parser; + +use brush_core::sys::fs::PathExt; +use brush_core::{ExecutionResult, Shell, builtins}; + +/// Inspect the type of a named shell item. +#[derive(Parser)] +pub(crate) struct TypeCommand { + /// Display all locations of the specified name, not just the first. + #[arg(short = 'a')] + all_locations: bool, + + /// Don't consider functions when resolving the name. + #[arg(short = 'f')] + suppress_func_lookup: bool, + + /// Force searching by file path, even if the name is an alias, built-in + /// command, or shell function. + #[arg(short = 'P')] + force_path_search: bool, + + /// Show file path only. + #[arg(short = 'p')] + show_path_only: bool, + + /// Only display the type of the specified name. + #[arg(short = 't')] + type_only: bool, + + /// Names to search for. + names: Vec, +} + +enum ResolvedType<'a> { + Alias(String), + Keyword, + Function(&'a ast::FunctionDefinition), + Builtin, + File { path: PathBuf, hashed: bool }, +} + +impl builtins::Command for TypeCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut result = ExecutionResult::success(); + + for name in &self.names { + let resolved_types = self.resolve_types(context.shell, name); + + if resolved_types.is_empty() { + if !self.type_only && !self.force_path_search && !self.show_path_only { + writeln!(context.stderr(), "type: {name} not found")?; + } + + result = ExecutionResult::general_error(); + continue; + } + + for resolved_type in resolved_types { + if self.show_path_only && !matches!(resolved_type, ResolvedType::File { .. }) { + // Do nothing. + } else if self.type_only { + match resolved_type { + ResolvedType::Alias(_) => { + writeln!(context.stdout(), "alias")?; + } + ResolvedType::Keyword => { + writeln!(context.stdout(), "keyword")?; + } + ResolvedType::Function(_) => { + writeln!(context.stdout(), "function")?; + } + ResolvedType::Builtin => { + writeln!(context.stdout(), "builtin")?; + } + ResolvedType::File { path, .. } => { + if self.show_path_only || self.force_path_search { + writeln!(context.stdout(), "{}", path.to_string_lossy())?; + } else { + writeln!(context.stdout(), "file")?; + } + } + } + } else { + match resolved_type { + ResolvedType::Alias(target) => { + writeln!(context.stdout(), "{name} is aliased to '{target}'")?; + } + ResolvedType::Keyword => { + writeln!(context.stdout(), "{name} is a shell keyword")?; + } + ResolvedType::Function(def) => { + writeln!(context.stdout(), "{name} is a function")?; + writeln!(context.stdout(), "{def}")?; + } + ResolvedType::Builtin => { + writeln!(context.stdout(), "{name} is a shell builtin")?; + } + ResolvedType::File { path, hashed } => { + if hashed && self.all_locations && !self.force_path_search { + // Do nothing. When we're displaying all locations, then + // we don't show hashed paths. + } else if self.show_path_only || self.force_path_search { + writeln!(context.stdout(), "{}", path.to_string_lossy())?; + } else if hashed { + writeln!( + context.stdout(), + "{name} is hashed ({path})", + name = name, + path = path.to_string_lossy() + )?; + } else { + writeln!( + context.stdout(), + "{name} is {path}", + name = name, + path = path.to_string_lossy() + )?; + } + } + } + } + + // If we only want the first, then break after the first. + if !self.all_locations { + break; + } + } + } + + Ok(result) + } +} + +impl TypeCommand { + fn resolve_types<'a>(&self, shell: &'a Shell, name: &str) -> Vec> { + let mut types = vec![]; + + if !self.force_path_search { + // Check for aliases. + if let Some(a) = shell.aliases.get(name) { + types.push(ResolvedType::Alias(a.clone())); + if !self.all_locations { + return types; + } + } + + // Check for keywords. + if shell.is_keyword(name) { + types.push(ResolvedType::Keyword); + if !self.all_locations { + return types; + } + } + + // Check for functions. + if !self.suppress_func_lookup { + if let Some(registration) = shell.funcs().get(name) { + types.push(ResolvedType::Function(registration.definition())); + if !self.all_locations { + return types; + } + } + } + + // Check for builtins. + if shell.builtins().get(name).is_some_and(|b| !b.disabled) { + types.push(ResolvedType::Builtin); + if !self.all_locations { + return types; + } + } + } + + // Look in path. + if name.contains(std::path::MAIN_SEPARATOR) { + if shell.absolute_path(Path::new(name)).executable() { + types.push(ResolvedType::File { + path: PathBuf::from(name), + hashed: false, + }); + + if !self.all_locations { + return types; + } + } + } else { + if let Some(path) = shell.program_location_cache.get(name) { + types.push(ResolvedType::File { path, hashed: true }); + if !self.all_locations { + return types; + } + } + + for item in shell.find_executables_in_path(name) { + types.push(ResolvedType::File { + path: item, + hashed: false, + }); + + if !self.all_locations { + return types; + } + } + } + + types + } +} diff --git a/crates/brush-builtins-vendored/src/ulimit.rs b/crates/brush-builtins-vendored/src/ulimit.rs new file mode 100644 index 000000000..330855995 --- /dev/null +++ b/crates/brush-builtins-vendored/src/ulimit.rs @@ -0,0 +1,500 @@ +use clap::{ + Parser, + builder::{IntoResettable, StyledStr}, +}; +use std::{ + io::{self, ErrorKind, Write}, + str::FromStr, +}; + +use brush_core::{ExecutionResult, builtins}; + +#[derive(Clone, Copy)] +enum Unit { + Block, + Bytes, + HalfKBytes, + KBytes, + Micros, + Number, + Seconds, +} + +impl Unit { + const fn scale(self) -> u64 { + match self { + Self::Block | Self::HalfKBytes => 512, + Self::KBytes => 1024, + _ => 1, + } + } +} + +#[derive(Clone, Copy)] +enum Virtual { + Pipe, + VMem, +} + +impl Virtual { + fn get(self) -> std::io::Result<(u64, u64)> { + match self { + Self::Pipe => { + let lim = nix::unistd::PathconfVar::PIPE_BUF as u64 * 512; + Ok((lim, lim)) + } + Self::VMem => rlimit::Resource::AS + .get() + .or_else(|_| rlimit::Resource::VMEM.get()), + } + } + fn set(self, soft: u64, hard: u64) -> std::io::Result<()> { + match self { + Self::Pipe => Err(std::io::Error::from(ErrorKind::Unsupported)), + Self::VMem => rlimit::Resource::AS + .set(soft, hard) + .or_else(|_| rlimit::Resource::VMEM.set(soft, hard)), + } + } + const fn is_supported(self) -> bool { + match self { + Self::Pipe => true, + Self::VMem => { + rlimit::Resource::AS.is_supported() || rlimit::Resource::VMEM.is_supported() + } + } + } +} + +#[derive(Clone, Copy)] +enum Resource { + Phy(rlimit::Resource), + Virt(Virtual), +} + +impl Resource { + fn get(self) -> std::io::Result<(u64, u64)> { + match self { + Self::Phy(res) => res.get(), + Self::Virt(res) => res.get(), + } + } + fn set(self, soft: u64, hard: u64) -> std::io::Result<()> { + match self { + Self::Phy(res) => res.set(soft, hard), + Self::Virt(res) => res.set(soft, hard), + } + } + const fn is_supported(self) -> bool { + match self { + Self::Phy(res) => res.is_supported(), + Self::Virt(res) => res.is_supported(), + } + } +} + +#[derive(Clone, Copy)] +struct ResourceDescription { + resource: Resource, + help: &'static str, + description: &'static str, + short: char, + unit: Unit, +} + +impl ResourceDescription { + const SBSIZE: Self = Self { + resource: Resource::Phy(rlimit::Resource::SBSIZE), + help: "the socket buffer size", + description: "socket buffer size", + short: 'b', + unit: Unit::Bytes, + }; + const CORE: Self = Self { + resource: Resource::Phy(rlimit::Resource::CORE), + help: "the maximum size of core files created", + description: "core file size", + short: 'c', + unit: Unit::Block, + }; + const DATA: Self = Self { + resource: Resource::Phy(rlimit::Resource::DATA), + help: "the maximum size of a process's data segment", + description: "data seg size", + short: 'd', + unit: Unit::KBytes, + }; + const NICE: Self = Self { + resource: Resource::Phy(rlimit::Resource::NICE), + help: "the maximum scheduling priority (`nice`)", + description: "scheduling priority", + short: 'e', + unit: Unit::Number, + }; + const FSIZE: Self = Self { + resource: Resource::Phy(rlimit::Resource::FSIZE), + help: "the maximum size of files written by the shell and its children", + description: "file size", + short: 'f', + unit: Unit::Block, + }; + const SIGPENDING: Self = Self { + resource: Resource::Phy(rlimit::Resource::SIGPENDING), + help: "the maximum number of pending signals", + description: "pending signals", + short: 'i', + unit: Unit::Number, + }; + const MEMLOCK: Self = Self { + resource: Resource::Phy(rlimit::Resource::MEMLOCK), + help: "the maximum size a process may lock into memory", + description: "max locked memory", + short: 'l', + unit: Unit::KBytes, + }; + const KQUEUES: Self = Self { + resource: Resource::Phy(rlimit::Resource::KQUEUES), + help: "the maximum number of kqueues allocated for this process", + description: "max kqueues", + short: 'k', + unit: Unit::Number, + }; + const RSS: Self = Self { + resource: Resource::Phy(rlimit::Resource::RSS), + help: "the maximum resident set size", + description: "max memory size", + short: 'm', + unit: Unit::KBytes, + }; + const LOCKS: Self = Self { + resource: Resource::Phy(rlimit::Resource::LOCKS), + help: "the maximum number of file locks", + description: "file locks", + short: 'x', + unit: Unit::Number, + }; + const NOFILE: Self = Self { + resource: Resource::Phy(rlimit::Resource::NOFILE), + help: "the maximum number of open file descriptors", + description: "open files", + short: 'n', + unit: Unit::Number, + }; + const MSGQUEUE: Self = Self { + resource: Resource::Phy(rlimit::Resource::MSGQUEUE), + help: "the maximum number of bytes in POSIX message queues", + description: "POSIX message queues", + short: 'q', + unit: Unit::Bytes, + }; + const PIPE: Self = Self { + resource: Resource::Virt(Virtual::Pipe), + help: "the pipe buffer size", + description: "pipe size", + short: 'p', + unit: Unit::HalfKBytes, + }; + const RTPRIO: Self = Self { + resource: Resource::Phy(rlimit::Resource::RTPRIO), + help: "the maximum real-time scheduling priority", + description: "real-time priority", + short: 'r', + unit: Unit::Number, + }; + const RTTIME: Self = Self { + resource: Resource::Phy(rlimit::Resource::RTTIME), + help: "the maximum real-time scheduling priority", + description: "real-time non-blocking time", + short: 'R', + unit: Unit::Micros, + }; + const STACK: Self = Self { + resource: Resource::Phy(rlimit::Resource::STACK), + help: "the maximum stack size", + description: "stack size", + short: 's', + unit: Unit::KBytes, + }; + const CPU: Self = Self { + resource: Resource::Phy(rlimit::Resource::CPU), + help: "the maximum amount of cpu time in seconds", + description: "cpu time", + short: 't', + unit: Unit::Seconds, + }; + const NPROC: Self = Self { + resource: Resource::Phy(rlimit::Resource::NPROC), + help: "the maximum number of user processes", + description: "max user processes", + short: 'u', + unit: Unit::Number, + }; + const VMEM: Self = Self { + resource: Resource::Virt(Virtual::VMem), + help: "the size of virtual memory", + description: "virtual memory", + short: 'v', + unit: Unit::KBytes, + }; + const THREADS: Self = Self { + resource: Resource::Phy(rlimit::Resource::THREADS), + help: "the maximum number of threads", + description: "number of threads", + short: 'T', + unit: Unit::Number, + }; + const NPTS: Self = Self { + resource: Resource::Phy(rlimit::Resource::NPTS), + help: "the maximum number of pseudoterminals", + description: "number of pseudoterminals", + short: 'P', + unit: Unit::Number, + }; + + fn get(&self, hard: bool) -> std::io::Result { + let (soft_limit, hard_limit) = self.resource.get()?; + let val = if hard { hard_limit } else { soft_limit }; + + if val == rlimit::INFINITY { + Ok("unlimited".into()) + } else { + Ok(format!("{}", val / self.unit.scale())) + } + } + + fn set(&self, set_hard: bool, value: LimitValue) -> std::io::Result<()> { + let (soft, hard) = self.resource.get()?; + let value = match value { + LimitValue::Soft => soft, + LimitValue::Hard => hard, + LimitValue::Unlimited => rlimit::INFINITY, + LimitValue::Value(v) => v * self.unit.scale(), + LimitValue::Unset => return Ok(()), + }; + + if set_hard { + self.resource.set(soft, value) + } else { + self.resource.set(value, hard) + } + } + + /// Print either soft or hard limit + fn print(&self, context: &brush_core::ExecutionContext<'_>, hard: bool) -> io::Result<()> { + if !self.resource.is_supported() { + return Ok(()); + } + let unit = match self.unit { + Unit::Block => format!("(block, -{})", self.short), + Unit::Bytes => format!("(bytes, -{})", self.short), + Unit::HalfKBytes => format!("(512 bytes, -{})", self.short), + Unit::KBytes => format!("(kbytes, -{})", self.short), + Unit::Micros => format!("(microseconds, -{})", self.short), + Unit::Number => format!("(-{})", self.short), + Unit::Seconds => format!("(seconds, -{})", self.short), + }; + let resource = self.get(hard).unwrap_or_else(|e| format!("{e}")); + writeln!( + context.stdout(), + "{:<26}{:>16} {}", + self.description, + unit, + resource + ) + } + + /// Provide the matching help String + fn help(&self) -> String { + format!( + "{} {}", + self.help, + if self.resource.is_supported() { + "(supported)" + } else { + "(unsupported)" + } + ) + } +} + +impl IntoResettable for ResourceDescription { + fn into_resettable(self) -> clap::builder::Resettable { + clap::builder::Resettable::Value(self.help().into()) + } +} + +#[derive(Debug, Clone, Copy)] +enum LimitValue { + Unset, + Unlimited, + Soft, + Hard, + Value(u64), +} + +impl FromStr for LimitValue { + type Err = ::Err; + fn from_str(s: &str) -> Result { + let v = match s { + "" => Self::Unset, + "unlimited" => Self::Unlimited, + "soft" => Self::Soft, + "hard" => Self::Hard, + _ => Self::Value(s.parse()?), + }; + Ok(v) + } +} + +/// Modify shell resource limits. +/// +/// Provides control over the resources available to the shell and processes +/// it creates, on systems that allow such control. +#[derive(Parser, Debug)] +pub(crate) struct ULimitCommand { + /// use the `soft` resource limit + #[arg(short = 'S')] + soft: bool, + /// use the `hard` resource limit + #[arg(short = 'H')] + hard: bool, + /// all current limits are reported + #[arg(short = 'a')] + all: bool, + /// the maximum socket buffer size + #[arg(short = 'b', default_missing_value = "", num_args(0..=1), help = ResourceDescription::SBSIZE)] + sbsize: Option, + /// the maximum size of core files created + #[arg(short = 'c', default_missing_value = "", num_args(0..=1), help = ResourceDescription::CORE)] + core: Option, + /// the maximum size of a process's data segment + #[arg(short = 'd', default_missing_value = "", num_args(0..=1), help = ResourceDescription::DATA)] + data: Option, + /// the maximum scheduling priority (`nice`) + #[arg(short = 'e', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NICE)] + nice: Option, + /// the maximum size of files written by the shell and its children + #[arg(short = 'f', default_missing_value = "", num_args(0..=1), help = ResourceDescription::FSIZE)] + file_size: Option, + /// the maximum number of pending signals + #[arg(short = 'i', default_missing_value = "", num_args(0..=1), help = ResourceDescription::SIGPENDING)] + sigpending: Option, + /// the maximum size a process may lock into memory + #[arg(short = 'l', default_missing_value = "", num_args(0..=1), help = ResourceDescription::MEMLOCK)] + memlock: Option, + /// the maximum number of kqueues allocated for this process + #[arg(short = 'k', default_missing_value = "", num_args(0..=1), help = ResourceDescription::KQUEUES)] + kqueues: Option, + /// the maximum resident set size + #[arg(short = 'm', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RSS)] + rss: Option, + /// the maximum number of open file descriptors + #[arg(short = 'n', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NOFILE)] + file_open: Option, + /// the pipe buffer size + #[arg(short = 'p', default_missing_value = "", num_args(0..=1), help = ResourceDescription::PIPE)] + pipe: Option, + /// the maximum number of bytes in POSIX message queues + #[arg(short = 'q', default_missing_value = "", num_args(0..=1), help = ResourceDescription::MSGQUEUE)] + msgqueue: Option, + /// the maximum real-time scheduling priority + #[arg(short = 'r', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RTPRIO)] + rtprio: Option, + /// the maximum stack size + #[arg(short = 's', default_missing_value = "", num_args(0..=1), help = ResourceDescription::STACK)] + stack: Option, + /// the maximum amount of cpu time in seconds + #[arg(short = 't', default_missing_value = "", num_args(0..=1), help = ResourceDescription::CPU)] + cpu: Option, + /// the size of virtual memory + #[arg(short = 'u', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NPROC)] + nproc: Option, + /// the size of virtual memory + #[arg(short = 'v', default_missing_value = "", num_args(0..=1), help = ResourceDescription::VMEM)] + vmem: Option, + /// the maximum number of file locks + #[arg(short = 'x', default_missing_value = "", num_args(0..=1), help = ResourceDescription::LOCKS)] + file_lock: Option, + /// the maximum number of pseudoterminals + #[arg(short = 'P', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NPTS)] + npts: Option, + /// real-time non-blocking time + #[arg(short = 'R', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RTTIME)] + rttime: Option, + /// the maximum number of threads + #[arg(short = 'T', default_missing_value = "", num_args(0..=1), help = ResourceDescription::THREADS)] + threads: Option, + + /// argument for the implicit limit (`-f`) + limit: Option, +} + +impl builtins::Command for ULimitCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let exit_code = ExecutionResult::success(); + let mut resources_to_set = Vec::new(); + let mut resources_to_get = Vec::new(); + + let mut set_or_get = |val, descr| { + match val { + Some(LimitValue::Unset) => resources_to_get.push(descr), + Some(v) => resources_to_set.push((descr, v)), + None => {} + } + if self.all { + resources_to_get.push(descr); + } + }; + + set_or_get(self.sbsize, ResourceDescription::SBSIZE); + set_or_get(self.core, ResourceDescription::CORE); + set_or_get(self.data, ResourceDescription::DATA); + set_or_get(self.file_size, ResourceDescription::FSIZE); + set_or_get(self.sigpending, ResourceDescription::SIGPENDING); + set_or_get(self.kqueues, ResourceDescription::KQUEUES); + set_or_get(self.memlock, ResourceDescription::MEMLOCK); + set_or_get(self.rss, ResourceDescription::RSS); + set_or_get(self.file_lock, ResourceDescription::LOCKS); + set_or_get(self.file_open, ResourceDescription::NOFILE); + set_or_get(self.pipe, ResourceDescription::PIPE); + set_or_get(self.npts, ResourceDescription::NPTS); + set_or_get(self.nice, ResourceDescription::NICE); + set_or_get(self.msgqueue, ResourceDescription::MSGQUEUE); + set_or_get(self.rtprio, ResourceDescription::RTPRIO); + set_or_get(self.rttime, ResourceDescription::RTTIME); + set_or_get(self.stack, ResourceDescription::STACK); + set_or_get(self.threads, ResourceDescription::THREADS); + set_or_get(self.cpu, ResourceDescription::CPU); + set_or_get(self.nproc, ResourceDescription::NPROC); + set_or_get(self.vmem, ResourceDescription::VMEM); + + if resources_to_set.is_empty() { + if resources_to_get.is_empty() { + if let Some(fsize) = self.limit { + resources_to_set.push((ResourceDescription::FSIZE, fsize)); + } else { + resources_to_get.push(ResourceDescription::FSIZE); + } + } + } + + for (resource, value) in resources_to_set { + resource.set(self.hard, value)?; + } + + if resources_to_get.len() == 1 { + writeln!(context.stdout(), "{}", resources_to_get[0].get(self.hard)?)?; + } else { + for resource in resources_to_get { + resource.print(&context, self.hard)?; + } + } + + Ok(exit_code) + } +} diff --git a/crates/brush-builtins-vendored/src/umask.rs b/crates/brush-builtins-vendored/src/umask.rs new file mode 100644 index 000000000..43a9d45b1 --- /dev/null +++ b/crates/brush-builtins-vendored/src/umask.rs @@ -0,0 +1,97 @@ +use brush_core::{ErrorKind, ExecutionResult, builtins}; +use cfg_if::cfg_if; +use clap::Parser; +#[cfg(not(target_os = "linux"))] +use nix::sys::stat::Mode; +use std::io::Write; + +/// Manage the process umask. +#[derive(Parser)] +pub(crate) struct UmaskCommand { + /// If MODE is omitted, output in a form that may be reused as input. + #[arg(short = 'p')] + print_roundtrippable: bool, + + /// Makes the output symbolic; otherwise an octal number is given. + #[arg(short = 'S')] + symbolic_output: bool, + + /// Mode mask. + mode: Option, +} + +impl builtins::Command for UmaskCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if let Some(mode) = &self.mode { + if mode.starts_with(|c: char| c.is_digit(8)) { + let parsed = nix::sys::stat::mode_t::from_str_radix(mode.as_str(), 8)?; + set_umask(parsed)?; + } else { + return brush_core::error::unimp("umask setting mode from symbolic value"); + } + } else { + let umask = get_umask()?; + + let formatted = if self.symbolic_output { + let u = symbolic_mask_from_bits((!umask & 0o700) >> 6); + let g = symbolic_mask_from_bits((!umask & 0o070) >> 3); + let o = symbolic_mask_from_bits(!umask & 0o007); + std::format!("u={u},g={g},o={o}") + } else { + std::format!("{umask:04o}") + }; + + if self.print_roundtrippable { + writeln!(context.stdout(), "umask {formatted}")?; + } else { + writeln!(context.stdout(), "{formatted}")?; + } + } + + Ok(ExecutionResult::success()) + } +} + +cfg_if! { + if #[cfg(target_os = "linux")] { + fn get_umask() -> Result { + let umask = procfs::process::Process::myself().ok().and_then(|me| me.status().ok()).and_then(|status| status.umask); + umask.ok_or_else(|| brush_core::ErrorKind::InvalidUmask.into()) + } + } else { + #[expect(clippy::unnecessary_wraps)] + fn get_umask() -> Result { + let u = nix::sys::stat::umask(Mode::empty()); + nix::sys::stat::umask(u); + Ok(u32::from(u.bits())) + } + } +} + +fn set_umask(value: nix::sys::stat::mode_t) -> Result<(), brush_core::Error> { + // value of mode_t can be platform dependent + let mode = nix::sys::stat::Mode::from_bits(value).ok_or_else(|| ErrorKind::InvalidUmask)?; + nix::sys::stat::umask(mode); + Ok(()) +} + +fn symbolic_mask_from_bits(bits: u32) -> String { + let mut result = String::new(); + + if (bits & 0b100) != 0 { + result.push('r'); + } + if (bits & 0b010) != 0 { + result.push('w'); + } + if (bits & 0b001) != 0 { + result.push('x'); + } + + result +} diff --git a/crates/brush-builtins-vendored/src/unalias.rs b/crates/brush-builtins-vendored/src/unalias.rs new file mode 100644 index 000000000..c527f4323 --- /dev/null +++ b/crates/brush-builtins-vendored/src/unalias.rs @@ -0,0 +1,44 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins}; + +/// Unset a shell alias. +#[derive(Parser)] +pub(crate) struct UnaliasCommand { + /// Remove all aliases. + #[arg(short = 'a')] + remove_all: bool, + + /// Names of aliases to operate on. + aliases: Vec, +} + +impl builtins::Command for UnaliasCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + let mut exit_code = ExecutionResult::success(); + + if self.remove_all { + context.shell.aliases.clear(); + } else { + for alias in &self.aliases { + if context.shell.aliases.remove(alias).is_none() { + writeln!( + context.stderr(), + "{}: {}: not found", + context.command_name, + alias + )?; + exit_code = ExecutionResult::general_error(); + } + } + } + + Ok(exit_code) + } +} diff --git a/crates/brush-builtins-vendored/src/unimp.rs b/crates/brush-builtins-vendored/src/unimp.rs new file mode 100644 index 000000000..749eb96da --- /dev/null +++ b/crates/brush-builtins-vendored/src/unimp.rs @@ -0,0 +1,35 @@ +use brush_core::{ExecutionExitCode, builtins, trace_categories}; + +use clap::Parser; + +/// (UNIMPLEMENTED COMMAND) +#[derive(Parser)] +pub(crate) struct UnimplementedCommand { + #[clap(allow_hyphen_values = true)] + args: Vec, + + #[clap(skip)] + declarations: Vec, +} + +impl builtins::Command for UnimplementedCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + tracing::warn!(target: trace_categories::UNIMPLEMENTED, + "unimplemented built-in: {} {}", + context.command_name, + self.args.join(" ") + ); + Ok(ExecutionExitCode::Unimplemented.into()) + } +} + +impl builtins::DeclarationCommand for UnimplementedCommand { + fn set_declarations(&mut self, declarations: Vec) { + self.declarations = declarations; + } +} diff --git a/crates/brush-builtins-vendored/src/unset.rs b/crates/brush-builtins-vendored/src/unset.rs new file mode 100644 index 000000000..7c9ec1bb5 --- /dev/null +++ b/crates/brush-builtins-vendored/src/unset.rs @@ -0,0 +1,122 @@ +use std::borrow::Cow; + +use clap::Parser; + +use brush_core::{ExecutionResult, Shell, ShellValue, builtins, variables::ShellValueUnsetType}; + +/// Unset a variable. +#[derive(Parser)] +pub(crate) struct UnsetCommand { + #[clap(flatten)] + name_interpretation: UnsetNameInterpretation, + + /// Names of variables to unset. + names: Vec, +} + +#[derive(Parser)] +#[clap(group = clap::ArgGroup::new("name-interpretation").multiple(false).required(false))] +pub(crate) struct UnsetNameInterpretation { + /// Treat each name as a shell function. + #[arg(short = 'f', group = "name-interpretation")] + shell_functions: bool, + + /// Treat each name as a shell variable. + #[arg(short = 'v', group = "name-interpretation")] + shell_variables: bool, + + /// Treat each name as a name reference. + #[arg(short = 'n', group = "name-interpretation")] + name_references: bool, +} + +impl UnsetNameInterpretation { + pub const fn unspecified(&self) -> bool { + !self.shell_functions && !self.shell_variables && !self.name_references + } +} + +impl builtins::Command for UnsetCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + // + // TODO: implement nameref + // + if self.name_interpretation.name_references { + return brush_core::error::unimp("unset: name references are not yet implemented"); + } + + let unspecified = self.name_interpretation.unspecified(); + + #[expect(clippy::needless_continue)] + for name in &self.names { + if unspecified || self.name_interpretation.shell_variables { + let parameter = + brush_parser::word::parse_parameter(name, &context.shell.parser_options())?; + + let result = match parameter { + brush_parser::word::Parameter::Positional(_) => continue, + brush_parser::word::Parameter::Special(_) => continue, + brush_parser::word::Parameter::Named(name) => { + context.shell.env.unset(name.as_str())?.is_some() + } + brush_parser::word::Parameter::NamedWithIndex { name, index } => { + unset_array_index(context.shell, name.as_str(), index.as_str())? + } + brush_parser::word::Parameter::NamedWithAllIndices { + name: _, + concatenate: _, + } => continue, + }; + + if result { + continue; + } + } + + // TODO: Deal with readonly functions + if unspecified || self.name_interpretation.shell_functions { + if context.shell.undefine_func(name) { + continue; + } + } + } + + Ok(ExecutionResult::success()) + } +} + +fn unset_array_index( + shell: &mut Shell, + name: &str, + index: &str, +) -> Result { + // First check to see if it's an associative array. + let is_assoc_array = if let Some((_, var)) = shell.env.get(name) { + matches!( + var.value(), + ShellValue::AssociativeArray(_) + | ShellValue::Unset(ShellValueUnsetType::AssociativeArray) + ) + } else { + false + }; + + // Compute which index we should actually use. For indexed arrays, we need to evaluate + // the index string as an arithmetic expression first. + let index_to_use: Cow<'_, str> = if is_assoc_array { + index.into() + } else { + // First evaluate the index expression. + let index_as_expr = brush_parser::arithmetic::parse(index)?; + let evaluated_index = shell.eval_arithmetic(&index_as_expr)?; + evaluated_index.to_string().into() + }; + + // Now we can try to unset, and return the result. + shell.env.unset_index(name, index_to_use.as_ref()) +} diff --git a/crates/brush-builtins-vendored/src/wait.rs b/crates/brush-builtins-vendored/src/wait.rs new file mode 100644 index 000000000..d41bd3f3a --- /dev/null +++ b/crates/brush-builtins-vendored/src/wait.rs @@ -0,0 +1,56 @@ +use clap::Parser; +use std::io::Write; + +use brush_core::{ExecutionResult, builtins, error}; + +/// Wait for jobs to terminate. +#[derive(Parser)] +pub(crate) struct WaitCommand { + /// Wait for specified job to terminate (instead of change status). + #[arg(short = 'f')] + wait_for_terminate: bool, + + /// Wait for a single job to change status; if jobs are specified, waits for + /// the first to change status, and otherwise waits for the next change. + #[arg(short = 'n')] + wait_for_first_or_next: bool, + + /// Name of variable to receive the job ID of the job whose status is indicated. + #[arg(short = 'p', value_name = "VAR_NAME")] + variable_to_receive_id: Option, + + /// Specs of jobs to wait for. + job_specs: Vec, +} + +impl builtins::Command for WaitCommand { + type Error = brush_core::Error; + + async fn execute( + &self, + context: brush_core::ExecutionContext<'_>, + ) -> Result { + if self.wait_for_terminate { + return error::unimp("wait -f"); + } + if self.wait_for_first_or_next { + return error::unimp("wait -n"); + } + if self.variable_to_receive_id.is_some() { + return error::unimp("wait -p"); + } + if !self.job_specs.is_empty() { + return error::unimp("wait with job specs"); + } + + let jobs = context.shell.jobs.wait_all().await?; + + if context.shell.options.enable_job_control { + for job in jobs { + writeln!(context.stdout(), "{job}")?; + } + } + + Ok(ExecutionResult::success()) + } +} diff --git a/crates/brush-core-vendored/src/processes.rs b/crates/brush-core-vendored/src/processes.rs index 015acdab9..cf2cf3793 100644 --- a/crates/brush-core-vendored/src/processes.rs +++ b/crates/brush-core-vendored/src/processes.rs @@ -97,7 +97,7 @@ impl ChildProcess { unsafe { #[expect(clippy::cast_sign_loss)] let handle = OpenProcess(PROCESS_TERMINATE, 0, pid as u32); - if handle != 0 { + if !handle.is_null() { let _ = TerminateProcess(handle, 1); CloseHandle(handle); } diff --git a/crates/pi-natives/Cargo.toml b/crates/pi-natives/Cargo.toml index b19f65b36..eccc5adab 100644 --- a/crates/pi-natives/Cargo.toml +++ b/crates/pi-natives/Cargo.toml @@ -18,7 +18,7 @@ tokio-util = "0.7.16" napi = { version = "3", features = ["napi10", "tokio_rt", "tokio_time"] } napi-derive = "3" brush-core = { version = "0.4.0", path = "../brush-core-vendored" } -brush-builtins = "0.1.0" +brush-builtins = { version = "0.1.0", path = "../brush-builtins-vendored" } parking_lot = "0.12.5" clap = { version = "4", features = ["derive"] } os_pipe = "1" diff --git a/packages/agent/package.json b/packages/agent/package.json index 27a283c6c..6a6d0701f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-agent-core", - "version": "9.8.0", + "version": "10.0.0", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./src/index.ts", diff --git a/packages/ai/package.json b/packages/ai/package.json index 36b073e7b..dbb45bad2 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-ai", - "version": "9.8.0", + "version": "10.0.0", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./src/index.ts", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0bc6530fc..5ed66653f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [10.0.0] - 2026-02-01 ### Added - Added `shell` subcommand for interactive shell console testing with brush-core diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 2600c8cd8..4a2da56e3 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-coding-agent", - "version": "9.8.0", + "version": "10.0.0", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "ompConfig": { diff --git a/packages/natives/package.json b/packages/natives/package.json index 79b690769..cd15a5cda 100644 --- a/packages/natives/package.json +++ b/packages/natives/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-natives", - "version": "9.8.0", + "version": "10.0.0", "description": "Native Rust functionality via N-API", "type": "module", "main": "./src/index.ts", diff --git a/packages/stats/package.json b/packages/stats/package.json index c486b84ac..1acf4a2e2 100644 --- a/packages/stats/package.json +++ b/packages/stats/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/omp-stats", - "version": "9.8.0", + "version": "10.0.0", "description": "Local observability dashboard for pi AI usage statistics", "type": "module", "main": "./src/index.ts", diff --git a/packages/tui/package.json b/packages/tui/package.json index 3e3c2cddf..d09fc8349 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-tui", - "version": "9.8.0", + "version": "10.0.0", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "./src/index.ts", diff --git a/packages/utils/package.json b/packages/utils/package.json index 3b1fb51d4..ad316c71e 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-utils", - "version": "9.8.0", + "version": "10.0.0", "description": "Shared utilities for pi packages", "type": "module", "main": "./src/index.ts", diff --git a/rustfmt.toml b/rustfmt.toml index bd9c95700..4135bab00 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -71,4 +71,4 @@ blank_lines_upper_bound = 1 force_explicit_abi = true hex_literal_case = "Lower" newline_style = "Unix" -ignore = ["crates/brush-core-vendored/**"] +ignore = ["crates/brush-core-vendored/**", "crates/brush-builtins-vendored/**"]