chore: bump version to 10.0.0
This commit is contained in:
Generated
+40
-8
@@ -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",
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+2269
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,187 @@
|
||||
<div align="center">
|
||||
<img src="https://github.com/user-attachments/assets/19351a8e-7b03-4338-81be-dd5b6d7e5abc"/>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<!-- Primary badges -->
|
||||
<p align="center">
|
||||
<!-- crates.io version badge -->
|
||||
<a href="https://crates.io/crates/brush-shell"><img src="https://img.shields.io/crates/v/brush-shell?style=flat-square"/></a>
|
||||
<!-- msrv badge -->
|
||||
<img src="https://img.shields.io/crates/msrv/brush-shell"/>
|
||||
<!-- LoC badge: badge generation seems broken; temporarily disabled -->
|
||||
<!-- <img src="https://tokei.rs/b1/github/reubeno/brush?category=code"/> -->
|
||||
<!-- license badge -->
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square"/>
|
||||
<!-- CI status badge -->
|
||||
<a href="https://github.com/reubeno/brush/actions/workflows/ci.yaml"><img src="https://github.com/reubeno/brush/actions/workflows/ci.yaml/badge.svg"/></a>
|
||||
<br/>
|
||||
<!-- crates.io download badge -->
|
||||
<a href="https://crates.io/crates/brush-shell"><img src="https://img.shields.io/crates/d/brush-shell?style=flat-square"/></a>
|
||||
<!-- Packaging badges -->
|
||||
<a href="https://repology.org/project/brush/versions">
|
||||
<img src="https://repology.org/badge/tiny-repos/brush.svg" alt="Packaging status"/>
|
||||
</a>
|
||||
<!-- Dependencies badges -->
|
||||
<a href="https://deps.rs/repo/github/reubeno/brush"><img src="https://deps.rs/repo/github/reubeno/brush/status.svg" alt="Dependency status"/></a>
|
||||
<!-- Social badges -->
|
||||
<a href="https://discord.gg/kPRgC9j3Tj">
|
||||
<img src="https://dcbadge.limes.pink/api/server/https://discord.gg/kPRgC9j3Tj?compact=true&style=flat" alt="Discord invite"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a href="https://repology.org/project/brush/versions">
|
||||
</a>
|
||||
|
||||
</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
`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.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/0e64d1b9-7e4e-43be-8593-6c1b9607ac52" width="80%"/>
|
||||
</p>
|
||||
|
||||
`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 :).
|
||||
|
||||
<br/>
|
||||
|
||||
## 📝 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._
|
||||
|
||||
<details open>
|
||||
<summary>🚀 <b>Installing prebuilt binaries from GitHub</b></summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>🚀 <b>Installing prebuilt binaries via `cargo binstall`</b></summary>
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> 🔨 <b>Installing from sources</b></summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🐧 <b>Installing using Nix</b></summary>
|
||||
|
||||
If you are a Nix user, you can use the registered version:
|
||||
|
||||
```bash
|
||||
nix run 'github:NixOS/nixpkgs/nixpkgs-unstable#brush' -- --version
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🐧 <b>Installing on Arch Linux</b></summary>
|
||||
|
||||
Arch Linux users can install `brush` from the official [extra repository](https://archlinux.org/packages/extra/x86_64/brush/):
|
||||
|
||||
```bash
|
||||
pacman -S brush
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🍺 <b>Installing using Homebrew</b></summary>
|
||||
|
||||
Homebrew users can install using [the `brush` formula](https://formulae.brew.sh/formula/brush):
|
||||
|
||||
```bash
|
||||
brew install brush
|
||||
```
|
||||
</details>
|
||||
|
||||
## 👥 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.
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for AliasCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for BgCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<BindKeyMap>,
|
||||
/// 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<String>,
|
||||
/// Remove all bindings for the given named function.
|
||||
#[arg(short = 'u', value_name = "FUNC_NAME")]
|
||||
remove_func_bindings: Option<String>,
|
||||
/// Remove the binding for the given key sequence.
|
||||
#[arg(short = 'r', value_name = "KEY_SEQ")]
|
||||
remove_key_seq_binding: Option<String>,
|
||||
/// Import bindings from the given file.
|
||||
#[arg(short = 'f', value_name = "PATH")]
|
||||
bindings_file: Option<String>,
|
||||
/// Bind key sequence to command.
|
||||
#[arg(short = 'x', value_name = "BINDING")]
|
||||
key_seq_bindings: Vec<String>,
|
||||
/// List key sequence bindings.
|
||||
#[arg(short = 'X')]
|
||||
list_key_seq_bindings: bool,
|
||||
/// Key sequence binding to readline function or command.
|
||||
key_sequence: Option<String>,
|
||||
}
|
||||
|
||||
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<brush_core::ExecutionResult, Self::Error> {
|
||||
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<Mutex<dyn interfaces::KeyBindings>>,
|
||||
context: &brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
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<Vec<interfaces::KeyStroke>, 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+<key> 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, brush_core::Error> {
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::ExecutionResult, Self::Error> {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -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<usize>,
|
||||
|
||||
/// 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<brush_core::ExecutionResult, Self::Error> {
|
||||
self.command_group.execute(&mut context).await
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandGroup {
|
||||
async fn execute(
|
||||
&self,
|
||||
context: &mut brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
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<brush_core::ExecutionResult, brush_core::Error> {
|
||||
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<brush_core::ExecutionResult, brush_core::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<S: brush_core::ShellBuilderState> ShellBuilderExt for brush_core::ShellBuilder<S> {
|
||||
fn default_builtins(self, set: BuiltinSet) -> Self {
|
||||
self.builtins(crate::default_builtins(set))
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
impl builtins::DeclarationCommand for BuiltinCommand {
|
||||
fn set_declarations(&mut self, args: Vec<brush_core::CommandArg>) {
|
||||
self.args = args;
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for BuiltinCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PathBuf>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CdCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
@@ -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<String, brush_core::Error> {
|
||||
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<I: Iterator<Item = S>, S: AsRef<str>>(
|
||||
_context: brush_core::ExecutionContext<'_>,
|
||||
_args: I,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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<ExecutionResult, Self::Error> {
|
||||
// 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<FoundCommand> {
|
||||
// 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<ExecutionResult, brush_core::Error> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CompleteOption>,
|
||||
|
||||
/// Actions to apply to generate completions.
|
||||
#[arg(short = 'A')]
|
||||
actions: Vec<CompleteAction>,
|
||||
|
||||
/// File glob pattern to be expanded to generate completions.
|
||||
#[arg(short = 'G', allow_hyphen_values = true, value_name = "GLOB")]
|
||||
glob_pattern: Option<String>,
|
||||
|
||||
/// List of words that will be considered as completions.
|
||||
#[arg(short = 'W', allow_hyphen_values = true)]
|
||||
word_list: Option<String>,
|
||||
|
||||
/// Name of a shell function to invoke to generate completions.
|
||||
#[arg(short = 'F', allow_hyphen_values = true, value_name = "FUNC_NAME")]
|
||||
function_name: Option<String>,
|
||||
|
||||
/// Command to execute to generate completions.
|
||||
#[arg(short = 'C', allow_hyphen_values = true)]
|
||||
command: Option<String>,
|
||||
|
||||
/// Pattern used as filter for completions.
|
||||
#[arg(short = 'X', allow_hyphen_values = true, value_name = "PATTERN")]
|
||||
filter_pattern: Option<String>,
|
||||
|
||||
/// Prefix pattern used as filter for completions.
|
||||
#[arg(short = 'P', allow_hyphen_values = true)]
|
||||
prefix: Option<String>,
|
||||
|
||||
/// Suffix pattern used as filter for completions.
|
||||
#[arg(short = 'S', allow_hyphen_values = true)]
|
||||
suffix: Option<String>,
|
||||
|
||||
/// 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<CompleteAction> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CompleteCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<bool, brush_core::Error> {
|
||||
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<bool, brush_core::Error> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CompGenCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<CompleteOption>,
|
||||
#[arg(long = concat!("+o"), hide = true)]
|
||||
disabled_options: Vec<CompleteOption>,
|
||||
|
||||
/// If specified, scopes updates to completions of the named commands.
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CompOptCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<CompleteOption, bool>) {
|
||||
Self::set_options(&mut spec.options, options);
|
||||
}
|
||||
|
||||
fn set_options(
|
||||
target_options: &mut completion::GenerationOptions,
|
||||
options: &HashMap<CompleteOption, bool>,
|
||||
) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::ExecutionResult, Self::Error> {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum DeclareVerb {
|
||||
Declare,
|
||||
Local,
|
||||
Readonly,
|
||||
}
|
||||
|
||||
impl builtins::DeclarationCommand for DeclareCommand {
|
||||
fn set_declarations(&mut self, declarations: Vec<brush_core::CommandArg>) {
|
||||
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<brush_core::ExecutionResult, Self::Error> {
|
||||
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<bool, brush_core::Error> {
|
||||
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<bool, brush_core::Error> {
|
||||
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<String>, Option<ShellValueLiteral>, 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<fancy_regex::Regex> =
|
||||
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<Box<dyn Fn((&String, &ShellVariable)) -> 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(())
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::ExecutionResult, Self::Error> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for DotCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// TODO: Handle trap inheritance.
|
||||
context
|
||||
.shell
|
||||
.source_script(
|
||||
Path::new(&self.script_path),
|
||||
self.script_args.iter(),
|
||||
&context.params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let (mut this, rest_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
|
||||
if let Some(args) = rest_args {
|
||||
this.args.extend(args);
|
||||
}
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for EnableCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for EvalCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ExecCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<i32>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ExitCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
impl builtins::DeclarationCommand for ExportCommand {
|
||||
fn set_declarations(&mut self, declarations: Vec<brush_core::CommandArg>) {
|
||||
self.declarations = declarations;
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for ExportCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<ExecutionResult, brush_core::Error> {
|
||||
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(())
|
||||
}
|
||||
@@ -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<String, builtins::Registration> {
|
||||
let mut m = HashMap::<String, builtins::Registration>::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::<break_::BreakCommand>().special());
|
||||
#[cfg(feature = "builtin.colon")]
|
||||
m.insert(
|
||||
":".into(),
|
||||
simple_builtin::<colon::ColonCommand>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.continue")]
|
||||
m.insert(
|
||||
"continue".into(),
|
||||
builtin::<continue_::ContinueCommand>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.dot")]
|
||||
m.insert(".".into(), builtin::<dot::DotCommand>().special());
|
||||
#[cfg(feature = "builtin.eval")]
|
||||
m.insert("eval".into(), builtin::<eval::EvalCommand>().special());
|
||||
#[cfg(all(feature = "builtin.exec", unix))]
|
||||
m.insert("exec".into(), builtin::<exec::ExecCommand>().special());
|
||||
#[cfg(feature = "builtin.exit")]
|
||||
m.insert("exit".into(), builtin::<exit::ExitCommand>().special());
|
||||
#[cfg(feature = "builtin.export")]
|
||||
m.insert(
|
||||
"export".into(),
|
||||
decl_builtin::<export::ExportCommand>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.return")]
|
||||
m.insert(
|
||||
"return".into(),
|
||||
builtin::<return_::ReturnCommand>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.set")]
|
||||
m.insert("set".into(), builtin::<set::SetCommand>().special());
|
||||
#[cfg(feature = "builtin.shift")]
|
||||
m.insert("shift".into(), builtin::<shift::ShiftCommand>().special());
|
||||
#[cfg(feature = "builtin.trap")]
|
||||
m.insert("trap".into(), builtin::<trap::TrapCommand>().special());
|
||||
#[cfg(feature = "builtin.unset")]
|
||||
m.insert("unset".into(), builtin::<unset::UnsetCommand>().special());
|
||||
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert(
|
||||
"readonly".into(),
|
||||
decl_builtin::<declare::DeclareCommand>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.times")]
|
||||
m.insert("times".into(), builtin::<times::TimesCommand>().special());
|
||||
|
||||
//
|
||||
// Non-special builtins
|
||||
//
|
||||
|
||||
#[cfg(feature = "builtin.alias")]
|
||||
m.insert("alias".into(), builtin::<alias::AliasCommand>()); // TODO: should be exec_declaration_builtin
|
||||
#[cfg(feature = "builtin.bg")]
|
||||
m.insert("bg".into(), builtin::<bg::BgCommand>());
|
||||
#[cfg(feature = "builtin.cd")]
|
||||
m.insert("cd".into(), builtin::<cd::CdCommand>());
|
||||
#[cfg(feature = "builtin.command")]
|
||||
m.insert("command".into(), builtin::<command::CommandCommand>());
|
||||
#[cfg(feature = "builtin.false")]
|
||||
m.insert("false".into(), builtin::<false_::FalseCommand>());
|
||||
#[cfg(feature = "builtin.fg")]
|
||||
m.insert("fg".into(), builtin::<fg::FgCommand>());
|
||||
#[cfg(feature = "builtin.getopts")]
|
||||
m.insert("getopts".into(), builtin::<getopts::GetOptsCommand>());
|
||||
#[cfg(feature = "builtin.hash")]
|
||||
m.insert("hash".into(), builtin::<hash::HashCommand>());
|
||||
#[cfg(feature = "builtin.help")]
|
||||
m.insert("help".into(), builtin::<help::HelpCommand>());
|
||||
#[cfg(feature = "builtin.jobs")]
|
||||
m.insert("jobs".into(), builtin::<jobs::JobsCommand>());
|
||||
#[cfg(all(feature = "builtin.kill", unix))]
|
||||
m.insert("kill".into(), builtin::<kill::KillCommand>());
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert("local".into(), decl_builtin::<declare::DeclareCommand>());
|
||||
#[cfg(feature = "builtin.pwd")]
|
||||
m.insert("pwd".into(), builtin::<pwd::PwdCommand>());
|
||||
#[cfg(feature = "builtin.read")]
|
||||
m.insert("read".into(), builtin::<read::ReadCommand>());
|
||||
#[cfg(feature = "builtin.true")]
|
||||
m.insert("true".into(), builtin::<true_::TrueCommand>());
|
||||
#[cfg(feature = "builtin.type")]
|
||||
m.insert("type".into(), builtin::<type_::TypeCommand>());
|
||||
#[cfg(all(feature = "builtin.ulimit", unix))]
|
||||
m.insert("ulimit".into(), builtin::<ulimit::ULimitCommand>());
|
||||
#[cfg(all(feature = "builtin.umask", unix))]
|
||||
m.insert("umask".into(), builtin::<umask::UmaskCommand>());
|
||||
#[cfg(feature = "builtin.unalias")]
|
||||
m.insert("unalias".into(), builtin::<unalias::UnaliasCommand>());
|
||||
#[cfg(feature = "builtin.wait")]
|
||||
m.insert("wait".into(), builtin::<wait::WaitCommand>());
|
||||
|
||||
#[cfg(feature = "builtin.fc")]
|
||||
m.insert("fc".into(), builtin::<fc::FcCommand>());
|
||||
|
||||
if matches!(set, BuiltinSet::BashMode) {
|
||||
#[cfg(feature = "builtin.builtin")]
|
||||
m.insert(
|
||||
"builtin".into(),
|
||||
raw_arg_builtin::<builtin_::BuiltinCommand>(),
|
||||
);
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert("declare".into(), decl_builtin::<declare::DeclareCommand>());
|
||||
#[cfg(feature = "builtin.echo")]
|
||||
m.insert("echo".into(), builtin::<echo::EchoCommand>());
|
||||
#[cfg(feature = "builtin.enable")]
|
||||
m.insert("enable".into(), builtin::<enable::EnableCommand>());
|
||||
#[cfg(feature = "builtin.let")]
|
||||
m.insert("let".into(), builtin::<let_::LetCommand>());
|
||||
#[cfg(feature = "builtin.mapfile")]
|
||||
m.insert("mapfile".into(), builtin::<mapfile::MapFileCommand>());
|
||||
#[cfg(feature = "builtin.mapfile")]
|
||||
m.insert("readarray".into(), builtin::<mapfile::MapFileCommand>());
|
||||
#[cfg(all(feature = "builtin.printf", any(unix, windows)))]
|
||||
m.insert("printf".into(), builtin::<printf::PrintfCommand>());
|
||||
#[cfg(feature = "builtin.shopt")]
|
||||
m.insert("shopt".into(), builtin::<shopt::ShoptCommand>());
|
||||
#[cfg(feature = "builtin.dot")]
|
||||
m.insert("source".into(), builtin::<dot::DotCommand>().special());
|
||||
#[cfg(all(feature = "builtin.suspend", unix))]
|
||||
m.insert("suspend".into(), builtin::<suspend::SuspendCommand>());
|
||||
#[cfg(feature = "builtin.test")]
|
||||
m.insert("test".into(), builtin::<test::TestCommand>());
|
||||
#[cfg(feature = "builtin.test")]
|
||||
m.insert("[".into(), builtin::<test::TestCommand>());
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert("typeset".into(), decl_builtin::<declare::DeclareCommand>());
|
||||
|
||||
// Completion builtins
|
||||
#[cfg(feature = "builtin.complete")]
|
||||
m.insert("complete".into(), builtin::<complete::CompleteCommand>());
|
||||
#[cfg(feature = "builtin.compgen")]
|
||||
m.insert("compgen".into(), builtin::<complete::CompGenCommand>());
|
||||
#[cfg(feature = "builtin.compopt")]
|
||||
m.insert("compopt".into(), builtin::<complete::CompOptCommand>());
|
||||
|
||||
// Dir stack builtins
|
||||
#[cfg(feature = "builtin.dirs")]
|
||||
m.insert("dirs".into(), builtin::<dirs::DirsCommand>());
|
||||
#[cfg(feature = "builtin.popd")]
|
||||
m.insert("popd".into(), builtin::<popd::PopdCommand>());
|
||||
#[cfg(feature = "builtin.pushd")]
|
||||
m.insert("pushd".into(), builtin::<pushd::PushdCommand>());
|
||||
|
||||
// Input configuration builtins
|
||||
#[cfg(feature = "builtin.bind")]
|
||||
m.insert("bind".into(), builtin::<bind::BindCommand>());
|
||||
|
||||
// History
|
||||
#[cfg(feature = "builtin.history")]
|
||||
m.insert("history".into(), builtin::<history::HistoryCommand>());
|
||||
|
||||
// TODO: implement caller builtin
|
||||
m.insert("caller".into(), builtin::<unimp::UnimplementedCommand>());
|
||||
|
||||
// TODO: implement disown builtin
|
||||
m.insert("disown".into(), builtin::<unimp::UnimplementedCommand>());
|
||||
|
||||
// TODO: implement logout builtin
|
||||
m.insert("logout".into(), builtin::<unimp::UnimplementedCommand>());
|
||||
}
|
||||
|
||||
//
|
||||
// Brush-specific builtins.
|
||||
//
|
||||
#[cfg(feature = "builtin.brushinfo")]
|
||||
m.insert("brushinfo".into(), builtin::<brushinfo::BrushInfoCommand>());
|
||||
|
||||
m
|
||||
}
|
||||
@@ -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<ExecutionResult, Self::Error> {
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// First command in range (number or string prefix).
|
||||
#[arg(value_name = "FIRST", allow_hyphen_values = true)]
|
||||
first: Option<String>,
|
||||
|
||||
/// Last command in range (number or string prefix).
|
||||
#[arg(value_name = "LAST", allow_hyphen_values = true)]
|
||||
last: Option<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for FcCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
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<ExecutionResult, brush_core::Error> {
|
||||
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<usize> = 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<ExecutionResult, brush_core::Error> {
|
||||
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<usize, brush_core::Error> {
|
||||
// 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::<i64>() 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<String, brush_core::Error> {
|
||||
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<usize, brush_core::Error> {
|
||||
// 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)
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for FgCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let (mut this, rest_args) = brush_core::builtins::try_parse_known::<Self>(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<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut args = HashMap::<char, bool>::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)
|
||||
}
|
||||
}
|
||||
@@ -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<PathBuf>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for HashCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for HelpCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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()
|
||||
}
|
||||
@@ -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<i64>,
|
||||
|
||||
/// 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<Option<String>>,
|
||||
|
||||
/// 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<Option<String>>,
|
||||
|
||||
/// 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<Option<String>>,
|
||||
|
||||
/// 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<Option<String>>,
|
||||
|
||||
/// History-expands positional arguments and displays them.
|
||||
#[arg(short = 'p', num_args = 0.., value_name = "ARG")]
|
||||
expand_args: Option<Vec<String>>,
|
||||
|
||||
/// Appends positional arguments as an entry in the current session.
|
||||
#[arg(short = 's', num_args = 0.., value_name = "ARG")]
|
||||
append_args_to_session: Option<Vec<String>>,
|
||||
|
||||
/// Arguments.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
struct HistoryConfig {
|
||||
default_history_file_path: Option<PathBuf>,
|
||||
time_format: Option<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for HistoryCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
// 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<ExecutionResult, brush_core::Error> {
|
||||
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<usize> = 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<usize>,
|
||||
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<PathBuf>,
|
||||
option: Option<&String>,
|
||||
) -> Option<PathBuf> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for JobsCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// Number of the signal to send.
|
||||
#[arg(short = 'n', value_name = "SIG_NUM")]
|
||||
signal_number: Option<usize>,
|
||||
|
||||
//
|
||||
// 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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for KillCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// 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::<i32>()?;
|
||||
|
||||
// It's a pid.
|
||||
sys::signal::kill_process(pid, trap_signal)?;
|
||||
}
|
||||
}
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_signals(
|
||||
context: &brush_core::ExecutionContext<'_>,
|
||||
signals: &[String],
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
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::<i32>() {
|
||||
// bash compatibility. `SIGHUP` -> `HUP`
|
||||
TrapSignal::try_from(n).map(|s| {
|
||||
PrintSignal::Name(s.as_str().strip_prefix("SIG").unwrap_or(s.as_str()))
|
||||
})
|
||||
} else {
|
||||
TrapSignal::try_from(s.as_str()).map(|sig| {
|
||||
i32::try_from(sig).map_or(PrintSignal::Name(sig.as_str()), PrintSignal::Num)
|
||||
})
|
||||
};
|
||||
|
||||
match signal {
|
||||
Ok(PrintSignal::Num(n)) => {
|
||||
writeln!(context.stdout(), "{n}")?;
|
||||
}
|
||||
Ok(PrintSignal::Name(s)) => {
|
||||
writeln!(context.stdout(), "{s}")?;
|
||||
}
|
||||
Err(e) => {
|
||||
writeln!(context.stderr(), "{e}")?;
|
||||
exit_code = 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)
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for LetCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<bool> {
|
||||
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<bool> {
|
||||
match (self._enable, self._disable) {
|
||||
(true, false) => Some(true),
|
||||
(false, true) => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// 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<brush_core::ExecutionResult, Self::Error> {
|
||||
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<variables::ArrayLiteral, brush_core::Error> {
|
||||
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<Option<brush_core::terminal::AutoModeGuard>, 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)
|
||||
}
|
||||
@@ -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<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// Format string + arguments to the format string.
|
||||
#[arg(trailing_var_arg = true, required = true, allow_hyphen_values = true)]
|
||||
format_and_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for PrintfCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
if let Some(variable_name) = &self.output_variable {
|
||||
// Format to a u8 vector.
|
||||
let mut result: Vec<u8> = 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<Item = impl AsRef<str>>,
|
||||
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<Vec<format::FormatItem<format::EscapedChar>>, brush_core::Error> {
|
||||
let format_items: Result<Vec<_>, _> =
|
||||
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<Item = impl AsRef<str>>,
|
||||
) -> Result<String> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// Optionally, a delimiter to use other than a newline character.
|
||||
#[clap(short = 'd')]
|
||||
delimiter: Option<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// 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<usize>,
|
||||
|
||||
/// Read exactly N characters, ignoring any specified delimiter.
|
||||
#[clap(short = 'N', value_name = "COUNT")]
|
||||
return_after_n_chars_no_delimiter: Option<usize>,
|
||||
|
||||
/// Prompt to display before reading.
|
||||
#[clap(short = 'p')]
|
||||
prompt: Option<String>,
|
||||
|
||||
/// 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<usize>,
|
||||
|
||||
/// File descriptor to read from instead of stdin.
|
||||
#[clap(short = 'u', name = "FD")]
|
||||
fd_num_to_read: Option<u8>,
|
||||
|
||||
/// Optionally, names of variables to receive read input.
|
||||
variable_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ReadCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<Option<String>, 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<Option<brush_core::terminal::AutoModeGuard>, 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<usize>) -> VecDeque<String> {
|
||||
// Separate out the chars to split by.
|
||||
let ifs_chars = ifs.chars().collect::<Vec<_>>();
|
||||
|
||||
// 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::<Vec<_>>();
|
||||
|
||||
// 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", ""]));
|
||||
}
|
||||
}
|
||||
@@ -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<i32>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ReturnCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<String>>,
|
||||
#[arg(long = concat!("+o"), name = "setopt_disable", hide = true, num_args=0..=1)]
|
||||
disable: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 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<bool>,
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
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<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
//
|
||||
// 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::<Self>(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<ExecutionResult, Self::Error> {
|
||||
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<String, bool> = 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(())
|
||||
}
|
||||
@@ -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<i32>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ShiftCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ShoptCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for TestCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<bool, brush_core::Error> {
|
||||
let test_command =
|
||||
brush_parser::test_command::parse(args).map_err(ErrorKind::TestCommandParseError)?;
|
||||
tests::eval_expr(&test_command, shell, params)
|
||||
}
|
||||
@@ -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<ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for TrapCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
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<TrapSignal>,
|
||||
handler: &str,
|
||||
) {
|
||||
for signal in signals {
|
||||
context
|
||||
.shell
|
||||
.traps
|
||||
.register_handler(signal, handler.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<brush_core::ExecutionResult, Self::Error> {
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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<brush_core::ExecutionResult, Self::Error> {
|
||||
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<ResolvedType<'a>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<String> {
|
||||
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<StyledStr> for ResourceDescription {
|
||||
fn into_resettable(self) -> clap::builder::Resettable<StyledStr> {
|
||||
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 = <u64 as FromStr>::Err;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<LimitValue>,
|
||||
/// the maximum size of core files created
|
||||
#[arg(short = 'c', default_missing_value = "", num_args(0..=1), help = ResourceDescription::CORE)]
|
||||
core: Option<LimitValue>,
|
||||
/// the maximum size of a process's data segment
|
||||
#[arg(short = 'd', default_missing_value = "", num_args(0..=1), help = ResourceDescription::DATA)]
|
||||
data: Option<LimitValue>,
|
||||
/// the maximum scheduling priority (`nice`)
|
||||
#[arg(short = 'e', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NICE)]
|
||||
nice: Option<LimitValue>,
|
||||
/// 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<LimitValue>,
|
||||
/// the maximum number of pending signals
|
||||
#[arg(short = 'i', default_missing_value = "", num_args(0..=1), help = ResourceDescription::SIGPENDING)]
|
||||
sigpending: Option<LimitValue>,
|
||||
/// the maximum size a process may lock into memory
|
||||
#[arg(short = 'l', default_missing_value = "", num_args(0..=1), help = ResourceDescription::MEMLOCK)]
|
||||
memlock: Option<LimitValue>,
|
||||
/// the maximum number of kqueues allocated for this process
|
||||
#[arg(short = 'k', default_missing_value = "", num_args(0..=1), help = ResourceDescription::KQUEUES)]
|
||||
kqueues: Option<LimitValue>,
|
||||
/// the maximum resident set size
|
||||
#[arg(short = 'm', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RSS)]
|
||||
rss: Option<LimitValue>,
|
||||
/// the maximum number of open file descriptors
|
||||
#[arg(short = 'n', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NOFILE)]
|
||||
file_open: Option<LimitValue>,
|
||||
/// the pipe buffer size
|
||||
#[arg(short = 'p', default_missing_value = "", num_args(0..=1), help = ResourceDescription::PIPE)]
|
||||
pipe: Option<LimitValue>,
|
||||
/// the maximum number of bytes in POSIX message queues
|
||||
#[arg(short = 'q', default_missing_value = "", num_args(0..=1), help = ResourceDescription::MSGQUEUE)]
|
||||
msgqueue: Option<LimitValue>,
|
||||
/// the maximum real-time scheduling priority
|
||||
#[arg(short = 'r', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RTPRIO)]
|
||||
rtprio: Option<LimitValue>,
|
||||
/// the maximum stack size
|
||||
#[arg(short = 's', default_missing_value = "", num_args(0..=1), help = ResourceDescription::STACK)]
|
||||
stack: Option<LimitValue>,
|
||||
/// the maximum amount of cpu time in seconds
|
||||
#[arg(short = 't', default_missing_value = "", num_args(0..=1), help = ResourceDescription::CPU)]
|
||||
cpu: Option<LimitValue>,
|
||||
/// the size of virtual memory
|
||||
#[arg(short = 'u', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NPROC)]
|
||||
nproc: Option<LimitValue>,
|
||||
/// the size of virtual memory
|
||||
#[arg(short = 'v', default_missing_value = "", num_args(0..=1), help = ResourceDescription::VMEM)]
|
||||
vmem: Option<LimitValue>,
|
||||
/// the maximum number of file locks
|
||||
#[arg(short = 'x', default_missing_value = "", num_args(0..=1), help = ResourceDescription::LOCKS)]
|
||||
file_lock: Option<LimitValue>,
|
||||
/// the maximum number of pseudoterminals
|
||||
#[arg(short = 'P', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NPTS)]
|
||||
npts: Option<LimitValue>,
|
||||
/// real-time non-blocking time
|
||||
#[arg(short = 'R', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RTTIME)]
|
||||
rttime: Option<LimitValue>,
|
||||
/// the maximum number of threads
|
||||
#[arg(short = 'T', default_missing_value = "", num_args(0..=1), help = ResourceDescription::THREADS)]
|
||||
threads: Option<LimitValue>,
|
||||
|
||||
/// argument for the implicit limit (`-f`)
|
||||
limit: Option<LimitValue>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ULimitCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for UmaskCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<u32, brush_core::Error> {
|
||||
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<u32, brush_core::Error> {
|
||||
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
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for UnaliasCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
#[clap(skip)]
|
||||
declarations: Vec<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
impl builtins::Command for UnimplementedCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
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<brush_core::CommandArg>) {
|
||||
self.declarations = declarations;
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[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<brush_core::ExecutionResult, Self::Error> {
|
||||
//
|
||||
// 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<bool, brush_core::Error> {
|
||||
// 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())
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
/// Specs of jobs to wait for.
|
||||
job_specs: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for WaitCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [10.0.0] - 2026-02-01
|
||||
### Added
|
||||
|
||||
- Added `shell` subcommand for interactive shell console testing with brush-core
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -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/**"]
|
||||
|
||||
Reference in New Issue
Block a user