chore: bump version to 10.0.0

This commit is contained in:
can1357
2026-02-01 19:29:36 +01:00
parent dbf4eeaf97
commit c8d5ada528
73 changed files with 10398 additions and 27 deletions
Generated
+40 -8
View File
@@ -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
View File
@@ -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
+7 -7
View File
@@ -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",
File diff suppressed because it is too large Load Diff
+324
View File
@@ -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
+21
View File
@@ -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.
+187
View File
@@ -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)
}
}
+47
View File
@@ -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)
}
}
+393
View File
@@ -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())
}
}
}
+97
View File
@@ -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(())
}
}
+101
View File
@@ -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())
}
}
+34
View File
@@ -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())
}
}
+299
View File
@@ -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)
}
+70
View File
@@ -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)
}
}
+101
View File
@@ -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)
}
}
+138
View File
@@ -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(())
}
}
+179
View File
@@ -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)
}
}
+157
View File
@@ -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())
}
}
+40
View File
@@ -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())
}
}
+396
View File
@@ -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())
}
}
}
+434
View File
@@ -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())
}
}
+153
View File
@@ -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())
}
}
+102
View File
@@ -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())
}
}
+217
View File
@@ -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;
}
}
+122
View File
@@ -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())
}
}
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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/**"]