build(deps): vendored brush-core crate locally for development and patching
- Vendored brush-core crate locally to enable local development and patching. - Added patch.crates-io section in Cargo.toml to redirect brush-core dependency to vendored version at crates/brush-core-vendored. - Updated pi-natives Cargo.toml to use local path dependency for brush-core instead of external crate version.
This commit is contained in:
Generated
+7
-2
@@ -95,6 +95,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
@@ -270,9 +276,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "brush-core"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a3ad2f2d4eb45ef7f11e74bc1a0816c8f3dce298072352c6b3b41a2c4bfdb74"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"bon",
|
||||
|
||||
@@ -9,6 +9,9 @@ license = "MIT"
|
||||
authors = ["Can Boluk"]
|
||||
repository = "https://github.com/can1357/oh-my-pi"
|
||||
|
||||
[patch.crates-io]
|
||||
brush-core = { path = "crates/brush-core-vendored" }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
|
||||
Generated
+1658
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,258 @@
|
||||
# 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-core"
|
||||
version = "0.4.0"
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Reusable core of 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"
|
||||
|
||||
[lib]
|
||||
name = "brush_core"
|
||||
path = "src/lib.rs"
|
||||
bench = false
|
||||
|
||||
[[example]]
|
||||
name = "call-func"
|
||||
path = "examples/call-func.rs"
|
||||
|
||||
[[example]]
|
||||
name = "custom-builtin"
|
||||
path = "examples/custom-builtin.rs"
|
||||
|
||||
[dependencies.async-recursion]
|
||||
version = "1.1.1"
|
||||
|
||||
[dependencies.async-trait]
|
||||
version = "0.1.89"
|
||||
|
||||
[dependencies.bon]
|
||||
version = "3.8.1"
|
||||
|
||||
[dependencies.brush-parser]
|
||||
version = "^0.3.0"
|
||||
|
||||
[dependencies.cached]
|
||||
version = "0.56.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.indexmap]
|
||||
version = "2.12.0"
|
||||
|
||||
[dependencies.itertools]
|
||||
version = "0.14.0"
|
||||
|
||||
[dependencies.normalize-path]
|
||||
version = "0.2.1"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.9.2"
|
||||
|
||||
[dependencies.rpds]
|
||||
version = "1.2.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"
|
||||
|
||||
[dependencies.tokio-util]
|
||||
version = "0.7.16"
|
||||
|
||||
[dev-dependencies.anyhow]
|
||||
version = "1.0.100"
|
||||
|
||||
[target."cfg(any(unix, windows))".dependencies.hostname]
|
||||
version = "0.4.1"
|
||||
|
||||
[target."cfg(any(unix, windows))".dependencies.tokio]
|
||||
version = "1.48.0"
|
||||
features = [
|
||||
"io-util",
|
||||
"macros",
|
||||
"process",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
"sync",
|
||||
]
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies.tokio]
|
||||
version = "1.48.0"
|
||||
features = [
|
||||
"io-util",
|
||||
"macros",
|
||||
"rt",
|
||||
"sync",
|
||||
]
|
||||
|
||||
[target."cfg(unix)".dependencies.command-fds]
|
||||
version = "0.3.2"
|
||||
|
||||
[target."cfg(unix)".dependencies.nix]
|
||||
version = "0.30.1"
|
||||
features = [
|
||||
"fs",
|
||||
"process",
|
||||
"resource",
|
||||
"signal",
|
||||
"term",
|
||||
"user",
|
||||
]
|
||||
|
||||
[target."cfg(unix)".dependencies.terminfo]
|
||||
version = "0.9.0"
|
||||
|
||||
[target."cfg(unix)".dependencies.uzers]
|
||||
version = "0.12.1"
|
||||
|
||||
[target."cfg(windows)".dependencies.homedir]
|
||||
version = "=0.3.4"
|
||||
|
||||
[target."cfg(windows)".dependencies.whoami]
|
||||
version = "1.6.1"
|
||||
|
||||
[target.wasm32-unknown-unknown.dependencies.getrandom]
|
||||
version = "0.3.4"
|
||||
features = ["wasm_js"]
|
||||
|
||||
[target.wasm32-unknown-unknown.dependencies.uuid]
|
||||
version = "1.18.1"
|
||||
features = ["js"]
|
||||
|
||||
[lints.clippy]
|
||||
bool_to_int_with_if = "allow"
|
||||
cognitive_complexity = "allow"
|
||||
collapsible_else_if = "allow"
|
||||
collapsible_if = "allow"
|
||||
expect_used = "deny"
|
||||
format_push_string = "deny"
|
||||
if_not_else = "allow"
|
||||
if_same_then_else = "allow"
|
||||
match_same_arms = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
multiple_crate_versions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
option_if_let_else = "allow"
|
||||
panic = "deny"
|
||||
panic_in_result_fn = "deny"
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
redundant_else = "allow"
|
||||
redundant_pub_crate = "allow"
|
||||
result_large_err = "allow"
|
||||
similar_names = "allow"
|
||||
string_slice = "deny"
|
||||
struct_excessive_bools = "allow"
|
||||
todo = "deny"
|
||||
undocumented_unsafe_blocks = "deny"
|
||||
unwrap_in_result = "deny"
|
||||
|
||||
[lints.clippy.all]
|
||||
level = "deny"
|
||||
priority = -1
|
||||
|
||||
[lints.clippy.cargo]
|
||||
level = "deny"
|
||||
priority = -1
|
||||
|
||||
[lints.clippy.nursery]
|
||||
level = "deny"
|
||||
priority = -1
|
||||
|
||||
[lints.clippy.pedantic]
|
||||
level = "deny"
|
||||
priority = -1
|
||||
|
||||
[lints.clippy.perf]
|
||||
level = "deny"
|
||||
priority = -1
|
||||
|
||||
[lints.rust]
|
||||
unnameable_types = "deny"
|
||||
unsafe_op_in_unsafe_fn = "deny"
|
||||
unused_attributes = "deny"
|
||||
unused_lifetimes = "deny"
|
||||
unused_macro_rules = "deny"
|
||||
|
||||
[lints.rust.future_incompatible]
|
||||
level = "deny"
|
||||
priority = 0
|
||||
|
||||
[lints.rust.missing_docs]
|
||||
level = "deny"
|
||||
priority = 0
|
||||
|
||||
[lints.rust.nonstandard_style]
|
||||
level = "deny"
|
||||
priority = 0
|
||||
|
||||
[lints.rust.rust_2018_idioms]
|
||||
level = "deny"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.unknown_lints]
|
||||
level = "allow"
|
||||
priority = -100
|
||||
|
||||
[lints.rust.warnings]
|
||||
level = "deny"
|
||||
priority = 0
|
||||
|
||||
[lints.rustdoc.all]
|
||||
level = "deny"
|
||||
priority = -1
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 reuben olinsky
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,187 @@
|
||||
<div align="center">
|
||||
<img src="https://github.com/user-attachments/assets/19351a8e-7b03-4338-81be-dd5b6d7e5abc"/>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<!-- Primary badges -->
|
||||
<p align="center">
|
||||
<!-- crates.io version badge -->
|
||||
<a href="https://crates.io/crates/brush-shell"><img src="https://img.shields.io/crates/v/brush-shell?style=flat-square"/></a>
|
||||
<!-- msrv badge -->
|
||||
<img src="https://img.shields.io/crates/msrv/brush-shell"/>
|
||||
<!-- LoC badge: badge generation seems broken; temporarily disabled -->
|
||||
<!-- <img src="https://tokei.rs/b1/github/reubeno/brush?category=code"/> -->
|
||||
<!-- license badge -->
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square"/>
|
||||
<!-- CI status badge -->
|
||||
<a href="https://github.com/reubeno/brush/actions/workflows/ci.yaml"><img src="https://github.com/reubeno/brush/actions/workflows/ci.yaml/badge.svg"/></a>
|
||||
<br/>
|
||||
<!-- crates.io download badge -->
|
||||
<a href="https://crates.io/crates/brush-shell"><img src="https://img.shields.io/crates/d/brush-shell?style=flat-square"/></a>
|
||||
<!-- Packaging badges -->
|
||||
<a href="https://repology.org/project/brush/versions">
|
||||
<img src="https://repology.org/badge/tiny-repos/brush.svg" alt="Packaging status"/>
|
||||
</a>
|
||||
<!-- Dependencies badges -->
|
||||
<a href="https://deps.rs/repo/github/reubeno/brush"><img src="https://deps.rs/repo/github/reubeno/brush/status.svg" alt="Dependency status"/></a>
|
||||
<!-- Social badges -->
|
||||
<a href="https://discord.gg/kPRgC9j3Tj">
|
||||
<img src="https://dcbadge.limes.pink/api/server/https://discord.gg/kPRgC9j3Tj?compact=true&style=flat" alt="Discord invite"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a href="https://repology.org/project/brush/versions">
|
||||
</a>
|
||||
|
||||
</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
`brush` (**B**o(u)rn(e) **RU**sty **SH**ell) is a [POSIX-](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html) and [bash-](https://www.gnu.org/software/bash/)compatible shell,
|
||||
implemented in Rust. At its core is an embeddable shell interpreter published for reuse
|
||||
in other Rust projects. It's built and tested on Linux, macOS, and WSL. Native Windows
|
||||
support is experimental.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/0e64d1b9-7e4e-43be-8593-6c1b9607ac52" width="80%"/>
|
||||
</p>
|
||||
|
||||
`brush` is functional for interactive use as a daily driver! It executes most `sh` and `bash` scripts we've
|
||||
encountered. Known limitations are tracked with filed issues. Out of an abundance of caution,
|
||||
we wouldn't recommend using it yet in _production_ scenarios in case it doesn't behave identically
|
||||
to your existing stable shell. (If you do find any behavioral differences, though, please report them with an
|
||||
issue!)
|
||||
|
||||
Contributions and feedback of all kinds are welcome! For more guidance, please consult our
|
||||
[contribution guidelines](CONTRIBUTING.md). For more technical details, please consult the
|
||||
[documentation](docs/README.md) in this repo.
|
||||
|
||||
This project was originally borne out of curiosity and a desire to learn. We're doing our best to keep that
|
||||
attitude :).
|
||||
|
||||
<br/>
|
||||
|
||||
## 📝 License
|
||||
|
||||
Available for use and distribution under the [MIT license](LICENSE).
|
||||
|
||||
## ⌨️ Installation
|
||||
|
||||
_When you run `brush`, it should look exactly as `bash` does on your system: it processes your `.bashrc` and
|
||||
other standard configuration. If you'd like to distinguish the look of `brush` from the other shells
|
||||
on your system, you may author a `~/.brushrc` file._
|
||||
|
||||
<details open>
|
||||
<summary>🚀 <b>Installing prebuilt binaries from GitHub</b></summary>
|
||||
|
||||
We publish prebuilt binaries of `brush` for Linux (x86_64, aarch64) and macOS (aarch64) to GitHub for official [releases](https://github.com/reubeno/brush/releases). You can manually download and extract the `brush` binary from one of the archives published there, or otherwise use the GitHub CLI to download it, e.g.:
|
||||
|
||||
```bash
|
||||
gh release download --repo reubeno/brush --pattern "brush-x86_64-unknown-linux-gnu.*"
|
||||
```
|
||||
|
||||
After downloading the archive for your platform, you may verify its authenticity using the [GitHub CLI](https://cli.github.com/), e.g.:
|
||||
|
||||
```bash
|
||||
gh attestation verify brush-x86_64-unknown-linux-gnu.tar.gz --repo reubeno/brush
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>🚀 <b>Installing prebuilt binaries via `cargo binstall`</b></summary>
|
||||
|
||||
You may use [cargo binstall](https://github.com/cargo-bins/cargo-binstall) to install pre-built `brush` binaries. Once you've installed `cargo-binstall` you can run:
|
||||
|
||||
```bash
|
||||
cargo binstall brush-shell
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> 🔨 <b>Installing from sources</b></summary>
|
||||
|
||||
To build from sources, first install a working (and recent) `rust` toolchain; we recommend installing it via [`rustup`](https://rustup.rs/). Then run:
|
||||
|
||||
```bash
|
||||
cargo install --locked brush-shell
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🐧 <b>Installing using Nix</b></summary>
|
||||
|
||||
If you are a Nix user, you can use the registered version:
|
||||
|
||||
```bash
|
||||
nix run 'github:NixOS/nixpkgs/nixpkgs-unstable#brush' -- --version
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🐧 <b>Installing on Arch Linux</b></summary>
|
||||
|
||||
Arch Linux users can install `brush` from the official [extra repository](https://archlinux.org/packages/extra/x86_64/brush/):
|
||||
|
||||
```bash
|
||||
pacman -S brush
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🍺 <b>Installing using Homebrew</b></summary>
|
||||
|
||||
Homebrew users can install using [the `brush` formula](https://formulae.brew.sh/formula/brush):
|
||||
|
||||
```bash
|
||||
brew install brush
|
||||
```
|
||||
</details>
|
||||
|
||||
## 👥 Community
|
||||
|
||||
`brush` has a community Discord server, available [here](https://discord.gg/kPRgC9j3Tj).
|
||||
|
||||
## 🔍 Known limitations
|
||||
|
||||
There are some known gaps in compatibility. Most notably:
|
||||
|
||||
* **Some `set` and `shopt` options.**
|
||||
The `set` builtin is implemented, as is `set -x` and many frequently used `set`/`shopt` options, but a number aren't fully implemented. For example, `set -e` will execute but its semantics aren't applied across execution.
|
||||
|
||||
If you're interested, we'd love contributions to improve compatibility, broaden test coverage, or really any other opportunities you can find to help us make this project better.
|
||||
|
||||
## 🧪 Testing strategy
|
||||
|
||||
This project is primarily tested by comparing its behavior with other existing shells, leveraging the latter as test oracles. The integration tests implemented in this repo include [850+ test cases](brush-shell/tests/cases) run on both this shell and an oracle, comparing standard output and exit codes.
|
||||
|
||||
For more details, please consult the [reference documentation on integration testing](docs/reference/integration-testing.md).
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
There's a long list of OSS crates whose shoulders this project rests on. Notably, the following crates are directly relied on for major portions of shell functionality:
|
||||
|
||||
* [`reedline`](https://github.com/nushell/reedline) - for readline-like input and interactive usage
|
||||
* [`clap`](https://github.com/clap-rs/clap) - command-line parsing, used both by the top-level brush CLI as well as built-in commands
|
||||
* [`fancy-regex`](https://github.com/fancy-regex/fancy-regex) - relied on for everything regex
|
||||
* [`tokio`](https://github.com/tokio-rs/tokio) - async, well, everything
|
||||
* [`nix` rust crate](https://github.com/nix-rust/nix) - higher-level APIs for Unix/POSIX system APIs
|
||||
|
||||
For testing, performance benchmarking, and other important engineering support, we use and love:
|
||||
|
||||
* [`pprof-rs`](https://github.com/tikv/pprof-rs) - for sampling-based CPU profiling
|
||||
* [`criterion.rs`](https://github.com/bheisler/criterion.rs) - for statistics-based benchmarking
|
||||
* [`bash-completion`](https://github.com/scop/bash-completion) - for its completion test suite and general completion support!
|
||||
|
||||
## 🔗 Links: other shell implementations
|
||||
|
||||
There are a number of other POSIX-ish shells implemented in a non-C/C++ implementation language. Some inspirational examples include:
|
||||
|
||||
* [`nushell`](https://www.nushell.sh/) - modern Rust-implemented shell (which also provides the `reedline` crate we use!)
|
||||
* [`rusty_bash`](https://github.com/shellgei/rusty_bash)
|
||||
* [`mvdan/sh`](https://github.com/mvdan/sh)
|
||||
* [`Oils`](https://github.com/oils-for-unix/oils)
|
||||
* [`fish`](https://fishshell.com) ([as of 4.0](https://fishshell.com/blog/rustport/))
|
||||
|
||||
We're sure there are plenty more; we're happy to include links to them as well.
|
||||
@@ -0,0 +1,351 @@
|
||||
//! Arithmetic evaluation
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{ExecutionParameters, Shell, env, expansion, variables};
|
||||
use brush_parser::ast;
|
||||
|
||||
/// Represents an error that occurs during evaluation of an arithmetic expression.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EvalError {
|
||||
/// Division by zero.
|
||||
#[error("division by zero")]
|
||||
DivideByZero,
|
||||
|
||||
/// Negative exponent.
|
||||
#[error("exponent less than 0")]
|
||||
NegativeExponent,
|
||||
|
||||
/// Failed to tokenize an arithmetic expression.
|
||||
#[error("failed to tokenize expression")]
|
||||
FailedToTokenizeExpression,
|
||||
|
||||
/// Failed to expand an arithmetic expression.
|
||||
#[error("failed to expand expression: '{0}'")]
|
||||
FailedToExpandExpression(String),
|
||||
|
||||
/// Failed to access an element of an array.
|
||||
#[error("failed to access array")]
|
||||
FailedToAccessArray,
|
||||
|
||||
/// Failed to update the shell environment in an assignment operator.
|
||||
#[error("failed to update environment")]
|
||||
FailedToUpdateEnvironment,
|
||||
|
||||
/// Failed to parse an arithmetic expression.
|
||||
#[error("failed to parse expression: '{0}'")]
|
||||
ParseError(String),
|
||||
|
||||
/// Failed to trace an arithmetic expression.
|
||||
#[error("failed tracing expression")]
|
||||
TraceError,
|
||||
}
|
||||
|
||||
/// Trait implemented by arithmetic expressions that can be evaluated.
|
||||
pub(crate) trait ExpandAndEvaluate {
|
||||
/// Evaluate the given expression, returning the resulting numeric value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `shell` - The shell to use for evaluation.
|
||||
/// * `trace_if_needed` - Whether to trace the evaluation.
|
||||
async fn eval(
|
||||
&self,
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
trace_if_needed: bool,
|
||||
) -> Result<i64, EvalError>;
|
||||
}
|
||||
|
||||
impl ExpandAndEvaluate for ast::UnexpandedArithmeticExpr {
|
||||
async fn eval(
|
||||
&self,
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
trace_if_needed: bool,
|
||||
) -> Result<i64, EvalError> {
|
||||
expand_and_eval(shell, params, self.value.as_str(), trace_if_needed).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the given arithmetic expression, returning the resulting numeric value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `shell` - The shell to use for evaluation.
|
||||
/// * `expr` - The unexpanded arithmetic expression to evaluate.
|
||||
/// * `trace_if_needed` - Whether to trace the evaluation.
|
||||
pub(crate) async fn expand_and_eval(
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
expr: &str,
|
||||
trace_if_needed: bool,
|
||||
) -> Result<i64, EvalError> {
|
||||
// Per documentation, first shell-expand it.
|
||||
let expanded_self = expansion::basic_expand_str_without_tilde(shell, params, expr)
|
||||
.await
|
||||
.map_err(|_e| EvalError::FailedToExpandExpression(expr.to_owned()))?;
|
||||
|
||||
// Now parse.
|
||||
let expr = brush_parser::arithmetic::parse(&expanded_self)
|
||||
.map_err(|_e| EvalError::ParseError(expanded_self))?;
|
||||
|
||||
// Trace if applicable.
|
||||
if trace_if_needed && shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("(( {expr} ))"))
|
||||
.await
|
||||
.map_err(|_err| EvalError::TraceError)?;
|
||||
}
|
||||
|
||||
// Now evaluate.
|
||||
expr.eval(shell)
|
||||
}
|
||||
|
||||
/// Trait implemented by evaluatable arithmetic expressions.
|
||||
pub trait Evaluatable {
|
||||
/// Evaluate the given arithmetic expression, returning the resulting numeric value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `shell` - The shell to use for evaluation.
|
||||
fn eval(&self, shell: &mut Shell) -> Result<i64, EvalError>;
|
||||
}
|
||||
|
||||
impl Evaluatable for ast::ArithmeticExpr {
|
||||
fn eval(&self, shell: &mut Shell) -> Result<i64, EvalError> {
|
||||
let value = match self {
|
||||
Self::Literal(l) => *l,
|
||||
Self::Reference(lvalue) => deref_lvalue(shell, lvalue)?,
|
||||
Self::UnaryOp(op, operand) => apply_unary_op(shell, *op, operand)?,
|
||||
Self::BinaryOp(op, left, right) => apply_binary_op(shell, *op, left, right)?,
|
||||
Self::Conditional(condition, then_expr, else_expr) => {
|
||||
let conditional_eval = condition.eval(shell)?;
|
||||
|
||||
// Ensure we only evaluate the branch indicated by the condition.
|
||||
if conditional_eval != 0 {
|
||||
then_expr.eval(shell)?
|
||||
} else {
|
||||
else_expr.eval(shell)?
|
||||
}
|
||||
}
|
||||
Self::Assignment(lvalue, expr) => {
|
||||
let expr_eval = expr.eval(shell)?;
|
||||
assign(shell, lvalue, expr_eval)?
|
||||
}
|
||||
Self::UnaryAssignment(op, lvalue) => apply_unary_assignment_op(shell, lvalue, *op)?,
|
||||
Self::BinaryAssignment(op, lvalue, operand) => {
|
||||
let value = apply_binary_op(shell, *op, &Self::Reference(lvalue.clone()), operand)?;
|
||||
assign(shell, lvalue, value)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn deref_lvalue(shell: &mut Shell, lvalue: &ast::ArithmeticTarget) -> Result<i64, EvalError> {
|
||||
let value_str: Cow<'_, str> = match lvalue {
|
||||
ast::ArithmeticTarget::Variable(name) => shell.env_str(name).unwrap_or(Cow::Borrowed("")),
|
||||
ast::ArithmeticTarget::ArrayElement(name, index_expr) => {
|
||||
let index_str = index_expr.eval(shell)?.to_string();
|
||||
|
||||
shell
|
||||
.env
|
||||
.get(name)
|
||||
.map_or_else(
|
||||
|| Ok(None),
|
||||
|(_, v)| v.value().get_at(index_str.as_str(), shell),
|
||||
)
|
||||
.map_err(|_err| EvalError::FailedToAccessArray)?
|
||||
.unwrap_or(Cow::Borrowed(""))
|
||||
}
|
||||
};
|
||||
|
||||
let parsed_value = brush_parser::arithmetic::parse(value_str.as_ref())
|
||||
.map_err(|_err| EvalError::ParseError(value_str.to_string()))?;
|
||||
|
||||
parsed_value.eval(shell)
|
||||
}
|
||||
|
||||
fn apply_unary_op(
|
||||
shell: &mut Shell,
|
||||
op: ast::UnaryOperator,
|
||||
operand: &ast::ArithmeticExpr,
|
||||
) -> Result<i64, EvalError> {
|
||||
let operand_eval = operand.eval(shell)?;
|
||||
|
||||
match op {
|
||||
ast::UnaryOperator::UnaryPlus => Ok(operand_eval),
|
||||
ast::UnaryOperator::UnaryMinus => Ok(-operand_eval),
|
||||
ast::UnaryOperator::BitwiseNot => Ok(!operand_eval),
|
||||
ast::UnaryOperator::LogicalNot => Ok(bool_to_i64(operand_eval == 0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_binary_op(
|
||||
shell: &mut Shell,
|
||||
op: ast::BinaryOperator,
|
||||
left: &ast::ArithmeticExpr,
|
||||
right: &ast::ArithmeticExpr,
|
||||
) -> Result<i64, EvalError> {
|
||||
// First, special-case short-circuiting operators. For those, we need
|
||||
// to ensure we don't eagerly evaluate both operands. After we
|
||||
// get these out of the way, we can easily just evaluate operands
|
||||
// for the other operators.
|
||||
match op {
|
||||
ast::BinaryOperator::LogicalAnd => {
|
||||
let left = left.eval(shell)?;
|
||||
if left == 0 {
|
||||
return Ok(bool_to_i64(false));
|
||||
}
|
||||
|
||||
let right = right.eval(shell)?;
|
||||
return Ok(bool_to_i64(right != 0));
|
||||
}
|
||||
ast::BinaryOperator::LogicalOr => {
|
||||
let left = left.eval(shell)?;
|
||||
if left != 0 {
|
||||
return Ok(bool_to_i64(true));
|
||||
}
|
||||
|
||||
let right = right.eval(shell)?;
|
||||
return Ok(bool_to_i64(right != 0));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// The remaining operators unconditionally operate both operands.
|
||||
let left = left.eval(shell)?;
|
||||
let right = right.eval(shell)?;
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
match op {
|
||||
ast::BinaryOperator::Power => {
|
||||
if right >= 0 {
|
||||
Ok(wrapping_pow_u64(left, right as u64))
|
||||
} else {
|
||||
Err(EvalError::NegativeExponent)
|
||||
}
|
||||
}
|
||||
ast::BinaryOperator::Multiply => Ok(left.wrapping_mul(right)),
|
||||
ast::BinaryOperator::Divide => {
|
||||
if right == 0 {
|
||||
Err(EvalError::DivideByZero)
|
||||
} else {
|
||||
Ok(left.wrapping_div(right))
|
||||
}
|
||||
}
|
||||
ast::BinaryOperator::Modulo => {
|
||||
if right == 0 {
|
||||
Err(EvalError::DivideByZero)
|
||||
} else {
|
||||
Ok(left % right)
|
||||
}
|
||||
}
|
||||
ast::BinaryOperator::Comma => Ok(right),
|
||||
ast::BinaryOperator::Add => Ok(left.wrapping_add(right)),
|
||||
ast::BinaryOperator::Subtract => Ok(left.wrapping_sub(right)),
|
||||
ast::BinaryOperator::ShiftLeft => Ok(left.wrapping_shl(right as u32)),
|
||||
ast::BinaryOperator::ShiftRight => Ok(left.wrapping_shr(right as u32)),
|
||||
ast::BinaryOperator::LessThan => Ok(bool_to_i64(left < right)),
|
||||
ast::BinaryOperator::LessThanOrEqualTo => Ok(bool_to_i64(left <= right)),
|
||||
ast::BinaryOperator::GreaterThan => Ok(bool_to_i64(left > right)),
|
||||
ast::BinaryOperator::GreaterThanOrEqualTo => Ok(bool_to_i64(left >= right)),
|
||||
ast::BinaryOperator::Equals => Ok(bool_to_i64(left == right)),
|
||||
ast::BinaryOperator::NotEquals => Ok(bool_to_i64(left != right)),
|
||||
ast::BinaryOperator::BitwiseAnd => Ok(left & right),
|
||||
ast::BinaryOperator::BitwiseXor => Ok(left ^ right),
|
||||
ast::BinaryOperator::BitwiseOr => Ok(left | right),
|
||||
ast::BinaryOperator::LogicalAnd => unreachable!("LogicalAnd covered above"),
|
||||
ast::BinaryOperator::LogicalOr => unreachable!("LogicalOr covered above"),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_unary_assignment_op(
|
||||
shell: &mut Shell,
|
||||
lvalue: &ast::ArithmeticTarget,
|
||||
op: ast::UnaryAssignmentOperator,
|
||||
) -> Result<i64, EvalError> {
|
||||
let value = deref_lvalue(shell, lvalue)?;
|
||||
|
||||
match op {
|
||||
ast::UnaryAssignmentOperator::PrefixIncrement => {
|
||||
let new_value = value + 1;
|
||||
assign(shell, lvalue, new_value)?;
|
||||
Ok(new_value)
|
||||
}
|
||||
ast::UnaryAssignmentOperator::PrefixDecrement => {
|
||||
let new_value = value - 1;
|
||||
assign(shell, lvalue, new_value)?;
|
||||
Ok(new_value)
|
||||
}
|
||||
ast::UnaryAssignmentOperator::PostfixIncrement => {
|
||||
let new_value = value + 1;
|
||||
assign(shell, lvalue, new_value)?;
|
||||
Ok(value)
|
||||
}
|
||||
ast::UnaryAssignmentOperator::PostfixDecrement => {
|
||||
let new_value = value - 1;
|
||||
assign(shell, lvalue, new_value)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assign(shell: &mut Shell, lvalue: &ast::ArithmeticTarget, value: i64) -> Result<i64, EvalError> {
|
||||
match lvalue {
|
||||
ast::ArithmeticTarget::Variable(name) => {
|
||||
shell
|
||||
.env
|
||||
.update_or_add(
|
||||
name.as_str(),
|
||||
variables::ShellValueLiteral::Scalar(value.to_string()),
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)
|
||||
.map_err(|_err| EvalError::FailedToUpdateEnvironment)?;
|
||||
}
|
||||
ast::ArithmeticTarget::ArrayElement(name, index_expr) => {
|
||||
let index_str = index_expr.eval(shell)?.to_string();
|
||||
|
||||
shell
|
||||
.env
|
||||
.update_or_add_array_element(
|
||||
name.as_str(),
|
||||
index_str,
|
||||
value.to_string(),
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)
|
||||
.map_err(|_err| EvalError::FailedToUpdateEnvironment)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
const fn bool_to_i64(value: bool) -> i64 {
|
||||
if value { 1 } else { 0 }
|
||||
}
|
||||
|
||||
// N.B. We implement our own version of wrapping_pow that takes a 64-bit exponent.
|
||||
// This seems to be the best way to guarantee that we handle overflow cases
|
||||
// with exponents correctly.
|
||||
const fn wrapping_pow_u64(mut base: i64, mut exponent: u64) -> i64 {
|
||||
let mut result: i64 = 1;
|
||||
|
||||
while exponent > 0 {
|
||||
if exponent % 2 == 1 {
|
||||
result = result.wrapping_mul(base);
|
||||
}
|
||||
|
||||
base = base.wrapping_mul(base);
|
||||
exponent /= 2;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use brush_parser::word;
|
||||
use itertools::Itertools;
|
||||
|
||||
pub(crate) fn generate_and_combine_brace_expansions(
|
||||
pieces: Vec<brush_parser::word::BraceExpressionOrText>,
|
||||
) -> impl IntoIterator<Item = String> {
|
||||
let expansions: Vec<Vec<String>> = pieces
|
||||
.into_iter()
|
||||
.map(|piece| expand_brace_expr_or_text(piece).collect())
|
||||
.collect();
|
||||
|
||||
expansions
|
||||
.into_iter()
|
||||
.multi_cartesian_product()
|
||||
.map(|v| v.join(""))
|
||||
}
|
||||
|
||||
fn expand_brace_expr_or_text(
|
||||
beot: word::BraceExpressionOrText,
|
||||
) -> Box<dyn Iterator<Item = String>> {
|
||||
match beot {
|
||||
word::BraceExpressionOrText::Expr(members) => {
|
||||
// Chain all member iterators together
|
||||
Box::new(members.into_iter().flat_map(expand_brace_expr_member))
|
||||
}
|
||||
word::BraceExpressionOrText::Text(text) => Box::new(std::iter::once(text)),
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
fn expand_brace_expr_member(bem: word::BraceExpressionMember) -> Box<dyn Iterator<Item = String>> {
|
||||
match bem {
|
||||
word::BraceExpressionMember::NumberSequence {
|
||||
start,
|
||||
end,
|
||||
increment,
|
||||
} => {
|
||||
let increment = increment.unsigned_abs() as usize;
|
||||
|
||||
if start <= end {
|
||||
Box::new((start..=end).step_by(increment).map(|n| n.to_string()))
|
||||
} else {
|
||||
Box::new(
|
||||
(end..=start)
|
||||
.step_by(increment)
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
word::BraceExpressionMember::CharSequence {
|
||||
start,
|
||||
end,
|
||||
increment,
|
||||
} => {
|
||||
let increment = increment.unsigned_abs() as usize;
|
||||
|
||||
if start <= end {
|
||||
Box::new((start..=end).step_by(increment).map(|c| c.to_string()))
|
||||
} else {
|
||||
Box::new(
|
||||
(end..=start)
|
||||
.step_by(increment)
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
word::BraceExpressionMember::Child(elements) => {
|
||||
// Chain all element iterators together
|
||||
Box::new(generate_and_combine_brace_expansions(elements).into_iter())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
//! Facilities for implementing and managing builtins
|
||||
|
||||
use clap::builder::styling;
|
||||
use futures::future::BoxFuture;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::{BuiltinError, CommandArg, commands, error, results};
|
||||
|
||||
/// Type of a function implementing a built-in command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * The context in which the command is being executed.
|
||||
/// * The arguments to the command.
|
||||
pub type CommandExecuteFunc = fn(
|
||||
commands::ExecutionContext<'_>,
|
||||
Vec<commands::CommandArg>,
|
||||
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>>;
|
||||
|
||||
/// Type of a function to retrieve help content for a built-in command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the command.
|
||||
/// * `content_type` - The type of content to retrieve.
|
||||
pub type CommandContentFunc = fn(&str, ContentType) -> Result<String, error::Error>;
|
||||
|
||||
/// Trait implemented by built-in shell commands.
|
||||
pub trait Command: clap::Parser {
|
||||
/// The error type returned by the command.
|
||||
type Error: BuiltinError + 'static;
|
||||
|
||||
/// Instantiates the built-in command with the given arguments.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The arguments to the command.
|
||||
fn new<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
if !Self::takes_plus_options() {
|
||||
Self::try_parse_from(args)
|
||||
} else {
|
||||
// N.B. clap doesn't support named options like '+x'. To work around this, we
|
||||
// establish a pattern of renaming them.
|
||||
let mut updated_args = vec![];
|
||||
for arg in args {
|
||||
if let Some(plus_options) = arg.strip_prefix("+") {
|
||||
for c in plus_options.chars() {
|
||||
updated_args.push(format!("--+{c}"));
|
||||
}
|
||||
} else {
|
||||
updated_args.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
Self::try_parse_from(updated_args)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not the command takes options with a leading '+' or '-' character.
|
||||
fn takes_plus_options() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Executes the built-in command in the provided context.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `context` - The context in which the command is being executed.
|
||||
// NOTE: we use desugared async here because we need a Send marker
|
||||
fn execute(
|
||||
&self,
|
||||
context: commands::ExecutionContext<'_>,
|
||||
) -> impl std::future::Future<Output = Result<results::ExecutionResult, Self::Error>>
|
||||
+ std::marker::Send;
|
||||
|
||||
/// Returns the textual help content associated with the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the command.
|
||||
/// * `content_type` - The type of content to retrieve.
|
||||
fn get_content(name: &str, content_type: ContentType) -> Result<String, error::Error> {
|
||||
let mut clap_command = Self::command()
|
||||
.styles(brush_help_styles())
|
||||
.next_line_help(false);
|
||||
clap_command.set_bin_name(name);
|
||||
|
||||
let s = match content_type {
|
||||
ContentType::DetailedHelp => clap_command.render_help().ansi().to_string(),
|
||||
ContentType::ShortUsage => get_builtin_short_usage(name, &clap_command),
|
||||
ContentType::ShortDescription => get_builtin_short_description(name, &clap_command),
|
||||
ContentType::ManPage => get_builtin_man_page(name, &clap_command)?,
|
||||
};
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implemented by built-in shell commands that take specially handled declarations
|
||||
/// as arguments.
|
||||
pub trait DeclarationCommand: Command {
|
||||
/// Stores the declarations within the command instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `declarations` - The declarations to store.
|
||||
fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>);
|
||||
}
|
||||
|
||||
/// Type of help content, typically associated with a built-in command.
|
||||
pub enum ContentType {
|
||||
/// Detailed help content for the command.
|
||||
DetailedHelp,
|
||||
/// Short usage information for the command.
|
||||
ShortUsage,
|
||||
/// Short description for the command.
|
||||
ShortDescription,
|
||||
/// man-style help page.
|
||||
ManPage,
|
||||
}
|
||||
|
||||
/// Encapsulates a registration for a built-in command.
|
||||
#[derive(Clone)]
|
||||
pub struct Registration {
|
||||
/// Function to execute the builtin.
|
||||
pub execute_func: CommandExecuteFunc,
|
||||
|
||||
/// Function to retrieve the builtin's content/help text.
|
||||
pub content_func: CommandContentFunc,
|
||||
|
||||
/// Has this registration been disabled?
|
||||
pub disabled: bool,
|
||||
|
||||
/// Is the builtin classified as "special" by specification?
|
||||
pub special_builtin: bool,
|
||||
|
||||
/// Is this builtin one that takes specially handled declarations?
|
||||
pub declaration_builtin: bool,
|
||||
}
|
||||
|
||||
impl Registration {
|
||||
/// Updates the given registration to mark it for a special builtin.
|
||||
#[must_use]
|
||||
pub const fn special(self) -> Self {
|
||||
Self {
|
||||
special_builtin: true,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_builtin_man_page(_name: &str, _command: &clap::Command) -> Result<String, error::Error> {
|
||||
error::unimp("man page rendering is not yet implemented")
|
||||
}
|
||||
|
||||
fn get_builtin_short_description(name: &str, command: &clap::Command) -> String {
|
||||
let about = command
|
||||
.get_about()
|
||||
.map_or_else(String::new, |s| s.to_string());
|
||||
|
||||
std::format!("{name} - {about}\n")
|
||||
}
|
||||
|
||||
fn get_builtin_short_usage(name: &str, command: &clap::Command) -> String {
|
||||
let mut usage = String::new();
|
||||
|
||||
let mut needs_space = false;
|
||||
|
||||
let mut optional_short_opts = vec![];
|
||||
let mut required_short_opts = vec![];
|
||||
for opt in command.get_opts() {
|
||||
if opt.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(c) = opt.get_short() {
|
||||
if !opt.is_required_set() {
|
||||
optional_short_opts.push(c);
|
||||
} else {
|
||||
required_short_opts.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !optional_short_opts.is_empty() {
|
||||
if needs_space {
|
||||
usage.push(' ');
|
||||
}
|
||||
|
||||
usage.push('[');
|
||||
usage.push('-');
|
||||
for c in optional_short_opts {
|
||||
usage.push(c);
|
||||
}
|
||||
|
||||
usage.push(']');
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
if !required_short_opts.is_empty() {
|
||||
if needs_space {
|
||||
usage.push(' ');
|
||||
}
|
||||
|
||||
usage.push('-');
|
||||
for c in required_short_opts {
|
||||
usage.push(c);
|
||||
}
|
||||
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
for pos in command.get_positionals() {
|
||||
if pos.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !pos.is_required_set() {
|
||||
if needs_space {
|
||||
usage.push(' ');
|
||||
}
|
||||
|
||||
usage.push('[');
|
||||
needs_space = false;
|
||||
}
|
||||
|
||||
if let Some(names) = pos.get_value_names() {
|
||||
for name in names {
|
||||
if needs_space {
|
||||
usage.push(' ');
|
||||
}
|
||||
|
||||
usage.push_str(name);
|
||||
needs_space = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !pos.is_required_set() {
|
||||
usage.push(']');
|
||||
needs_space = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::format!("{name}: {name} {usage}\n")
|
||||
}
|
||||
|
||||
fn brush_help_styles() -> clap::builder::Styles {
|
||||
styling::Styles::styled()
|
||||
.header(
|
||||
styling::AnsiColor::Yellow.on_default()
|
||||
| styling::Effects::BOLD
|
||||
| styling::Effects::UNDERLINE,
|
||||
)
|
||||
.usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
|
||||
.literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
|
||||
.placeholder(styling::AnsiColor::Cyan.on_default())
|
||||
}
|
||||
|
||||
/// This function and the [`try_parse_known`] exists to deal with
|
||||
/// the Clap's limitation of treating `--` like a regular value
|
||||
/// `https://github.com/clap-rs/clap/issues/5055`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - An Iterator from [`std::env::args`]
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * a parsed struct T from [`clap::Parser::parse_from`]
|
||||
/// * the remain iterator `args` with `--` and the rest arguments if they present otherwise None
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use clap::{builder::styling, Parser};
|
||||
/// #[derive(Parser)]
|
||||
/// struct CommandLineArgs {
|
||||
/// #[clap(allow_hyphen_values = true, num_args=1..)]
|
||||
/// script_args: Vec<String>,
|
||||
/// }
|
||||
///
|
||||
/// let (mut parsed_args, raw_args) =
|
||||
/// brush_core::parse_known::<CommandLineArgs, _>(std::env::args());
|
||||
/// if raw_args.is_some() {
|
||||
/// parsed_args.script_args = raw_args.unwrap().collect();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn parse_known<T: clap::Parser, S>(
|
||||
args: impl IntoIterator<Item = S>,
|
||||
) -> (T, Option<impl Iterator<Item = S>>)
|
||||
where
|
||||
S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
|
||||
{
|
||||
let mut args = args.into_iter();
|
||||
// the best way to save `--` is to get it out with a side effect while `clap` iterates over the
|
||||
// args this way we can be 100% sure that we have '--' and the remaining args
|
||||
// and we will iterate only once
|
||||
let mut hyphen = None;
|
||||
let args_before_hyphen = args.by_ref().take_while(|a| {
|
||||
let is_hyphen = *a == "--";
|
||||
if is_hyphen {
|
||||
hyphen = Some(a.clone());
|
||||
}
|
||||
!is_hyphen
|
||||
});
|
||||
let parsed_args = T::parse_from(args_before_hyphen);
|
||||
let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
|
||||
(parsed_args, raw_args)
|
||||
}
|
||||
|
||||
/// Similar to [`parse_known`] but with [`clap::Parser::try_parse_from`]
|
||||
/// This function is used to parse arguments in builtins such as
|
||||
/// `crate::echo::EchoCommand`
|
||||
pub fn try_parse_known<T: clap::Parser>(
|
||||
args: impl IntoIterator<Item = String>,
|
||||
) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
|
||||
let mut args = args.into_iter();
|
||||
let mut hyphen = None;
|
||||
let args_before_hyphen = args.by_ref().take_while(|a| {
|
||||
let is_hyphen = a == "--";
|
||||
if is_hyphen {
|
||||
hyphen = Some(a.clone());
|
||||
}
|
||||
!is_hyphen
|
||||
});
|
||||
let parsed_args = T::try_parse_from(args_before_hyphen)?;
|
||||
|
||||
let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
|
||||
Ok((parsed_args, raw_args))
|
||||
}
|
||||
|
||||
/// A simple command that can be registered as a built-in.
|
||||
pub trait SimpleCommand {
|
||||
/// Returns the content of the built-in command.
|
||||
fn get_content(name: &str, content_type: ContentType) -> Result<String, error::Error>;
|
||||
|
||||
/// Executes the built-in command.
|
||||
fn execute<I: Iterator<Item = S>, S: AsRef<str>>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: I,
|
||||
) -> Result<results::ExecutionResult, error::Error>;
|
||||
}
|
||||
|
||||
/// Returns a built-in command registration, given an implementation of the
|
||||
/// `SimpleCommand` trait.
|
||||
pub fn simple_builtin<B: SimpleCommand + Send + Sync>() -> Registration {
|
||||
Registration {
|
||||
execute_func: exec_simple_builtin::<B>,
|
||||
content_func: B::get_content,
|
||||
disabled: false,
|
||||
special_builtin: false,
|
||||
declaration_builtin: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a built-in command registration, given an implementation of the
|
||||
/// `Command` trait.
|
||||
pub fn builtin<B: Command + Send + Sync>() -> Registration {
|
||||
Registration {
|
||||
execute_func: exec_builtin::<B>,
|
||||
content_func: get_builtin_content::<B>,
|
||||
disabled: false,
|
||||
special_builtin: false,
|
||||
declaration_builtin: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a built-in command registration, given an implementation of the
|
||||
/// `DeclarationCommand` trait. Used for select commands that can take parsed
|
||||
/// declarations as arguments.
|
||||
pub fn decl_builtin<B: DeclarationCommand + Send + Sync>() -> Registration {
|
||||
Registration {
|
||||
execute_func: exec_declaration_builtin::<B>,
|
||||
content_func: get_builtin_content::<B>,
|
||||
disabled: false,
|
||||
special_builtin: false,
|
||||
declaration_builtin: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_long_first_doc_paragraph)]
|
||||
/// Returns a built-in command registration, given an implementation of the
|
||||
/// `DeclarationCommand` trait that can be default-constructed. The command
|
||||
/// implementation is expected to implement clap's `Parser` trait solely
|
||||
/// for help/usage information. Arguments are passed directly to the command
|
||||
/// via `set_declarations`. This is primarily only expected to be used with
|
||||
/// select builtin commands that wrap other builtins (e.g., "builtin").
|
||||
pub fn raw_arg_builtin<B: DeclarationCommand + Default + Send + Sync>() -> Registration {
|
||||
Registration {
|
||||
execute_func: exec_raw_arg_builtin::<B>,
|
||||
content_func: get_builtin_content::<B>,
|
||||
disabled: false,
|
||||
special_builtin: false,
|
||||
declaration_builtin: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_builtin_content<T: Command + Send + Sync>(
|
||||
name: &str,
|
||||
content_type: ContentType,
|
||||
) -> Result<String, error::Error> {
|
||||
T::get_content(name, content_type)
|
||||
}
|
||||
|
||||
fn exec_simple_builtin<T: SimpleCommand + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
|
||||
Box::pin(async move { exec_simple_builtin_impl::<T>(context, args).await })
|
||||
}
|
||||
|
||||
#[expect(clippy::unused_async)]
|
||||
async fn exec_simple_builtin_impl<T: SimpleCommand + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> Result<results::ExecutionResult, error::Error> {
|
||||
let plain_args = args.into_iter().map(|arg| match arg {
|
||||
CommandArg::String(s) => s,
|
||||
CommandArg::Assignment(a) => a.to_string(),
|
||||
});
|
||||
|
||||
T::execute(context, plain_args)
|
||||
}
|
||||
|
||||
fn exec_builtin<T: Command + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
|
||||
Box::pin(async move { exec_builtin_impl::<T>(context, args).await })
|
||||
}
|
||||
|
||||
async fn exec_builtin_impl<T: Command + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> Result<results::ExecutionResult, error::Error> {
|
||||
let plain_args = args.into_iter().map(|arg| match arg {
|
||||
CommandArg::String(s) => s,
|
||||
CommandArg::Assignment(a) => a.to_string(),
|
||||
});
|
||||
|
||||
let result = T::new(plain_args);
|
||||
let command = match result {
|
||||
Ok(command) => command,
|
||||
Err(e) => {
|
||||
writeln!(context.stderr(), "{e}")?;
|
||||
return Ok(results::ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
};
|
||||
|
||||
call_builtin(command, context).await
|
||||
}
|
||||
|
||||
fn exec_declaration_builtin<T: DeclarationCommand + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
|
||||
Box::pin(async move { exec_declaration_builtin_impl::<T>(context, args).await })
|
||||
}
|
||||
|
||||
async fn exec_declaration_builtin_impl<T: DeclarationCommand + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> Result<results::ExecutionResult, error::Error> {
|
||||
let mut options = vec![];
|
||||
let mut declarations = vec![];
|
||||
|
||||
for (i, arg) in args.into_iter().enumerate() {
|
||||
match arg {
|
||||
CommandArg::String(s)
|
||||
if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
|
||||
{
|
||||
options.push(s);
|
||||
}
|
||||
_ => declarations.push(arg),
|
||||
}
|
||||
}
|
||||
|
||||
let result = T::new(options);
|
||||
let mut command = match result {
|
||||
Ok(command) => command,
|
||||
Err(e) => {
|
||||
writeln!(context.stderr(), "{e}")?;
|
||||
return Ok(results::ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
};
|
||||
|
||||
command.set_declarations(declarations);
|
||||
|
||||
call_builtin(command, context).await
|
||||
}
|
||||
|
||||
fn exec_raw_arg_builtin<T: DeclarationCommand + Default + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
|
||||
Box::pin(async move { exec_raw_arg_builtin_impl::<T>(context, args).await })
|
||||
}
|
||||
|
||||
async fn exec_raw_arg_builtin_impl<T: DeclarationCommand + Default + Send + Sync>(
|
||||
context: commands::ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> Result<results::ExecutionResult, error::Error> {
|
||||
let mut command = T::default();
|
||||
command.set_declarations(args);
|
||||
|
||||
call_builtin(command, context).await
|
||||
}
|
||||
|
||||
async fn call_builtin(
|
||||
command: impl Command,
|
||||
context: commands::ExecutionContext<'_>,
|
||||
) -> Result<results::ExecutionResult, error::Error> {
|
||||
let builtin_name = context.command_name.clone();
|
||||
let result = command
|
||||
.execute(context)
|
||||
.await
|
||||
.map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
//! Command execution
|
||||
|
||||
use std::{borrow::Cow, ffi::OsStr, fmt::Display, process::Stdio, sync::Arc};
|
||||
|
||||
use brush_parser::ast;
|
||||
use itertools::Itertools;
|
||||
use sys::commands::{CommandExt, CommandFdInjectionExt, CommandFgControlExt};
|
||||
|
||||
use crate::{
|
||||
ErrorKind, ExecutionControlFlow, ExecutionParameters, ExecutionResult, Shell, ShellFd,
|
||||
builtins, env, error, escape,
|
||||
interp::{self, Execute, ProcessGroupPolicy},
|
||||
openfiles::{self, OpenFile, OpenFiles},
|
||||
pathsearch, processes,
|
||||
results::ExecutionSpawnResult,
|
||||
sys, trace_categories, traps, variables,
|
||||
};
|
||||
|
||||
/// Encapsulates the result of waiting for a command to complete.
|
||||
pub enum CommandWaitResult {
|
||||
/// The command completed.
|
||||
CommandCompleted(ExecutionResult),
|
||||
/// The command was stopped before it completed.
|
||||
CommandStopped(ExecutionResult, processes::ChildProcess),
|
||||
}
|
||||
|
||||
/// Represents the context for executing a command.
|
||||
pub struct ExecutionContext<'a> {
|
||||
/// The shell in which the command is being executed.
|
||||
pub shell: &'a mut Shell,
|
||||
/// The name of the command being executed.
|
||||
pub command_name: String,
|
||||
/// The parameters for the execution.
|
||||
pub params: ExecutionParameters,
|
||||
}
|
||||
|
||||
impl ExecutionContext<'_> {
|
||||
/// Returns the standard input file; usable with `write!` et al.
|
||||
pub fn stdin(&self) -> impl std::io::Read + 'static {
|
||||
self.params.stdin(self.shell)
|
||||
}
|
||||
|
||||
/// Returns the standard output file; usable with `write!` et al.
|
||||
pub fn stdout(&self) -> impl std::io::Write + 'static {
|
||||
self.params.stdout(self.shell)
|
||||
}
|
||||
|
||||
/// Returns the standard error file; usable with `write!` et al.
|
||||
pub fn stderr(&self) -> impl std::io::Write + 'static {
|
||||
self.params.stderr(self.shell)
|
||||
}
|
||||
|
||||
/// Returns the file descriptor with the given number. Returns `None`
|
||||
/// if the file descriptor is not open.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - The file descriptor number to retrieve.
|
||||
pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
|
||||
self.params.try_fd(self.shell, fd)
|
||||
}
|
||||
|
||||
/// Iterates over all open file descriptors.
|
||||
pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
|
||||
self.params.iter_fds(self.shell)
|
||||
}
|
||||
|
||||
pub(crate) const fn should_cmd_lead_own_process_group(&self) -> bool {
|
||||
self.shell.options.interactive
|
||||
&& matches!(
|
||||
self.params.process_group_policy,
|
||||
ProcessGroupPolicy::NewProcessGroup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// An argument to a command.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CommandArg {
|
||||
/// A simple string argument.
|
||||
String(String),
|
||||
/// An assignment/declaration; typically treated as a string, but will
|
||||
/// be specially handled by a limited set of built-in commands.
|
||||
Assignment(ast::Assignment),
|
||||
}
|
||||
|
||||
impl Display for CommandArg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::String(s) => f.write_str(s),
|
||||
Self::Assignment(a) => write!(f, "{a}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CommandArg {
|
||||
fn from(s: String) -> Self {
|
||||
Self::String(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for CommandArg {
|
||||
fn from(value: &String) -> Self {
|
||||
Self::String(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandArg {
|
||||
pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
|
||||
Self::Assignment(a) => {
|
||||
let mut s = a.name.to_string();
|
||||
let op = if a.append { "+=" } else { "=" };
|
||||
s.push_str(op);
|
||||
s.push_str(&escape::quote_if_needed(
|
||||
a.value.to_string().as_str(),
|
||||
escape::QuoteMode::SingleQuote,
|
||||
));
|
||||
s.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Composes a `std::process::Command` to execute the given command. Appropriately
|
||||
/// configures the command name and arguments, redirections, injected file
|
||||
/// descriptors, environment variables, etc.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `context` - The execution context in which the command is being composed.
|
||||
/// * `command_name` - The name of the command to execute.
|
||||
/// * `argv0` - The value to use for `argv[0]` (may be different from the command).
|
||||
/// * `args` - The arguments to pass to the command.
|
||||
/// * `empty_env` - If true, the command will be executed with an empty
|
||||
/// environment; if false, the command will inherit environment variables
|
||||
/// marked as exported in the provided `Shell`.
|
||||
#[allow(unused_variables, reason = "argv0 is only used on unix platforms")]
|
||||
pub fn compose_std_command<S: AsRef<OsStr>>(
|
||||
context: &ExecutionContext<'_>,
|
||||
command_name: &str,
|
||||
argv0: &str,
|
||||
args: &[S],
|
||||
empty_env: bool,
|
||||
) -> Result<std::process::Command, error::Error> {
|
||||
let mut cmd = std::process::Command::new(command_name);
|
||||
|
||||
// Override argv[0].
|
||||
// NOTE: Not supported on all platforms.
|
||||
cmd.arg0(argv0);
|
||||
|
||||
// Pass through args.
|
||||
cmd.args(args);
|
||||
|
||||
// Use the shell's current working dir.
|
||||
cmd.current_dir(context.shell.working_dir());
|
||||
|
||||
// Start with a clear environment.
|
||||
cmd.env_clear();
|
||||
|
||||
// Add in exported variables.
|
||||
if !empty_env {
|
||||
for (k, v) in context.shell.env.iter_exported() {
|
||||
// NOTE: To match bash behavior, we only include exported variables
|
||||
// that are set (i.e., have a value). This means a variable that
|
||||
// shows up in `declare -p` but has no *set* value will be omitted.
|
||||
if v.value().is_set() {
|
||||
cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add in exported functions.
|
||||
if !empty_env {
|
||||
for (func_name, registration) in context.shell.funcs().iter() {
|
||||
if registration.is_exported() {
|
||||
let var_name = std::format!("BASH_FUNC_{func_name}%%");
|
||||
let value = std::format!("() {}", registration.definition.body);
|
||||
cmd.env(var_name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect stdin, if applicable.
|
||||
match context.try_fd(OpenFiles::STDIN_FD) {
|
||||
Some(OpenFile::Stdin(_)) | None => (),
|
||||
Some(stdin_file) => {
|
||||
let as_stdio: Stdio = stdin_file.into();
|
||||
cmd.stdin(as_stdio);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect stdout, if applicable.
|
||||
match context.try_fd(OpenFiles::STDOUT_FD) {
|
||||
Some(OpenFile::Stdout(_)) | None => (),
|
||||
Some(stdout_file) => {
|
||||
let as_stdio: Stdio = stdout_file.into();
|
||||
cmd.stdout(as_stdio);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect stderr, if applicable.
|
||||
match context.try_fd(OpenFiles::STDERR_FD) {
|
||||
Some(OpenFile::Stderr(_)) | None => {}
|
||||
Some(stderr_file) => {
|
||||
let as_stdio: Stdio = stderr_file.into();
|
||||
cmd.stderr(as_stdio);
|
||||
}
|
||||
}
|
||||
|
||||
// Inject any other fds.
|
||||
let other_files = context.iter_fds().filter(|(fd, _)| {
|
||||
*fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
|
||||
});
|
||||
cmd.inject_fds(other_files)?;
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
pub(crate) async fn on_preexecute(
|
||||
context: &mut ExecutionContext<'_>,
|
||||
args: &[CommandArg],
|
||||
) -> Result<(), error::Error> {
|
||||
// See if we have a DEBUG trap handler registered; call it if we do.
|
||||
invoke_debug_trap_handler_if_registered(context, args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn invoke_debug_trap_handler_if_registered(
|
||||
context: &mut ExecutionContext<'_>,
|
||||
args: &[CommandArg],
|
||||
) -> Result<(), error::Error> {
|
||||
if context.shell.traps.handler_depth == 0 {
|
||||
let debug_trap_handler = context
|
||||
.shell
|
||||
.traps
|
||||
.handlers
|
||||
.get(&traps::TrapSignal::Debug)
|
||||
.cloned();
|
||||
if let Some(debug_trap_handler) = debug_trap_handler {
|
||||
// TODO: Confirm whether trap handlers should be executed in the same process group.
|
||||
let mut handler_params = context.params.clone();
|
||||
handler_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
|
||||
|
||||
let full_cmd = args.iter().map(|arg| arg.to_string()).join(" ");
|
||||
|
||||
// TODO: This shouldn't *just* be set in a trap situation.
|
||||
context.shell.env.update_or_add(
|
||||
"BASH_COMMAND",
|
||||
variables::ShellValueLiteral::Scalar(full_cmd),
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)?;
|
||||
|
||||
context.shell.traps.handler_depth += 1;
|
||||
|
||||
// TODO: Discard result?
|
||||
let _ = context
|
||||
.shell
|
||||
.run_string(debug_trap_handler, &handler_params)
|
||||
.await;
|
||||
|
||||
context.shell.traps.handler_depth -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Executes a simple command.
|
||||
///
|
||||
/// The command may be a builtin, a shell function, or an externally
|
||||
/// executed command. This function's implementation is responsible for
|
||||
/// dispatching it appropriately according to the context provided.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cmd_context` - The context in which the command is being executed.
|
||||
/// * `process_group_id` - The process group ID to use for externally
|
||||
/// executed commands. This may be modified if a new process group is
|
||||
/// created.
|
||||
/// * `args` - The arguments to the command.
|
||||
/// * `use_functions` - If true, the command name will be checked against
|
||||
/// shell functions; if not, shell functions will not be consulted.
|
||||
/// * `path_dirs` - If provided, these directories will be searched for
|
||||
/// external commands; if not provided, the default search logic will
|
||||
/// be used.
|
||||
pub async fn execute(
|
||||
cmd_context: ExecutionContext<'_>,
|
||||
process_group_id: &mut Option<i32>,
|
||||
args: Vec<CommandArg>,
|
||||
use_functions: bool,
|
||||
path_dirs: Option<Vec<String>>,
|
||||
) -> Result<ExecutionSpawnResult, error::Error> {
|
||||
// First see if it's the name of a builtin.
|
||||
let builtin = cmd_context
|
||||
.shell
|
||||
.builtins()
|
||||
.get(&cmd_context.command_name)
|
||||
.cloned();
|
||||
|
||||
// If we found a special builtin (that's not disabled), then invoke it.
|
||||
if builtin
|
||||
.as_ref()
|
||||
.is_some_and(|r| !r.disabled && r.special_builtin)
|
||||
{
|
||||
return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
|
||||
}
|
||||
|
||||
// Assuming we weren't requested not to do so, check if it's the name of
|
||||
// a shell function.
|
||||
if use_functions {
|
||||
if let Some(func_reg) = cmd_context
|
||||
.shell
|
||||
.funcs()
|
||||
.get(cmd_context.command_name.as_str())
|
||||
{
|
||||
// Strip the function name off args.
|
||||
return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a (non-special) builtin and it's not disabled, then invoke it.
|
||||
if let Some(builtin) = builtin {
|
||||
if !builtin.disabled {
|
||||
return execute_builtin_command(&builtin, cmd_context, args).await;
|
||||
}
|
||||
}
|
||||
|
||||
// We still haven't found a command to invoke. We'll need to look for an external command.
|
||||
if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
|
||||
// All else failed; if we were given path directories to search, try to look through them
|
||||
// for a matching executable. Otherwise, use our default search logic.
|
||||
let path = if let Some(path_dirs) = path_dirs {
|
||||
pathsearch::search_for_executable(
|
||||
path_dirs.iter().map(String::as_str),
|
||||
cmd_context.command_name.as_str(),
|
||||
)
|
||||
.next()
|
||||
} else {
|
||||
cmd_context
|
||||
.shell
|
||||
.find_first_executable_in_path_using_cache(&cmd_context.command_name)
|
||||
};
|
||||
|
||||
if let Some(path) = path {
|
||||
let resolved_path = path.to_string_lossy();
|
||||
execute_external_command(
|
||||
cmd_context,
|
||||
resolved_path.as_ref(),
|
||||
process_group_id,
|
||||
&args[1..],
|
||||
)
|
||||
} else {
|
||||
Err(ErrorKind::CommandNotFound(cmd_context.command_name).into())
|
||||
}
|
||||
} else {
|
||||
let resolved_path = cmd_context.command_name.clone();
|
||||
|
||||
// Strip the command name off args.
|
||||
execute_external_command(
|
||||
cmd_context,
|
||||
resolved_path.as_str(),
|
||||
process_group_id,
|
||||
&args[1..],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn execute_external_command(
|
||||
context: ExecutionContext<'_>,
|
||||
executable_path: &str,
|
||||
process_group_id: &mut Option<i32>,
|
||||
args: &[CommandArg],
|
||||
) -> Result<ExecutionSpawnResult, error::Error> {
|
||||
// Filter out the args; we only want strings.
|
||||
let mut cmd_args = vec![];
|
||||
for arg in args {
|
||||
if let CommandArg::String(s) = arg {
|
||||
cmd_args.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Before we lose ownership of the open files, figure out if stdin will be a terminal.
|
||||
let child_stdin_is_terminal = context
|
||||
.try_fd(openfiles::OpenFiles::STDIN_FD)
|
||||
.is_some_and(|f| f.is_term());
|
||||
|
||||
// Figure out if we should be setting up a new process group.
|
||||
let new_pg = context.should_cmd_lead_own_process_group();
|
||||
|
||||
// Compose the std::process::Command that encapsulates what we want to launch.
|
||||
#[allow(unused_mut, reason = "only mutated on unix platforms")]
|
||||
let mut cmd = compose_std_command(
|
||||
&context,
|
||||
executable_path,
|
||||
context.command_name.as_str(),
|
||||
cmd_args.as_slice(),
|
||||
false, /* empty environment? */
|
||||
)?;
|
||||
|
||||
// Set up process group state.
|
||||
if new_pg {
|
||||
// We need to set up a new process group.
|
||||
cmd.process_group(0);
|
||||
} else {
|
||||
// We need to join an established process group.
|
||||
if let Some(pgid) = process_group_id {
|
||||
cmd.process_group(*pgid);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're to lead our own process group and stdin is a terminal,
|
||||
// then we need to arrange for the new process to move itself
|
||||
// to the foreground.
|
||||
if new_pg && child_stdin_is_terminal {
|
||||
cmd.take_foreground();
|
||||
}
|
||||
|
||||
// When tracing is enabled, report.
|
||||
tracing::debug!(
|
||||
target: trace_categories::COMMANDS,
|
||||
"Spawning: cmd='{} {}'",
|
||||
cmd.get_program().to_string_lossy().to_string(),
|
||||
cmd.get_args()
|
||||
.map(|a| a.to_string_lossy().to_string())
|
||||
.join(" ")
|
||||
);
|
||||
|
||||
match sys::process::spawn(cmd) {
|
||||
Ok(child) => {
|
||||
// Retrieve the pid.
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
let pid = child.id().map(|id| id as i32);
|
||||
if let Some(pid) = &pid {
|
||||
if new_pg {
|
||||
*process_group_id = Some(*pid);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("could not retrieve pid for child process");
|
||||
}
|
||||
|
||||
Ok(ExecutionSpawnResult::StartedProcess(
|
||||
processes::ChildProcess::new(pid, child),
|
||||
))
|
||||
}
|
||||
Err(spawn_err) => {
|
||||
if context.shell.options.interactive {
|
||||
sys::terminal::move_self_to_foreground()?;
|
||||
}
|
||||
|
||||
if spawn_err.kind() == std::io::ErrorKind::NotFound {
|
||||
if !context.shell.working_dir().exists() {
|
||||
Err(
|
||||
error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned())
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
Err(error::ErrorKind::CommandNotFound(context.command_name).into())
|
||||
}
|
||||
} else {
|
||||
Err(
|
||||
error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_builtin_command(
|
||||
builtin: &builtins::Registration,
|
||||
context: ExecutionContext<'_>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> Result<ExecutionSpawnResult, error::Error> {
|
||||
let result = (builtin.execute_func)(context, args).await?;
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
pub(crate) async fn invoke_shell_function(
|
||||
function_definition: Arc<ast::FunctionDefinition>,
|
||||
mut context: ExecutionContext<'_>,
|
||||
args: &[CommandArg],
|
||||
) -> Result<ExecutionSpawnResult, error::Error> {
|
||||
let ast::FunctionBody(body, redirects) = &function_definition.body;
|
||||
|
||||
// Apply any redirects specified at function definition-time.
|
||||
if let Some(redirects) = redirects {
|
||||
for redirect in &redirects.0 {
|
||||
interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily replace positional parameters.
|
||||
let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
|
||||
context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
|
||||
|
||||
// Pass through open files.
|
||||
let params = context.params.clone();
|
||||
|
||||
// Note that we're going deeper. Once we do this, we need to make sure we don't bail early
|
||||
// before "exiting" the function.
|
||||
context
|
||||
.shell
|
||||
.enter_function(context.command_name.as_str(), &function_definition)?;
|
||||
|
||||
// Invoke the function.
|
||||
let result = body.execute(context.shell, ¶ms).await;
|
||||
|
||||
// Clean up parameters so any owned files are closed.
|
||||
drop(params);
|
||||
|
||||
// We've come back out, reflect it.
|
||||
context.shell.leave_function()?;
|
||||
|
||||
// Restore positional parameters.
|
||||
context.shell.positional_parameters = prior_positional_params;
|
||||
|
||||
// Get the actual execution result from the body of the function.
|
||||
let mut result = result?;
|
||||
|
||||
// Handle control-flow.
|
||||
match result.next_control_flow {
|
||||
ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
|
||||
return error::unimp("break or continue returned from function invocation");
|
||||
}
|
||||
ExecutionControlFlow::ReturnFromFunctionOrScript => {
|
||||
// It's now been handled.
|
||||
result.next_control_flow = ExecutionControlFlow::Normal;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
pub(crate) async fn invoke_command_in_subshell_and_get_output(
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
s: String,
|
||||
) -> Result<String, error::Error> {
|
||||
// Instantiate a subshell to run the command in.
|
||||
let subshell = shell.clone();
|
||||
|
||||
// Get our own set of parameters we can customize and use.
|
||||
let mut params = params.clone();
|
||||
params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
|
||||
|
||||
// Set up pipe so we can read the output.
|
||||
let (reader, writer) = std::io::pipe()?;
|
||||
params.set_fd(OpenFiles::STDOUT_FD, writer.into());
|
||||
|
||||
// Start the execution of the command, but don't wait for it to
|
||||
// complete. In case the command generates lots of output, we
|
||||
// need to start reading in parallel so the command doesn't block
|
||||
// when the pipe's buffer fills up. We pass ownership of the
|
||||
// subshell and params to run_substitution_command; we must
|
||||
// ensure that they're both dropped by the time this call
|
||||
// returns (so they're not holding onto the write end of the pipe).
|
||||
let cmd_join_handle = tokio::task::spawn_blocking(move || {
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
rt.block_on(run_substitution_command(subshell, params, s))
|
||||
});
|
||||
|
||||
// Extract output.
|
||||
let output_str = std::io::read_to_string(reader)?;
|
||||
|
||||
// Now observe the command's completion.
|
||||
let run_result = cmd_join_handle.await?;
|
||||
let cmd_result = run_result?;
|
||||
|
||||
// Store the status.
|
||||
*shell.last_exit_status_mut() = cmd_result.exit_code.into();
|
||||
|
||||
Ok(output_str)
|
||||
}
|
||||
|
||||
async fn run_substitution_command(
|
||||
mut shell: Shell,
|
||||
mut params: ExecutionParameters,
|
||||
command: String,
|
||||
) -> Result<ExecutionResult, error::Error> {
|
||||
// Parse the string into a whole shell program.
|
||||
let parse_result = shell.parse_string(command);
|
||||
|
||||
// Check for a command that is only an input redirection ("< file").
|
||||
// If detected, emulate `cat file` to stdout and return immediately.
|
||||
// If we failed to parse, then we'll fall below and handle it there.
|
||||
if let Ok(program) = &parse_result {
|
||||
if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
|
||||
interp::setup_redirect(&mut shell, &mut params, redir).await?;
|
||||
std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
|
||||
return Ok(ExecutionResult::new(0));
|
||||
}
|
||||
}
|
||||
|
||||
let source_info = brush_parser::SourceInfo {
|
||||
source: String::from("main"),
|
||||
};
|
||||
|
||||
// Handle the parse result using default shell behavior.
|
||||
shell
|
||||
.run_parsed_result(parse_result, &source_info, ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
// Detects a subshell command that consists solely of a single input redirection
|
||||
// (e.g., "< file"), returning the IoRedirect when present.
|
||||
fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
|
||||
// We're looking for exactly one complete command...
|
||||
let [complete] = program.complete_commands.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// ...a single list item...
|
||||
let ast::CompoundList(items) = complete;
|
||||
let [item] = items.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// ...with a single pipeline (no && or || chaining)...
|
||||
let and_or = &item.0;
|
||||
if !and_or.additional.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// ...not negated...
|
||||
let pipeline = &and_or.first;
|
||||
if pipeline.bang {
|
||||
return None;
|
||||
}
|
||||
|
||||
// ...with a single command in the pipeline...
|
||||
let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// ...with no program word/name and no suffix...
|
||||
if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// ...and exactly one prefix containing an I/O redirect...
|
||||
let prefix = simple_cmd.prefix.as_ref()?;
|
||||
let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// ...that is a file input redirection to a filename, targeting stdin.
|
||||
match redir {
|
||||
ast::IoRedirect::File(
|
||||
fd,
|
||||
ast::IoFileRedirectKind::Read,
|
||||
ast::IoFileRedirectTarget::Filename(..),
|
||||
) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,611 @@
|
||||
//! Implements a shell variable environment.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map;
|
||||
|
||||
use crate::error;
|
||||
use crate::shell;
|
||||
use crate::variables::{self, ShellValue, ShellValueUnsetType, ShellVariable};
|
||||
|
||||
/// Represents the policy for looking up variables in a shell environment.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EnvironmentLookup {
|
||||
/// Look anywhere.
|
||||
Anywhere,
|
||||
/// Look only in the global scope.
|
||||
OnlyInGlobal,
|
||||
/// Look only in the current local scope.
|
||||
OnlyInCurrentLocal,
|
||||
/// Look only in local scopes.
|
||||
OnlyInLocal,
|
||||
}
|
||||
|
||||
/// Represents a shell environment scope.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EnvironmentScope {
|
||||
/// Scope local to a function instance
|
||||
Local,
|
||||
/// Globals
|
||||
Global,
|
||||
/// Transient overrides for a command invocation
|
||||
Command,
|
||||
}
|
||||
|
||||
/// Represents the shell variable environment, composed of a stack of scopes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ShellEnvironment {
|
||||
/// Stack of scopes, with the top of the stack being the current scope.
|
||||
scopes: Vec<(EnvironmentScope, ShellVariableMap)>,
|
||||
/// Whether or not to auto-export variables on creation or modification.
|
||||
export_variables_on_modification: bool,
|
||||
/// Count of total entries (may include duplicates with shadowed variables).
|
||||
entry_count: usize,
|
||||
}
|
||||
|
||||
impl Default for ShellEnvironment {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellEnvironment {
|
||||
/// Returns a new shell environment.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scopes: vec![(EnvironmentScope::Global, ShellVariableMap::default())],
|
||||
export_variables_on_modification: false,
|
||||
entry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a new scope of the given type onto the environment's scope stack.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `scope_type` - The type of scope to push.
|
||||
pub fn push_scope(&mut self, scope_type: EnvironmentScope) {
|
||||
self.scopes.push((scope_type, ShellVariableMap::default()));
|
||||
}
|
||||
|
||||
/// Pops the top-most scope off the environment's scope stack.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `expected_scope_type` - The type of scope that is expected to be atop the stack.
|
||||
pub fn pop_scope(&mut self, expected_scope_type: EnvironmentScope) -> Result<(), error::Error> {
|
||||
// TODO: Should we panic instead on failure? It's effectively a broken invariant.
|
||||
match self.scopes.pop() {
|
||||
Some((actual_scope_type, _)) if actual_scope_type == expected_scope_type => Ok(()),
|
||||
_ => Err(error::ErrorKind::MissingScope.into()),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Iterators/Getters
|
||||
//
|
||||
|
||||
/// Returns an iterator over all exported variables defined in the variable.
|
||||
pub fn iter_exported(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
|
||||
// We won't actually need to store all entries, but we expect it should be
|
||||
// within the same order.
|
||||
let mut visible_vars: HashMap<&String, &ShellVariable> =
|
||||
HashMap::with_capacity(self.entry_count);
|
||||
|
||||
for (_, var_map) in self.scopes.iter().rev() {
|
||||
for (name, var) in var_map.iter().filter(|(_, v)| v.is_exported()) {
|
||||
// Only insert the variable if it hasn't been seen yet.
|
||||
if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
|
||||
entry.insert(var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible_vars.into_iter()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all the variables defined in the environment.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
|
||||
self.iter_using_policy(EnvironmentLookup::Anywhere)
|
||||
}
|
||||
|
||||
/// Returns an iterator over all the variables defined in the environment,
|
||||
/// using the given lookup policy.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `lookup_policy` - The policy to use when looking up variables.
|
||||
pub fn iter_using_policy(
|
||||
&self,
|
||||
lookup_policy: EnvironmentLookup,
|
||||
) -> impl Iterator<Item = (&String, &ShellVariable)> {
|
||||
// We won't actually need to store all entries, but we expect it should be
|
||||
// within the same order.
|
||||
let mut visible_vars: HashMap<&String, &ShellVariable> =
|
||||
HashMap::with_capacity(self.entry_count);
|
||||
|
||||
let mut local_count = 0;
|
||||
for (scope_type, var_map) in self.scopes.iter().rev() {
|
||||
if matches!(scope_type, EnvironmentScope::Local) {
|
||||
local_count += 1;
|
||||
}
|
||||
|
||||
match lookup_policy {
|
||||
EnvironmentLookup::Anywhere => (),
|
||||
EnvironmentLookup::OnlyInGlobal => {
|
||||
if !matches!(scope_type, EnvironmentScope::Global) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
EnvironmentLookup::OnlyInCurrentLocal => {
|
||||
if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
EnvironmentLookup::OnlyInLocal => {
|
||||
if !matches!(scope_type, EnvironmentScope::Local) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (name, var) in var_map.iter() {
|
||||
// Only insert the variable if it hasn't been seen yet.
|
||||
if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
|
||||
entry.insert(var);
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(scope_type, EnvironmentScope::Local)
|
||||
&& matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
visible_vars.into_iter()
|
||||
}
|
||||
|
||||
/// Tries to retrieve an immutable reference to the variable with the given name
|
||||
/// in the environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to retrieve.
|
||||
pub fn get<S: AsRef<str>>(&self, name: S) -> Option<(EnvironmentScope, &ShellVariable)> {
|
||||
// Look through scopes, from the top of the stack on down.
|
||||
for (scope_type, map) in self.scopes.iter().rev() {
|
||||
if let Some(var) = map.get(name.as_ref()) {
|
||||
return Some((*scope_type, var));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Tries to retrieve a mutable reference to the variable with the given name
|
||||
/// in the environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to retrieve.
|
||||
pub fn get_mut<S: AsRef<str>>(
|
||||
&mut self,
|
||||
name: S,
|
||||
) -> Option<(EnvironmentScope, &mut ShellVariable)> {
|
||||
// Look through scopes, from the top of the stack on down.
|
||||
for (scope_type, map) in self.scopes.iter_mut().rev() {
|
||||
if let Some(var) = map.get_mut(name.as_ref()) {
|
||||
return Some((*scope_type, var));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Tries to retrieve the string value of the variable with the given name in the
|
||||
/// environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to retrieve.
|
||||
/// * `shell` - The shell owning the environment.
|
||||
pub fn get_str<S: AsRef<str>>(&self, name: S, shell: &shell::Shell) -> Option<Cow<'_, str>> {
|
||||
self.get(name.as_ref())
|
||||
.map(|(_, v)| v.value().to_cow_str(shell))
|
||||
}
|
||||
|
||||
/// Checks if a variable of the given name is set in the environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to check.
|
||||
pub fn is_set<S: AsRef<str>>(&self, name: S) -> bool {
|
||||
if let Some((_, var)) = self.get(name) {
|
||||
!matches!(var.value(), ShellValue::Unset(_))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Setters
|
||||
//
|
||||
|
||||
/// Tries to unset the variable with the given name in the environment, returning
|
||||
/// whether or not such a variable existed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to unset.
|
||||
pub fn unset(&mut self, name: &str) -> Result<Option<ShellVariable>, error::Error> {
|
||||
let mut local_count = 0;
|
||||
for (scope_type, map) in self.scopes.iter_mut().rev() {
|
||||
if matches!(scope_type, EnvironmentScope::Local) {
|
||||
local_count += 1;
|
||||
}
|
||||
|
||||
let unset_result = Self::try_unset_in_map(map, name)?;
|
||||
|
||||
if unset_result.is_some() {
|
||||
// If we end up finding a local in the top-most local frame, then we replace
|
||||
// it with a placeholder.
|
||||
if matches!(scope_type, EnvironmentScope::Local) && local_count == 1 {
|
||||
map.set(
|
||||
name,
|
||||
ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped)),
|
||||
);
|
||||
} else if self.entry_count > 0 {
|
||||
// Entry count should never be 0 here, but we're being defensive.
|
||||
self.entry_count -= 1;
|
||||
}
|
||||
|
||||
return Ok(unset_result);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Tries to unset an array element from the environment, using the given name and
|
||||
/// element index for lookup. Returns whether or not an element was unset.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the array variable to unset an element from.
|
||||
/// * `index` - The index of the element to unset.
|
||||
pub fn unset_index(&mut self, name: &str, index: &str) -> Result<bool, error::Error> {
|
||||
if let Some((_, var)) = self.get_mut(name) {
|
||||
var.unset_index(index)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn try_unset_in_map(
|
||||
map: &mut ShellVariableMap,
|
||||
name: &str,
|
||||
) -> Result<Option<ShellVariable>, error::Error> {
|
||||
match map.get(name).map(|v| v.is_readonly()) {
|
||||
Some(true) => Err(error::ErrorKind::ReadonlyVariable.into()),
|
||||
Some(false) => Ok(map.unset(name)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to retrieve an immutable reference to a variable from the environment,
|
||||
/// using the given name and lookup policy.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to retrieve.
|
||||
/// * `lookup_policy` - The policy to use when looking up the variable.
|
||||
pub fn get_using_policy<N: AsRef<str>>(
|
||||
&self,
|
||||
name: N,
|
||||
lookup_policy: EnvironmentLookup,
|
||||
) -> Option<&ShellVariable> {
|
||||
let mut local_count = 0;
|
||||
for (scope_type, var_map) in self.scopes.iter().rev() {
|
||||
if matches!(scope_type, EnvironmentScope::Local) {
|
||||
local_count += 1;
|
||||
}
|
||||
|
||||
match lookup_policy {
|
||||
EnvironmentLookup::Anywhere => (),
|
||||
EnvironmentLookup::OnlyInGlobal => {
|
||||
if !matches!(scope_type, EnvironmentScope::Global) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
EnvironmentLookup::OnlyInCurrentLocal => {
|
||||
if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
EnvironmentLookup::OnlyInLocal => {
|
||||
if !matches!(scope_type, EnvironmentScope::Local) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(var) = var_map.get(name.as_ref()) {
|
||||
return Some(var);
|
||||
}
|
||||
|
||||
if matches!(scope_type, EnvironmentScope::Local)
|
||||
&& matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Tries to retrieve a mutable reference to a variable from the environment,
|
||||
/// using the given name and lookup policy.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to retrieve.
|
||||
/// * `lookup_policy` - The policy to use when looking up the variable.
|
||||
pub fn get_mut_using_policy<N: AsRef<str>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
lookup_policy: EnvironmentLookup,
|
||||
) -> Option<&mut ShellVariable> {
|
||||
let mut local_count = 0;
|
||||
for (scope_type, var_map) in self.scopes.iter_mut().rev() {
|
||||
if matches!(scope_type, EnvironmentScope::Local) {
|
||||
local_count += 1;
|
||||
}
|
||||
|
||||
match lookup_policy {
|
||||
EnvironmentLookup::Anywhere => (),
|
||||
EnvironmentLookup::OnlyInGlobal => {
|
||||
if !matches!(scope_type, EnvironmentScope::Global) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
EnvironmentLookup::OnlyInCurrentLocal => {
|
||||
if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
EnvironmentLookup::OnlyInLocal => {
|
||||
if !matches!(scope_type, EnvironmentScope::Local) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(var) = var_map.get_mut(name.as_ref()) {
|
||||
return Some(var);
|
||||
}
|
||||
|
||||
if matches!(scope_type, EnvironmentScope::Local)
|
||||
&& matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Update a variable in the environment, or add it if it doesn't already exist.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to update or add.
|
||||
/// * `value` - The value to assign to the variable.
|
||||
/// * `updater` - A function to call to update the variable after assigning the value.
|
||||
/// * `lookup_policy` - The policy to use when looking up the variable.
|
||||
/// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist.
|
||||
pub fn update_or_add<N: Into<String>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
value: variables::ShellValueLiteral,
|
||||
updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
|
||||
lookup_policy: EnvironmentLookup,
|
||||
scope_if_creating: EnvironmentScope,
|
||||
) -> Result<(), error::Error> {
|
||||
let name = name.into();
|
||||
|
||||
let auto_export = self.export_variables_on_modification;
|
||||
if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
|
||||
var.assign(value, false)?;
|
||||
if auto_export {
|
||||
var.export();
|
||||
}
|
||||
updater(var)
|
||||
} else {
|
||||
let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
|
||||
var.assign(value, false)?;
|
||||
if auto_export {
|
||||
var.export();
|
||||
}
|
||||
updater(&mut var)?;
|
||||
|
||||
self.add(name, var, scope_if_creating)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an array element in the environment, or add it if it doesn't already exist.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to update or add.
|
||||
/// * `index` - The index of the element to update or add.
|
||||
/// * `value` - The value to assign to the variable.
|
||||
/// * `updater` - A function to call to update the variable after assigning the value.
|
||||
/// * `lookup_policy` - The policy to use when looking up the variable.
|
||||
/// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist.
|
||||
pub fn update_or_add_array_element<N: Into<String>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
index: String,
|
||||
value: String,
|
||||
updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
|
||||
lookup_policy: EnvironmentLookup,
|
||||
scope_if_creating: EnvironmentScope,
|
||||
) -> Result<(), error::Error> {
|
||||
let name = name.into();
|
||||
|
||||
if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
|
||||
var.assign_at_index(index, value, false)?;
|
||||
updater(var)
|
||||
} else {
|
||||
let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
|
||||
var.assign(
|
||||
variables::ShellValueLiteral::Array(variables::ArrayLiteral(vec![(
|
||||
Some(index),
|
||||
value,
|
||||
)])),
|
||||
false,
|
||||
)?;
|
||||
updater(&mut var)?;
|
||||
|
||||
self.add(name, var, scope_if_creating)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a variable to the environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to add.
|
||||
/// * `var` - The variable to add.
|
||||
/// * `target_scope` - The scope to add the variable to.
|
||||
pub fn add<N: Into<String>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
mut var: ShellVariable,
|
||||
target_scope: EnvironmentScope,
|
||||
) -> Result<(), error::Error> {
|
||||
if self.export_variables_on_modification {
|
||||
var.export();
|
||||
}
|
||||
|
||||
for (scope_type, map) in self.scopes.iter_mut().rev() {
|
||||
if *scope_type == target_scope {
|
||||
let prev_var = map.set(name, var);
|
||||
if prev_var.is_none() {
|
||||
self.entry_count += 1;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(error::ErrorKind::MissingScope.into())
|
||||
}
|
||||
|
||||
/// Sets a global variable in the environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to set.
|
||||
/// * `var` - The variable to set.
|
||||
pub fn set_global<N: Into<String>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
var: ShellVariable,
|
||||
) -> Result<(), error::Error> {
|
||||
self.add(name, var, EnvironmentScope::Global)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a map from names to shell variables.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ShellVariableMap {
|
||||
variables: HashMap<String, ShellVariable>,
|
||||
}
|
||||
|
||||
impl ShellVariableMap {
|
||||
//
|
||||
// Iterators/Getters
|
||||
//
|
||||
|
||||
/// Returns an iterator over all the variables in the map.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
|
||||
self.variables.iter()
|
||||
}
|
||||
|
||||
/// Tries to retrieve an immutable reference to the variable with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to retrieve.
|
||||
pub fn get(&self, name: &str) -> Option<&ShellVariable> {
|
||||
self.variables.get(name)
|
||||
}
|
||||
|
||||
/// Tries to retrieve a mutable reference to the variable with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to retrieve.
|
||||
pub fn get_mut(&mut self, name: &str) -> Option<&mut ShellVariable> {
|
||||
self.variables.get_mut(name)
|
||||
}
|
||||
|
||||
//
|
||||
// Setters
|
||||
//
|
||||
|
||||
/// Tries to unset the variable with the given name, returning the removed
|
||||
/// variable or None if it was not already set.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to unset.
|
||||
pub fn unset(&mut self, name: &str) -> Option<ShellVariable> {
|
||||
self.variables.remove(name)
|
||||
}
|
||||
|
||||
/// Sets a variable in the map.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to set.
|
||||
/// * `var` - The variable to set.
|
||||
pub fn set<N: Into<String>>(&mut self, name: N, var: ShellVariable) -> Option<ShellVariable> {
|
||||
self.variables.insert(name.into(), var)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the given name is a valid variable name.
|
||||
pub fn valid_variable_name(s: &str) -> bool {
|
||||
let mut cs = s.chars();
|
||||
match cs.next() {
|
||||
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
|
||||
cs.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
Some(_) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_variable_name() {
|
||||
assert!(!valid_variable_name(""));
|
||||
assert!(!valid_variable_name("1"));
|
||||
assert!(!valid_variable_name(" a"));
|
||||
assert!(!valid_variable_name(" "));
|
||||
|
||||
assert!(valid_variable_name("_"));
|
||||
assert!(valid_variable_name("_a"));
|
||||
assert!(valid_variable_name("_1"));
|
||||
assert!(valid_variable_name("_a1"));
|
||||
assert!(valid_variable_name("a"));
|
||||
assert!(valid_variable_name("A"));
|
||||
assert!(valid_variable_name("a1"));
|
||||
assert!(valid_variable_name("A1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
//! Error facilities
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{Shell, ShellFd, results, sys};
|
||||
|
||||
/// Unified error type for this crate. Contains just a kind for now,
|
||||
/// but will be extended later with additional context.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error(transparent)]
|
||||
pub struct Error {
|
||||
/// The kind of error.
|
||||
kind: ErrorKind,
|
||||
}
|
||||
|
||||
/// Monolithic error type for the shell
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ErrorKind {
|
||||
/// A tilde expression was used without a valid HOME variable
|
||||
#[error("cannot expand tilde expression with HOME not set")]
|
||||
TildeWithoutValidHome,
|
||||
|
||||
/// An attempt was made to assign a list to an array member
|
||||
#[error("cannot assign list to array member")]
|
||||
AssigningListToArrayMember,
|
||||
|
||||
/// An attempt was made to convert an associative array to an indexed array.
|
||||
#[error("cannot convert associative array to indexed array")]
|
||||
ConvertingAssociativeArrayToIndexedArray,
|
||||
|
||||
/// An attempt was made to convert an indexed array to an associative array.
|
||||
#[error("cannot convert indexed array to associative array")]
|
||||
ConvertingIndexedArrayToAssociativeArray,
|
||||
|
||||
/// An error occurred while sourcing the indicated script file.
|
||||
#[error("failed to source file: {0}")]
|
||||
FailedSourcingFile(PathBuf, #[source] std::io::Error),
|
||||
|
||||
/// The shell failed to send a signal to a process.
|
||||
#[error("failed to send signal to process")]
|
||||
FailedToSendSignal,
|
||||
|
||||
/// An attempt was made to assign a value to a special parameter.
|
||||
#[error("cannot assign in this way")]
|
||||
CannotAssignToSpecialParameter,
|
||||
|
||||
/// Checked expansion error.
|
||||
#[error("expansion error: {0}")]
|
||||
CheckedExpansionError(String),
|
||||
|
||||
/// A reference was made to an unknown shell function.
|
||||
#[error("function not found: {0}")]
|
||||
FunctionNotFound(String),
|
||||
|
||||
/// Command was not found.
|
||||
#[error("command not found: {0}")]
|
||||
CommandNotFound(String),
|
||||
|
||||
/// Not a builtin.
|
||||
#[error("not a shell builtin: {0}")]
|
||||
BuiltinNotFound(String),
|
||||
|
||||
/// The working directory does not exist.
|
||||
#[error("working directory does not exist: {0}")]
|
||||
WorkingDirMissing(PathBuf),
|
||||
|
||||
/// Failed to execute command.
|
||||
#[error("failed to execute command '{0}': {1}")]
|
||||
FailedToExecuteCommand(String, #[source] std::io::Error),
|
||||
|
||||
/// History item was not found.
|
||||
#[error("history item not found")]
|
||||
HistoryItemNotFound,
|
||||
|
||||
/// The requested functionality has not yet been implemented in this shell.
|
||||
#[error("not yet implemented: {0}")]
|
||||
Unimplemented(&'static str),
|
||||
|
||||
/// The requested functionality has not yet been implemented in this shell; it is tracked in a
|
||||
/// GitHub issue.
|
||||
#[error("not yet implemented: {0}; see https://github.com/reubeno/brush/issues/{1}")]
|
||||
UnimplementedAndTracked(&'static str, u32),
|
||||
|
||||
/// An expected environment scope could not be found.
|
||||
#[error("missing scope")]
|
||||
MissingScope,
|
||||
|
||||
/// The given path is not a directory.
|
||||
#[error("not a directory: {0}")]
|
||||
NotADirectory(PathBuf),
|
||||
|
||||
/// The given path is a directory.
|
||||
#[error("path is a directory")]
|
||||
IsADirectory,
|
||||
|
||||
/// The given variable is not an array.
|
||||
#[error("variable is not an array")]
|
||||
NotArray,
|
||||
|
||||
/// The current user could not be determined.
|
||||
#[error("no current user")]
|
||||
NoCurrentUser,
|
||||
|
||||
/// The requested input or output redirection is invalid.
|
||||
#[error("invalid redirection")]
|
||||
InvalidRedirection,
|
||||
|
||||
/// An error occurred while redirecting input or output with the given file.
|
||||
#[error("failed to redirect to {0}: {1}")]
|
||||
RedirectionFailure(String, String),
|
||||
|
||||
/// An error occurred evaluating an arithmetic expression.
|
||||
#[error("arithmetic evaluation error: {0}")]
|
||||
EvalError(#[from] crate::arithmetic::EvalError),
|
||||
|
||||
/// The given string could not be parsed as an integer.
|
||||
#[error("failed to parse integer")]
|
||||
IntParseError(#[from] std::num::ParseIntError),
|
||||
|
||||
/// The given string could not be parsed as an integer.
|
||||
#[error("failed to parse integer")]
|
||||
TryIntParseError(#[from] std::num::TryFromIntError),
|
||||
|
||||
/// A byte sequence could not be decoded as a valid UTF-8 string.
|
||||
#[error("failed to decode utf-8")]
|
||||
FromUtf8Error(#[from] std::string::FromUtf8Error),
|
||||
|
||||
/// A byte sequence could not be decoded as a valid UTF-8 string.
|
||||
#[error("failed to decode utf-8")]
|
||||
Utf8Error(#[from] std::str::Utf8Error),
|
||||
|
||||
/// An attempt was made to modify a readonly variable.
|
||||
#[error("cannot mutate readonly variable")]
|
||||
ReadonlyVariable,
|
||||
|
||||
/// The indicated pattern is invalid.
|
||||
#[error("invalid pattern: '{0}'")]
|
||||
InvalidPattern(String),
|
||||
|
||||
/// A regular expression error occurred
|
||||
#[error("regex error: {0}")]
|
||||
RegexError(#[from] fancy_regex::Error),
|
||||
|
||||
/// An invalid regular expression was provided.
|
||||
#[error("invalid regex: {0}; expression: '{1}'")]
|
||||
InvalidRegexError(fancy_regex::Error, String),
|
||||
|
||||
/// An I/O error occurred.
|
||||
#[error("i/o error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// Invalid substitution syntax.
|
||||
#[error("bad substitution: {0}")]
|
||||
BadSubstitution(String),
|
||||
|
||||
/// An error occurred while creating a child process.
|
||||
#[error("failed to create child process")]
|
||||
ChildCreationFailure,
|
||||
|
||||
/// An error occurred while formatting a string.
|
||||
#[error(transparent)]
|
||||
FormattingError(#[from] std::fmt::Error),
|
||||
|
||||
/// An error occurred while parsing.
|
||||
#[error("{1}: {0}")]
|
||||
ParseError(brush_parser::ParseError, brush_parser::SourceInfo),
|
||||
|
||||
/// An error occurred while parsing a function body.
|
||||
#[error("{0}: {1}")]
|
||||
FunctionParseError(String, brush_parser::ParseError),
|
||||
|
||||
/// An error occurred while parsing a word.
|
||||
#[error(transparent)]
|
||||
WordParseError(#[from] brush_parser::WordParseError),
|
||||
|
||||
/// Unable to parse a test command.
|
||||
#[error(transparent)]
|
||||
TestCommandParseError(#[from] brush_parser::TestCommandParseError),
|
||||
|
||||
/// Unable to parse a key binding specification.
|
||||
#[error(transparent)]
|
||||
BindingParseError(#[from] brush_parser::BindingParseError),
|
||||
|
||||
/// A threading error occurred.
|
||||
#[error("threading error")]
|
||||
ThreadingError(#[from] tokio::task::JoinError),
|
||||
|
||||
/// An invalid signal was referenced.
|
||||
#[error("{0}: invalid signal specification")]
|
||||
InvalidSignal(String),
|
||||
|
||||
/// A platform error occurred.
|
||||
#[error("platform error: {0}")]
|
||||
PlatformError(#[from] sys::PlatformError),
|
||||
|
||||
/// An invalid umask was provided.
|
||||
#[error("invalid umask value")]
|
||||
InvalidUmask,
|
||||
|
||||
/// The given open file cannot be read from.
|
||||
#[error("cannot read from {0}")]
|
||||
OpenFileNotReadable(&'static str),
|
||||
|
||||
/// The given open file cannot be written to.
|
||||
#[error("cannot write to {0}")]
|
||||
OpenFileNotWritable(&'static str),
|
||||
|
||||
/// Bad file descriptor.
|
||||
#[error("bad file descriptor: {0}")]
|
||||
BadFileDescriptor(ShellFd),
|
||||
|
||||
/// Printf failure
|
||||
#[error("printf failure: {0}")]
|
||||
PrintfFailure(i32),
|
||||
|
||||
/// Printf invalid usage
|
||||
#[error("printf: {0}")]
|
||||
PrintfInvalidUsage(String),
|
||||
|
||||
/// Interrupted
|
||||
#[error("interrupted")]
|
||||
Interrupted,
|
||||
|
||||
/// Maximum function call depth was exceeded.
|
||||
#[error("maximum function call depth exceeded")]
|
||||
MaxFunctionCallDepthExceeded,
|
||||
|
||||
/// System time error.
|
||||
#[error("system time error: {0}")]
|
||||
TimeError(#[from] std::time::SystemTimeError),
|
||||
|
||||
/// Array index out of range.
|
||||
#[error("array index out of range: {0}")]
|
||||
ArrayIndexOutOfRange(i64),
|
||||
|
||||
/// Unhandled key code.
|
||||
#[error("unhandled key code: {0:?}")]
|
||||
UnhandledKeyCode(Vec<u8>),
|
||||
|
||||
/// An error occurred in a built-in command.
|
||||
#[error("{1}: {0}")]
|
||||
BuiltinError(Box<dyn BuiltinError>, String),
|
||||
|
||||
/// Operation not supported on this platform.
|
||||
#[error("operation not supported on this platform: {0}")]
|
||||
NotSupportedOnThisPlatform(&'static str),
|
||||
|
||||
/// Command history is not enabled in this shell.
|
||||
#[error("command history is not enabled in this shell")]
|
||||
HistoryNotEnabled,
|
||||
|
||||
/// Unknown key binding function.
|
||||
#[error("unknown key binding function: {0}")]
|
||||
UnknownKeyBindingFunction(String),
|
||||
}
|
||||
|
||||
impl BuiltinError for Error {}
|
||||
|
||||
/// Trait implementable by built-in commands to represent errors.
|
||||
pub trait BuiltinError: std::error::Error + ConvertibleToExitCode + Send + Sync {}
|
||||
|
||||
/// Helper trait for converting values to exit codes.
|
||||
pub trait ConvertibleToExitCode {
|
||||
/// Converts to an exit code.
|
||||
fn as_exit_code(&self) -> results::ExecutionExitCode;
|
||||
}
|
||||
|
||||
impl<T> ConvertibleToExitCode for T
|
||||
where
|
||||
results::ExecutionExitCode: for<'a> From<&'a T>,
|
||||
{
|
||||
fn as_exit_code(&self) -> results::ExecutionExitCode {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ErrorKind> for results::ExecutionExitCode {
|
||||
fn from(value: &ErrorKind) -> Self {
|
||||
match value {
|
||||
ErrorKind::CommandNotFound(..) => Self::NotFound,
|
||||
ErrorKind::Unimplemented(..) | ErrorKind::UnimplementedAndTracked(..) => {
|
||||
Self::Unimplemented
|
||||
}
|
||||
ErrorKind::ParseError(..) => Self::InvalidUsage,
|
||||
ErrorKind::FunctionParseError(..) => Self::InvalidUsage,
|
||||
ErrorKind::FailedToExecuteCommand(..) => Self::CannotExecute,
|
||||
ErrorKind::BuiltinError(inner, ..) => inner.as_exit_code(),
|
||||
_ => Self::GeneralError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Error> for results::ExecutionExitCode {
|
||||
fn from(error: &Error) -> Self {
|
||||
Self::from(&error.kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Error
|
||||
where
|
||||
ErrorKind: From<T>,
|
||||
{
|
||||
fn from(convertible_to_kind: T) -> Self {
|
||||
Self {
|
||||
kind: convertible_to_kind.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implementable by consumers of this crate to customize formatting errors into
|
||||
/// displayable text.
|
||||
pub trait ErrorFormatter: Send {
|
||||
/// Format the given error for display within the context of the provided shell.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `error` - The error to format.
|
||||
/// * `shell` - The shell in which the error occurred.
|
||||
fn format_error(&self, error: &Error, shell: &Shell) -> String;
|
||||
}
|
||||
|
||||
/// Default implementation of the [`ErrorFormatter`] trait.
|
||||
pub(crate) struct DefaultErrorFormatter {}
|
||||
|
||||
impl DefaultErrorFormatter {
|
||||
pub const fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorFormatter for DefaultErrorFormatter {
|
||||
fn format_error(&self, err: &Error, _shell: &Shell) -> String {
|
||||
std::format!("error: {err:#}\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function for returning an error for unimplemented functionality.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `msg` - The message to include in the error
|
||||
pub fn unimp<T>(msg: &'static str) -> Result<T, Error> {
|
||||
Err(ErrorKind::Unimplemented(msg).into())
|
||||
}
|
||||
|
||||
/// Convenience function for returning an error for *tracked*, unimplemented functionality.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `msg` - The message to include in the error
|
||||
/// * `project_issue_id` - The GitHub issue ID where the implementation is tracked.
|
||||
#[allow(unused)]
|
||||
pub fn unimp_with_issue<T>(msg: &'static str, project_issue_id: u32) -> Result<T, Error> {
|
||||
Err(ErrorKind::UnimplementedAndTracked(msg, project_issue_id).into())
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
//! String escaping utilities
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Escape expansion mode.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EscapeExpansionMode {
|
||||
/// echo builtin mode.
|
||||
EchoBuiltin,
|
||||
/// ANSI-C quotes.
|
||||
AnsiCQuotes,
|
||||
}
|
||||
|
||||
/// Expands backslash escapes in the provided string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to expand.
|
||||
/// * `mode` - The mode to use for expansion.
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub fn expand_backslash_escapes(
|
||||
s: &str,
|
||||
mode: EscapeExpansionMode,
|
||||
) -> Result<(Vec<u8>, bool), error::Error> {
|
||||
let mut result: Vec<u8> = vec![];
|
||||
let mut it = s.chars();
|
||||
while let Some(c) = it.next() {
|
||||
if c != '\\' {
|
||||
// Not a backslash, add and move on.
|
||||
result.append(c.to_string().into_bytes().as_mut());
|
||||
continue;
|
||||
}
|
||||
|
||||
match it.next() {
|
||||
Some('a') => result.push(b'\x07'),
|
||||
Some('b') => result.push(b'\x08'),
|
||||
Some('c') => {
|
||||
match mode {
|
||||
EscapeExpansionMode::EchoBuiltin => {
|
||||
// Stop all additional output!
|
||||
return Ok((result, false));
|
||||
}
|
||||
EscapeExpansionMode::AnsiCQuotes => {
|
||||
if let Some(_next_next) = it.next() {
|
||||
return error::unimp("control character in ANSI C quotes");
|
||||
} else {
|
||||
result.push(b'\\');
|
||||
result.push(b'c');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some('e' | 'E') => result.push(b'\x1b'),
|
||||
Some('f') => result.push(b'\x0c'),
|
||||
Some('n') => result.push(b'\n'),
|
||||
Some('r') => result.push(b'\r'),
|
||||
Some('t') => result.push(b'\t'),
|
||||
Some('v') => result.push(b'\x0b'),
|
||||
Some('\\') => result.push(b'\\'),
|
||||
Some('\'') if matches!(mode, EscapeExpansionMode::AnsiCQuotes) => result.push(b'\''),
|
||||
Some('\"') if matches!(mode, EscapeExpansionMode::AnsiCQuotes) => result.push(b'\"'),
|
||||
Some('?') if matches!(mode, EscapeExpansionMode::AnsiCQuotes) => result.push(b'?'),
|
||||
Some('0') => {
|
||||
// Consume 0-3 valid octal chars
|
||||
let mut taken_so_far = 0;
|
||||
let mut octal_chars: String = it
|
||||
.take_while_ref(|c| {
|
||||
if taken_so_far < 3 && matches!(*c, '0'..='7') {
|
||||
taken_so_far += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if octal_chars.is_empty() {
|
||||
octal_chars.push('0');
|
||||
}
|
||||
|
||||
let value = u8::from_str_radix(octal_chars.as_str(), 8)?;
|
||||
result.push(value);
|
||||
}
|
||||
Some('x') => {
|
||||
// Consume 1-2 valid hex chars
|
||||
let mut taken_so_far = 0;
|
||||
let hex_chars: String = it
|
||||
.take_while_ref(|c| {
|
||||
if taken_so_far < 2 && c.is_ascii_hexdigit() {
|
||||
taken_so_far += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if hex_chars.is_empty() {
|
||||
result.push(b'\\');
|
||||
result.append(c.to_string().into_bytes().as_mut());
|
||||
} else {
|
||||
let value = u8::from_str_radix(hex_chars.as_str(), 16)?;
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
Some('u') => {
|
||||
// Consume 1-4 hex digits
|
||||
let mut taken_so_far = 0;
|
||||
let hex_chars: String = it
|
||||
.take_while_ref(|c| {
|
||||
if taken_so_far < 4 && c.is_ascii_hexdigit() {
|
||||
taken_so_far += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if hex_chars.is_empty() {
|
||||
result.push(b'\\');
|
||||
result.append(c.to_string().into_bytes().as_mut());
|
||||
} else {
|
||||
let value = u16::from_str_radix(hex_chars.as_str(), 16)?;
|
||||
|
||||
if let Some(decoded) = char::from_u32(u32::from(value)) {
|
||||
result.append(decoded.to_string().into_bytes().as_mut());
|
||||
} else {
|
||||
result.push(b'\\');
|
||||
result.append(c.to_string().into_bytes().as_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some('U') => {
|
||||
// Consume 1-8 hex digits
|
||||
let mut taken_so_far = 0;
|
||||
let hex_chars: String = it
|
||||
.take_while_ref(|c| {
|
||||
if taken_so_far < 8 && c.is_ascii_hexdigit() {
|
||||
taken_so_far += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if hex_chars.is_empty() {
|
||||
result.push(b'\\');
|
||||
result.append(c.to_string().into_bytes().as_mut());
|
||||
} else {
|
||||
let value = u32::from_str_radix(hex_chars.as_str(), 16)?;
|
||||
|
||||
if let Some(decoded) = char::from_u32(value) {
|
||||
result.append(decoded.to_string().into_bytes().as_mut());
|
||||
} else {
|
||||
result.push(b'\\');
|
||||
result.append(c.to_string().into_bytes().as_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(c) => {
|
||||
// Not a valid escape sequence.
|
||||
result.push(b'\\');
|
||||
result.append(c.to_string().into_bytes().as_mut());
|
||||
}
|
||||
None => {
|
||||
// Trailing backslash.
|
||||
result.push(b'\\');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((result, true))
|
||||
}
|
||||
|
||||
/// Quoting mode to use for escaping.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub enum QuoteMode {
|
||||
/// Single-quote.
|
||||
#[default]
|
||||
SingleQuote,
|
||||
/// Double-quote.
|
||||
DoubleQuote,
|
||||
/// Backslash-escape.
|
||||
BackslashEscape,
|
||||
}
|
||||
|
||||
/// Options influencing how to escape/quote an input string.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct QuoteOptions {
|
||||
/// Whether or not to *always* escape or quote the input; if false, then escaping/quoting
|
||||
/// will only be applied if the input contains characters that *require* it.
|
||||
pub always_quote: bool,
|
||||
/// Preferred mode for quoting/escaping. Quoting may be "upgraded" to a more expressive
|
||||
/// format if the input is not expressible otherwise.
|
||||
pub preferred_mode: QuoteMode,
|
||||
/// Whether or not to *avoid* using ANSI C quoting just for the benefit of newline characters.
|
||||
/// Default is for newline characters to require upgrading the string's quoting to
|
||||
/// ANSI C quoting.
|
||||
pub avoid_ansi_c_quoting_newline: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn quote<'a>(s: &'a str, options: &QuoteOptions) -> Cow<'a, str> {
|
||||
let use_ansi_c_quotes = s.contains(|c| {
|
||||
needs_ansi_c_quoting(c) && (!options.avoid_ansi_c_quoting_newline || c != '\n')
|
||||
});
|
||||
|
||||
if use_ansi_c_quotes {
|
||||
return ansi_c_quote(s).into();
|
||||
}
|
||||
|
||||
let use_default_quotes =
|
||||
!use_ansi_c_quotes && (options.always_quote || s.is_empty() || s.contains(needs_escaping));
|
||||
|
||||
if !use_default_quotes {
|
||||
return s.into();
|
||||
}
|
||||
|
||||
match options.preferred_mode {
|
||||
QuoteMode::BackslashEscape => backslash_escape(s).into(),
|
||||
QuoteMode::SingleQuote => single_quote(s).into(),
|
||||
QuoteMode::DoubleQuote => double_quote(s).into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape the given string, forcing quoting.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to escape.
|
||||
/// * `mode` - The quoting mode to use.
|
||||
pub fn force_quote(s: &str, mode: QuoteMode) -> String {
|
||||
let options = QuoteOptions {
|
||||
always_quote: true,
|
||||
preferred_mode: mode,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
quote(s, &options).to_string()
|
||||
}
|
||||
|
||||
/// Applies the given quoting mode to the provided string, only changing it if required.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to escape.
|
||||
/// * `mode` - The quoting mode to use.
|
||||
pub fn quote_if_needed(s: &str, mode: QuoteMode) -> Cow<'_, str> {
|
||||
let options = QuoteOptions {
|
||||
always_quote: false,
|
||||
preferred_mode: mode,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
quote(s, &options)
|
||||
}
|
||||
|
||||
fn backslash_escape(s: &str) -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
// TODO: Handle other interesting sequences.
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
c if needs_escaping(c) => {
|
||||
output.push('\\');
|
||||
output.push(c);
|
||||
}
|
||||
c => output.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn single_quote(s: &str) -> String {
|
||||
// Special-case the empty string.
|
||||
if s.is_empty() {
|
||||
return "''".into();
|
||||
}
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
// Go through the string; put everything in single quotes except for
|
||||
// the single quote character itself. It will get escaped outside
|
||||
// all quoting.
|
||||
let mut first = true;
|
||||
for part in s.split('\'') {
|
||||
if !first {
|
||||
result.push('\\');
|
||||
result.push('\'');
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
if !part.is_empty() {
|
||||
result.push('\'');
|
||||
result.push_str(part);
|
||||
result.push('\'');
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn double_quote(s: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
result.push('"');
|
||||
|
||||
for c in s.chars() {
|
||||
if matches!(c, '$' | '`' | '"' | '\\') {
|
||||
result.push('\\');
|
||||
}
|
||||
|
||||
result.push(c);
|
||||
}
|
||||
|
||||
result.push('"');
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn ansi_c_quote(s: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
result.push_str("$'");
|
||||
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\x07' => result.push_str("\\a"),
|
||||
'\x08' => result.push_str("\\b"),
|
||||
'\x1b' => result.push_str("\\E"),
|
||||
'\x0c' => result.push_str("\\f"),
|
||||
'\n' => result.push_str("\\n"),
|
||||
'\r' => result.push_str("\\r"),
|
||||
'\t' => result.push_str("\\t"),
|
||||
'\x0b' => result.push_str("\\v"),
|
||||
'\\' => result.push_str("\\\\"),
|
||||
'\'' => result.push_str("\\'"),
|
||||
c if needs_ansi_c_quoting(c) => {
|
||||
result.push_str(std::format!("\\{:03o}", c as u8).as_str());
|
||||
}
|
||||
_ => result.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
result.push('\'');
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// Returns whether or not the given character needs to be escaped (or quoted) if outside
|
||||
// quotes.
|
||||
const fn needs_escaping(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'(' | ')'
|
||||
| '['
|
||||
| ']'
|
||||
| '{'
|
||||
| '}'
|
||||
| '$'
|
||||
| '*'
|
||||
| '?'
|
||||
| '|'
|
||||
| '&'
|
||||
| ';'
|
||||
| '<'
|
||||
| '>'
|
||||
| '`'
|
||||
| '\\'
|
||||
| '"'
|
||||
| '!'
|
||||
| '^'
|
||||
| ','
|
||||
| ' '
|
||||
| '\''
|
||||
)
|
||||
}
|
||||
|
||||
const fn needs_ansi_c_quoting(c: char) -> bool {
|
||||
c.is_ascii_control()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_backslash_escape() {
|
||||
assert_eq!(quote_if_needed("a", QuoteMode::BackslashEscape), "a");
|
||||
assert_eq!(quote_if_needed("a b", QuoteMode::BackslashEscape), r"a\ b");
|
||||
assert_eq!(quote_if_needed("", QuoteMode::BackslashEscape), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_quote_escape() {
|
||||
assert_eq!(quote_if_needed("a", QuoteMode::SingleQuote), "a");
|
||||
assert_eq!(quote_if_needed("a b", QuoteMode::SingleQuote), "'a b'");
|
||||
assert_eq!(quote_if_needed("", QuoteMode::SingleQuote), "''");
|
||||
assert_eq!(quote_if_needed("'", QuoteMode::SingleQuote), "\\'");
|
||||
}
|
||||
|
||||
fn assert_echo_expands_to(unexpanded: &str, expected: &str) {
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
expand_backslash_escapes(unexpanded, EscapeExpansionMode::EchoBuiltin)
|
||||
.unwrap()
|
||||
.0
|
||||
)
|
||||
.unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_echo_expansion() {
|
||||
assert_echo_expands_to("a", "a");
|
||||
assert_echo_expands_to(r"\M", "\\M");
|
||||
assert_echo_expands_to(r"a\nb", "a\nb");
|
||||
assert_echo_expands_to(r"\a", "\x07");
|
||||
assert_echo_expands_to(r"\b", "\x08");
|
||||
assert_echo_expands_to(r"\e", "\x1b");
|
||||
assert_echo_expands_to(r"\f", "\x0c");
|
||||
assert_echo_expands_to(r"\n", "\n");
|
||||
assert_echo_expands_to(r"\r", "\r");
|
||||
assert_echo_expands_to(r"\t", "\t");
|
||||
assert_echo_expands_to(r"\v", "\x0b");
|
||||
assert_echo_expands_to(r"\\", "\\");
|
||||
assert_echo_expands_to(r"\'", "\\'");
|
||||
assert_echo_expands_to(r#"\""#, r#"\""#);
|
||||
assert_echo_expands_to(r"\?", "\\?");
|
||||
assert_echo_expands_to(r"\0", "\0");
|
||||
assert_echo_expands_to(r"\00", "\0");
|
||||
assert_echo_expands_to(r"\000", "\0");
|
||||
assert_echo_expands_to(r"\081", "\081");
|
||||
assert_echo_expands_to(r"\0101", "A");
|
||||
assert_echo_expands_to(r"abc\", "abc\\");
|
||||
assert_echo_expands_to(r"\x41", "A");
|
||||
assert_echo_expands_to(r"\xf0\x9f\x90\x8d", "🐍");
|
||||
assert_echo_expands_to(r"\u2620", "☠");
|
||||
assert_echo_expands_to(r"\U0001f602", "😂");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,609 @@
|
||||
use brush_parser::ast;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
ExecutionParameters, Shell, ShellFd, arithmetic, env, error, escape, expansion, namedoptions,
|
||||
patterns,
|
||||
sys::{
|
||||
fs::{MetadataExt, PathExt},
|
||||
users,
|
||||
},
|
||||
variables::{self, ArrayLiteral},
|
||||
};
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
pub(crate) async fn eval_extended_test_expr(
|
||||
expr: &ast::ExtendedTestExpr,
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
) -> Result<bool, error::Error> {
|
||||
match expr {
|
||||
ast::ExtendedTestExpr::UnaryTest(op, operand) => {
|
||||
apply_unary_predicate(op, operand, shell, params).await
|
||||
}
|
||||
ast::ExtendedTestExpr::BinaryTest(op, left, right) => {
|
||||
apply_binary_predicate(op, left, right, shell, params).await
|
||||
}
|
||||
ast::ExtendedTestExpr::And(left, right) => {
|
||||
let result = eval_extended_test_expr(left, shell, params).await?
|
||||
&& eval_extended_test_expr(right, shell, params).await?;
|
||||
Ok(result)
|
||||
}
|
||||
ast::ExtendedTestExpr::Or(left, right) => {
|
||||
let result = eval_extended_test_expr(left, shell, params).await?
|
||||
|| eval_extended_test_expr(right, shell, params).await?;
|
||||
Ok(result)
|
||||
}
|
||||
ast::ExtendedTestExpr::Not(expr) => {
|
||||
let result = !eval_extended_test_expr(expr, shell, params).await?;
|
||||
Ok(result)
|
||||
}
|
||||
ast::ExtendedTestExpr::Parenthesized(expr) => {
|
||||
eval_extended_test_expr(expr, shell, params).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_unary_predicate(
|
||||
op: &ast::UnaryPredicate,
|
||||
operand: &ast::Word,
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
) -> Result<bool, error::Error> {
|
||||
let expanded_operand = expansion::basic_expand_word(shell, params, operand).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(
|
||||
params,
|
||||
std::format!(
|
||||
"[[ {op} {} ]]",
|
||||
escape::quote_if_needed(&expanded_operand, escape::QuoteMode::SingleQuote)
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
apply_unary_predicate_to_str(op, expanded_operand.as_str(), shell, params)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub(crate) fn apply_unary_predicate_to_str(
|
||||
op: &ast::UnaryPredicate,
|
||||
operand: &str,
|
||||
shell: &Shell,
|
||||
params: &ExecutionParameters,
|
||||
) -> Result<bool, error::Error> {
|
||||
match op {
|
||||
ast::UnaryPredicate::StringHasNonZeroLength => Ok(!operand.is_empty()),
|
||||
ast::UnaryPredicate::StringHasZeroLength => Ok(operand.is_empty()),
|
||||
ast::UnaryPredicate::FileExists => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists_and_is_block_device())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsCharSpecialFile => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists_and_is_char_device())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsDir => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.is_dir())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsRegularFile => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.is_file())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsSetgid => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists_and_is_setgid())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsSymlink => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.is_symlink())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndHasStickyBit => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists_and_is_sticky_bit())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsFifo => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists_and_is_fifo())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsReadable => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.readable())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsNotZeroLength => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
if let Ok(metadata) = path.metadata() {
|
||||
Ok(metadata.len() > 0)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
ast::UnaryPredicate::FdIsOpenTerminal => {
|
||||
if let Ok(fd) = operand.parse::<ShellFd>() {
|
||||
if let Some(open_file) = params.try_fd(shell, fd) {
|
||||
Ok(open_file.is_term())
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsSetuid => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists_and_is_setuid())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsWritable => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.writable())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsExecutable => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.executable())
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let md = path.metadata()?;
|
||||
Ok(md.gid() == users::get_effective_gid()?)
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead => {
|
||||
error::unimp("unary extended test predicate: FileExistsAndModifiedSinceLastRead")
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let md = path.metadata()?;
|
||||
Ok(md.uid() == users::get_effective_uid()?)
|
||||
}
|
||||
ast::UnaryPredicate::FileExistsAndIsSocket => {
|
||||
let path = shell.absolute_path(Path::new(operand));
|
||||
Ok(path.exists_and_is_socket())
|
||||
}
|
||||
ast::UnaryPredicate::ShellOptionEnabled => {
|
||||
let shopt_name = operand;
|
||||
if let Some(option) =
|
||||
namedoptions::options(namedoptions::ShellOptionKind::SetO).get(shopt_name)
|
||||
{
|
||||
Ok(option.get(&shell.options))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
ast::UnaryPredicate::ShellVariableIsSetAndAssigned => Ok(shell.env.is_set(operand)),
|
||||
ast::UnaryPredicate::ShellVariableIsSetAndNameRef => match shell.env.get(operand) {
|
||||
Some((_, reffed)) => Ok(reffed.value().is_set() && reffed.is_treated_as_nameref()),
|
||||
None => Ok(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
async fn apply_binary_predicate(
|
||||
op: &ast::BinaryPredicate,
|
||||
left: &ast::Word,
|
||||
right: &ast::Word,
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
) -> Result<bool, error::Error> {
|
||||
match op {
|
||||
ast::BinaryPredicate::StringMatchesRegex => {
|
||||
let s = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let regex = expansion::basic_expand_regex(shell, params, right)
|
||||
.await?
|
||||
.set_multiline(true);
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {s} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let (matches, captures) = match regex.matches(s.as_str()) {
|
||||
Ok(Some(captures)) => (true, captures),
|
||||
Ok(None) => (false, vec![]),
|
||||
// If we can't compile the regex, don't abort the whole operation but make sure to
|
||||
// report it.
|
||||
// TODO: Docs indicate we should yield 2 on an invalid regex (not 1).
|
||||
Err(e) => {
|
||||
tracing::warn!("error using regex: {}", e);
|
||||
(false, vec![])
|
||||
}
|
||||
};
|
||||
|
||||
let captures_value = variables::ShellValueLiteral::Array(ArrayLiteral(
|
||||
captures
|
||||
.into_iter()
|
||||
.map(|c| (None, c.unwrap_or_default()))
|
||||
.collect(),
|
||||
));
|
||||
|
||||
shell.env.update_or_add(
|
||||
"BASH_REMATCH",
|
||||
captures_value,
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)?;
|
||||
|
||||
Ok(matches)
|
||||
}
|
||||
ast::BinaryPredicate::StringExactlyMatchesString => {
|
||||
let left = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left == right)
|
||||
}
|
||||
ast::BinaryPredicate::StringDoesNotExactlyMatchString => {
|
||||
let left = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left != right)
|
||||
}
|
||||
ast::BinaryPredicate::StringContainsSubstring => {
|
||||
let s = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let substring = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {s} {op} {substring} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(s.contains(substring.as_str()))
|
||||
}
|
||||
ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => {
|
||||
let left = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
files_refer_to_same_device_and_inode_numbers(shell, left, right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => {
|
||||
let left = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
left_file_is_newer_or_exists_when_right_does_not(shell, left, right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => {
|
||||
let left = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
left_file_is_older_or_does_not_exist_when_right_does(shell, left, right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftSortsBeforeRight => {
|
||||
let left = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// TODO: According to docs, should be lexicographical order of the current locale.
|
||||
Ok(left < right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftSortsAfterRight => {
|
||||
let left = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// TODO: According to docs, should be lexicographical order of the current locale.
|
||||
Ok(left > right)
|
||||
}
|
||||
ast::BinaryPredicate::ArithmeticEqualTo => {
|
||||
let left =
|
||||
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
|
||||
let right =
|
||||
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left == right)
|
||||
}
|
||||
ast::BinaryPredicate::ArithmeticNotEqualTo => {
|
||||
let left =
|
||||
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
|
||||
let right =
|
||||
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left != right)
|
||||
}
|
||||
ast::BinaryPredicate::ArithmeticLessThan => {
|
||||
let left =
|
||||
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
|
||||
let right =
|
||||
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left < right)
|
||||
}
|
||||
ast::BinaryPredicate::ArithmeticLessThanOrEqualTo => {
|
||||
let left =
|
||||
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
|
||||
let right =
|
||||
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left <= right)
|
||||
}
|
||||
ast::BinaryPredicate::ArithmeticGreaterThan => {
|
||||
let left =
|
||||
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
|
||||
let right =
|
||||
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left > right)
|
||||
}
|
||||
ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo => {
|
||||
let left =
|
||||
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
|
||||
let right =
|
||||
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(left >= right)
|
||||
}
|
||||
// N.B. The "=", "==", and "!=" operators don't compare 2 strings; they check
|
||||
// for whether the lefthand operand (a string) is matched by the righthand
|
||||
// operand (treated as a shell pattern).
|
||||
// TODO: implement case-insensitive matching if relevant via shopt options (nocasematch).
|
||||
ast::BinaryPredicate::StringExactlyMatchesPattern => {
|
||||
let s = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let pattern = expansion::basic_expand_pattern(shell, params, right)
|
||||
.await?
|
||||
.set_extended_globbing(shell.options.extended_globbing)
|
||||
.set_case_insensitive(shell.options.case_insensitive_conditionals);
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
let expanded_right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
let escaped_right = escape::quote_if_needed(
|
||||
expanded_right.as_str(),
|
||||
escape::QuoteMode::BackslashEscape,
|
||||
);
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {s} {op} {escaped_right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
pattern.exactly_matches(s.as_str())
|
||||
}
|
||||
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
|
||||
let s = expansion::basic_expand_word(shell, params, left).await?;
|
||||
let pattern = expansion::basic_expand_pattern(shell, params, right)
|
||||
.await?
|
||||
.set_extended_globbing(shell.options.extended_globbing)
|
||||
.set_case_insensitive(shell.options.case_insensitive_conditionals);
|
||||
|
||||
if shell.options.print_commands_and_arguments {
|
||||
let expanded_right = expansion::basic_expand_word(shell, params, right).await?;
|
||||
let escaped_right = escape::quote_if_needed(
|
||||
expanded_right.as_str(),
|
||||
escape::QuoteMode::BackslashEscape,
|
||||
);
|
||||
shell
|
||||
.trace_command(params, std::format!("[[ {s} {op} {escaped_right} ]]"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let eq = pattern.exactly_matches(s.as_str())?;
|
||||
Ok(!eq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_binary_predicate_to_strs(
|
||||
op: &ast::BinaryPredicate,
|
||||
left: &str,
|
||||
right: &str,
|
||||
shell: &Shell,
|
||||
) -> Result<bool, error::Error> {
|
||||
match op {
|
||||
ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => {
|
||||
files_refer_to_same_device_and_inode_numbers(shell, left, right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => {
|
||||
left_file_is_newer_or_exists_when_right_does_not(shell, left, right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => {
|
||||
left_file_is_older_or_does_not_exist_when_right_does(shell, left, right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftSortsBeforeRight => {
|
||||
// TODO: According to docs, should be lexicographical order of the current locale.
|
||||
Ok(left < right)
|
||||
}
|
||||
ast::BinaryPredicate::LeftSortsAfterRight => {
|
||||
// TODO: According to docs, should be lexicographical order of the current locale.
|
||||
Ok(left > right)
|
||||
}
|
||||
ast::BinaryPredicate::ArithmeticEqualTo => Ok(apply_test_binary_arithmetic_predicate(
|
||||
left,
|
||||
right,
|
||||
|left, right| left == right,
|
||||
)),
|
||||
ast::BinaryPredicate::ArithmeticNotEqualTo => Ok(apply_test_binary_arithmetic_predicate(
|
||||
left,
|
||||
right,
|
||||
|left, right| left != right,
|
||||
)),
|
||||
ast::BinaryPredicate::ArithmeticLessThan => Ok(apply_test_binary_arithmetic_predicate(
|
||||
left,
|
||||
right,
|
||||
|left, right| left < right,
|
||||
)),
|
||||
ast::BinaryPredicate::ArithmeticLessThanOrEqualTo => Ok(
|
||||
apply_test_binary_arithmetic_predicate(left, right, |left, right| left <= right),
|
||||
),
|
||||
ast::BinaryPredicate::ArithmeticGreaterThan => Ok(apply_test_binary_arithmetic_predicate(
|
||||
left,
|
||||
right,
|
||||
|left, right| left > right,
|
||||
)),
|
||||
ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo => Ok(
|
||||
apply_test_binary_arithmetic_predicate(left, right, |left, right| left >= right),
|
||||
),
|
||||
ast::BinaryPredicate::StringExactlyMatchesPattern => {
|
||||
let pattern = patterns::Pattern::from(right)
|
||||
.set_extended_globbing(shell.options.extended_globbing)
|
||||
.set_case_insensitive(shell.options.case_insensitive_conditionals);
|
||||
|
||||
pattern.exactly_matches(left)
|
||||
}
|
||||
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
|
||||
let pattern = patterns::Pattern::from(right)
|
||||
.set_extended_globbing(shell.options.extended_globbing)
|
||||
.set_case_insensitive(shell.options.case_insensitive_conditionals);
|
||||
|
||||
let eq = pattern.exactly_matches(left)?;
|
||||
Ok(!eq)
|
||||
}
|
||||
ast::BinaryPredicate::StringExactlyMatchesString => Ok(left == right),
|
||||
ast::BinaryPredicate::StringDoesNotExactlyMatchString => Ok(left != right),
|
||||
_ => error::unimp("unsupported test binary predicate"),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_test_binary_arithmetic_predicate(
|
||||
left: &str,
|
||||
right: &str,
|
||||
op: fn(i64, i64) -> bool,
|
||||
) -> bool {
|
||||
let left: Result<i64, _> = left.parse();
|
||||
let right: Result<i64, _> = right.parse();
|
||||
|
||||
if let (Ok(left), Ok(right)) = (left, right) {
|
||||
op(left, right)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn left_file_is_older_or_does_not_exist_when_right_does(
|
||||
shell: &Shell,
|
||||
left: impl AsRef<str>,
|
||||
right: impl AsRef<str>,
|
||||
) -> Result<bool, error::Error> {
|
||||
let (l_path, r_path) = (
|
||||
shell.absolute_path(Path::new(left.as_ref())),
|
||||
shell.absolute_path(Path::new(right.as_ref())),
|
||||
);
|
||||
|
||||
match (l_path.metadata(), r_path.metadata()) {
|
||||
(Ok(m1), Ok(m2)) => Ok(m1.modified()? < m2.modified()?),
|
||||
(Err(_), Ok(_)) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn left_file_is_newer_or_exists_when_right_does_not(
|
||||
shell: &Shell,
|
||||
left: impl AsRef<str>,
|
||||
right: impl AsRef<str>,
|
||||
) -> Result<bool, error::Error> {
|
||||
let (l_path, r_path) = (
|
||||
shell.absolute_path(Path::new(left.as_ref())),
|
||||
shell.absolute_path(Path::new(right.as_ref())),
|
||||
);
|
||||
|
||||
match (l_path.metadata(), r_path.metadata()) {
|
||||
(Ok(m1), Ok(m2)) => Ok(m1.modified()? > m2.modified()?),
|
||||
(Ok(_), Err(_)) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn files_refer_to_same_device_and_inode_numbers(
|
||||
shell: &Shell,
|
||||
left: impl AsRef<str>,
|
||||
right: impl AsRef<str>,
|
||||
) -> Result<bool, error::Error> {
|
||||
let (l_path, r_path) = (
|
||||
shell.absolute_path(Path::new(left.as_ref())),
|
||||
shell.absolute_path(Path::new(right.as_ref())),
|
||||
);
|
||||
|
||||
if !l_path.readable() || !r_path.readable() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(l_path.get_device_and_inode()? == r_path.get_device_and_inode()?)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Structures for managing function registrations and calls.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use brush_parser::ast;
|
||||
|
||||
/// An environment for defined, named functions.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FunctionEnv {
|
||||
functions: HashMap<String, Registration>,
|
||||
}
|
||||
|
||||
impl FunctionEnv {
|
||||
/// Tries to retrieve the registration for a function by name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the function to retrieve.
|
||||
pub fn get(&self, name: &str) -> Option<&Registration> {
|
||||
self.functions.get(name)
|
||||
}
|
||||
|
||||
/// Tries to retrieve a mutable reference to the registration for a
|
||||
/// function by name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the function to retrieve.
|
||||
pub fn get_mut(&mut self, name: &str) -> Option<&mut Registration> {
|
||||
self.functions.get_mut(name)
|
||||
}
|
||||
|
||||
/// Unregisters a function from the environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the function to remove.
|
||||
pub fn remove(&mut self, name: &str) -> Option<Registration> {
|
||||
self.functions.remove(name)
|
||||
}
|
||||
|
||||
/// Updates a function registration in this environment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the function to update.
|
||||
/// * `registration` - The new registration for the function.
|
||||
pub fn update(&mut self, name: String, registration: Registration) {
|
||||
self.functions.insert(name, registration);
|
||||
}
|
||||
|
||||
/// Clear all functions in this environment.
|
||||
pub fn clear(&mut self) {
|
||||
self.functions.clear();
|
||||
}
|
||||
|
||||
/// Returns an iterator over the functions registered in this environment.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&String, &Registration)> {
|
||||
self.functions.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates a registration for a defined function.
|
||||
#[derive(Clone)]
|
||||
pub struct Registration {
|
||||
/// The definition of the function.
|
||||
pub(crate) definition: Arc<brush_parser::ast::FunctionDefinition>,
|
||||
/// Whether or not this function definition should be exported to children.
|
||||
exported: bool,
|
||||
}
|
||||
|
||||
impl From<brush_parser::ast::FunctionDefinition> for Registration {
|
||||
fn from(definition: brush_parser::ast::FunctionDefinition) -> Self {
|
||||
Self {
|
||||
definition: Arc::new(definition),
|
||||
exported: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registration {
|
||||
/// Returns a reference to the function definition.
|
||||
pub fn definition(&self) -> &brush_parser::ast::FunctionDefinition {
|
||||
&self.definition
|
||||
}
|
||||
|
||||
/// Marks the function for export.
|
||||
pub const fn export(&mut self) {
|
||||
self.exported = true;
|
||||
}
|
||||
|
||||
/// Unmarks the function for export.
|
||||
pub const fn unexport(&mut self) {
|
||||
self.exported = false;
|
||||
}
|
||||
|
||||
/// Returns whether this function is exported.
|
||||
pub const fn is_exported(&self) -> bool {
|
||||
self.exported
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an active shell function call.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FunctionCall {
|
||||
/// The name of the function invoked.
|
||||
pub function_name: String,
|
||||
/// The definition of the invoked function.
|
||||
pub function_definition: Arc<brush_parser::ast::FunctionDefinition>,
|
||||
}
|
||||
|
||||
/// Encapsulates a function call stack.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CallStack {
|
||||
frames: VecDeque<FunctionCall>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CallStack {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
writeln!(f, "Function call stack (most recent first):")?;
|
||||
|
||||
for (index, frame) in self.iter().enumerate() {
|
||||
writeln!(f, " #{}| {}", index, frame.function_name)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CallStack {
|
||||
/// Creates a new empty function call stack.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Removes the top from from the stack. If the stack is empty, does nothing and
|
||||
/// returns `None`; otherwise, returns the removed call frame.
|
||||
pub fn pop(&mut self) -> Option<FunctionCall> {
|
||||
self.frames.pop_front()
|
||||
}
|
||||
|
||||
/// Pushes a new frame onto the stack.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the function being called.
|
||||
/// * `function_def` - The definition of the function being called.
|
||||
pub fn push(&mut self, name: impl Into<String>, function_def: &Arc<ast::FunctionDefinition>) {
|
||||
self.frames.push_front(FunctionCall {
|
||||
function_name: name.into(),
|
||||
function_definition: function_def.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the current depth of the function call stack.
|
||||
pub fn depth(&self) -> usize {
|
||||
self.frames.len()
|
||||
}
|
||||
|
||||
/// Returns whether or not the function call stack is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.frames.is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the function call frames, starting from the most
|
||||
/// recent.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &FunctionCall> {
|
||||
self.frames.iter()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
//! Facilities for tracking and persisting the shell's command history.
|
||||
|
||||
use chrono::Utc;
|
||||
use std::{
|
||||
io::{BufRead, Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Represents a unique identifier for a history item.
|
||||
type ItemId = i64;
|
||||
|
||||
/// Interface for querying and manipulating the shell's recorded history of commands.
|
||||
// TODO: support maximum item count
|
||||
#[derive(Clone, Default)]
|
||||
pub struct History {
|
||||
items: rpds::VectorSync<ItemId>,
|
||||
id_map: rpds::HashTrieMapSync<ItemId, Item>,
|
||||
next_id: ItemId,
|
||||
}
|
||||
|
||||
impl History {
|
||||
/// Constructs a new `History` instance, with its contents initialized from the given readable
|
||||
/// stream. If errors are encountered reading lines from the stream, unreadable lines will
|
||||
/// be skipped but the call will still return successfully, with a warning logged. An error
|
||||
/// result will be returned only if an internal error occurs updating the history.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reader` - The readable stream to import history from.
|
||||
pub fn import(reader: impl Read) -> Result<Self, error::Error> {
|
||||
let mut history = Self::default();
|
||||
|
||||
let buf_reader = std::io::BufReader::new(reader);
|
||||
|
||||
let mut next_timestamp = None;
|
||||
for line_result in buf_reader.lines() {
|
||||
// If we couldn't decode the line (perhaps it wasn't valid UTF8?), skip it and make
|
||||
// a best-effort attempt to proceed on. We'll later warn the user.
|
||||
let line = match line_result {
|
||||
Ok(line) => line,
|
||||
Err(err) => {
|
||||
tracing::warn!("unreadable history line; {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Look for timestamp comments; ignore other comment lines.
|
||||
if let Some(comment) = line.strip_prefix("#") {
|
||||
if let Ok(seconds_since_epoch) = comment.trim().parse() {
|
||||
next_timestamp =
|
||||
chrono::DateTime::<Utc>::from_timestamp(seconds_since_epoch, 0);
|
||||
} else {
|
||||
next_timestamp = None;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let item = Item {
|
||||
id: history.next_id,
|
||||
command_line: line,
|
||||
timestamp: next_timestamp.take(),
|
||||
dirty: false,
|
||||
};
|
||||
|
||||
history.add(item)?;
|
||||
}
|
||||
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
/// Tries to retrieve a history item by its unique identifier. Returns `None` if no item is
|
||||
/// found.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - The unique identifier of the history item to retrieve.
|
||||
pub fn get_by_id(&self, id: ItemId) -> Result<Option<&Item>, error::Error> {
|
||||
Ok(self.id_map.get(&id))
|
||||
}
|
||||
|
||||
/// Replaces the history item with the given ID with a new item. Returns an error if the item
|
||||
/// cannot be updated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - The unique identifier of the history item to update.
|
||||
/// * `item` - The new history item to replace the old one.
|
||||
pub fn update_by_id(&mut self, id: ItemId, item: Item) -> Result<(), error::Error> {
|
||||
let existing_item = self
|
||||
.id_map
|
||||
.get_mut(&id)
|
||||
.ok_or(error::ErrorKind::HistoryItemNotFound)?;
|
||||
*existing_item = item;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the nth item from the history. Returns the removed item, or `None` if no such item
|
||||
/// exists (i.e., because it was out of range).
|
||||
pub fn remove_nth_item(&mut self, n: usize) -> bool {
|
||||
if let Some(id) = self.items.get(n).copied() {
|
||||
self.items = self
|
||||
.items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, id)| if i != n { Some(id) } else { None })
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
self.id_map.remove_mut(&id);
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new history item. Returns the unique identifier of the newly added item.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `item` - The history item to add.
|
||||
pub fn add(&mut self, mut item: Item) -> Result<ItemId, error::Error> {
|
||||
let id = self.next_id;
|
||||
|
||||
item.id = id;
|
||||
self.next_id += 1;
|
||||
|
||||
self.items.push_back_mut(item.id);
|
||||
self.id_map.insert_mut(item.id, item);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Deletes a history item by its unique identifier. Returns an error if the item cannot be
|
||||
/// deleted.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - The unique identifier of the history item to delete.
|
||||
pub fn delete_item_by_id(&mut self, id: ItemId) -> Result<(), error::Error> {
|
||||
self.id_map.remove_mut(&id);
|
||||
self.items = self
|
||||
.items
|
||||
.into_iter()
|
||||
.filter(|&item_id| *item_id != id)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears all history items.
|
||||
pub fn clear(&mut self) -> Result<(), error::Error> {
|
||||
self.id_map = rpds::HashTrieMapSync::new_sync();
|
||||
self.items = rpds::VectorSync::new_sync();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flushes the history to backing storage (if relevant).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `history_file_path` - The path to the history file.
|
||||
/// * `append` - Whether to append to the file or overwrite it.
|
||||
/// * `unsaved_items_only` - Whether to only write unsaved items; if true, any items will be marked as "saved" once saved.
|
||||
/// * `write_timestamps` - Whether to write timestamps for each command line.
|
||||
pub fn flush(
|
||||
&mut self,
|
||||
history_file_path: impl AsRef<Path>,
|
||||
append: bool,
|
||||
unsaved_items_only: bool,
|
||||
write_timestamps: bool,
|
||||
) -> Result<(), error::Error> {
|
||||
// Open the file
|
||||
let mut file_options = std::fs::File::options();
|
||||
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
|
||||
let mut file = file_options.create(true).open(history_file_path.as_ref())?;
|
||||
|
||||
for item_id in &self.items {
|
||||
if let Some(item) = self.id_map.get_mut(item_id) {
|
||||
if unsaved_items_only && !item.dirty {
|
||||
continue;
|
||||
}
|
||||
|
||||
if write_timestamps {
|
||||
if let Some(timestamp) = item.timestamp {
|
||||
writeln!(file, "#{}", timestamp.timestamp())?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(file, "{}", item.command_line)?;
|
||||
|
||||
if unsaved_items_only {
|
||||
item.dirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Searches through history using the given query.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `query` - The query to use.
|
||||
pub fn search(&self, query: Query) -> Result<impl Iterator<Item = &self::Item>, error::Error> {
|
||||
Ok(Search::new(self, query))
|
||||
}
|
||||
|
||||
/// Returns an iterator over the history items.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &self::Item> {
|
||||
Search::all(self)
|
||||
}
|
||||
|
||||
/// Retrieves the nth history item, if it exists. Returns `None` if no such item exists.
|
||||
/// Indexing is zero-based, with an index of 0 referencing the oldest item in the history.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `index` - The index of the history item to retrieve.
|
||||
pub fn get(&self, index: usize) -> Option<&Item> {
|
||||
if let Some(id) = self.items.get(index) {
|
||||
self.id_map.get(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of items in the history.
|
||||
pub fn count(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an item in the history.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Item {
|
||||
/// The unique identifier of the history item.
|
||||
pub id: ItemId,
|
||||
/// The actual command line.
|
||||
pub command_line: String,
|
||||
/// The timestamp when the command was started.
|
||||
pub timestamp: Option<chrono::DateTime<Utc>>,
|
||||
/// Whether or not the item is dirty, i.e., has not yet been written to backing storage.
|
||||
pub dirty: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Constructs a new `Item` with the given command line.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `command_line` - The command line of the item.
|
||||
pub fn new(command_line: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: 0, // NOTE: ID will be assigned when added to the history.
|
||||
command_line: command_line.into(),
|
||||
timestamp: Some(chrono::Utc::now()),
|
||||
dirty: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates query parameters for searching through history.
|
||||
#[derive(Default)]
|
||||
pub struct Query {
|
||||
/// Whether to search forward or backward
|
||||
pub direction: Direction,
|
||||
/// Optionally, clamp results to items with a timestamp strictly after this.
|
||||
pub not_at_or_before_time: Option<chrono::DateTime<Utc>>,
|
||||
/// Optionally, clamp results to items with a timestamp strictly before this.
|
||||
pub not_at_or_after_time: Option<chrono::DateTime<Utc>>,
|
||||
/// Optionally, clamp results to items with an ID equal strictly after this.
|
||||
pub not_at_or_before_id: Option<ItemId>,
|
||||
/// Optionally, clamp results to items with an ID equal strictly before this.
|
||||
pub not_at_or_after_id: Option<ItemId>,
|
||||
/// Optionally, maximum number of items to retrieve
|
||||
pub max_items: Option<i64>,
|
||||
/// Optionally, a string-based filter on command line.
|
||||
pub command_line_filter: Option<CommandLineFilter>,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
/// Checks if the query includes the given item.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `item` - The item to check.
|
||||
pub fn includes(&self, item: &Item) -> bool {
|
||||
// Filter based on not_at_or_before_time.
|
||||
if let Some(not_at_or_before_time) = &self.not_at_or_before_time {
|
||||
if item
|
||||
.timestamp
|
||||
.is_some_and(|ts| ts <= *not_at_or_before_time)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter based on not_at_or_after_time
|
||||
if let Some(not_at_or_after_time) = &self.not_at_or_after_time {
|
||||
if item.timestamp.is_some_and(|ts| ts >= *not_at_or_after_time) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter based on not_at_or_before_id
|
||||
if self
|
||||
.not_at_or_before_id
|
||||
.is_some_and(|query_id| item.id <= query_id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter based on not_at_or_after_id
|
||||
if self
|
||||
.not_at_or_after_id
|
||||
.is_some_and(|query_id| item.id >= query_id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter based on command_line_filter
|
||||
if let Some(command_line_filter) = &self.command_line_filter {
|
||||
match command_line_filter {
|
||||
CommandLineFilter::Prefix(prefix) => {
|
||||
if !item.command_line.starts_with(prefix) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
CommandLineFilter::Suffix(suffix) => {
|
||||
if !item.command_line.ends_with(suffix) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
CommandLineFilter::Contains(contains) => {
|
||||
if !item.command_line.contains(contains) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
CommandLineFilter::Exact(exact) => {
|
||||
if item.command_line != *exact {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the direction of a search operation.
|
||||
#[derive(Default)]
|
||||
pub enum Direction {
|
||||
/// Search forward from the oldest part of history.
|
||||
#[default]
|
||||
Forward,
|
||||
/// Search backward from the youngest part of history.
|
||||
Backward,
|
||||
}
|
||||
|
||||
/// Filter criteria for command lines.
|
||||
pub enum CommandLineFilter {
|
||||
/// The command line must start with this string.
|
||||
Prefix(String),
|
||||
/// The command line must end with this string.
|
||||
Suffix(String),
|
||||
/// The command line must contain this string.
|
||||
Contains(String),
|
||||
/// The command line must match this string exactly.
|
||||
Exact(String),
|
||||
}
|
||||
|
||||
/// Represents a search operation.
|
||||
pub struct Search<'a> {
|
||||
/// The history to search through.
|
||||
history: &'a History,
|
||||
/// The query to apply.
|
||||
query: Query,
|
||||
/// The next index in `items`.
|
||||
next_index: Option<usize>,
|
||||
/// Count of items returned so far.
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<'a> Search<'a> {
|
||||
/// Constructs a new search against the provided history, querying *all* items.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `history` - The history to search through.
|
||||
pub fn all(history: &'a History) -> Self {
|
||||
Self::new(history, Query::default())
|
||||
}
|
||||
|
||||
/// Constructs a new search against the provided history, using the given query.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `history` - The history to search through.
|
||||
/// * `query` - The query to use.
|
||||
pub fn new(history: &'a History, query: Query) -> Self {
|
||||
let next_index = match query.direction {
|
||||
Direction::Forward => Some(0),
|
||||
Direction::Backward => {
|
||||
if history.items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(history.items.len() - 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
history,
|
||||
query,
|
||||
next_index,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const fn increment_next_index(&mut self) {
|
||||
if let Some(index) = self.next_index {
|
||||
self.next_index = match self.query.direction {
|
||||
Direction::Forward => Some(index + 1),
|
||||
Direction::Backward => {
|
||||
if index == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(index - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Search<'a> {
|
||||
type Item = &'a Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if let Some(index) = self.next_index {
|
||||
// Make sure we haven't hit the end of the history.
|
||||
if index >= self.history.items.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = self.history.items[index];
|
||||
self.increment_next_index();
|
||||
|
||||
if let Some(item) = self.history.id_map.get(&id) {
|
||||
// Filter based on max_items. Once we hit the limit,
|
||||
// we stop searching.
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
if self
|
||||
.query
|
||||
.max_items
|
||||
.is_some_and(|max_items| self.count >= max_items as usize)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check other filters. If they don't match, then we
|
||||
// skip but keep searching.
|
||||
if self.query.includes(item) {
|
||||
self.count += 1;
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Exports traits for shell interfaces implemented by callers.
|
||||
|
||||
mod keybindings;
|
||||
|
||||
pub use keybindings::{InputFunction, Key, KeyAction, KeyBindings, KeySequence, KeyStroke};
|
||||
@@ -0,0 +1,337 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// Represents an action that can be taken in response to a key sequence.
|
||||
#[derive(Debug)]
|
||||
pub enum KeyAction {
|
||||
/// Execute a shell command.
|
||||
ShellCommand(String),
|
||||
/// Execute an input "function".
|
||||
DoInputFunction(InputFunction),
|
||||
}
|
||||
|
||||
impl Display for KeyAction {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ShellCommand(command) => write!(f, "shell command: {command}"),
|
||||
Self::DoInputFunction(function) => function.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines all input functions.
|
||||
#[derive(Debug, strum_macros::EnumString, strum_macros::Display, strum_macros::EnumIter)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[expect(missing_docs)]
|
||||
pub enum InputFunction {
|
||||
Abort,
|
||||
AcceptLine,
|
||||
AliasExpandLine,
|
||||
ArrowKeyPrefix,
|
||||
BackwardByte,
|
||||
BackwardChar,
|
||||
BackwardDeleteChar,
|
||||
BackwardKillLine,
|
||||
BackwardKillWord,
|
||||
BackwardWord,
|
||||
BeginningOfHistory,
|
||||
BeginningOfLine,
|
||||
BracketedPasteBegin,
|
||||
CallLastKbdMacro,
|
||||
CapitalizeWord,
|
||||
CharacterSearch,
|
||||
CharacterSearchBackward,
|
||||
ClearDisplay,
|
||||
ClearScreen,
|
||||
Complete,
|
||||
CompleteCommand,
|
||||
CompleteFilename,
|
||||
CompleteHostname,
|
||||
CompleteIntoBraces,
|
||||
CompleteUsername,
|
||||
CompleteVariable,
|
||||
CopyBackwardWord,
|
||||
CopyForwardWord,
|
||||
CopyRegionAsKill,
|
||||
DabbrevExpand,
|
||||
DeleteChar,
|
||||
DeleteCharOrList,
|
||||
DeleteHorizontalSpace,
|
||||
DigitArgument,
|
||||
DisplayShellVersion,
|
||||
DoLowercaseVersion,
|
||||
DowncaseWord,
|
||||
DumpFunctions,
|
||||
DumpMacros,
|
||||
DumpVariables,
|
||||
DynamicCompleteHistory,
|
||||
EditAndExecuteCommand,
|
||||
EmacsEditingMode,
|
||||
EndKbdMacro,
|
||||
EndOfHistory,
|
||||
EndOfLine,
|
||||
ExchangePointAndMark,
|
||||
ForwardBackwardDeleteChar,
|
||||
ForwardByte,
|
||||
ForwardChar,
|
||||
ForwardSearchHistory,
|
||||
ForwardWord,
|
||||
GlobCompleteWord,
|
||||
GlobExpandWord,
|
||||
GlobListExpansions,
|
||||
HistoryAndAliasExpandLine,
|
||||
HistoryExpandLine,
|
||||
HistorySearchBackward,
|
||||
HistorySearchForward,
|
||||
HistorySubstringSearchBackward,
|
||||
HistorySubstringSearchForward,
|
||||
InsertComment,
|
||||
InsertCompletions,
|
||||
InsertLastArgument,
|
||||
KillLine,
|
||||
KillRegion,
|
||||
KillWholeLine,
|
||||
KillWord,
|
||||
MagicSpace,
|
||||
MenuComplete,
|
||||
MenuCompleteBackward,
|
||||
NextHistory,
|
||||
NextScreenLine,
|
||||
NonIncrementalForwardSearchHistory,
|
||||
NonIncrementalForwardSearchHistoryAgain,
|
||||
NonIncrementalReverseSearchHistory,
|
||||
NonIncrementalReverseSearchHistoryAgain,
|
||||
OldMenuComplete,
|
||||
OperateAndGetNext,
|
||||
OverwriteMode,
|
||||
PossibleCommandCompletions,
|
||||
PossibleCompletions,
|
||||
PossibleFilenameCompletions,
|
||||
PossibleHostnameCompletions,
|
||||
PossibleUsernameCompletions,
|
||||
PossibleVariableCompletions,
|
||||
PreviousHistory,
|
||||
PreviousScreenLine,
|
||||
PrintLastKbdMacro,
|
||||
QuotedInsert,
|
||||
ReReadInitFile,
|
||||
RedrawCurrentLine,
|
||||
ReverseSearchHistory,
|
||||
RevertLine,
|
||||
SelfInsert,
|
||||
SetMark,
|
||||
ShellBackwardKillWord,
|
||||
ShellBackwardWord,
|
||||
ShellExpandLine,
|
||||
ShellForwardWord,
|
||||
ShellKillWord,
|
||||
ShellTransposeWords,
|
||||
SkipCsiSequence,
|
||||
StartKbdMacro,
|
||||
TabInsert,
|
||||
TildeExpand,
|
||||
TransposeChars,
|
||||
TransposeWords,
|
||||
TtyStatus,
|
||||
Undo,
|
||||
UniversalArgument,
|
||||
UnixFilenameRubout,
|
||||
UnixLineDiscard,
|
||||
UnixWordRubout,
|
||||
UpcaseWord,
|
||||
ViAppendEol,
|
||||
ViAppendMode,
|
||||
ViArgDigit,
|
||||
ViBWord,
|
||||
ViBackToIndent,
|
||||
ViBackwardBigword,
|
||||
ViBackwardWord,
|
||||
ViBword,
|
||||
ViChangeCase,
|
||||
ViChangeChar,
|
||||
ViChangeTo,
|
||||
ViCharSearch,
|
||||
ViColumn,
|
||||
ViComplete,
|
||||
ViDelete,
|
||||
ViDeleteTo,
|
||||
ViEWord,
|
||||
ViEditingMode,
|
||||
ViEndBigword,
|
||||
ViEndWord,
|
||||
ViEofMaybe,
|
||||
ViEword,
|
||||
ViFWord,
|
||||
ViFetchHistory,
|
||||
ViFirstPrint,
|
||||
ViForwardBigword,
|
||||
ViForwardWord,
|
||||
ViFword,
|
||||
ViGotoMark,
|
||||
ViInsertBeg,
|
||||
ViInsertionMode,
|
||||
ViMatch,
|
||||
ViMovementMode,
|
||||
ViNextWord,
|
||||
ViOverstrike,
|
||||
ViOverstrikeDelete,
|
||||
ViPrevWord,
|
||||
ViPut,
|
||||
ViRedo,
|
||||
ViReplace,
|
||||
ViRubout,
|
||||
ViSearch,
|
||||
ViSearchAgain,
|
||||
ViSetMark,
|
||||
ViSubst,
|
||||
ViTildeExpand,
|
||||
ViUnixWordRubout,
|
||||
ViYankArg,
|
||||
ViYankPop,
|
||||
ViYankTo,
|
||||
Yank,
|
||||
YankLastArg,
|
||||
YankNthArg,
|
||||
YankPop,
|
||||
}
|
||||
|
||||
/// Represents a sequence of keys.
|
||||
#[derive(Debug, Eq, Hash, PartialEq)]
|
||||
pub struct KeySequence {
|
||||
/// The strokes in the sequence.
|
||||
pub strokes: Vec<KeyStroke>,
|
||||
}
|
||||
|
||||
impl Display for KeySequence {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
for stroke in &self.strokes {
|
||||
stroke.fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyStroke> for KeySequence {
|
||||
/// Creates a new key sequence with a single stroke.
|
||||
fn from(value: KeyStroke) -> Self {
|
||||
Self {
|
||||
strokes: vec![value],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, Hash, PartialEq)]
|
||||
/// Represents a single key press.
|
||||
pub struct KeyStroke {
|
||||
/// Alt key was pressed.
|
||||
pub alt: bool,
|
||||
/// Control key was pressed.
|
||||
pub control: bool,
|
||||
/// Shift key was pressed.
|
||||
pub shift: bool,
|
||||
/// Primary key pressed.
|
||||
pub key: Key,
|
||||
}
|
||||
|
||||
impl Display for KeyStroke {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if self.alt {
|
||||
write!(f, "\\e")?;
|
||||
}
|
||||
if self.control {
|
||||
write!(f, "\\C-")?;
|
||||
}
|
||||
if self.shift {
|
||||
// TODO: Figure out what to do here or if the key encodes the shift in it.
|
||||
}
|
||||
self.key.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Key> for KeyStroke {
|
||||
/// Creates a new key stroke with a single key.
|
||||
fn from(value: Key) -> Self {
|
||||
Self {
|
||||
alt: false,
|
||||
control: false,
|
||||
shift: false,
|
||||
key: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
/// Represents a single key.
|
||||
pub enum Key {
|
||||
/// A simple character key.
|
||||
Character(char),
|
||||
/// Backspace key.
|
||||
Backspace,
|
||||
/// Enter key.
|
||||
Enter,
|
||||
/// Left arrow key.
|
||||
Left,
|
||||
/// Right arrow key.
|
||||
Right,
|
||||
/// Up arrow key.
|
||||
Up,
|
||||
/// Down arrow key.
|
||||
Down,
|
||||
/// Home key.
|
||||
Home,
|
||||
/// End key.
|
||||
End,
|
||||
/// Page up key.
|
||||
PageUp,
|
||||
/// Page down key.
|
||||
PageDown,
|
||||
/// Tab key.
|
||||
Tab,
|
||||
/// Shift + Tab key.
|
||||
BackTab,
|
||||
/// Delete key.
|
||||
Delete,
|
||||
/// Insert key.
|
||||
Insert,
|
||||
/// F key.
|
||||
F(u8),
|
||||
/// Escape key.
|
||||
Escape,
|
||||
}
|
||||
|
||||
impl Display for Key {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Character(c @ ('\\' | '\"' | '\'')) => write!(f, "\\{c}")?,
|
||||
Self::Character(c) => write!(f, "{c}")?,
|
||||
Self::Backspace => write!(f, "Backspace")?,
|
||||
Self::Enter => write!(f, "Enter")?,
|
||||
Self::Left => write!(f, "Left")?,
|
||||
Self::Right => write!(f, "Right")?,
|
||||
Self::Up => write!(f, "Up")?,
|
||||
Self::Down => write!(f, "Down")?,
|
||||
Self::Home => write!(f, "Home")?,
|
||||
Self::End => write!(f, "End")?,
|
||||
Self::PageUp => write!(f, "PageUp")?,
|
||||
Self::PageDown => write!(f, "PageDown")?,
|
||||
Self::Tab => write!(f, "Tab")?,
|
||||
Self::BackTab => write!(f, "BackTab")?,
|
||||
Self::Delete => write!(f, "Delete")?,
|
||||
Self::Insert => write!(f, "Insert")?,
|
||||
Self::F(n) => write!(f, "F{n}")?,
|
||||
Self::Escape => write!(f, "Esc")?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates the shell's interaction with key bindings for input.
|
||||
pub trait KeyBindings: Send {
|
||||
/// Retrieves current bindings.
|
||||
fn get_current(&self) -> HashMap<KeySequence, KeyAction>;
|
||||
/// Updates a binding.
|
||||
fn bind(&mut self, seq: KeySequence, action: KeyAction) -> Result<(), std::io::Error>;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,444 @@
|
||||
//! Job management
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Display;
|
||||
|
||||
use futures::FutureExt;
|
||||
|
||||
use crate::ExecutionResult;
|
||||
use crate::error;
|
||||
use crate::processes;
|
||||
use crate::sys;
|
||||
use crate::trace_categories;
|
||||
use crate::traps;
|
||||
|
||||
pub(crate) type JobJoinHandle = tokio::task::JoinHandle<Result<ExecutionResult, error::Error>>;
|
||||
pub(crate) type JobResult = (Job, Result<ExecutionResult, error::Error>);
|
||||
|
||||
/// Manages the jobs that are currently managed by the shell.
|
||||
#[derive(Default)]
|
||||
pub struct JobManager {
|
||||
/// The jobs that are currently managed by the shell.
|
||||
pub jobs: Vec<Job>,
|
||||
}
|
||||
|
||||
/// Represents a task that is part of a job.
|
||||
pub enum JobTask {
|
||||
/// An external process.
|
||||
External(processes::ChildProcess),
|
||||
/// An internal asynchronous task.
|
||||
Internal(JobJoinHandle),
|
||||
}
|
||||
|
||||
/// Represents the result of waiting on a job task.
|
||||
pub enum JobTaskWaitResult {
|
||||
/// The task has completed.
|
||||
Completed(ExecutionResult),
|
||||
/// The task was stopped.
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl JobTask {
|
||||
/// Waits for the task to complete. Returns the result of the wait.
|
||||
pub async fn wait(&mut self) -> Result<JobTaskWaitResult, error::Error> {
|
||||
match self {
|
||||
Self::External(process) => {
|
||||
let wait_result = process.wait().await?;
|
||||
match wait_result {
|
||||
processes::ProcessWaitResult::Completed(output) => {
|
||||
Ok(JobTaskWaitResult::Completed(output.into()))
|
||||
}
|
||||
processes::ProcessWaitResult::Stopped => Ok(JobTaskWaitResult::Stopped),
|
||||
}
|
||||
}
|
||||
Self::Internal(handle) => Ok(JobTaskWaitResult::Completed(handle.await??)),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_in_result)]
|
||||
fn poll(&mut self) -> Option<Result<ExecutionResult, error::Error>> {
|
||||
match self {
|
||||
Self::External(process) => {
|
||||
let check_result = process.poll();
|
||||
check_result.map(|polled_result| polled_result.map(|output| output.into()))
|
||||
}
|
||||
Self::Internal(handle) => {
|
||||
let checkable_handle = handle;
|
||||
checkable_handle.now_or_never().map(|r| r.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JobManager {
|
||||
/// Returns a new job manager.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Adds a job to the job manager and marks it as the current job;
|
||||
/// returns an immutable reference to the job.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `job` - The job to add.
|
||||
pub fn add_as_current(&mut self, mut job: Job) -> &Job {
|
||||
for j in &mut self.jobs {
|
||||
if matches!(j.annotation, JobAnnotation::Current) {
|
||||
j.annotation = JobAnnotation::Previous;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let id = self.jobs.len() + 1;
|
||||
job.id = id;
|
||||
job.annotation = JobAnnotation::Current;
|
||||
self.jobs.push(job);
|
||||
self.jobs.last().unwrap()
|
||||
}
|
||||
|
||||
/// Returns the current job, if there is one.
|
||||
pub fn current_job(&self) -> Option<&Job> {
|
||||
self.jobs
|
||||
.iter()
|
||||
.find(|j| matches!(j.annotation, JobAnnotation::Current))
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the current job, if there is one.
|
||||
pub fn current_job_mut(&mut self) -> Option<&mut Job> {
|
||||
self.jobs
|
||||
.iter_mut()
|
||||
.find(|j| matches!(j.annotation, JobAnnotation::Current))
|
||||
}
|
||||
|
||||
/// Returns the previous job, if there is one.
|
||||
pub fn prev_job(&self) -> Option<&Job> {
|
||||
self.jobs
|
||||
.iter()
|
||||
.find(|j| matches!(j.annotation, JobAnnotation::Previous))
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the previous job, if there is one.
|
||||
pub fn prev_job_mut(&mut self) -> Option<&mut Job> {
|
||||
self.jobs
|
||||
.iter_mut()
|
||||
.find(|j| matches!(j.annotation, JobAnnotation::Previous))
|
||||
}
|
||||
|
||||
/// Tries to resolve the given job specification to a job.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `job_spec` - The job specification to resolve.
|
||||
pub fn resolve_job_spec(&mut self, job_spec: &str) -> Option<&mut Job> {
|
||||
let remainder = job_spec.strip_prefix('%')?;
|
||||
|
||||
match remainder {
|
||||
"%" | "+" => self.current_job_mut(),
|
||||
"-" => self.prev_job_mut(),
|
||||
s if s.chars().all(char::is_numeric) => {
|
||||
let id = s.parse::<usize>().ok()?;
|
||||
self.jobs.iter_mut().find(|j| j.id == id)
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!(target: trace_categories::UNIMPLEMENTED, "unimplemented: job spec naming command: '{job_spec}'");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for all managed jobs to complete.
|
||||
pub async fn wait_all(&mut self) -> Result<Vec<Job>, error::Error> {
|
||||
for job in &mut self.jobs {
|
||||
job.wait().await?;
|
||||
}
|
||||
|
||||
Ok(self.sweep_completed_jobs())
|
||||
}
|
||||
|
||||
/// Polls all managed jobs for completion.
|
||||
pub fn poll(&mut self) -> Result<Vec<JobResult>, error::Error> {
|
||||
let mut results = vec![];
|
||||
|
||||
let mut i = 0;
|
||||
while i != self.jobs.len() {
|
||||
if let Some(result) = self.jobs[i].poll_done()? {
|
||||
let job = self.jobs.remove(i);
|
||||
results.push((job, result));
|
||||
} else if matches!(self.jobs[i].state, JobState::Done) {
|
||||
// TODO: This is a workaround to remove jobs that are done but for which we don't
|
||||
// know what happened.
|
||||
results.push((self.jobs.remove(i), Ok(ExecutionResult::success())));
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn sweep_completed_jobs(&mut self) -> Vec<Job> {
|
||||
let mut completed_jobs = vec![];
|
||||
|
||||
let mut i = 0;
|
||||
while i != self.jobs.len() {
|
||||
if self.jobs[i].tasks.is_empty() {
|
||||
completed_jobs.push(self.jobs.remove(i));
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
completed_jobs
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the current execution state of a job.
|
||||
#[derive(Clone)]
|
||||
pub enum JobState {
|
||||
/// Unknown state.
|
||||
Unknown,
|
||||
/// The job is running.
|
||||
Running,
|
||||
/// The job is stopped.
|
||||
Stopped,
|
||||
/// The job has completed.
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Display for JobState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Unknown => write!(f, "Unknown"),
|
||||
Self::Running => write!(f, "Running"),
|
||||
Self::Stopped => write!(f, "Stopped"),
|
||||
Self::Done => write!(f, "Done"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an annotation for a job.
|
||||
#[derive(Clone)]
|
||||
pub enum JobAnnotation {
|
||||
/// No annotation.
|
||||
None,
|
||||
/// The job is the current job.
|
||||
Current,
|
||||
/// The job is the previous job.
|
||||
Previous,
|
||||
}
|
||||
|
||||
impl Display for JobAnnotation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, ""),
|
||||
Self::Current => write!(f, "+"),
|
||||
Self::Previous => write!(f, "-"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates a set of processes managed by the shell as a single unit.
|
||||
pub struct Job {
|
||||
/// The tasks that make up the job.
|
||||
tasks: VecDeque<JobTask>,
|
||||
|
||||
/// If available, the process group ID of the job's processes.
|
||||
pgid: Option<sys::process::ProcessId>,
|
||||
|
||||
/// The annotation of the job (e.g., current, previous).
|
||||
annotation: JobAnnotation,
|
||||
|
||||
/// The shell-internal ID of the job.
|
||||
pub id: usize,
|
||||
|
||||
/// The command line of the job.
|
||||
pub command_line: String,
|
||||
|
||||
/// The current operational state of the job.
|
||||
pub state: JobState,
|
||||
}
|
||||
|
||||
impl Display for Job {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"[{}]{:3}{}\t{}",
|
||||
self.id,
|
||||
self.annotation.to_string(),
|
||||
self.state,
|
||||
self.command_line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// Returns a new job object.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `children` - The job's known child processes.
|
||||
/// * `command_line` - The command line of the job.
|
||||
/// * `state` - The current operational state of the job.
|
||||
pub(crate) fn new<I>(tasks: I, command_line: String, state: JobState) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = JobTask>,
|
||||
{
|
||||
Self {
|
||||
id: 0,
|
||||
tasks: tasks.into_iter().collect(),
|
||||
pgid: None,
|
||||
annotation: JobAnnotation::None,
|
||||
command_line,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a pid-style string for the job.
|
||||
pub fn to_pid_style_string(&self) -> String {
|
||||
let display_pid = self
|
||||
.representative_pid()
|
||||
.map_or_else(|| String::from("<pid unknown>"), |pid| pid.to_string());
|
||||
std::format!("[{}]{}\t{}", self.id, self.annotation, display_pid)
|
||||
}
|
||||
|
||||
/// Returns the annotation of the job.
|
||||
pub fn annotation(&self) -> JobAnnotation {
|
||||
self.annotation.clone()
|
||||
}
|
||||
|
||||
/// Returns the command name of the job.
|
||||
pub fn command_name(&self) -> &str {
|
||||
self.command_line
|
||||
.split_ascii_whitespace()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns whether the job is the current job.
|
||||
pub const fn is_current(&self) -> bool {
|
||||
matches!(self.annotation, JobAnnotation::Current)
|
||||
}
|
||||
|
||||
/// Returns whether the job is the previous job.
|
||||
pub const fn is_prev(&self) -> bool {
|
||||
matches!(self.annotation, JobAnnotation::Previous)
|
||||
}
|
||||
|
||||
/// Polls whether the job has completed.
|
||||
pub fn poll_done(
|
||||
&mut self,
|
||||
) -> Result<Option<Result<ExecutionResult, error::Error>>, error::Error> {
|
||||
let mut result: Option<Result<ExecutionResult, error::Error>> = None;
|
||||
|
||||
tracing::debug!(target: trace_categories::JOBS, "Polling job {} for completion...", self.id);
|
||||
|
||||
while !self.tasks.is_empty() {
|
||||
let task = &mut self.tasks[0];
|
||||
match task.poll() {
|
||||
Some(r) => {
|
||||
self.tasks.remove(0);
|
||||
result = Some(r);
|
||||
}
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(target: trace_categories::JOBS, "Job {} has completed.", self.id);
|
||||
|
||||
self.state = JobState::Done;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Waits for the job to complete.
|
||||
pub async fn wait(&mut self) -> Result<ExecutionResult, error::Error> {
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
while let Some(task) = self.tasks.back_mut() {
|
||||
match task.wait().await? {
|
||||
JobTaskWaitResult::Completed(execution_result) => {
|
||||
result = execution_result;
|
||||
self.tasks.pop_back();
|
||||
}
|
||||
JobTaskWaitResult::Stopped => {
|
||||
self.state = JobState::Stopped;
|
||||
return Ok(ExecutionResult::stopped());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.state = JobState::Done;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Moves the job to execute in the background.
|
||||
pub fn move_to_background(&mut self) -> Result<(), error::Error> {
|
||||
if matches!(self.state, JobState::Stopped) {
|
||||
if let Some(pgid) = self.process_group_id() {
|
||||
sys::signal::continue_process(pgid)?;
|
||||
self.state = JobState::Running;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error::ErrorKind::FailedToSendSignal.into())
|
||||
}
|
||||
} else {
|
||||
error::unimp("move job to background")
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the job to execute in the foreground.
|
||||
pub fn move_to_foreground(&mut self) -> Result<(), error::Error> {
|
||||
if matches!(self.state, JobState::Stopped) {
|
||||
if let Some(pgid) = self.process_group_id() {
|
||||
sys::signal::continue_process(pgid)?;
|
||||
self.state = JobState::Running;
|
||||
} else {
|
||||
return Err(error::ErrorKind::FailedToSendSignal.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pgid) = self.process_group_id() {
|
||||
sys::terminal::move_to_foreground(pgid)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kills the job.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `signal` - The signal to send to the job.
|
||||
pub fn kill(&self, signal: traps::TrapSignal) -> Result<(), error::Error> {
|
||||
if let Some(pid) = self.process_group_id() {
|
||||
sys::signal::kill_process(pid, signal)
|
||||
} else {
|
||||
Err(error::ErrorKind::FailedToSendSignal.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to retrieve a "representative" pid for the job.
|
||||
pub fn representative_pid(&self) -> Option<sys::process::ProcessId> {
|
||||
for task in &self.tasks {
|
||||
match task {
|
||||
JobTask::External(p) => {
|
||||
if let Some(pid) = p.pid() {
|
||||
return Some(pid);
|
||||
}
|
||||
}
|
||||
JobTask::Internal(_) => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Tries to retrieve the process group ID (PGID) of the job.
|
||||
pub fn process_group_id(&self) -> Option<sys::process::ProcessId> {
|
||||
// TODO: Don't assume that the first PID is the PGID.
|
||||
self.pgid.or_else(|| self.representative_pid())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
fn get_keywords(sh_mode_only: bool) -> HashSet<String> {
|
||||
let mut keywords = HashSet::new();
|
||||
keywords.insert(String::from("!"));
|
||||
keywords.insert(String::from("{"));
|
||||
keywords.insert(String::from("}"));
|
||||
keywords.insert(String::from("case"));
|
||||
keywords.insert(String::from("do"));
|
||||
keywords.insert(String::from("done"));
|
||||
keywords.insert(String::from("elif"));
|
||||
keywords.insert(String::from("else"));
|
||||
keywords.insert(String::from("esac"));
|
||||
keywords.insert(String::from("fi"));
|
||||
keywords.insert(String::from("for"));
|
||||
keywords.insert(String::from("if"));
|
||||
keywords.insert(String::from("in"));
|
||||
keywords.insert(String::from("then"));
|
||||
keywords.insert(String::from("until"));
|
||||
keywords.insert(String::from("while"));
|
||||
|
||||
if !sh_mode_only {
|
||||
keywords.insert(String::from("[["));
|
||||
keywords.insert(String::from("]]"));
|
||||
keywords.insert(String::from("coproc"));
|
||||
keywords.insert(String::from("function"));
|
||||
keywords.insert(String::from("select"));
|
||||
keywords.insert(String::from("time"));
|
||||
}
|
||||
|
||||
keywords
|
||||
}
|
||||
|
||||
pub(crate) static SH_MODE_KEYWORDS: LazyLock<HashSet<String>> =
|
||||
LazyLock::new(|| get_keywords(true));
|
||||
pub(crate) static KEYWORDS: LazyLock<HashSet<String>> = LazyLock::new(|| get_keywords(false));
|
||||
@@ -0,0 +1,46 @@
|
||||
//! Core implementation of the brush shell. Implements the shell's abstraction, its interpreter, and
|
||||
//! various facilities used internally by the shell.
|
||||
|
||||
pub mod arithmetic;
|
||||
mod braceexpansion;
|
||||
pub mod builtins;
|
||||
pub mod commands;
|
||||
pub mod completion;
|
||||
pub mod env;
|
||||
pub mod error;
|
||||
pub mod escape;
|
||||
pub mod expansion;
|
||||
mod extendedtests;
|
||||
pub mod functions;
|
||||
pub mod history;
|
||||
pub mod interfaces;
|
||||
mod interp;
|
||||
pub mod jobs;
|
||||
mod keywords;
|
||||
pub mod namedoptions;
|
||||
pub mod openfiles;
|
||||
pub mod options;
|
||||
pub mod pathcache;
|
||||
pub mod pathsearch;
|
||||
pub mod patterns;
|
||||
pub mod processes;
|
||||
mod prompt;
|
||||
mod regex;
|
||||
pub mod results;
|
||||
pub mod scripts;
|
||||
mod shell;
|
||||
pub mod sys;
|
||||
pub mod terminal;
|
||||
pub mod tests;
|
||||
pub mod timing;
|
||||
pub mod trace_categories;
|
||||
pub mod traps;
|
||||
pub mod variables;
|
||||
mod wellknownvars;
|
||||
|
||||
pub use commands::{CommandArg, ExecutionContext};
|
||||
pub use error::{BuiltinError, Error, ErrorKind};
|
||||
pub use interp::{ExecutionParameters, ProcessGroupPolicy};
|
||||
pub use results::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, ExecutionSpawnResult};
|
||||
pub use shell::{CreateOptions, Shell, ShellBuilder, ShellBuilderState, ShellFd};
|
||||
pub use variables::{ShellValue, ShellVariable};
|
||||
@@ -0,0 +1,829 @@
|
||||
//! Defines shell options.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::options::RuntimeOptions;
|
||||
|
||||
type OptionGetter = fn(shell: &RuntimeOptions) -> bool;
|
||||
type OptionSetter = fn(shell: &mut RuntimeOptions, value: bool) -> ();
|
||||
|
||||
/// Defines an option.
|
||||
pub struct ShellOptionDef {
|
||||
/// Getter function that retrieves the current value of the option.
|
||||
getter: OptionGetter,
|
||||
/// Setter function that may be used to set the current value of the option.
|
||||
setter: OptionSetter,
|
||||
}
|
||||
|
||||
impl ShellOptionDef {
|
||||
/// Constructs a new option definition.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `getter` - A function that retrieves the current value of the option.
|
||||
/// * `setter` - A function that sets the current value of the option.
|
||||
fn new(getter: OptionGetter, setter: OptionSetter) -> Self {
|
||||
Self { getter, setter }
|
||||
}
|
||||
|
||||
/// Retrieves the current value of this option from the given runtime options.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `options` - The runtime options to retrieve the value from.
|
||||
pub fn get(&self, options: &RuntimeOptions) -> bool {
|
||||
(self.getter)(options)
|
||||
}
|
||||
|
||||
/// Sets the value of this option in the given runtime options.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `options` - The runtime options to modify.
|
||||
/// * `value` - The new value to set for the option.
|
||||
pub fn set(&self, options: &mut RuntimeOptions, value: bool) {
|
||||
(self.setter)(options, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a shell option.
|
||||
pub struct ShellOption {
|
||||
/// The name of the option.
|
||||
pub name: &'static str,
|
||||
/// The definition of the option.
|
||||
pub definition: &'static ShellOptionDef,
|
||||
}
|
||||
|
||||
/// Describes a set of shell options.
|
||||
pub struct ShellOptionSet {
|
||||
inner: &'static HashMap<&'static str, ShellOptionDef>,
|
||||
}
|
||||
|
||||
/// Kind of shell option.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ShellOptionKind {
|
||||
/// `set` option.
|
||||
Set,
|
||||
/// `set -o` option.
|
||||
SetO,
|
||||
/// `shopt` option.
|
||||
Shopt,
|
||||
}
|
||||
|
||||
/// Returns the options for the given shell option kind.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `kind` - The kind of shell options to retrieve.
|
||||
pub fn options(kind: ShellOptionKind) -> ShellOptionSet {
|
||||
match kind {
|
||||
ShellOptionKind::Set => ShellOptionSet {
|
||||
inner: &SET_OPTIONS,
|
||||
},
|
||||
ShellOptionKind::SetO => ShellOptionSet {
|
||||
inner: &SET_O_OPTIONS,
|
||||
},
|
||||
ShellOptionKind::Shopt => ShellOptionSet {
|
||||
inner: &SHOPT_OPTIONS,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellOptionSet {
|
||||
/// Returns an iterator over the options defined in this set.
|
||||
pub fn iter(&self) -> impl Iterator<Item = ShellOption> {
|
||||
self.inner
|
||||
.iter()
|
||||
.map(|(&name, definition)| ShellOption { name, definition })
|
||||
}
|
||||
|
||||
/// Returns the option with the given name, if it exists.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the option to retrieve.
|
||||
pub fn get(&self, name: &str) -> Option<&'static ShellOptionDef> {
|
||||
self.inner.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
static SET_OPTIONS: LazyLock<HashMap<&'static str, ShellOptionDef>> = LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"a",
|
||||
ShellOptionDef::new(
|
||||
|options| options.export_variables_on_modification,
|
||||
|options, value| options.export_variables_on_modification = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"b",
|
||||
ShellOptionDef::new(
|
||||
|options| options.notify_job_termination_immediately,
|
||||
|options, value| options.notify_job_termination_immediately = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"e",
|
||||
ShellOptionDef::new(
|
||||
|options| options.exit_on_nonzero_command_exit,
|
||||
|options, value| options.exit_on_nonzero_command_exit = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"f",
|
||||
ShellOptionDef::new(
|
||||
|options| options.disable_filename_globbing,
|
||||
|options, value| options.disable_filename_globbing = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"h",
|
||||
ShellOptionDef::new(
|
||||
|options| options.remember_command_locations,
|
||||
|options, value| options.remember_command_locations = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"i",
|
||||
ShellOptionDef::new(
|
||||
|options| options.interactive,
|
||||
|options, value| options.interactive = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"k",
|
||||
ShellOptionDef::new(
|
||||
|options| options.place_all_assignment_args_in_command_env,
|
||||
|options, value| options.place_all_assignment_args_in_command_env = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"m",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_job_control,
|
||||
|options, value| options.enable_job_control = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"n",
|
||||
ShellOptionDef::new(
|
||||
|options| options.do_not_execute_commands,
|
||||
|options, value| options.do_not_execute_commands = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"p",
|
||||
ShellOptionDef::new(
|
||||
|options| options.real_effective_uid_mismatch,
|
||||
|options, value| options.real_effective_uid_mismatch = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"t",
|
||||
ShellOptionDef::new(
|
||||
|options| options.exit_after_one_command,
|
||||
|options, value| options.exit_after_one_command = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"u",
|
||||
ShellOptionDef::new(
|
||||
|options| options.treat_unset_variables_as_error,
|
||||
|options, value| options.treat_unset_variables_as_error = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"v",
|
||||
ShellOptionDef::new(
|
||||
|options| options.print_shell_input_lines,
|
||||
|options, value| options.print_shell_input_lines = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"x",
|
||||
ShellOptionDef::new(
|
||||
|options| options.print_commands_and_arguments,
|
||||
|options, value| options.print_commands_and_arguments = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"B",
|
||||
ShellOptionDef::new(
|
||||
|options| options.perform_brace_expansion,
|
||||
|options, value| options.perform_brace_expansion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"C",
|
||||
ShellOptionDef::new(
|
||||
|options| options.disallow_overwriting_regular_files_via_output_redirection,
|
||||
|options, value| {
|
||||
options.disallow_overwriting_regular_files_via_output_redirection = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
"E",
|
||||
ShellOptionDef::new(
|
||||
|options| options.shell_functions_inherit_err_trap,
|
||||
|options, value| options.shell_functions_inherit_err_trap = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"H",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_bang_style_history_substitution,
|
||||
|options, value| options.enable_bang_style_history_substitution = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"P",
|
||||
ShellOptionDef::new(
|
||||
|options| options.do_not_resolve_symlinks_when_changing_dir,
|
||||
|options, value| options.do_not_resolve_symlinks_when_changing_dir = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"T",
|
||||
ShellOptionDef::new(
|
||||
|options| options.shell_functions_inherit_debug_and_return_traps,
|
||||
|options, value| options.shell_functions_inherit_debug_and_return_traps = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"s",
|
||||
ShellOptionDef::new(
|
||||
|options| options.read_commands_from_stdin,
|
||||
|options, value| options.read_commands_from_stdin = value,
|
||||
),
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
static SET_O_OPTIONS: LazyLock<HashMap<&'static str, ShellOptionDef>> = LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"allexport",
|
||||
ShellOptionDef::new(
|
||||
|options| options.export_variables_on_modification,
|
||||
|options, value| options.export_variables_on_modification = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"braceexpand",
|
||||
ShellOptionDef::new(
|
||||
|options| options.perform_brace_expansion,
|
||||
|options, value| options.perform_brace_expansion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"emacs",
|
||||
ShellOptionDef::new(
|
||||
|options| options.emacs_mode,
|
||||
|options, value| options.emacs_mode = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"errexit",
|
||||
ShellOptionDef::new(
|
||||
|options| options.exit_on_nonzero_command_exit,
|
||||
|options, value| options.exit_on_nonzero_command_exit = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"errtrace",
|
||||
ShellOptionDef::new(
|
||||
|options| options.shell_functions_inherit_err_trap,
|
||||
|options, value| options.shell_functions_inherit_err_trap = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"functrace",
|
||||
ShellOptionDef::new(
|
||||
|options| options.shell_functions_inherit_debug_and_return_traps,
|
||||
|options, value| options.shell_functions_inherit_debug_and_return_traps = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"hashall",
|
||||
ShellOptionDef::new(
|
||||
|options| options.remember_command_locations,
|
||||
|options, value| options.remember_command_locations = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"histexpand",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_bang_style_history_substitution,
|
||||
|options, value| options.enable_bang_style_history_substitution = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"history",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_command_history,
|
||||
|options, value| options.enable_command_history = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"ignoreeof",
|
||||
ShellOptionDef::new(
|
||||
|options| options.ignore_eof,
|
||||
|options, value| options.ignore_eof = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"interactive-comments",
|
||||
ShellOptionDef::new(
|
||||
|options| options.interactive_comments,
|
||||
|options, value| options.interactive_comments = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"keyword",
|
||||
ShellOptionDef::new(
|
||||
|options| options.place_all_assignment_args_in_command_env,
|
||||
|options, value| options.place_all_assignment_args_in_command_env = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"monitor",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_job_control,
|
||||
|options, value| options.enable_job_control = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"noclobber",
|
||||
ShellOptionDef::new(
|
||||
|options| options.disallow_overwriting_regular_files_via_output_redirection,
|
||||
|options, value| {
|
||||
options.disallow_overwriting_regular_files_via_output_redirection = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
"noexec",
|
||||
ShellOptionDef::new(
|
||||
|options| options.do_not_execute_commands,
|
||||
|options, value| options.do_not_execute_commands = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"noglob",
|
||||
ShellOptionDef::new(
|
||||
|options| options.disable_filename_globbing,
|
||||
|options, value| options.disable_filename_globbing = value,
|
||||
),
|
||||
),
|
||||
("nolog", ShellOptionDef::new(|_| false, |_, _| ())),
|
||||
(
|
||||
"notify",
|
||||
ShellOptionDef::new(
|
||||
|options| options.notify_job_termination_immediately,
|
||||
|options, value| options.notify_job_termination_immediately = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"nounset",
|
||||
ShellOptionDef::new(
|
||||
|options| options.treat_unset_variables_as_error,
|
||||
|options, value| options.treat_unset_variables_as_error = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"onecmd",
|
||||
ShellOptionDef::new(
|
||||
|options| options.exit_after_one_command,
|
||||
|options, value| options.exit_after_one_command = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"physical",
|
||||
ShellOptionDef::new(
|
||||
|options| options.do_not_resolve_symlinks_when_changing_dir,
|
||||
|options, value| options.do_not_resolve_symlinks_when_changing_dir = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"pipefail",
|
||||
ShellOptionDef::new(
|
||||
|options| options.return_first_failure_from_pipeline,
|
||||
|options, value| options.return_first_failure_from_pipeline = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"posix",
|
||||
ShellOptionDef::new(
|
||||
|options| options.posix_mode,
|
||||
|options, value| options.posix_mode = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"privileged",
|
||||
ShellOptionDef::new(
|
||||
|options| options.real_effective_uid_mismatch,
|
||||
|options, value| options.real_effective_uid_mismatch = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"verbose",
|
||||
ShellOptionDef::new(
|
||||
|options| options.print_shell_input_lines,
|
||||
|options, value| options.print_shell_input_lines = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"vi",
|
||||
ShellOptionDef::new(
|
||||
|options| options.vi_mode,
|
||||
|options, value| options.vi_mode = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"xtrace",
|
||||
ShellOptionDef::new(
|
||||
|options| options.print_commands_and_arguments,
|
||||
|options, value| options.print_commands_and_arguments = value,
|
||||
),
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
static SHOPT_OPTIONS: LazyLock<HashMap<&'static str, ShellOptionDef>> = LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"autocd",
|
||||
ShellOptionDef::new(
|
||||
|options| options.auto_cd,
|
||||
|options, value| options.auto_cd = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"assoc_expand_once",
|
||||
ShellOptionDef::new(
|
||||
|options| options.assoc_expand_once,
|
||||
|options, value| options.assoc_expand_once = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"cdable_vars",
|
||||
ShellOptionDef::new(
|
||||
|options| options.cdable_vars,
|
||||
|options, value| options.cdable_vars = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"cdspell",
|
||||
ShellOptionDef::new(
|
||||
|options| options.cd_autocorrect_spelling,
|
||||
|options, value| options.cd_autocorrect_spelling = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"checkhash",
|
||||
ShellOptionDef::new(
|
||||
|options| options.check_hashtable_before_command_exec,
|
||||
|options, value| options.check_hashtable_before_command_exec = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"checkjobs",
|
||||
ShellOptionDef::new(
|
||||
|options| options.check_jobs_before_exit,
|
||||
|options, value| options.check_jobs_before_exit = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"checkwinsize",
|
||||
ShellOptionDef::new(
|
||||
|options| options.check_window_size_after_external_commands,
|
||||
|options, value| options.check_window_size_after_external_commands = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"cmdhist",
|
||||
ShellOptionDef::new(
|
||||
|options| options.save_multiline_cmds_in_history,
|
||||
|options, value| options.save_multiline_cmds_in_history = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"compat31",
|
||||
ShellOptionDef::new(
|
||||
|options| options.compat31,
|
||||
|options, value| options.compat31 = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"compat32",
|
||||
ShellOptionDef::new(
|
||||
|options| options.compat32,
|
||||
|options, value| options.compat32 = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"compat40",
|
||||
ShellOptionDef::new(
|
||||
|options| options.compat40,
|
||||
|options, value| options.compat40 = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"compat41",
|
||||
ShellOptionDef::new(
|
||||
|options| options.compat41,
|
||||
|options, value| options.compat41 = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"compat42",
|
||||
ShellOptionDef::new(
|
||||
|options| options.compat42,
|
||||
|options, value| options.compat42 = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"compat43",
|
||||
ShellOptionDef::new(
|
||||
|options| options.compat43,
|
||||
|options, value| options.compat43 = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"compat44",
|
||||
ShellOptionDef::new(
|
||||
|options| options.compat44,
|
||||
|options, value| options.compat44 = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"complete_fullquote",
|
||||
ShellOptionDef::new(
|
||||
|options| options.quote_all_metachars_in_completion,
|
||||
|options, value| options.quote_all_metachars_in_completion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"direxpand",
|
||||
ShellOptionDef::new(
|
||||
|options| options.expand_dir_names_on_completion,
|
||||
|options, value| options.expand_dir_names_on_completion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"dirspell",
|
||||
ShellOptionDef::new(
|
||||
|options| options.autocorrect_dir_spelling_on_completion,
|
||||
|options, value| options.autocorrect_dir_spelling_on_completion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"dotglob",
|
||||
ShellOptionDef::new(
|
||||
|options| options.glob_matches_dotfiles,
|
||||
|options, value| options.glob_matches_dotfiles = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"execfail",
|
||||
ShellOptionDef::new(
|
||||
|options| options.exit_on_exec_fail,
|
||||
|options, value| options.exit_on_exec_fail = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"expand_aliases",
|
||||
ShellOptionDef::new(
|
||||
|options| options.expand_aliases,
|
||||
|options, value| options.expand_aliases = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"extdebug",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_debugger,
|
||||
|options, value| options.enable_debugger = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"extglob",
|
||||
ShellOptionDef::new(
|
||||
|options| options.extended_globbing,
|
||||
|options, value| options.extended_globbing = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"extquote",
|
||||
ShellOptionDef::new(
|
||||
|options| options.extquote,
|
||||
|options, value| options.extquote = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"failglob",
|
||||
ShellOptionDef::new(
|
||||
|options| options.fail_expansion_on_globs_without_match,
|
||||
|options, value| options.fail_expansion_on_globs_without_match = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"force_fignore",
|
||||
ShellOptionDef::new(
|
||||
|options| options.force_fignore,
|
||||
|options, value| options.force_fignore = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"globasciiranges",
|
||||
ShellOptionDef::new(
|
||||
|options| options.glob_ranges_use_c_locale,
|
||||
|options, value| options.glob_ranges_use_c_locale = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"globstar",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_star_star_glob,
|
||||
|options, value| options.enable_star_star_glob = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"gnu_errfmt",
|
||||
ShellOptionDef::new(
|
||||
|options| options.errors_in_gnu_format,
|
||||
|options, value| options.errors_in_gnu_format = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"histappend",
|
||||
ShellOptionDef::new(
|
||||
|options| options.append_to_history_file,
|
||||
|options, value| options.append_to_history_file = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"histreedit",
|
||||
ShellOptionDef::new(
|
||||
|options| options.allow_reedit_failed_history_subst,
|
||||
|options, value| options.allow_reedit_failed_history_subst = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"histverify",
|
||||
ShellOptionDef::new(
|
||||
|options| options.allow_modifying_history_substitution,
|
||||
|options, value| options.allow_modifying_history_substitution = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"hostcomplete",
|
||||
ShellOptionDef::new(
|
||||
|options| options.enable_hostname_completion,
|
||||
|options, value| options.enable_hostname_completion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"huponexit",
|
||||
ShellOptionDef::new(
|
||||
|options| options.send_sighup_to_all_jobs_on_exit,
|
||||
|options, value| options.send_sighup_to_all_jobs_on_exit = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"inherit_errexit",
|
||||
ShellOptionDef::new(
|
||||
|options| options.command_subst_inherits_errexit,
|
||||
|options, value| options.command_subst_inherits_errexit = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"interactive_comments",
|
||||
ShellOptionDef::new(
|
||||
|options| options.interactive_comments,
|
||||
|options, value| options.interactive_comments = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"lastpipe",
|
||||
ShellOptionDef::new(
|
||||
|options| options.run_last_pipeline_cmd_in_current_shell,
|
||||
|options, value| options.run_last_pipeline_cmd_in_current_shell = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"lithist",
|
||||
ShellOptionDef::new(
|
||||
|options| options.embed_newlines_in_multiline_cmds_in_history,
|
||||
|options, value| options.embed_newlines_in_multiline_cmds_in_history = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"localvar_inherit",
|
||||
ShellOptionDef::new(
|
||||
|options| options.local_vars_inherit_value_and_attrs,
|
||||
|options, value| options.local_vars_inherit_value_and_attrs = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"localvar_unset",
|
||||
ShellOptionDef::new(
|
||||
|options| options.localvar_unset,
|
||||
|options, value| options.localvar_unset = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"login_shell",
|
||||
ShellOptionDef::new(
|
||||
|options| options.login_shell,
|
||||
|options, value| options.login_shell = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"mailwarn",
|
||||
ShellOptionDef::new(
|
||||
|options| options.mail_warn,
|
||||
|options, value| options.mail_warn = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"no_empty_cmd_completion",
|
||||
ShellOptionDef::new(
|
||||
|options| options.no_empty_cmd_completion,
|
||||
|options, value| options.no_empty_cmd_completion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"nocaseglob",
|
||||
ShellOptionDef::new(
|
||||
|options| options.case_insensitive_pathname_expansion,
|
||||
|options, value| options.case_insensitive_pathname_expansion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"nocasematch",
|
||||
ShellOptionDef::new(
|
||||
|options| options.case_insensitive_conditionals,
|
||||
|options, value| options.case_insensitive_conditionals = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"nullglob",
|
||||
ShellOptionDef::new(
|
||||
|options| options.expand_non_matching_patterns_to_null,
|
||||
|options, value| options.expand_non_matching_patterns_to_null = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"progcomp",
|
||||
ShellOptionDef::new(
|
||||
|options| options.programmable_completion,
|
||||
|options, value| options.programmable_completion = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"progcomp_alias",
|
||||
ShellOptionDef::new(
|
||||
|options| options.programmable_completion_alias,
|
||||
|options, value| options.programmable_completion_alias = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"promptvars",
|
||||
ShellOptionDef::new(
|
||||
|options| options.expand_prompt_strings,
|
||||
|options, value| options.expand_prompt_strings = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"restricted_shell",
|
||||
ShellOptionDef::new(
|
||||
|options| options.restricted_shell,
|
||||
|options, value| options.restricted_shell = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"shift_verbose",
|
||||
ShellOptionDef::new(
|
||||
|options| options.shift_verbose,
|
||||
|options, value| options.shift_verbose = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"sourcepath",
|
||||
ShellOptionDef::new(
|
||||
|options| options.source_builtin_searches_path,
|
||||
|options, value| options.source_builtin_searches_path = value,
|
||||
),
|
||||
),
|
||||
(
|
||||
"xpg_echo",
|
||||
ShellOptionDef::new(
|
||||
|options| options.echo_builtin_expands_escape_sequences,
|
||||
|options, value| options.echo_builtin_expands_escape_sequences = value,
|
||||
),
|
||||
),
|
||||
])
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
//! Managing files open within a shell instance.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::IsTerminal;
|
||||
use std::process::Stdio;
|
||||
|
||||
use crate::ShellFd;
|
||||
use crate::error;
|
||||
use crate::sys;
|
||||
|
||||
/// Represents a file open in a shell context.
|
||||
pub enum OpenFile {
|
||||
/// The original standard input this process was started with.
|
||||
Stdin(std::io::Stdin),
|
||||
/// The original standard output this process was started with.
|
||||
Stdout(std::io::Stdout),
|
||||
/// The original standard error this process was started with.
|
||||
Stderr(std::io::Stderr),
|
||||
/// A file open for reading or writing.
|
||||
File(std::fs::File),
|
||||
/// A read end of a pipe.
|
||||
PipeReader(std::io::PipeReader),
|
||||
/// A write end of a pipe.
|
||||
PipeWriter(std::io::PipeWriter),
|
||||
}
|
||||
|
||||
/// Returns an open file that will discard all I/O.
|
||||
pub fn null() -> Result<OpenFile, error::Error> {
|
||||
let file = sys::fs::open_null_file()?;
|
||||
Ok(OpenFile::File(file))
|
||||
}
|
||||
|
||||
impl Clone for OpenFile {
|
||||
fn clone(&self) -> Self {
|
||||
self.try_clone().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for OpenFile {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Stdin(_) => write!(f, "stdin"),
|
||||
Self::Stdout(_) => write!(f, "stdout"),
|
||||
Self::Stderr(_) => write!(f, "stderr"),
|
||||
Self::File(_) => write!(f, "file"),
|
||||
Self::PipeReader(_) => write!(f, "pipe reader"),
|
||||
Self::PipeWriter(_) => write!(f, "pipe writer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenFile {
|
||||
/// Tries to duplicate the open file.
|
||||
pub fn try_clone(&self) -> Result<Self, std::io::Error> {
|
||||
let result = match self {
|
||||
Self::Stdin(_) => Self::Stdin(std::io::stdin()),
|
||||
Self::Stdout(_) => Self::Stdout(std::io::stdout()),
|
||||
Self::Stderr(_) => Self::Stderr(std::io::stderr()),
|
||||
Self::File(f) => Self::File(f.try_clone()?),
|
||||
Self::PipeReader(f) => Self::PipeReader(f.try_clone()?),
|
||||
Self::PipeWriter(f) => Self::PipeWriter(f.try_clone()?),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Converts the open file into an `OwnedFd`.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn into_owned_fd(self) -> Result<std::os::fd::OwnedFd, error::Error> {
|
||||
use std::os::fd::AsFd as _;
|
||||
|
||||
match self {
|
||||
Self::Stdin(f) => Ok(f.as_fd().try_clone_to_owned()?),
|
||||
Self::Stdout(f) => Ok(f.as_fd().try_clone_to_owned()?),
|
||||
Self::Stderr(f) => Ok(f.as_fd().try_clone_to_owned()?),
|
||||
Self::File(f) => Ok(f.into()),
|
||||
Self::PipeReader(r) => Ok(std::os::fd::OwnedFd::from(r)),
|
||||
Self::PipeWriter(w) => Ok(std::os::fd::OwnedFd::from(w)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_dir(&self) -> bool {
|
||||
match self {
|
||||
Self::Stdin(_) | Self::Stdout(_) | Self::Stderr(_) => false,
|
||||
Self::File(file) => file.metadata().map(|m| m.is_dir()).unwrap_or(false),
|
||||
Self::PipeReader(_) | Self::PipeWriter(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_term(&self) -> bool {
|
||||
match self {
|
||||
Self::Stdin(f) => f.is_terminal(),
|
||||
Self::Stdout(f) => f.is_terminal(),
|
||||
Self::Stderr(f) => f.is_terminal(),
|
||||
Self::File(f) => f.is_terminal(),
|
||||
Self::PipeReader(_) => false,
|
||||
Self::PipeWriter(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl std::os::fd::AsFd for OpenFile {
|
||||
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
|
||||
match self {
|
||||
Self::Stdin(f) => f.as_fd(),
|
||||
Self::Stdout(f) => f.as_fd(),
|
||||
Self::Stderr(f) => f.as_fd(),
|
||||
Self::File(f) => f.as_fd(),
|
||||
Self::PipeReader(r) => r.as_fd(),
|
||||
Self::PipeWriter(w) => w.as_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::fs::File> for OpenFile {
|
||||
fn from(file: std::fs::File) -> Self {
|
||||
Self::File(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::PipeReader> for OpenFile {
|
||||
fn from(reader: std::io::PipeReader) -> Self {
|
||||
Self::PipeReader(reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::PipeWriter> for OpenFile {
|
||||
fn from(writer: std::io::PipeWriter) -> Self {
|
||||
Self::PipeWriter(writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpenFile> for Stdio {
|
||||
fn from(open_file: OpenFile) -> Self {
|
||||
match open_file {
|
||||
OpenFile::Stdin(_) => Self::inherit(),
|
||||
OpenFile::Stdout(_) => Self::inherit(),
|
||||
OpenFile::Stderr(_) => Self::inherit(),
|
||||
OpenFile::File(f) => f.into(),
|
||||
OpenFile::PipeReader(f) => f.into(),
|
||||
OpenFile::PipeWriter(f) => f.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Read for OpenFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
Self::Stdin(f) => f.read(buf),
|
||||
Self::Stdout(_) => Err(std::io::Error::other(
|
||||
error::ErrorKind::OpenFileNotReadable("stdout"),
|
||||
)),
|
||||
Self::Stderr(_) => Err(std::io::Error::other(
|
||||
error::ErrorKind::OpenFileNotReadable("stderr"),
|
||||
)),
|
||||
Self::File(f) => f.read(buf),
|
||||
Self::PipeReader(reader) => reader.read(buf),
|
||||
Self::PipeWriter(_) => Err(std::io::Error::other(
|
||||
error::ErrorKind::OpenFileNotReadable("pipe writer"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for OpenFile {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
Self::Stdin(_) => Err(std::io::Error::other(
|
||||
error::ErrorKind::OpenFileNotWritable("stdin"),
|
||||
)),
|
||||
Self::Stdout(f) => f.write(buf),
|
||||
Self::Stderr(f) => f.write(buf),
|
||||
Self::File(f) => f.write(buf),
|
||||
Self::PipeReader(_) => Err(std::io::Error::other(
|
||||
error::ErrorKind::OpenFileNotWritable("pipe reader"),
|
||||
)),
|
||||
Self::PipeWriter(writer) => writer.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
match self {
|
||||
Self::Stdin(_) => Ok(()),
|
||||
Self::Stdout(f) => f.flush(),
|
||||
Self::Stderr(f) => f.flush(),
|
||||
Self::File(f) => f.flush(),
|
||||
Self::PipeReader(_) => Ok(()),
|
||||
Self::PipeWriter(writer) => writer.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tristate representing the an `OpenFile` entry in an `OpenFiles` structure.
|
||||
pub enum OpenFileEntry<'a> {
|
||||
/// File descriptor is present and has a valid associated `OpenFile`.
|
||||
Open(&'a OpenFile),
|
||||
/// File descriptor is explicitly marked as not being mapped to any `OpenFile`.
|
||||
NotPresent,
|
||||
/// File descriptor is not specified in any way; it may be provided by a
|
||||
/// parent context of some kind.
|
||||
NotSpecified,
|
||||
}
|
||||
|
||||
/// Represents the open files in a shell context.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct OpenFiles {
|
||||
/// Maps shell file descriptors to open files.
|
||||
files: HashMap<ShellFd, Option<OpenFile>>,
|
||||
}
|
||||
|
||||
impl OpenFiles {
|
||||
/// File descriptor used for standard input.
|
||||
pub const STDIN_FD: ShellFd = 0;
|
||||
/// File descriptor used for standard output.
|
||||
pub const STDOUT_FD: ShellFd = 1;
|
||||
/// File descriptor used for standard error.
|
||||
pub const STDERR_FD: ShellFd = 2;
|
||||
|
||||
/// Creates a new `OpenFiles` instance populated with stdin, stdout, and stderr
|
||||
/// from the host environment.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
files: HashMap::from([
|
||||
(Self::STDIN_FD, Some(OpenFile::Stdin(std::io::stdin()))),
|
||||
(Self::STDOUT_FD, Some(OpenFile::Stdout(std::io::stdout()))),
|
||||
(Self::STDERR_FD, Some(OpenFile::Stderr(std::io::stderr()))),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the open files from the provided iterator of (fd number, `OpenFile`) pairs.
|
||||
/// Any existing entries for the provided file descriptors will be overwritten.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `files`: An iterator of (fd number, `OpenFile`) pairs to update the open files with.
|
||||
pub fn update_from(&mut self, files: impl Iterator<Item = (ShellFd, OpenFile)>) {
|
||||
for (fd, file) in files {
|
||||
let _ = self.files.insert(fd, Some(file));
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the file backing standard input in this context.
|
||||
pub fn try_stdin(&self) -> Option<&OpenFile> {
|
||||
self.files.get(&Self::STDIN_FD).and_then(|f| f.as_ref())
|
||||
}
|
||||
|
||||
/// Retrieves the file backing standard output in this context.
|
||||
pub fn try_stdout(&self) -> Option<&OpenFile> {
|
||||
self.files.get(&Self::STDOUT_FD).and_then(|f| f.as_ref())
|
||||
}
|
||||
|
||||
/// Retrieves the file backing standard error in this context.
|
||||
pub fn try_stderr(&self) -> Option<&OpenFile> {
|
||||
self.files.get(&Self::STDERR_FD).and_then(|f| f.as_ref())
|
||||
}
|
||||
|
||||
/// Tries to remove an open file by its file descriptor. If the file descriptor
|
||||
/// is not used, `None` will be returned; otherwise, the removed file will
|
||||
/// be returned.
|
||||
///
|
||||
/// Arguments:
|
||||
///
|
||||
/// * `fd`: The file descriptor to remove.
|
||||
pub fn remove_fd(&mut self, fd: ShellFd) -> Option<OpenFile> {
|
||||
self.files.insert(fd, None).and_then(|f| f)
|
||||
}
|
||||
|
||||
/// Tries to lookup the `OpenFile` associated with a file descriptor.
|
||||
/// Returns `None` if the file descriptor is not present.
|
||||
///
|
||||
/// Arguments:
|
||||
///
|
||||
/// * `fd`: The file descriptor to lookup.
|
||||
pub fn try_fd(&self, fd: ShellFd) -> Option<&OpenFile> {
|
||||
self.files.get(&fd).and_then(|f| f.as_ref())
|
||||
}
|
||||
|
||||
/// Tries to lookup the `OpenFile` associated with a file descriptor. Returns
|
||||
/// an `OpenFileEntry` representing the state of the file descriptor.
|
||||
///
|
||||
/// Arguments:
|
||||
///
|
||||
/// * `fd`: The file descriptor to lookup.
|
||||
pub fn fd_entry(&self, fd: ShellFd) -> OpenFileEntry<'_> {
|
||||
self.files
|
||||
.get(&fd)
|
||||
.map_or(OpenFileEntry::NotSpecified, |opt_file| match opt_file {
|
||||
Some(f) => OpenFileEntry::Open(f),
|
||||
None => OpenFileEntry::NotPresent,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the given file descriptor is in use.
|
||||
pub fn contains_fd(&self, fd: ShellFd) -> bool {
|
||||
self.files.contains_key(&fd)
|
||||
}
|
||||
|
||||
/// Associates the given file descriptor with the provided file. If the file descriptor
|
||||
/// is already in use, the previous file will be returned; otherwise, `None`
|
||||
/// will be returned.
|
||||
///
|
||||
/// Arguments:
|
||||
///
|
||||
/// * `fd`: The file descriptor to associate with the file.
|
||||
/// * `file`: The file to associate with the file descriptor.
|
||||
pub fn set_fd(&mut self, fd: ShellFd, file: OpenFile) -> Option<OpenFile> {
|
||||
self.files.insert(fd, Some(file)).and_then(|f| f)
|
||||
}
|
||||
|
||||
/// Iterates over all file descriptors.
|
||||
pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, &OpenFile)> {
|
||||
self.files
|
||||
.iter()
|
||||
.filter_map(|(fd, file)| file.as_ref().map(|f| (*fd, f)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> From<I> for OpenFiles
|
||||
where
|
||||
I: Iterator<Item = (ShellFd, OpenFile)>,
|
||||
{
|
||||
fn from(iter: I) -> Self {
|
||||
let files = iter.map(|(fd, file)| (fd, Some(file))).collect();
|
||||
Self { files }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
//! Defines runtime options for the shell.
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{CreateOptions, namedoptions};
|
||||
|
||||
/// Runtime changeable options for a shell instance.
|
||||
#[derive(Clone, Default)]
|
||||
#[expect(clippy::module_name_repetitions)]
|
||||
pub struct RuntimeOptions {
|
||||
//
|
||||
// Single-character options.
|
||||
/// -a
|
||||
pub export_variables_on_modification: bool,
|
||||
/// -b
|
||||
pub notify_job_termination_immediately: bool,
|
||||
/// -e
|
||||
pub exit_on_nonzero_command_exit: bool,
|
||||
/// -f
|
||||
pub disable_filename_globbing: bool,
|
||||
/// -h
|
||||
pub remember_command_locations: bool,
|
||||
/// -k
|
||||
pub place_all_assignment_args_in_command_env: bool,
|
||||
/// -m
|
||||
pub enable_job_control: bool,
|
||||
/// -n
|
||||
pub do_not_execute_commands: bool,
|
||||
/// -p
|
||||
pub real_effective_uid_mismatch: bool,
|
||||
/// -t
|
||||
pub exit_after_one_command: bool,
|
||||
/// -u
|
||||
pub treat_unset_variables_as_error: bool,
|
||||
/// -v
|
||||
pub print_shell_input_lines: bool,
|
||||
/// -x
|
||||
pub print_commands_and_arguments: bool,
|
||||
/// -B
|
||||
pub perform_brace_expansion: bool,
|
||||
/// -C
|
||||
pub disallow_overwriting_regular_files_via_output_redirection: bool,
|
||||
/// -E
|
||||
pub shell_functions_inherit_err_trap: bool,
|
||||
/// -H
|
||||
pub enable_bang_style_history_substitution: bool,
|
||||
/// -P
|
||||
pub do_not_resolve_symlinks_when_changing_dir: bool,
|
||||
/// -T
|
||||
pub shell_functions_inherit_debug_and_return_traps: bool,
|
||||
|
||||
//
|
||||
// Options set through -o.
|
||||
/// 'emacs'
|
||||
pub emacs_mode: bool,
|
||||
/// 'history'
|
||||
pub enable_command_history: bool,
|
||||
/// 'ignoreeof'
|
||||
pub ignore_eof: bool,
|
||||
/// 'pipefail'
|
||||
pub return_first_failure_from_pipeline: bool,
|
||||
/// 'posix'
|
||||
pub posix_mode: bool,
|
||||
/// 'vi'
|
||||
pub vi_mode: bool,
|
||||
|
||||
//
|
||||
// Options set through shopt.
|
||||
/// `assoc_expand_once`
|
||||
pub assoc_expand_once: bool,
|
||||
/// 'autocd'
|
||||
pub auto_cd: bool,
|
||||
/// `cdable_vars`
|
||||
pub cdable_vars: bool,
|
||||
/// 'cdspell'
|
||||
pub cd_autocorrect_spelling: bool,
|
||||
/// 'checkhash'
|
||||
pub check_hashtable_before_command_exec: bool,
|
||||
/// 'checkjobs'
|
||||
pub check_jobs_before_exit: bool,
|
||||
/// 'checkwinsize'
|
||||
pub check_window_size_after_external_commands: bool,
|
||||
/// 'cmdhist'
|
||||
pub save_multiline_cmds_in_history: bool,
|
||||
/// 'compat31'
|
||||
pub compat31: bool,
|
||||
/// 'compat32'
|
||||
pub compat32: bool,
|
||||
/// 'compat40'
|
||||
pub compat40: bool,
|
||||
/// 'compat41'
|
||||
pub compat41: bool,
|
||||
/// 'compat42'
|
||||
pub compat42: bool,
|
||||
/// 'compat43'
|
||||
pub compat43: bool,
|
||||
/// 'compat44'
|
||||
pub compat44: bool,
|
||||
/// `complete_fullquote`
|
||||
pub quote_all_metachars_in_completion: bool,
|
||||
/// 'direxpand'
|
||||
pub expand_dir_names_on_completion: bool,
|
||||
/// 'dirspell'
|
||||
pub autocorrect_dir_spelling_on_completion: bool,
|
||||
/// 'dotglob'
|
||||
pub glob_matches_dotfiles: bool,
|
||||
/// 'execfail'
|
||||
pub exit_on_exec_fail: bool,
|
||||
/// `expand_aliases`
|
||||
pub expand_aliases: bool,
|
||||
/// 'extdebug'
|
||||
pub enable_debugger: bool,
|
||||
/// 'extglob'
|
||||
pub extended_globbing: bool,
|
||||
/// 'extquote'
|
||||
pub extquote: bool,
|
||||
/// 'failglob'
|
||||
pub fail_expansion_on_globs_without_match: bool,
|
||||
/// `force_fignore`
|
||||
pub force_fignore: bool,
|
||||
/// 'globasciiranges'
|
||||
pub glob_ranges_use_c_locale: bool,
|
||||
/// 'globstar'
|
||||
pub enable_star_star_glob: bool,
|
||||
/// `gnu_errfmt`
|
||||
pub errors_in_gnu_format: bool,
|
||||
/// 'histappend'
|
||||
pub append_to_history_file: bool,
|
||||
/// 'histreedit'
|
||||
pub allow_reedit_failed_history_subst: bool,
|
||||
/// 'histverify'
|
||||
pub allow_modifying_history_substitution: bool,
|
||||
/// 'hostcomplete'
|
||||
pub enable_hostname_completion: bool,
|
||||
/// 'huponexit'
|
||||
pub send_sighup_to_all_jobs_on_exit: bool,
|
||||
/// `inherit_errexit`
|
||||
pub command_subst_inherits_errexit: bool,
|
||||
/// `interactive_comments`
|
||||
pub interactive_comments: bool,
|
||||
/// 'lastpipe'
|
||||
pub run_last_pipeline_cmd_in_current_shell: bool,
|
||||
/// 'lithist'
|
||||
pub embed_newlines_in_multiline_cmds_in_history: bool,
|
||||
/// `localvar_inherit`
|
||||
pub local_vars_inherit_value_and_attrs: bool,
|
||||
/// `localvar_unset`
|
||||
pub localvar_unset: bool,
|
||||
/// `login_shell`
|
||||
pub login_shell: bool,
|
||||
/// 'mailwarn'
|
||||
pub mail_warn: bool,
|
||||
/// `no_empty_cmd_completion`
|
||||
pub no_empty_cmd_completion: bool,
|
||||
/// 'nocaseglob'
|
||||
pub case_insensitive_pathname_expansion: bool,
|
||||
/// 'nocasematch'
|
||||
pub case_insensitive_conditionals: bool,
|
||||
/// 'nullglob'
|
||||
pub expand_non_matching_patterns_to_null: bool,
|
||||
/// 'progcomp'
|
||||
pub programmable_completion: bool,
|
||||
/// `progcomp_alias`
|
||||
pub programmable_completion_alias: bool,
|
||||
/// 'promptvars'
|
||||
pub expand_prompt_strings: bool,
|
||||
/// `restricted_shell`
|
||||
pub restricted_shell: bool,
|
||||
/// `shift_verbose`
|
||||
pub shift_verbose: bool,
|
||||
/// `sourcepath`
|
||||
pub source_builtin_searches_path: bool,
|
||||
/// `xpg_echo`
|
||||
pub echo_builtin_expands_escape_sequences: bool,
|
||||
|
||||
//
|
||||
// Options set by the shell.
|
||||
/// Whether or not the shell is interactive.
|
||||
pub interactive: bool,
|
||||
/// Whether or not the shell is reading commands from standard input.
|
||||
pub read_commands_from_stdin: bool,
|
||||
/// Whether or not the shell is in maximal `sh` compatibility mode.
|
||||
pub sh_mode: bool,
|
||||
/// Maximum function call depth.
|
||||
pub max_function_call_depth: Option<usize>,
|
||||
}
|
||||
|
||||
impl RuntimeOptions {
|
||||
/// Creates a default set of runtime options based on the given creation options.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `create_options` - The options used to create the shell.
|
||||
pub fn defaults_from(create_options: &CreateOptions) -> Self {
|
||||
// There's a set of options enabled by default for all shells.
|
||||
let mut options = Self {
|
||||
interactive: create_options.interactive,
|
||||
disallow_overwriting_regular_files_via_output_redirection: create_options
|
||||
.disallow_overwriting_regular_files_via_output_redirection,
|
||||
do_not_execute_commands: create_options.do_not_execute_commands,
|
||||
enable_command_history: create_options.interactive,
|
||||
enable_job_control: create_options.interactive,
|
||||
exit_after_one_command: create_options.exit_after_one_command,
|
||||
read_commands_from_stdin: create_options.read_commands_from_stdin,
|
||||
sh_mode: create_options.sh_mode,
|
||||
posix_mode: create_options.posix,
|
||||
print_commands_and_arguments: create_options.print_commands_and_arguments,
|
||||
print_shell_input_lines: create_options.verbose,
|
||||
remember_command_locations: true,
|
||||
check_window_size_after_external_commands: true,
|
||||
save_multiline_cmds_in_history: true,
|
||||
extquote: true,
|
||||
force_fignore: true,
|
||||
enable_hostname_completion: true,
|
||||
interactive_comments: true,
|
||||
expand_prompt_strings: true,
|
||||
source_builtin_searches_path: true,
|
||||
perform_brace_expansion: true,
|
||||
quote_all_metachars_in_completion: true,
|
||||
programmable_completion: true,
|
||||
glob_ranges_use_c_locale: true,
|
||||
max_function_call_depth: create_options.max_function_call_depth,
|
||||
..Self::default()
|
||||
};
|
||||
|
||||
// Additional options are enabled by default for interactive shells.
|
||||
if create_options.interactive {
|
||||
options.enable_bang_style_history_substitution = true;
|
||||
options.emacs_mode = !create_options.no_editing;
|
||||
options.expand_aliases = true;
|
||||
}
|
||||
|
||||
// Update any options.
|
||||
for enabled_option in &create_options.enabled_options {
|
||||
if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
|
||||
.get(enabled_option.as_str())
|
||||
{
|
||||
option.set(&mut options, true);
|
||||
}
|
||||
}
|
||||
for disabled_option in &create_options.disabled_options {
|
||||
if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
|
||||
.get(disabled_option.as_str())
|
||||
{
|
||||
option.set(&mut options, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update any shopt options.
|
||||
for enabled_option in &create_options.enabled_shopt_options {
|
||||
if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
|
||||
.get(enabled_option.as_str())
|
||||
{
|
||||
shopt_option.set(&mut options, true);
|
||||
}
|
||||
}
|
||||
for disabled_option in &create_options.disabled_shopt_options {
|
||||
if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
|
||||
.get(disabled_option.as_str())
|
||||
{
|
||||
shopt_option.set(&mut options, false);
|
||||
}
|
||||
}
|
||||
|
||||
options
|
||||
}
|
||||
|
||||
/// Returns a string representing the current `set`-style option flags set in the shell.
|
||||
pub fn option_flags(&self) -> String {
|
||||
let mut cs = vec![];
|
||||
|
||||
for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
|
||||
if o.definition.get(self) {
|
||||
cs.push(o.name.chars().next().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the flags in a way that matches what bash does.
|
||||
cs.sort_by(|a, b| {
|
||||
if a == b {
|
||||
std::cmp::Ordering::Equal
|
||||
} else if *a == 's' {
|
||||
std::cmp::Ordering::Greater
|
||||
} else if *b == 's' {
|
||||
std::cmp::Ordering::Less
|
||||
} else if a.is_ascii_lowercase() && b.is_ascii_uppercase() {
|
||||
std::cmp::Ordering::Less
|
||||
} else if a.is_ascii_uppercase() && b.is_ascii_lowercase() {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
a.cmp(b)
|
||||
}
|
||||
});
|
||||
|
||||
cs.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Returns a colon-separated list of sorted 'set -o' options enabled.
|
||||
pub fn seto_optstr(&self) -> String {
|
||||
let mut cs = vec![];
|
||||
|
||||
for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
|
||||
if option.definition.get(self) {
|
||||
cs.push(option.name);
|
||||
}
|
||||
}
|
||||
|
||||
cs.sort_unstable();
|
||||
cs.into_iter().join(":")
|
||||
}
|
||||
|
||||
/// Returns a colon-separated list of sorted 'shopt' options enabled.
|
||||
pub fn shopt_optstr(&self) -> String {
|
||||
let mut cs = vec![];
|
||||
|
||||
for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter() {
|
||||
if option.definition.get(self) {
|
||||
cs.push(option.name);
|
||||
}
|
||||
}
|
||||
|
||||
cs.sort_unstable();
|
||||
cs.into_iter().join(":")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! Path cache
|
||||
|
||||
use crate::{error, variables};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A cache of paths associated with names.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PathCache {
|
||||
/// The cache itself.
|
||||
cache: std::collections::HashMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
impl PathCache {
|
||||
/// Clears all elements from the cache.
|
||||
pub fn reset(&mut self) {
|
||||
self.cache.clear();
|
||||
}
|
||||
|
||||
/// Returns the path associated with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name to lookup.
|
||||
pub fn get<S: AsRef<str>>(&self, name: S) -> Option<PathBuf> {
|
||||
self.cache.get(name.as_ref()).cloned()
|
||||
}
|
||||
|
||||
/// Sets the path associated with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name to set.
|
||||
/// * `path` - The path to associate with the name.
|
||||
pub fn set<S: AsRef<str>>(&mut self, name: S, path: PathBuf) {
|
||||
self.cache.insert(name.as_ref().to_string(), path);
|
||||
}
|
||||
|
||||
/// Projects the cache into a shell value.
|
||||
pub fn to_value(&self) -> Result<variables::ShellValue, error::Error> {
|
||||
let pairs = self
|
||||
.cache
|
||||
.iter()
|
||||
.map(|(k, v)| (Some(k.to_owned()), v.to_string_lossy().to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
variables::ShellValue::associative_array_from_literals(variables::ArrayLiteral(pairs))
|
||||
}
|
||||
|
||||
/// Removes the path associated with the given name, if there is one.
|
||||
/// Returns whether or not an entry was removed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name to remove.
|
||||
pub fn unset<S: AsRef<str>>(&mut self, name: S) -> bool {
|
||||
self.cache.remove(name.as_ref()).is_some()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Path searching utilities.
|
||||
|
||||
use std::{collections::VecDeque, path::PathBuf};
|
||||
|
||||
use crate::sys::fs::PathExt;
|
||||
|
||||
/// Encapsulates the result of a path search.
|
||||
pub struct ExecutablePathSearch<PI, N>
|
||||
where
|
||||
PI: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
paths: VecDeque<PI>,
|
||||
filename: N,
|
||||
}
|
||||
|
||||
impl<PI, N> Iterator for ExecutablePathSearch<PI, N>
|
||||
where
|
||||
PI: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
type Item = PathBuf;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(path) = self.paths.pop_front() {
|
||||
let path = PathBuf::from(path.as_ref()).join(self.filename.as_ref());
|
||||
if path.is_file() && path.as_path().executable() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ExecutablePathPrefixSearch<PI>
|
||||
where
|
||||
PI: AsRef<str>,
|
||||
{
|
||||
paths: VecDeque<PI>,
|
||||
queued_items: VecDeque<PathBuf>,
|
||||
filename_prefix: String,
|
||||
case_insensitive: bool,
|
||||
}
|
||||
|
||||
impl<PI> Iterator for ExecutablePathPrefixSearch<PI>
|
||||
where
|
||||
PI: AsRef<str>,
|
||||
{
|
||||
type Item = PathBuf;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// If we already found some items and queued them, then yield one now.
|
||||
if let Some(item) = self.queued_items.pop_front() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
while let Some(path) = self.paths.pop_front() {
|
||||
let path = PathBuf::from(path.as_ref());
|
||||
|
||||
if let Ok(readdir) = path.read_dir() {
|
||||
for entry in readdir.flatten() {
|
||||
if let Ok(mut filename) = entry.file_name().into_string() {
|
||||
if self.case_insensitive {
|
||||
filename = filename.to_ascii_lowercase();
|
||||
}
|
||||
|
||||
if !filename.starts_with(&self.filename_prefix) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let entry_path = entry.path();
|
||||
|
||||
if let Ok(file_type) = entry.file_type() {
|
||||
if file_type.is_file() && entry_path.executable() {
|
||||
self.queued_items.push_back(entry_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(item) = self.queued_items.pop_front() {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Search for the given executable name in the provided paths.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `paths` - An iterator over the paths to search.
|
||||
/// * `filename` - The name of the executable file to search for.
|
||||
pub fn search_for_executable<P, PI, N>(paths: P, filename: N) -> ExecutablePathSearch<PI, N>
|
||||
where
|
||||
P: Iterator<Item = PI>,
|
||||
PI: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
ExecutablePathSearch {
|
||||
paths: paths.collect(),
|
||||
filename,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_for_executable_with_prefix<P, PI>(
|
||||
paths: P,
|
||||
filename_prefix: &str,
|
||||
case_insensitive: bool,
|
||||
) -> ExecutablePathPrefixSearch<PI>
|
||||
where
|
||||
P: Iterator<Item = PI>,
|
||||
PI: AsRef<str>,
|
||||
{
|
||||
let stored_prefix = if case_insensitive {
|
||||
filename_prefix.to_ascii_lowercase()
|
||||
} else {
|
||||
filename_prefix.into()
|
||||
};
|
||||
|
||||
ExecutablePathPrefixSearch {
|
||||
paths: paths.collect(),
|
||||
queued_items: VecDeque::new(),
|
||||
filename_prefix: stored_prefix,
|
||||
case_insensitive,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
//! Shell patterns
|
||||
|
||||
use crate::{error, regex, trace_categories};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Represents a piece of a shell pattern.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum PatternPiece {
|
||||
/// A pattern that should be interpreted as a shell pattern.
|
||||
Pattern(String),
|
||||
/// A literal string that should be matched exactly.
|
||||
Literal(String),
|
||||
}
|
||||
|
||||
impl PatternPiece {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Pattern(s) => s,
|
||||
Self::Literal(s) => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PatternWord = Vec<PatternPiece>;
|
||||
|
||||
/// Options for filename expansion.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct FilenameExpansionOptions {
|
||||
pub require_dot_in_pattern_to_match_dot_files: bool,
|
||||
}
|
||||
|
||||
/// Encapsulates a shell pattern.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Pattern {
|
||||
pieces: PatternWord,
|
||||
enable_extended_globbing: bool,
|
||||
multiline: bool,
|
||||
case_insensitive: bool,
|
||||
}
|
||||
|
||||
impl Default for Pattern {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pieces: vec![],
|
||||
enable_extended_globbing: false,
|
||||
multiline: true,
|
||||
case_insensitive: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatternWord> for Pattern {
|
||||
fn from(pieces: PatternWord) -> Self {
|
||||
Self {
|
||||
pieces,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PatternWord> for Pattern {
|
||||
fn from(value: &PatternWord) -> Self {
|
||||
Self {
|
||||
pieces: value.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Pattern {
|
||||
fn from(value: &str) -> Self {
|
||||
Self {
|
||||
pieces: vec![PatternPiece::Pattern(value.to_owned())],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Pattern {
|
||||
fn from(value: String) -> Self {
|
||||
Self {
|
||||
pieces: vec![PatternPiece::Pattern(value)],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
/// Enables (or disables) extended globbing support for this pattern.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - Whether or not to enable extended globbing (extglob).
|
||||
#[must_use]
|
||||
pub const fn set_extended_globbing(mut self, value: bool) -> Self {
|
||||
self.enable_extended_globbing = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables (or disables) multiline support for this pattern.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - Whether or not to enable multiline matching.
|
||||
#[must_use]
|
||||
pub const fn set_multiline(mut self, value: bool) -> Self {
|
||||
self.multiline = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables (or disables) case-insensitive matching for this pattern.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - Whether or not to enable case-insensitive matching.
|
||||
#[must_use]
|
||||
pub const fn set_case_insensitive(mut self, value: bool) -> Self {
|
||||
self.case_insensitive = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether or not the pattern is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pieces.iter().all(|p| p.as_str().is_empty())
|
||||
}
|
||||
|
||||
/// Placeholder function that always returns true.
|
||||
pub(crate) const fn accept_all_expand_filter(_path: &Path) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Expands the pattern into a list of matching file paths.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `working_dir` - The current working directory, used for relative paths.
|
||||
/// * `path_filter` - Optionally provides a function that filters paths after expansion.
|
||||
#[expect(clippy::too_many_lines)]
|
||||
#[allow(clippy::unwrap_in_result)]
|
||||
pub(crate) fn expand<PF>(
|
||||
&self,
|
||||
working_dir: &Path,
|
||||
path_filter: Option<&PF>,
|
||||
options: &FilenameExpansionOptions,
|
||||
) -> Result<Vec<String>, error::Error>
|
||||
where
|
||||
PF: Fn(&Path) -> bool,
|
||||
{
|
||||
// If the pattern is completely empty, then short-circuit the function; there's
|
||||
// no reason to proceed onward when we know there's no expansions.
|
||||
if self.is_empty() {
|
||||
return Ok(vec![]);
|
||||
|
||||
// Similarly, if we're *confident* the pattern doesn't require expansion, then we
|
||||
// know there's a single expansion (before filtering).
|
||||
} else if !self.pieces.iter().any(|piece| {
|
||||
matches!(piece, PatternPiece::Pattern(_)) && requires_expansion(piece.as_str())
|
||||
}) {
|
||||
let concatenated: String = self.pieces.iter().map(|piece| piece.as_str()).collect();
|
||||
|
||||
if let Some(filter) = path_filter {
|
||||
if !filter(Path::new(&concatenated)) {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(vec![concatenated]);
|
||||
}
|
||||
|
||||
tracing::debug!(target: trace_categories::PATTERN, "expanding pattern: {self:?}");
|
||||
|
||||
let mut components: Vec<PatternWord> = vec![];
|
||||
for piece in &self.pieces {
|
||||
let mut split_result = piece
|
||||
.as_str()
|
||||
.split(std::path::MAIN_SEPARATOR)
|
||||
.map(|s| match piece {
|
||||
PatternPiece::Pattern(_) => PatternPiece::Pattern(s.to_owned()),
|
||||
PatternPiece::Literal(_) => PatternPiece::Literal(s.to_owned()),
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
if let Some(first_piece) = split_result.pop_front() {
|
||||
if let Some(last_component) = components.last_mut() {
|
||||
last_component.push(first_piece);
|
||||
} else {
|
||||
components.push(vec![first_piece]);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(piece) = split_result.pop_front() {
|
||||
components.push(vec![piece]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the path appears to be absolute.
|
||||
let is_absolute = if let Some(first_component) = components.first() {
|
||||
first_component
|
||||
.iter()
|
||||
.all(|piece| piece.as_str().is_empty())
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let prefix_to_remove;
|
||||
let mut paths_so_far = if is_absolute {
|
||||
prefix_to_remove = None;
|
||||
// TODO: Figure out appropriate thing to do on non-Unix platforms.
|
||||
vec![PathBuf::from(std::path::MAIN_SEPARATOR_STR)]
|
||||
} else {
|
||||
let mut working_dir_str = working_dir.to_string_lossy().to_string();
|
||||
|
||||
if !working_dir_str.ends_with(std::path::MAIN_SEPARATOR) {
|
||||
working_dir_str.push(std::path::MAIN_SEPARATOR);
|
||||
}
|
||||
|
||||
prefix_to_remove = Some(working_dir_str);
|
||||
vec![working_dir.to_path_buf()]
|
||||
};
|
||||
|
||||
for component in components {
|
||||
if !component.iter().any(|piece| {
|
||||
matches!(piece, PatternPiece::Pattern(_)) && requires_expansion(piece.as_str())
|
||||
}) {
|
||||
for p in &mut paths_so_far {
|
||||
let flattened = component
|
||||
.iter()
|
||||
.map(|piece| piece.as_str())
|
||||
.collect::<String>();
|
||||
p.push(flattened);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let current_paths = std::mem::take(&mut paths_so_far);
|
||||
for current_path in current_paths {
|
||||
let subpattern = Self::from(&component)
|
||||
.set_extended_globbing(self.enable_extended_globbing)
|
||||
.set_case_insensitive(self.case_insensitive);
|
||||
|
||||
let subpattern_starts_with_dot = subpattern
|
||||
.pieces
|
||||
.first()
|
||||
.is_some_and(|piece| piece.as_str().starts_with('.'));
|
||||
|
||||
let allow_dot_files = !options.require_dot_in_pattern_to_match_dot_files
|
||||
|| subpattern_starts_with_dot;
|
||||
|
||||
let matches_dotfile_policy = |dir_entry: &std::fs::DirEntry| {
|
||||
!dir_entry.file_name().to_string_lossy().starts_with('.') || allow_dot_files
|
||||
};
|
||||
|
||||
let regex = subpattern.to_regex(true, true)?;
|
||||
let matches_regex = |dir_entry: &std::fs::DirEntry| {
|
||||
regex
|
||||
.is_match(dir_entry.file_name().to_string_lossy().as_ref())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
let mut matching_paths_in_dir: Vec<_> = current_path
|
||||
.read_dir()
|
||||
.map_or_else(|_| vec![], |dir| dir.into_iter().collect())
|
||||
.into_iter()
|
||||
.filter_map(|result| result.ok())
|
||||
.filter(matches_regex)
|
||||
.filter(matches_dotfile_policy)
|
||||
.map(|entry| entry.path())
|
||||
.collect();
|
||||
|
||||
matching_paths_in_dir.sort();
|
||||
|
||||
paths_so_far.append(&mut matching_paths_in_dir);
|
||||
}
|
||||
}
|
||||
|
||||
let results: Vec<_> = paths_so_far
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
if let Some(filter) = path_filter {
|
||||
if !filter(path.as_path()) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let path_str = path.to_string_lossy();
|
||||
let mut path_ref = path_str.as_ref();
|
||||
|
||||
if let Some(prefix_to_remove) = &prefix_to_remove {
|
||||
path_ref = path_ref.strip_prefix(prefix_to_remove).unwrap();
|
||||
}
|
||||
|
||||
Some(path_ref.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
tracing::debug!(target: trace_categories::PATTERN, " => results: {results:?}");
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Converts the pattern to a regular expression string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `strict_prefix_match` - Whether or not the pattern should strictly match the beginning of
|
||||
/// the string.
|
||||
/// * `strict_suffix_match` - Whether or not the pattern should strictly match the end of the
|
||||
/// string.
|
||||
pub(crate) fn to_regex_str(
|
||||
&self,
|
||||
strict_prefix_match: bool,
|
||||
strict_suffix_match: bool,
|
||||
) -> Result<String, error::Error> {
|
||||
let mut regex_str = String::new();
|
||||
|
||||
if strict_prefix_match {
|
||||
regex_str.push('^');
|
||||
}
|
||||
|
||||
let mut current_pattern = String::new();
|
||||
for piece in &self.pieces {
|
||||
match piece {
|
||||
PatternPiece::Pattern(s) => {
|
||||
current_pattern.push_str(s);
|
||||
}
|
||||
PatternPiece::Literal(s) => {
|
||||
for c in s.chars() {
|
||||
current_pattern.push('\\');
|
||||
current_pattern.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let regex_piece =
|
||||
pattern_to_regex_str(current_pattern.as_str(), self.enable_extended_globbing)?;
|
||||
regex_str.push_str(regex_piece.as_str());
|
||||
|
||||
if strict_suffix_match {
|
||||
regex_str.push('$');
|
||||
}
|
||||
|
||||
Ok(regex_str)
|
||||
}
|
||||
|
||||
/// Converts the pattern to a regular expression.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `strict_prefix_match` - Whether or not the pattern should strictly match the beginning of
|
||||
/// the string.
|
||||
/// * `strict_suffix_match` - Whether or not the pattern should strictly match the end of the
|
||||
/// string.
|
||||
pub(crate) fn to_regex(
|
||||
&self,
|
||||
strict_prefix_match: bool,
|
||||
strict_suffix_match: bool,
|
||||
) -> Result<fancy_regex::Regex, error::Error> {
|
||||
let regex_str = self.to_regex_str(strict_prefix_match, strict_suffix_match)?;
|
||||
|
||||
tracing::debug!(target: trace_categories::PATTERN, "pattern: '{self:?}' => regex: '{regex_str}'");
|
||||
|
||||
let re = regex::compile_regex(regex_str, self.case_insensitive, self.multiline)?;
|
||||
Ok(re)
|
||||
}
|
||||
|
||||
/// Checks if the pattern exactly matches the given string. An error result
|
||||
/// is returned if the pattern is found to be invalid or malformed
|
||||
/// during processing.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The string to check for a match.
|
||||
pub fn exactly_matches(&self, value: &str) -> Result<bool, error::Error> {
|
||||
let re = self.to_regex(true, true)?;
|
||||
Ok(re.is_match(value)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_expansion(s: &str) -> bool {
|
||||
// TODO: Make this more accurate.
|
||||
s.contains(['*', '?', '[', ']', '(', ')'])
|
||||
}
|
||||
|
||||
fn pattern_to_regex_str(
|
||||
pattern: &str,
|
||||
enable_extended_globbing: bool,
|
||||
) -> Result<String, error::Error> {
|
||||
Ok(brush_parser::pattern::pattern_to_regex_str(
|
||||
pattern,
|
||||
enable_extended_globbing,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Removes the largest matching prefix from a string that matches the given pattern.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to remove the prefix from.
|
||||
/// * `pattern` - The pattern to match.
|
||||
#[expect(clippy::ref_option)]
|
||||
pub(crate) fn remove_largest_matching_prefix<'a>(
|
||||
s: &'a str,
|
||||
pattern: &Option<Pattern>,
|
||||
) -> Result<&'a str, error::Error> {
|
||||
if let Some(pattern) = pattern {
|
||||
let indices = s.char_indices().rev();
|
||||
let mut last_idx = s.len();
|
||||
|
||||
#[allow(
|
||||
clippy::string_slice,
|
||||
reason = "because we get the indices from char_indices()"
|
||||
)]
|
||||
for (idx, _) in indices {
|
||||
let prefix = &s[0..last_idx];
|
||||
if pattern.exactly_matches(prefix)? {
|
||||
return Ok(&s[last_idx..]);
|
||||
}
|
||||
|
||||
last_idx = idx;
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Removes the smallest matching prefix from a string that matches the given pattern.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to remove the prefix from.
|
||||
/// * `pattern` - The pattern to match.
|
||||
#[expect(clippy::ref_option)]
|
||||
pub(crate) fn remove_smallest_matching_prefix<'a>(
|
||||
s: &'a str,
|
||||
pattern: &Option<Pattern>,
|
||||
) -> Result<&'a str, error::Error> {
|
||||
if let Some(pattern) = pattern {
|
||||
let mut indices = s.char_indices();
|
||||
|
||||
#[allow(
|
||||
clippy::string_slice,
|
||||
reason = "because we get the indices from char_indices()"
|
||||
)]
|
||||
while indices.next().is_some() {
|
||||
let next_index = indices.offset();
|
||||
let prefix = &s[0..next_index];
|
||||
if pattern.exactly_matches(prefix)? {
|
||||
return Ok(&s[next_index..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Removes the largest matching suffix from a string that matches the given pattern.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to remove the suffix from.
|
||||
/// * `pattern` - The pattern to match.
|
||||
#[expect(clippy::ref_option)]
|
||||
pub(crate) fn remove_largest_matching_suffix<'a>(
|
||||
s: &'a str,
|
||||
pattern: &Option<Pattern>,
|
||||
) -> Result<&'a str, error::Error> {
|
||||
if let Some(pattern) = pattern {
|
||||
#[allow(
|
||||
clippy::string_slice,
|
||||
reason = "because we get the indices from char_indices()"
|
||||
)]
|
||||
for (idx, _) in s.char_indices() {
|
||||
let suffix = &s[idx..];
|
||||
if pattern.exactly_matches(suffix)? {
|
||||
return Ok(&s[..idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Removes the smallest matching suffix from a string that matches the given pattern.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to remove the suffix from.
|
||||
/// * `pattern` - The pattern to match.
|
||||
#[expect(clippy::ref_option)]
|
||||
pub(crate) fn remove_smallest_matching_suffix<'a>(
|
||||
s: &'a str,
|
||||
pattern: &Option<Pattern>,
|
||||
) -> Result<&'a str, error::Error> {
|
||||
if let Some(pattern) = pattern {
|
||||
#[allow(
|
||||
clippy::string_slice,
|
||||
reason = "because we get the indices from char_indices()"
|
||||
)]
|
||||
for (idx, _) in s.char_indices().rev() {
|
||||
let suffix = &s[idx..];
|
||||
if pattern.exactly_matches(suffix)? {
|
||||
return Ok(&s[..idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[expect(clippy::panic_in_result_fn)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
|
||||
fn pattern_to_exact_regex_str<P>(pattern: P) -> Result<String, error::Error>
|
||||
where
|
||||
P: Into<Pattern>,
|
||||
{
|
||||
let pattern: Pattern = pattern
|
||||
.into()
|
||||
.set_extended_globbing(true)
|
||||
.set_multiline(false);
|
||||
|
||||
pattern.to_regex_str(true, true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_translation() -> Result<()> {
|
||||
assert_eq!(pattern_to_exact_regex_str("a")?.as_str(), "^a$");
|
||||
assert_eq!(pattern_to_exact_regex_str("a*")?.as_str(), "^a.*$");
|
||||
assert_eq!(pattern_to_exact_regex_str("a?")?.as_str(), "^a.$");
|
||||
assert_eq!(pattern_to_exact_regex_str("a@(b|c)")?.as_str(), "^a(b|c)$");
|
||||
assert_eq!(pattern_to_exact_regex_str("a?(b|c)")?.as_str(), "^a(b|c)?$");
|
||||
assert_eq!(
|
||||
pattern_to_exact_regex_str("a*(ab|ac)")?.as_str(),
|
||||
"^a(ab|ac)*$"
|
||||
);
|
||||
assert_eq!(
|
||||
pattern_to_exact_regex_str("a+(ab|ac)")?.as_str(),
|
||||
"^a(ab|ac)+$"
|
||||
);
|
||||
assert_eq!(pattern_to_exact_regex_str("[ab]")?.as_str(), "^[ab]$");
|
||||
assert_eq!(pattern_to_exact_regex_str("[ab]*")?.as_str(), "^[ab].*$");
|
||||
assert_eq!(
|
||||
pattern_to_exact_regex_str("[<{().[]*")?.as_str(),
|
||||
r"^[<{().\[].*$"
|
||||
);
|
||||
assert_eq!(pattern_to_exact_regex_str("[a-d]")?.as_str(), "^[a-d]$");
|
||||
assert_eq!(pattern_to_exact_regex_str(r"\*")?.as_str(), r"^\*$");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_word_translation() -> Result<()> {
|
||||
assert_eq!(
|
||||
pattern_to_exact_regex_str(vec![PatternPiece::Pattern("a*".to_owned())])?.as_str(),
|
||||
"^a.*$"
|
||||
);
|
||||
assert_eq!(
|
||||
pattern_to_exact_regex_str(vec![
|
||||
PatternPiece::Pattern("a*".to_owned()),
|
||||
PatternPiece::Literal("b".to_owned()),
|
||||
])?
|
||||
.as_str(),
|
||||
"^a.*b$"
|
||||
);
|
||||
assert_eq!(
|
||||
pattern_to_exact_regex_str(vec![
|
||||
PatternPiece::Literal("a*".to_owned()),
|
||||
PatternPiece::Pattern("b".to_owned()),
|
||||
])?
|
||||
.as_str(),
|
||||
r"^a\*b$"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_largest_matching_prefix() -> Result<()> {
|
||||
assert_eq!(
|
||||
remove_largest_matching_prefix("ooof", &Some(Pattern::from("")))?,
|
||||
"ooof"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_prefix("ooof", &Some(Pattern::from("x")))?,
|
||||
"ooof"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_prefix("ooof", &Some(Pattern::from("o")))?,
|
||||
"oof"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_prefix("ooof", &Some(Pattern::from("o*o")))?,
|
||||
"f"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_prefix("ooof", &Some(Pattern::from("o*")))?,
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_prefix("🚀🚀🚀rocket", &Some(Pattern::from("🚀")))?,
|
||||
"🚀🚀rocket"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_smallest_matching_prefix() -> Result<()> {
|
||||
assert_eq!(
|
||||
remove_smallest_matching_prefix("ooof", &Some(Pattern::from("")))?,
|
||||
"ooof"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_prefix("ooof", &Some(Pattern::from("x")))?,
|
||||
"ooof"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_prefix("ooof", &Some(Pattern::from("o")))?,
|
||||
"oof"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_prefix("ooof", &Some(Pattern::from("o*o")))?,
|
||||
"of"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_prefix("ooof", &Some(Pattern::from("o*")))?,
|
||||
"oof"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_prefix("ooof", &Some(Pattern::from("ooof")))?,
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_prefix("🚀🚀🚀rocket", &Some(Pattern::from("🚀")))?,
|
||||
"🚀🚀rocket"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_largest_matching_suffix() -> Result<()> {
|
||||
assert_eq!(
|
||||
remove_largest_matching_suffix("foo", &Some(Pattern::from("")))?,
|
||||
"foo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_suffix("foo", &Some(Pattern::from("x")))?,
|
||||
"foo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_suffix("foo", &Some(Pattern::from("o")))?,
|
||||
"fo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_suffix("foo", &Some(Pattern::from("o*")))?,
|
||||
"f"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_suffix("foo", &Some(Pattern::from("foo")))?,
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
remove_largest_matching_suffix("rocket🚀🚀🚀", &Some(Pattern::from("🚀")))?,
|
||||
"rocket🚀🚀"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_smallest_matching_suffix() -> Result<()> {
|
||||
assert_eq!(
|
||||
remove_smallest_matching_suffix("fooo", &Some(Pattern::from("")))?,
|
||||
"fooo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_suffix("fooo", &Some(Pattern::from("x")))?,
|
||||
"fooo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_suffix("fooo", &Some(Pattern::from("o")))?,
|
||||
"foo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_suffix("fooo", &Some(Pattern::from("o*o")))?,
|
||||
"fo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_suffix("fooo", &Some(Pattern::from("o*")))?,
|
||||
"foo"
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_suffix("fooo", &Some(Pattern::from("fooo")))?,
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
remove_smallest_matching_suffix("rocket🚀🚀🚀", &Some(Pattern::from("🚀")))?,
|
||||
"rocket🚀🚀"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::cognitive_complexity)]
|
||||
fn test_matching() -> Result<()> {
|
||||
assert!(Pattern::from("abc").exactly_matches("abc")?);
|
||||
|
||||
assert!(!Pattern::from("abc").exactly_matches("ABC")?);
|
||||
assert!(!Pattern::from("abc").exactly_matches("xabcx")?);
|
||||
assert!(!Pattern::from("abc").exactly_matches("")?);
|
||||
assert!(!Pattern::from("abc").exactly_matches("abcd")?);
|
||||
assert!(!Pattern::from("abc").exactly_matches("def")?);
|
||||
|
||||
assert!(Pattern::from("*").exactly_matches("")?);
|
||||
assert!(Pattern::from("*").exactly_matches("abc")?);
|
||||
assert!(Pattern::from("*").exactly_matches(" ")?);
|
||||
|
||||
assert!(Pattern::from("a*").exactly_matches("a")?);
|
||||
assert!(Pattern::from("a*").exactly_matches("ab")?);
|
||||
assert!(Pattern::from("a*").exactly_matches("a ")?);
|
||||
|
||||
assert!(!Pattern::from("a*").exactly_matches("A")?);
|
||||
assert!(!Pattern::from("a*").exactly_matches("")?);
|
||||
assert!(!Pattern::from("a*").exactly_matches("bc")?);
|
||||
assert!(!Pattern::from("a*").exactly_matches("xax")?);
|
||||
assert!(!Pattern::from("a*").exactly_matches(" a")?);
|
||||
|
||||
assert!(Pattern::from("*a").exactly_matches("a")?);
|
||||
assert!(Pattern::from("*a").exactly_matches("ba")?);
|
||||
assert!(Pattern::from("*a").exactly_matches("aa")?);
|
||||
assert!(Pattern::from("*a").exactly_matches(" a")?);
|
||||
|
||||
assert!(!Pattern::from("*a").exactly_matches("BA")?);
|
||||
assert!(!Pattern::from("*a").exactly_matches("")?);
|
||||
assert!(!Pattern::from("*a").exactly_matches("ab")?);
|
||||
assert!(!Pattern::from("*a").exactly_matches("xax")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_extglob(s: &str) -> Pattern {
|
||||
let pattern = Pattern::from(s).set_extended_globbing(true);
|
||||
let regex_str = pattern.to_regex_str(true, true).unwrap();
|
||||
eprintln!("pattern: '{s}' => regex: '{regex_str}'");
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extglob_or_matching() -> Result<()> {
|
||||
assert!(make_extglob("@(a|b)").exactly_matches("a")?);
|
||||
assert!(make_extglob("@(a|b)").exactly_matches("b")?);
|
||||
|
||||
assert!(!make_extglob("@(a|b)").exactly_matches("")?);
|
||||
assert!(!make_extglob("@(a|b)").exactly_matches("c")?);
|
||||
assert!(!make_extglob("@(a|b)").exactly_matches("ab")?);
|
||||
|
||||
assert!(!make_extglob("@(a|b)").exactly_matches("")?);
|
||||
assert!(make_extglob("@(a*b|b)").exactly_matches("ab")?);
|
||||
assert!(make_extglob("@(a*b|b)").exactly_matches("axb")?);
|
||||
assert!(make_extglob("@(a*b|b)").exactly_matches("b")?);
|
||||
|
||||
assert!(!make_extglob("@(a*b|b)").exactly_matches("a")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extglob_not_matching() -> Result<()> {
|
||||
// Basic cases.
|
||||
assert!(make_extglob("!(a)").exactly_matches("")?);
|
||||
assert!(make_extglob("!(a)").exactly_matches(" ")?);
|
||||
assert!(make_extglob("!(a)").exactly_matches("x")?);
|
||||
assert!(make_extglob("!(a)").exactly_matches(" a ")?);
|
||||
assert!(make_extglob("!(a)").exactly_matches("a ")?);
|
||||
assert!(make_extglob("!(a)").exactly_matches("aa")?);
|
||||
assert!(!make_extglob("!(a)").exactly_matches("a")?);
|
||||
|
||||
assert!(make_extglob("a!(a)a").exactly_matches("aa")?);
|
||||
assert!(make_extglob("a!(a)a").exactly_matches("aaaa")?);
|
||||
assert!(make_extglob("a!(a)a").exactly_matches("aba")?);
|
||||
assert!(!make_extglob("a!(a)a").exactly_matches("a")?);
|
||||
assert!(!make_extglob("a!(a)a").exactly_matches("aaa")?);
|
||||
assert!(!make_extglob("a!(a)a").exactly_matches("baaa")?);
|
||||
|
||||
// Alternates.
|
||||
assert!(make_extglob("!(a|b)").exactly_matches("c")?);
|
||||
assert!(make_extglob("!(a|b)").exactly_matches("ab")?);
|
||||
assert!(make_extglob("!(a|b)").exactly_matches("aa")?);
|
||||
assert!(make_extglob("!(a|b)").exactly_matches("bb")?);
|
||||
assert!(!make_extglob("!(a|b)").exactly_matches("a")?);
|
||||
assert!(!make_extglob("!(a|b)").exactly_matches("b")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extglob_advanced_not_matching() -> Result<()> {
|
||||
assert!(make_extglob("!(a*)").exactly_matches("b")?);
|
||||
assert!(make_extglob("!(a*)").exactly_matches("")?);
|
||||
assert!(!make_extglob("!(a*)").exactly_matches("a")?);
|
||||
assert!(!make_extglob("!(a*)").exactly_matches("abc")?);
|
||||
assert!(!make_extglob("!(a*)").exactly_matches("aabc")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extglob_not_degenerate_matching() -> Result<()> {
|
||||
// Degenerate case.
|
||||
assert!(make_extglob("!()").exactly_matches("a")?);
|
||||
assert!(!make_extglob("!()").exactly_matches("")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extglob_zero_or_more_matching() -> Result<()> {
|
||||
assert!(make_extglob("x*(a)x").exactly_matches("xx")?);
|
||||
assert!(make_extglob("x*(a)x").exactly_matches("xax")?);
|
||||
assert!(make_extglob("x*(a)x").exactly_matches("xaax")?);
|
||||
|
||||
assert!(!make_extglob("x*(a)x").exactly_matches("x")?);
|
||||
assert!(!make_extglob("x*(a)x").exactly_matches("xa")?);
|
||||
assert!(!make_extglob("x*(a)x").exactly_matches("xxx")?);
|
||||
|
||||
assert!(make_extglob("*(a|b)").exactly_matches("")?);
|
||||
assert!(make_extglob("*(a|b)").exactly_matches("a")?);
|
||||
assert!(make_extglob("*(a|b)").exactly_matches("b")?);
|
||||
assert!(make_extglob("*(a|b)").exactly_matches("aba")?);
|
||||
assert!(make_extglob("*(a|b)").exactly_matches("aaa")?);
|
||||
|
||||
assert!(!make_extglob("*(a|b)").exactly_matches("c")?);
|
||||
assert!(!make_extglob("*(a|b)").exactly_matches("ca")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extglob_one_or_more_matching() -> Result<()> {
|
||||
fn make_extglob(s: &str) -> Pattern {
|
||||
Pattern::from(s).set_extended_globbing(true)
|
||||
}
|
||||
|
||||
assert!(make_extglob("x+(a)x").exactly_matches("xax")?);
|
||||
assert!(make_extglob("x+(a)x").exactly_matches("xaax")?);
|
||||
|
||||
assert!(!make_extglob("x+(a)x").exactly_matches("xx")?);
|
||||
assert!(!make_extglob("x+(a)x").exactly_matches("x")?);
|
||||
assert!(!make_extglob("x+(a)x").exactly_matches("xa")?);
|
||||
assert!(!make_extglob("x+(a)x").exactly_matches("xxx")?);
|
||||
|
||||
assert!(make_extglob("+(a|b)").exactly_matches("a")?);
|
||||
assert!(make_extglob("+(a|b)").exactly_matches("b")?);
|
||||
assert!(make_extglob("+(a|b)").exactly_matches("aba")?);
|
||||
assert!(make_extglob("+(a|b)").exactly_matches("aaa")?);
|
||||
|
||||
assert!(!make_extglob("+(a|b)").exactly_matches("")?);
|
||||
assert!(!make_extglob("+(a|b)").exactly_matches("c")?);
|
||||
assert!(!make_extglob("+(a|b)").exactly_matches("ca")?);
|
||||
|
||||
assert!(make_extglob("+(x+(ab)y)").exactly_matches("xaby")?);
|
||||
assert!(make_extglob("+(x+(ab)y)").exactly_matches("xababy")?);
|
||||
assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababy")?);
|
||||
assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababyxabababyxabababy")?);
|
||||
|
||||
assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xy")?);
|
||||
assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xay")?);
|
||||
assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xyxy")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Process management
|
||||
|
||||
use futures::FutureExt;
|
||||
|
||||
use crate::{error, sys};
|
||||
|
||||
/// A waitable future that will yield the results of a child process's execution.
|
||||
pub(crate) type WaitableChildProcess = std::pin::Pin<
|
||||
Box<dyn futures::Future<Output = Result<std::process::Output, std::io::Error>> + Send + Sync>,
|
||||
>;
|
||||
|
||||
/// Tracks a child process being awaited.
|
||||
pub struct ChildProcess {
|
||||
/// If available, the process ID of the child.
|
||||
pid: Option<sys::process::ProcessId>,
|
||||
/// A waitable future that will yield the results of a child process's execution.
|
||||
exec_future: WaitableChildProcess,
|
||||
}
|
||||
|
||||
impl ChildProcess {
|
||||
/// Wraps a child process and its future.
|
||||
pub fn new(pid: Option<sys::process::ProcessId>, child: sys::process::Child) -> Self {
|
||||
Self {
|
||||
pid,
|
||||
exec_future: Box::pin(child.wait_with_output()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the process's ID.
|
||||
pub const fn pid(&self) -> Option<sys::process::ProcessId> {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// Waits for the process to exit.
|
||||
pub async fn wait(&mut self) -> Result<ProcessWaitResult, error::Error> {
|
||||
#[allow(unused_mut, reason = "only mutated on some platforms")]
|
||||
let mut sigtstp = sys::signal::tstp_signal_listener()?;
|
||||
#[allow(unused_mut, reason = "only mutated on some platforms")]
|
||||
let mut sigchld = sys::signal::chld_signal_listener()?;
|
||||
|
||||
#[allow(clippy::ignored_unit_patterns)]
|
||||
loop {
|
||||
tokio::select! {
|
||||
output = &mut self.exec_future => {
|
||||
break Ok(ProcessWaitResult::Completed(output?))
|
||||
},
|
||||
_ = sigtstp.recv() => {
|
||||
break Ok(ProcessWaitResult::Stopped)
|
||||
},
|
||||
_ = sigchld.recv() => {
|
||||
if sys::signal::poll_for_stopped_children()? {
|
||||
break Ok(ProcessWaitResult::Stopped);
|
||||
}
|
||||
},
|
||||
_ = sys::signal::await_ctrl_c() => {
|
||||
// SIGINT got thrown. Handle it and continue looping. The child should
|
||||
// have received it as well, and either handled it or ended up getting
|
||||
// terminated (in which case we'll see the child exit).
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn poll(&mut self) -> Option<Result<std::process::Output, error::Error>> {
|
||||
let checkable_future = &mut self.exec_future;
|
||||
checkable_future
|
||||
.now_or_never()
|
||||
.map(|result| result.map_err(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the result of waiting for an executing process.
|
||||
pub enum ProcessWaitResult {
|
||||
/// The process completed.
|
||||
Completed(std::process::Output),
|
||||
/// The process stopped and has not yet completed.
|
||||
Stopped,
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
use crate::{
|
||||
ExecutionParameters, error, expansion,
|
||||
shell::Shell,
|
||||
sys::{self, users},
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
const VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
|
||||
const VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
|
||||
const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
|
||||
|
||||
pub(crate) async fn expand_prompt(
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
spec: String,
|
||||
) -> Result<String, error::Error> {
|
||||
// Parse the prompt spec into its pieces.
|
||||
let prompt_pieces = parse_prompt(spec)?;
|
||||
|
||||
// Now, render each piece.
|
||||
let mut formatted_prompt = String::new();
|
||||
for piece in prompt_pieces {
|
||||
let needs_escaping = matches!(
|
||||
piece,
|
||||
brush_parser::prompt::PromptPiece::EscapedSequence(_)
|
||||
| brush_parser::prompt::PromptPiece::DollarOrPound
|
||||
);
|
||||
|
||||
let formatted_piece = format_prompt_piece(shell, piece)?;
|
||||
|
||||
if shell.options.expand_prompt_strings && needs_escaping {
|
||||
formatted_prompt.push('\\');
|
||||
}
|
||||
|
||||
formatted_prompt.push_str(&formatted_piece);
|
||||
}
|
||||
|
||||
if shell.options.expand_prompt_strings {
|
||||
// Now expand any remaining escape sequences.
|
||||
formatted_prompt = expansion::basic_expand_str(shell, params, &formatted_prompt).await?;
|
||||
}
|
||||
|
||||
Ok(formatted_prompt)
|
||||
}
|
||||
|
||||
#[cached::proc_macro::cached(size = 64, result = true)]
|
||||
fn parse_prompt(
|
||||
spec: String,
|
||||
) -> Result<Vec<brush_parser::prompt::PromptPiece>, brush_parser::WordParseError> {
|
||||
brush_parser::prompt::parse(spec.as_str())
|
||||
}
|
||||
|
||||
fn format_prompt_piece(
|
||||
shell: &Shell,
|
||||
piece: brush_parser::prompt::PromptPiece,
|
||||
) -> Result<String, error::Error> {
|
||||
let formatted = match piece {
|
||||
brush_parser::prompt::PromptPiece::EscapedSequence(s) => s,
|
||||
brush_parser::prompt::PromptPiece::Literal(l) => l,
|
||||
brush_parser::prompt::PromptPiece::AsciiCharacter(c) => {
|
||||
char::from_u32(c).map_or_else(String::new, |c| c.to_string())
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::Backslash => "\\".to_owned(),
|
||||
brush_parser::prompt::PromptPiece::BellCharacter => "\x07".to_owned(),
|
||||
brush_parser::prompt::PromptPiece::CarriageReturn => "\r".to_owned(),
|
||||
brush_parser::prompt::PromptPiece::CurrentCommandNumber => {
|
||||
return error::unimp("prompt: current command number");
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::CurrentHistoryNumber => {
|
||||
return error::unimp("prompt: current history number");
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::CurrentUser => users::get_current_username()?,
|
||||
brush_parser::prompt::PromptPiece::CurrentWorkingDirectory {
|
||||
tilde_replaced,
|
||||
basename,
|
||||
} => format_current_working_directory(shell, tilde_replaced, basename),
|
||||
brush_parser::prompt::PromptPiece::Date(format) => {
|
||||
format_date(&chrono::Local::now(), &format)
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::DollarOrPound => {
|
||||
if users::is_root() {
|
||||
"#".to_owned()
|
||||
} else {
|
||||
"$".to_owned()
|
||||
}
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::EndNonPrintingSequence => String::new(),
|
||||
brush_parser::prompt::PromptPiece::EscapeCharacter => "\x1b".to_owned(),
|
||||
brush_parser::prompt::PromptPiece::Hostname {
|
||||
only_up_to_first_dot,
|
||||
} => {
|
||||
let hn = sys::network::get_hostname()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if only_up_to_first_dot {
|
||||
if let Some((first, _)) = hn.split_once('.') {
|
||||
return Ok(first.to_owned());
|
||||
}
|
||||
}
|
||||
hn
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::Newline => "\n".to_owned(),
|
||||
brush_parser::prompt::PromptPiece::NumberOfManagedJobs => shell.jobs.jobs.len().to_string(),
|
||||
brush_parser::prompt::PromptPiece::ShellBaseName => {
|
||||
if let Some(shell_name) = &shell.shell_name {
|
||||
Path::new(shell_name)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::ShellRelease => {
|
||||
std::format!("{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_PATCH}")
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::ShellVersion => {
|
||||
std::format!("{VERSION_MAJOR}.{VERSION_MINOR}")
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::StartNonPrintingSequence => String::new(),
|
||||
brush_parser::prompt::PromptPiece::TerminalDeviceBaseName => {
|
||||
return error::unimp("prompt: terminal device base name");
|
||||
}
|
||||
brush_parser::prompt::PromptPiece::Time(time_fmt) => {
|
||||
format_time(&chrono::Local::now(), &time_fmt)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
fn format_current_working_directory(shell: &Shell, tilde_replaced: bool, basename: bool) -> String {
|
||||
let mut working_dir_str = shell.working_dir().to_string_lossy().to_string();
|
||||
|
||||
if tilde_replaced {
|
||||
working_dir_str = shell.tilde_shorten(working_dir_str);
|
||||
}
|
||||
|
||||
if basename {
|
||||
if let Some(filename) = Path::new(&working_dir_str).file_name() {
|
||||
working_dir_str = filename.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(windows) {
|
||||
working_dir_str = working_dir_str.replace('\\', "/");
|
||||
}
|
||||
|
||||
working_dir_str
|
||||
}
|
||||
|
||||
fn format_time<Tz: chrono::TimeZone>(
|
||||
datetime: &chrono::DateTime<Tz>,
|
||||
format: &brush_parser::prompt::PromptTimeFormat,
|
||||
) -> String
|
||||
where
|
||||
Tz::Offset: std::fmt::Display,
|
||||
{
|
||||
let formatted = match format {
|
||||
brush_parser::prompt::PromptTimeFormat::TwelveHourAM => datetime.format("%I:%M %p"),
|
||||
brush_parser::prompt::PromptTimeFormat::TwelveHourHHMMSS => datetime.format("%I:%M:%S"),
|
||||
brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMM => datetime.format("%H:%M"),
|
||||
brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMMSS => datetime.format("%H:%M:%S"),
|
||||
};
|
||||
|
||||
formatted.to_string()
|
||||
}
|
||||
|
||||
fn format_date<Tz: chrono::TimeZone>(
|
||||
datetime: &chrono::DateTime<Tz>,
|
||||
format: &brush_parser::prompt::PromptDateFormat,
|
||||
) -> String
|
||||
where
|
||||
Tz::Offset: std::fmt::Display,
|
||||
{
|
||||
match format {
|
||||
brush_parser::prompt::PromptDateFormat::WeekdayMonthDate => {
|
||||
datetime.format("%a %b %d").to_string()
|
||||
}
|
||||
brush_parser::prompt::PromptDateFormat::Custom(fmt) => {
|
||||
let fmt_items = chrono::format::StrftimeItems::new(fmt);
|
||||
datetime.format_with_items(fmt_items).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_time() {
|
||||
// Create a well-known test date/time.
|
||||
let dt = chrono::DateTime::parse_from_rfc3339("2024-12-25T13:34:56.789Z").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
format_time(&dt, &brush_parser::prompt::PromptTimeFormat::TwelveHourAM),
|
||||
"01:34 PM"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_time(
|
||||
&dt,
|
||||
&brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMMSS
|
||||
),
|
||||
"13:34:56"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_time(
|
||||
&dt,
|
||||
&brush_parser::prompt::PromptTimeFormat::TwelveHourHHMMSS
|
||||
),
|
||||
"01:34:56"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_date() {
|
||||
// Create a well-known test date/time.
|
||||
let dt = chrono::DateTime::parse_from_rfc3339("2024-12-25T12:34:56.789Z").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
format_date(
|
||||
&dt,
|
||||
&brush_parser::prompt::PromptDateFormat::WeekdayMonthDate
|
||||
),
|
||||
"Wed Dec 25"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_date(
|
||||
&dt,
|
||||
&brush_parser::prompt::PromptDateFormat::Custom(String::from("%Y-%m-%d"))
|
||||
),
|
||||
"2024-12-25"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_date(
|
||||
&dt,
|
||||
&brush_parser::prompt::PromptDateFormat::Custom(String::from(
|
||||
"%Y-%m-%d %H:%M:%S.%f"
|
||||
))
|
||||
),
|
||||
"2024-12-25 12:34:56.789000000"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Represents a piece of a regular expression.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum RegexPiece {
|
||||
/// A pattern that should be interpreted as a regular expression.
|
||||
Pattern(String),
|
||||
/// A literal string that should be matched exactly.
|
||||
Literal(String),
|
||||
}
|
||||
|
||||
impl RegexPiece {
|
||||
fn to_regex_str(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
Self::Pattern(s) => Cow::Borrowed(s.as_str()),
|
||||
Self::Literal(s) => escape_literal_regex_piece(s.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RegexWord = Vec<RegexPiece>;
|
||||
|
||||
/// Encapsulates a regular expression usable in the shell.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Regex {
|
||||
pieces: RegexWord,
|
||||
case_insensitive: bool,
|
||||
multiline: bool,
|
||||
}
|
||||
|
||||
impl From<RegexWord> for Regex {
|
||||
fn from(pieces: RegexWord) -> Self {
|
||||
Self {
|
||||
pieces,
|
||||
case_insensitive: false,
|
||||
multiline: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Regex {
|
||||
/// Sets the regular expression's case sensitivity.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The new case sensitivity value.
|
||||
pub const fn set_case_insensitive(mut self, value: bool) -> Self {
|
||||
self.case_insensitive = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables (or disables) multiline support for this pattern.
|
||||
/// This enables matching across lines as well as enables `.`
|
||||
/// to match newline characters.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The new multiline value.
|
||||
pub const fn set_multiline(mut self, value: bool) -> Self {
|
||||
self.multiline = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Computes if the regular expression matches the given string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The string to check for a match.
|
||||
pub fn matches(&self, value: &str) -> Result<Option<Vec<Option<String>>>, error::Error> {
|
||||
let regex_pattern: String = self
|
||||
.pieces
|
||||
.iter()
|
||||
.map(|piece| piece.to_regex_str())
|
||||
.collect();
|
||||
|
||||
let re = compile_regex(regex_pattern, self.case_insensitive, self.multiline)?;
|
||||
|
||||
Ok(re.captures(value)?.map(|captures| {
|
||||
captures
|
||||
.iter()
|
||||
.map(|c| c.map(|m| m.as_str().to_owned()))
|
||||
.collect()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cached::proc_macro::cached(size = 64, result = true)]
|
||||
pub(crate) fn compile_regex(
|
||||
regex_str: String,
|
||||
case_insensitive: bool,
|
||||
multiline: bool,
|
||||
) -> Result<fancy_regex::Regex, error::Error> {
|
||||
// Handle identified cases where a shell-supported regex isn't supported directly by
|
||||
// `fancy_regex` -- specifically, adding missing escape characters.
|
||||
let mut regex_str = add_missing_escape_chars_to_regex(regex_str.as_str());
|
||||
|
||||
// Handle multiline enablement.
|
||||
if multiline {
|
||||
// The fancy_regex crate internally seems to have flags that can be used
|
||||
// to enable multiline support, but they're not exposed via its
|
||||
// RegexBuilder. We instead just prefix with the right flags.
|
||||
let updated_str = std::format!("(?ms){regex_str}");
|
||||
regex_str = updated_str.into();
|
||||
}
|
||||
|
||||
let mut builder = fancy_regex::RegexBuilder::new(regex_str.as_ref());
|
||||
builder.case_insensitive(case_insensitive);
|
||||
|
||||
match builder.build() {
|
||||
Ok(re) => Ok(re),
|
||||
Err(e) => Err(error::ErrorKind::InvalidRegexError(e, regex_str.to_string()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_missing_escape_chars_to_regex(s: &str) -> Cow<'_, str> {
|
||||
// We may see a character class with an unescaped '[' (open bracket) character. We need
|
||||
// to escape that character.
|
||||
let mut in_escape = false;
|
||||
let mut in_brackets = false;
|
||||
let mut insertion_positions = vec![];
|
||||
|
||||
let mut peekable = s.char_indices().peekable();
|
||||
while let Some((byte_offset, c)) = peekable.next() {
|
||||
let next_is_colon = peekable.peek().is_some_and(|(_, c)| *c == ':');
|
||||
|
||||
match c {
|
||||
'[' if !in_escape && !in_brackets => {
|
||||
in_brackets = true;
|
||||
}
|
||||
'[' if !in_escape && in_brackets && !next_is_colon => {
|
||||
// Need to escape.
|
||||
insertion_positions.push(byte_offset);
|
||||
}
|
||||
']' if !in_escape && in_brackets => {
|
||||
in_brackets = false;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
in_escape = !in_escape && c == '\\';
|
||||
}
|
||||
|
||||
if insertion_positions.is_empty() {
|
||||
return s.into();
|
||||
}
|
||||
|
||||
let mut updated = s.to_owned();
|
||||
for pos in insertion_positions.iter().rev() {
|
||||
updated.insert(*pos, '\\');
|
||||
}
|
||||
|
||||
updated.into()
|
||||
}
|
||||
|
||||
fn escape_literal_regex_piece(s: &str) -> Cow<'_, str> {
|
||||
let mut result = String::new();
|
||||
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
c if regex_char_is_special(c) => {
|
||||
result.push('\\');
|
||||
result.push(c);
|
||||
}
|
||||
c => result.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
result.into()
|
||||
}
|
||||
|
||||
const fn regex_char_is_special(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'\\' | '^' | '$' | '.' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{' | '}'
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add_missing_escape_chars_to_regex() {
|
||||
// Negative cases -- where we don't need to escape.
|
||||
assert_eq!(add_missing_escape_chars_to_regex("a[b]"), "a[b]");
|
||||
assert_eq!(add_missing_escape_chars_to_regex(r"a\[b\]"), r"a\[b\]");
|
||||
assert_eq!(add_missing_escape_chars_to_regex(r"a[b\[]"), r"a[b\[]");
|
||||
|
||||
// Positive case -- where we need to escape.
|
||||
assert_eq!(add_missing_escape_chars_to_regex(r"a[b[]"), r"a[b\[]");
|
||||
assert_eq!(add_missing_escape_chars_to_regex(r"a[[]"), r"a[\[]");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
//! Encapsulation of execution results.
|
||||
|
||||
use crate::{error, processes};
|
||||
|
||||
/// Represents the result of executing a command or similar item.
|
||||
#[derive(Default)]
|
||||
pub struct ExecutionResult {
|
||||
/// The control flow transition to apply after execution.
|
||||
pub next_control_flow: ExecutionControlFlow,
|
||||
/// The exit code resulting from execution.
|
||||
pub exit_code: ExecutionExitCode,
|
||||
}
|
||||
|
||||
impl ExecutionResult {
|
||||
/// Returns a new `ExecutionResult` with the given exit code.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `exit_code` - The exit code of the command.
|
||||
pub fn new(exit_code: u8) -> Self {
|
||||
Self {
|
||||
exit_code: exit_code.into(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new `ExecutionResult` reflecting a process that was stopped.
|
||||
pub fn stopped() -> Self {
|
||||
// TODO: Decide how to sort this out in a platform-independent way.
|
||||
const SIGTSTP: std::os::raw::c_int = 20;
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
Self::new(128 + SIGTSTP as u8)
|
||||
}
|
||||
|
||||
/// Returns a new `ExecutionResult` with an exit code of 0.
|
||||
pub const fn success() -> Self {
|
||||
Self {
|
||||
next_control_flow: ExecutionControlFlow::Normal,
|
||||
exit_code: ExecutionExitCode::Success,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new `ExecutionResult` with a general error exit code.
|
||||
pub const fn general_error() -> Self {
|
||||
Self {
|
||||
next_control_flow: ExecutionControlFlow::Normal,
|
||||
exit_code: ExecutionExitCode::GeneralError,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the command was successful.
|
||||
pub const fn is_success(&self) -> bool {
|
||||
self.exit_code.is_success()
|
||||
}
|
||||
|
||||
/// Returns whether the execution result indicates normal control flow.
|
||||
/// Returns `false` if there is any control flow transition requested.
|
||||
pub const fn is_normal_flow(&self) -> bool {
|
||||
matches!(self.next_control_flow, ExecutionControlFlow::Normal)
|
||||
}
|
||||
|
||||
/// Returns whether the execution result indicates a loop break.
|
||||
pub const fn is_break(&self) -> bool {
|
||||
matches!(
|
||||
self.next_control_flow,
|
||||
ExecutionControlFlow::BreakLoop { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns whether the execution result indicates a loop continue.
|
||||
pub const fn is_continue(&self) -> bool {
|
||||
matches!(
|
||||
self.next_control_flow,
|
||||
ExecutionControlFlow::ContinueLoop { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns whether the execution result indicates an early return
|
||||
/// from a function or script, or an exit from the shell. Returns `false`
|
||||
/// otherwise, including loop breaks or continues.
|
||||
pub const fn is_return_or_exit(&self) -> bool {
|
||||
matches!(
|
||||
self.next_control_flow,
|
||||
ExecutionControlFlow::ReturnFromFunctionOrScript | ExecutionControlFlow::ExitShell
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExecutionExitCode> for ExecutionResult {
|
||||
fn from(exit_code: ExecutionExitCode) -> Self {
|
||||
Self {
|
||||
next_control_flow: ExecutionControlFlow::Normal,
|
||||
exit_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an exit code from execution.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub enum ExecutionExitCode {
|
||||
/// Indicates successful execution.
|
||||
#[default]
|
||||
Success,
|
||||
/// Indicates a general error.
|
||||
GeneralError,
|
||||
/// Indicates invalid usage.
|
||||
InvalidUsage,
|
||||
/// Cannot execute the command.
|
||||
CannotExecute,
|
||||
/// Indicates a command or similar item was not found.
|
||||
NotFound,
|
||||
/// Indicates execution was interrupted.
|
||||
Interrupted,
|
||||
/// Indicates unimplemented functionality was encountered.
|
||||
Unimplemented,
|
||||
/// A custom exit code.
|
||||
Custom(u8),
|
||||
}
|
||||
|
||||
impl ExecutionExitCode {
|
||||
/// Returns whether the exit code indicates success.
|
||||
pub const fn is_success(&self) -> bool {
|
||||
matches!(self, Self::Success)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for ExecutionExitCode {
|
||||
fn from(code: u8) -> Self {
|
||||
match code {
|
||||
0 => Self::Success,
|
||||
1 => Self::GeneralError,
|
||||
2 => Self::InvalidUsage,
|
||||
99 => Self::Unimplemented,
|
||||
126 => Self::CannotExecute,
|
||||
127 => Self::NotFound,
|
||||
130 => Self::Interrupted,
|
||||
code => Self::Custom(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExecutionExitCode> for u8 {
|
||||
fn from(code: ExecutionExitCode) -> Self {
|
||||
Self::from(&code)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ExecutionExitCode> for u8 {
|
||||
fn from(code: &ExecutionExitCode) -> Self {
|
||||
match code {
|
||||
ExecutionExitCode::Success => 0,
|
||||
ExecutionExitCode::GeneralError => 1,
|
||||
ExecutionExitCode::InvalidUsage => 2,
|
||||
ExecutionExitCode::Unimplemented => 99,
|
||||
ExecutionExitCode::CannotExecute => 126,
|
||||
ExecutionExitCode::NotFound => 127,
|
||||
ExecutionExitCode::Interrupted => 130,
|
||||
ExecutionExitCode::Custom(code) => *code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a control flow transition to apply.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub enum ExecutionControlFlow {
|
||||
/// Continue normal execution.
|
||||
#[default]
|
||||
Normal,
|
||||
/// Break out of an enclosing loop.
|
||||
BreakLoop {
|
||||
/// Identifies which level of nested loops to break out of. 0 indicates the innermost loop,
|
||||
/// 1 indicates the next outer loop, and so on.
|
||||
levels: usize,
|
||||
},
|
||||
/// Continue to the next iteration of an enclosing loop.
|
||||
ContinueLoop {
|
||||
/// Identifies which level of nested loops to continue. 0 indicates the innermost loop,
|
||||
/// 1 indicates the next outer loop, and so on.
|
||||
levels: usize,
|
||||
},
|
||||
/// Return from the current function or script.
|
||||
ReturnFromFunctionOrScript,
|
||||
/// Exit the shell.
|
||||
ExitShell,
|
||||
}
|
||||
|
||||
impl ExecutionControlFlow {
|
||||
/// Attempts to decrement the loop levels for `BreakLoop` or `ContinueLoop`.
|
||||
/// If the levels reach zero, transitions to `Normal`. If the control flow is not
|
||||
/// a loop break or continue, no changes are made.
|
||||
#[must_use]
|
||||
pub const fn try_decrement_loop_levels(&self) -> Self {
|
||||
match self {
|
||||
Self::BreakLoop { levels: 0 } | Self::ContinueLoop { levels: 0 } => Self::Normal,
|
||||
Self::BreakLoop { levels } => Self::BreakLoop {
|
||||
levels: *levels - 1,
|
||||
},
|
||||
Self::ContinueLoop { levels } => Self::ContinueLoop {
|
||||
levels: *levels - 1,
|
||||
},
|
||||
control_flow => *control_flow,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the result of spawning an execution; captures both execution
|
||||
/// that immediately returns as well as execution that starts a process
|
||||
/// asynchronously.
|
||||
pub enum ExecutionSpawnResult {
|
||||
/// Indicates that the execution completed.
|
||||
Completed(ExecutionResult),
|
||||
/// Indicates that a process was started and had not yet completed.
|
||||
StartedProcess(processes::ChildProcess),
|
||||
}
|
||||
|
||||
impl From<ExecutionResult> for ExecutionSpawnResult {
|
||||
fn from(result: ExecutionResult) -> Self {
|
||||
Self::Completed(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecutionSpawnResult {
|
||||
/// Waits for the command to complete.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `no_wait` - If true, do not wait for the command to complete; return immediately.
|
||||
pub async fn wait(self, no_wait: bool) -> Result<ExecutionWaitResult, error::Error> {
|
||||
match self {
|
||||
Self::StartedProcess(mut child) => {
|
||||
let process_wait_result = if !no_wait {
|
||||
// Wait for the process to exit or for a relevant signal, whichever happens
|
||||
// first.
|
||||
child.wait().await?
|
||||
} else {
|
||||
processes::ProcessWaitResult::Stopped
|
||||
};
|
||||
|
||||
let wait_result = match process_wait_result {
|
||||
processes::ProcessWaitResult::Completed(output) => {
|
||||
ExecutionWaitResult::Completed(ExecutionResult::from(output))
|
||||
}
|
||||
processes::ProcessWaitResult::Stopped => ExecutionWaitResult::Stopped(child),
|
||||
};
|
||||
|
||||
Ok(wait_result)
|
||||
}
|
||||
Self::Completed(result) => Ok(ExecutionWaitResult::Completed(result)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the result of waiting for an execution to complete.
|
||||
pub enum ExecutionWaitResult {
|
||||
/// Indicates that the execution completed.
|
||||
Completed(ExecutionResult),
|
||||
/// Indicates that the execution was stopped.
|
||||
Stopped(processes::ChildProcess),
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
//! Call stack representations.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Represents an executing script.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CallType {
|
||||
/// The script was sourced.
|
||||
Sourced,
|
||||
/// The script was executed.
|
||||
Executed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CallType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Sourced => write!(f, "sourced"),
|
||||
Self::Executed => write!(f, "executed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a single frame in a script call stack.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CallFrame {
|
||||
/// The type of script call that resulted in this frame.
|
||||
pub call_type: CallType,
|
||||
/// The source of the script (e.g., file path).
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
/// Encapsulates a script call stack.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CallStack {
|
||||
frames: VecDeque<CallFrame>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CallStack {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
writeln!(f, "Script call stack (most recent first):")?;
|
||||
|
||||
for (index, frame) in self.iter().enumerate() {
|
||||
writeln!(f, " #{}| {} ({})", index, frame.source, frame.call_type)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CallStack {
|
||||
/// Creates a new empty script call stack.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Removes the top from from the stack. If the stack is empty, does nothing and
|
||||
/// returns `None`; otherwise, returns the removed call frame.
|
||||
pub fn pop(&mut self) -> Option<CallFrame> {
|
||||
self.frames.pop_front()
|
||||
}
|
||||
|
||||
/// Pushes a new frame onto the stack.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `call_type` - The type of script call (sourced or executed).
|
||||
/// * `source` - The source of the script (e.g., file path).
|
||||
pub fn push(&mut self, call_type: CallType, source: impl Into<String>) {
|
||||
self.frames.push_front(CallFrame {
|
||||
call_type,
|
||||
source: source.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns whether or not the current script stack frame is a sourced script.
|
||||
pub fn in_sourced_script(&self) -> bool {
|
||||
self.frames
|
||||
.front()
|
||||
.is_some_and(|frame| matches!(frame.call_type, CallType::Sourced))
|
||||
}
|
||||
|
||||
/// Returns the current depth of the script call stack.
|
||||
pub fn depth(&self) -> usize {
|
||||
self.frames.len()
|
||||
}
|
||||
|
||||
/// Returns whether or not the script call stack is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.frames.is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the script call frames, starting from the most
|
||||
/// recent.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &CallFrame> {
|
||||
self.frames.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_call_type_display() {
|
||||
assert_eq!(CallType::Sourced.to_string(), "sourced");
|
||||
assert_eq!(CallType::Executed.to_string(), "executed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_new() {
|
||||
let stack = CallStack::new();
|
||||
assert!(stack.is_empty());
|
||||
assert_eq!(stack.depth(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_default() {
|
||||
let stack = CallStack::default();
|
||||
assert!(stack.is_empty());
|
||||
assert_eq!(stack.depth(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_push_pop() {
|
||||
let mut stack = CallStack::new();
|
||||
|
||||
stack.push(CallType::Sourced, "script1.sh");
|
||||
assert!(!stack.is_empty());
|
||||
assert_eq!(stack.depth(), 1);
|
||||
|
||||
stack.push(CallType::Executed, "script2.sh");
|
||||
assert_eq!(stack.depth(), 2);
|
||||
|
||||
let frame = stack.pop().unwrap();
|
||||
assert_eq!(frame.source, "script2.sh");
|
||||
assert!(matches!(frame.call_type, CallType::Executed));
|
||||
assert_eq!(stack.depth(), 1);
|
||||
|
||||
let frame = stack.pop().unwrap();
|
||||
assert_eq!(frame.source, "script1.sh");
|
||||
assert!(matches!(frame.call_type, CallType::Sourced));
|
||||
assert_eq!(stack.depth(), 0);
|
||||
assert!(stack.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_pop_empty() {
|
||||
let mut stack = CallStack::new();
|
||||
assert!(stack.pop().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_sourced_script() {
|
||||
let mut stack = CallStack::new();
|
||||
assert!(!stack.in_sourced_script());
|
||||
|
||||
stack.push(CallType::Executed, "script1.sh");
|
||||
assert!(!stack.in_sourced_script());
|
||||
|
||||
stack.push(CallType::Sourced, "script2.sh");
|
||||
assert!(stack.in_sourced_script());
|
||||
|
||||
stack.pop();
|
||||
assert!(!stack.in_sourced_script());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_iter() {
|
||||
let mut stack = CallStack::new();
|
||||
stack.push(CallType::Sourced, "script1.sh");
|
||||
stack.push(CallType::Executed, "script2.sh");
|
||||
stack.push(CallType::Sourced, "script3.sh");
|
||||
|
||||
let frames: Vec<_> = stack.iter().collect();
|
||||
assert_eq!(frames.len(), 3);
|
||||
assert_eq!(frames[0].source, "script3.sh");
|
||||
assert_eq!(frames[1].source, "script2.sh");
|
||||
assert_eq!(frames[2].source, "script1.sh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_display_empty() {
|
||||
let stack = CallStack::new();
|
||||
assert_eq!(stack.to_string(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_display_with_frames() {
|
||||
let mut stack = CallStack::new();
|
||||
stack.push(CallType::Sourced, "script1.sh");
|
||||
stack.push(CallType::Executed, "script2.sh");
|
||||
|
||||
let output = stack.to_string();
|
||||
assert!(output.contains("Script call stack (most recent first):"));
|
||||
assert!(output.contains("#0| script2.sh (executed)"));
|
||||
assert!(output.contains("#1| script1.sh (sourced)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_frame_clone() {
|
||||
let frame1 = CallFrame {
|
||||
call_type: CallType::Sourced,
|
||||
source: "test.sh".to_string(),
|
||||
};
|
||||
let frame2 = frame1.clone();
|
||||
|
||||
assert_eq!(frame1.source, frame2.source);
|
||||
assert!(matches!(frame1.call_type, CallType::Sourced));
|
||||
assert!(matches!(frame2.call_type, CallType::Sourced));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_stack_clone() {
|
||||
let mut stack1 = CallStack::new();
|
||||
stack1.push(CallType::Sourced, "script1.sh");
|
||||
stack1.push(CallType::Executed, "script2.sh");
|
||||
|
||||
let stack2 = stack1.clone();
|
||||
assert_eq!(stack1.depth(), stack2.depth());
|
||||
|
||||
let frames1: Vec<_> = stack1.iter().map(|f| &f.source).collect();
|
||||
let frames2: Vec<_> = stack2.iter().map(|f| &f.source).collect();
|
||||
assert_eq!(frames1, frames2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_with_string_types() {
|
||||
let mut stack = CallStack::new();
|
||||
|
||||
// Test with &str
|
||||
stack.push(CallType::Sourced, "script1.sh");
|
||||
|
||||
// Test with String
|
||||
stack.push(CallType::Executed, String::from("script2.sh"));
|
||||
|
||||
// Test with owned string reference
|
||||
let owned = "script3.sh".to_string();
|
||||
stack.push(CallType::Sourced, &owned);
|
||||
|
||||
assert_eq!(stack.depth(), 3);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
//! Platform abstraction facilities
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(crate) mod unix;
|
||||
#[cfg(unix)]
|
||||
pub(crate) use unix as platform;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod windows;
|
||||
#[cfg(windows)]
|
||||
pub(crate) use windows as platform;
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub(crate) mod wasm;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub(crate) use wasm as platform;
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) mod stubs;
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
pub(crate) mod hostname;
|
||||
#[cfg(any(unix, windows))]
|
||||
pub mod tokio_process;
|
||||
|
||||
pub mod fs;
|
||||
|
||||
pub use platform::commands;
|
||||
pub use platform::fd;
|
||||
pub use platform::input;
|
||||
pub(crate) use platform::network;
|
||||
pub use platform::process;
|
||||
pub use platform::resource;
|
||||
pub use platform::signal;
|
||||
pub use platform::terminal;
|
||||
pub(crate) use platform::users;
|
||||
|
||||
pub use platform::PlatformError;
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Filesystem utilities
|
||||
|
||||
pub use super::platform::fs::*;
|
||||
|
||||
/// Extension trait for path-related filesystem operations.
|
||||
pub trait PathExt {
|
||||
/// Returns true if the path exists and is readable by the current user.
|
||||
fn readable(&self) -> bool;
|
||||
/// Returns true if the path exists and is writable by the current user.
|
||||
fn writable(&self) -> bool;
|
||||
/// Returns true if the path exists and is executable by the current user.
|
||||
fn executable(&self) -> bool;
|
||||
|
||||
/// Returns true if the path exists and is a block device.
|
||||
fn exists_and_is_block_device(&self) -> bool;
|
||||
/// Returns true if the path exists and is a character device.
|
||||
fn exists_and_is_char_device(&self) -> bool;
|
||||
/// Returns true if the path exists and is a FIFO (named pipe).
|
||||
fn exists_and_is_fifo(&self) -> bool;
|
||||
/// Returns true if the path exists and is a socket.
|
||||
fn exists_and_is_socket(&self) -> bool;
|
||||
/// Returns true if the path exists and has the setgid bit set.
|
||||
fn exists_and_is_setgid(&self) -> bool;
|
||||
/// Returns true if the path exists and has the setuid bit set.
|
||||
fn exists_and_is_setuid(&self) -> bool;
|
||||
/// Returns true if the path exists and has the sticky bit set.
|
||||
fn exists_and_is_sticky_bit(&self) -> bool;
|
||||
|
||||
/// Returns the device ID and inode number for the path.
|
||||
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get() -> std::io::Result<std::ffi::OsString> {
|
||||
hostname::get()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::missing_const_for_fn)]
|
||||
#![allow(clippy::needless_pass_by_ref_mut)]
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
#![allow(clippy::unnecessary_wraps)]
|
||||
#![allow(clippy::unused_async)]
|
||||
#![allow(clippy::unused_self)]
|
||||
|
||||
pub mod commands;
|
||||
pub mod fd;
|
||||
pub mod fs;
|
||||
pub mod input;
|
||||
pub(crate) mod network;
|
||||
pub(crate) mod pipes;
|
||||
pub mod process;
|
||||
pub mod resource;
|
||||
pub mod signal;
|
||||
pub mod terminal;
|
||||
pub(crate) mod users;
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Command execution utilities.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use crate::{ShellFd, error, openfiles};
|
||||
|
||||
/// Extension trait for Unix-like command extensions.
|
||||
pub trait CommandExt {
|
||||
/// Sets the zeroth argument (argv[0]) of the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `arg` - The argument to set as argv[0].
|
||||
fn arg0<S>(&mut self, arg: S) -> &mut Self
|
||||
where
|
||||
S: AsRef<OsStr>;
|
||||
|
||||
/// Sets the process group ID of the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pgroup` - The process group ID to set.
|
||||
fn process_group(&mut self, pgroup: i32) -> &mut Self;
|
||||
}
|
||||
|
||||
impl CommandExt for std::process::Command {
|
||||
fn arg0<S>(&mut self, _arg: S) -> &mut Self
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
// NOTE: no-op.
|
||||
self
|
||||
}
|
||||
|
||||
fn process_group(&mut self, _pgroup: i32) -> &mut Self {
|
||||
// NOTE: no-op.
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for Unix-like exit status extensions.
|
||||
pub trait ExitStatusExt {
|
||||
/// Returns the signal that terminated the process, if any.
|
||||
fn signal(&self) -> Option<i32>;
|
||||
}
|
||||
|
||||
impl ExitStatusExt for std::process::ExitStatus {
|
||||
fn signal(&self) -> Option<i32> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for injecting file descriptors into commands.
|
||||
pub trait CommandFdInjectionExt {
|
||||
/// Injects the given open files as file descriptors into the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `open_files` - A mapping of child file descriptors to open files.
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error>;
|
||||
}
|
||||
|
||||
impl CommandFdInjectionExt for std::process::Command {
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
mut open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error> {
|
||||
if open_files.next().is_some() {
|
||||
return Err(error::ErrorKind::NotSupportedOnThisPlatform("fd redirections").into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for arranging for commands to take the foreground.
|
||||
pub trait CommandFgControlExt {
|
||||
/// Arranges for the command to take the foreground when it is executed.
|
||||
fn take_foreground(&mut self);
|
||||
}
|
||||
|
||||
impl CommandFgControlExt for std::process::Command {
|
||||
fn take_foreground(&mut self) {
|
||||
// NOTE: This is a no-op.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! File descriptor utilities.
|
||||
|
||||
use crate::{ShellFd, error, openfiles};
|
||||
|
||||
/// Stub implementation for platforms that do not support enumerating file descriptors.
|
||||
pub fn try_iter_open_fds() -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
|
||||
std::iter::empty()
|
||||
}
|
||||
|
||||
/// Stub implementation for platforms that do not support opening file descriptors.
|
||||
pub fn try_get_file_for_open_fd(_fd: ShellFd) -> Option<openfiles::OpenFile> {
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Filesystem utilities.
|
||||
|
||||
use crate::error;
|
||||
|
||||
impl crate::sys::fs::PathExt for std::path::Path {
|
||||
fn readable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn writable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn executable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn exists_and_is_block_device(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_char_device(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_fifo(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_socket(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_setgid(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_setuid(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_sticky_bit(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
|
||||
Ok((0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait MetadataExt {
|
||||
fn gid(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn uid(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataExt for std::fs::Metadata {}
|
||||
|
||||
pub(crate) fn get_default_executable_search_paths() -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Returns the default paths where standard Unix utilities are typically installed.
|
||||
/// This is a stub implementation that returns an empty vector.
|
||||
pub fn get_default_standard_utils_paths() -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Opens a null file that will discard all I/O.
|
||||
///
|
||||
/// This is a stub implementation that returns an error.
|
||||
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("opening null file").into())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Terminal input utilities
|
||||
|
||||
use crate::{error, interfaces};
|
||||
|
||||
/// Translates a key code (byte sequence) into a `Key` enum value. Returns `None`
|
||||
/// if the key code is not recognized.
|
||||
///
|
||||
/// This is a stub implementation that always returns `None`.
|
||||
pub fn try_get_key_from_key_code(_key_code: &[u8]) -> Option<interfaces::Key> {
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
|
||||
Ok("".into())
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/// Stub implementation of a pipe reader.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PipeReader {}
|
||||
|
||||
impl PipeReader {
|
||||
/// Tries to clone the reader.
|
||||
pub fn try_clone(&self) -> std::io::Result<Self> {
|
||||
Ok((*self).clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PipeReader> for std::process::Stdio {
|
||||
fn from(_reader: PipeReader) -> Self {
|
||||
Self::null()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Read for PipeReader {
|
||||
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
// TODO: implement
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub implementation o a pipe writer.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PipeWriter {}
|
||||
|
||||
impl PipeWriter {
|
||||
/// Tries to clone the writer.
|
||||
pub fn try_clone(&self) -> std::io::Result<Self> {
|
||||
Ok((*self).clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PipeWriter> for std::process::Stdio {
|
||||
fn from(_writer: PipeWriter) -> Self {
|
||||
Self::null()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for PipeWriter {
|
||||
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
|
||||
// TODO: implement
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pipe() -> std::io::Result<(PipeReader, PipeWriter)> {
|
||||
Ok((PipeReader {}, PipeWriter {}))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Process management utilities
|
||||
|
||||
pub(crate) type ProcessId = i32;
|
||||
|
||||
/// Provides access to a child process.
|
||||
pub struct Child {
|
||||
inner: std::process::Child,
|
||||
}
|
||||
|
||||
pub(crate) use std::process::ExitStatus;
|
||||
pub(crate) use std::process::Output;
|
||||
|
||||
impl Child {
|
||||
/// Returns the process ID of the child process, if available.
|
||||
pub fn id(&self) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Asynchronously waits for the child process to exit.
|
||||
pub async fn wait(&mut self) -> std::io::Result<ExitStatus> {
|
||||
self.inner.wait()
|
||||
}
|
||||
|
||||
/// Asynchronously waits for the child process to exit and collects its
|
||||
/// output.
|
||||
pub async fn wait_with_output(self) -> std::io::Result<Output> {
|
||||
self.inner.wait_with_output()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn(mut command: std::process::Command) -> std::io::Result<Child> {
|
||||
let child = command.spawn()?;
|
||||
Ok(Child { inner: child })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Signal processing utilities
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Returns the user and system CPU time used by the current process.
|
||||
///
|
||||
/// This is a stub implementation that returns zero durations.
|
||||
pub fn get_self_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
Ok((std::time::Duration::ZERO, std::time::Duration::ZERO))
|
||||
}
|
||||
|
||||
/// Returns the user and system CPU time used by child processes.
|
||||
///
|
||||
/// This is a stub implementation that returns zero durations.
|
||||
pub fn get_children_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
Ok((std::time::Duration::ZERO, std::time::Duration::ZERO))
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Signal processing utilities
|
||||
|
||||
use crate::{error, sys, traps};
|
||||
|
||||
/// A stub enum representing system signals on unsupported platforms.
|
||||
#[allow(unnameable_types)]
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum Signal {}
|
||||
|
||||
impl Signal {
|
||||
/// Returns an iterator over all possible signals.
|
||||
pub fn iterator() -> impl Iterator<Item = Self> {
|
||||
std::iter::empty()
|
||||
}
|
||||
|
||||
/// Converts the signal into its corresponding name as a `&'static str`.
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
/// Creates a `Signal` from a string representation.
|
||||
pub fn from_str(s: &str) -> Result<Self, error::Error> {
|
||||
Err(error::ErrorKind::InvalidSignal(s.into()).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for Signal {
|
||||
type Error = error::Error;
|
||||
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
Err(error::ErrorKind::InvalidSignal(std::format!("{value}")).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn continue_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("continuing process").into())
|
||||
}
|
||||
|
||||
/// Sends a signal to a specific process.
|
||||
///
|
||||
/// This is a stub implementation that returns an error.
|
||||
pub fn kill_process(
|
||||
_pid: sys::process::ProcessId,
|
||||
_signal: traps::TrapSignal,
|
||||
) -> Result<(), error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("killing process").into())
|
||||
}
|
||||
|
||||
pub(crate) fn lead_new_process_group() -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) struct FakeSignal {}
|
||||
|
||||
impl FakeSignal {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub async fn recv(&self) {
|
||||
futures::future::pending::<()>().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tstp_signal_listener() -> Result<FakeSignal, error::Error> {
|
||||
Ok(FakeSignal::new())
|
||||
}
|
||||
|
||||
pub(crate) fn chld_signal_listener() -> Result<FakeSignal, error::Error> {
|
||||
Ok(FakeSignal::new())
|
||||
}
|
||||
|
||||
pub(crate) async fn await_ctrl_c() -> std::io::Result<()> {
|
||||
FakeSignal::new().recv().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn mask_sigttou() -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn poll_for_stopped_children() -> Result<bool, error::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Terminal utilities.
|
||||
|
||||
use crate::{error, sys, terminal};
|
||||
|
||||
/// Terminal configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config;
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
impl Config {
|
||||
/// Creates a new `Config` from the actual terminal attributes of the terminal associated
|
||||
/// with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_fd` - The file descriptor of the terminal.
|
||||
pub fn from_term<Fd>(_fd: Fd) -> Result<Self, error::Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
/// Applies the terminal settings to the terminal associated with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_fd` - The file descriptor of the terminal.
|
||||
pub fn apply_to_term<Fd>(&self, _fd: Fd) -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies the given high-level terminal settings to this configuration. Does not modify any
|
||||
/// terminal itself.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_settings` - The high-level terminal settings to apply to this configuration.
|
||||
pub fn update(&mut self, _settings: &terminal::Settings) {}
|
||||
}
|
||||
|
||||
/// Get the process ID of this process's parent.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the process group ID for this process's process group.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the foreground process ID of the attached terminal.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Move the specified process to the foreground of the attached terminal.
|
||||
///
|
||||
/// This is a stub implementation that takes no action.
|
||||
pub fn move_to_foreground(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves the current process to the foreground of the attached terminal.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn move_self_to_foreground() -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) fn get_user_home_dir(_username: &str) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn is_root() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current uid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current gid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective uid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective gid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_username() -> Result<String, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current username").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Process management utilities
|
||||
|
||||
pub(crate) type ProcessId = i32;
|
||||
pub(crate) use tokio::process::Child;
|
||||
|
||||
pub(crate) fn spawn(command: std::process::Command) -> std::io::Result<Child> {
|
||||
let mut command = tokio::process::Command::from(command);
|
||||
command.spawn()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
pub mod commands;
|
||||
pub mod fd;
|
||||
pub mod fs;
|
||||
pub mod input;
|
||||
pub(crate) mod network;
|
||||
use crate::error;
|
||||
pub use crate::sys::tokio_process as process;
|
||||
pub mod resource;
|
||||
pub mod signal;
|
||||
pub mod terminal;
|
||||
pub(crate) mod users;
|
||||
|
||||
/// Platform-specific errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PlatformError {
|
||||
/// A system error occurred.
|
||||
#[error("system error: {0}")]
|
||||
ErrnoError(#[from] nix::errno::Errno),
|
||||
}
|
||||
|
||||
impl From<nix::errno::Errno> for error::ErrorKind {
|
||||
fn from(err: nix::errno::Errno) -> Self {
|
||||
PlatformError::ErrnoError(err).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Command execution utilities.
|
||||
|
||||
pub use std::os::unix::process::CommandExt;
|
||||
pub use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
use command_fds::{CommandFdExt, FdMapping};
|
||||
|
||||
use crate::ShellFd;
|
||||
use crate::error;
|
||||
use crate::openfiles;
|
||||
|
||||
/// Extension trait for injecting file descriptors into commands.
|
||||
pub trait CommandFdInjectionExt {
|
||||
/// Injects the given open files as file descriptors into the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `open_files` - A mapping of child file descriptors to open files.
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error>;
|
||||
}
|
||||
|
||||
impl CommandFdInjectionExt for std::process::Command {
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error> {
|
||||
let fd_mappings = open_files
|
||||
.map(|(child_fd, open_file)| FdMapping {
|
||||
child_fd,
|
||||
parent_fd: open_file.into_owned_fd().unwrap(),
|
||||
})
|
||||
.collect();
|
||||
self.fd_mappings(fd_mappings)
|
||||
.map_err(|_e| error::ErrorKind::ChildCreationFailure)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for arranging for commands to take the foreground.
|
||||
pub trait CommandFgControlExt {
|
||||
/// Arranges for the command to take the foreground when it is executed.
|
||||
fn take_foreground(&mut self);
|
||||
}
|
||||
|
||||
impl CommandFgControlExt for std::process::Command {
|
||||
fn take_foreground(&mut self) {
|
||||
// SAFETY:
|
||||
// This arranges for a provided function to run in the context of
|
||||
// the forked process before it exec's the target command. In general,
|
||||
// rust can't guarantee safety of code running in such a context.
|
||||
unsafe {
|
||||
self.pre_exec(setup_process_before_exec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_process_before_exec() -> Result<(), std::io::Error> {
|
||||
use crate::sys;
|
||||
|
||||
sys::terminal::move_self_to_foreground().map_err(std::io::Error::other)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! File descriptor utilities.
|
||||
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use crate::{ShellFd, error, openfiles};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const FD_DIR_PATH: &str = "/proc/self/fd";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const FD_DIR_PATH: &str = "/dev/fd";
|
||||
|
||||
/// Makes a best-effort attempt to iterate over all open file descriptors
|
||||
/// for the current process.
|
||||
///
|
||||
/// If the platform does not support enumerating file descriptors, an empty iterator
|
||||
/// is returned. This function will skip any file descriptors that cannot be opened.
|
||||
pub fn try_iter_open_fds() -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
|
||||
let mut opened_entries = vec![];
|
||||
|
||||
if let Ok(fd_dir) = std::fs::read_dir(FD_DIR_PATH) {
|
||||
for entry in fd_dir.into_iter().flatten() {
|
||||
if let Ok(filename) = entry.file_name().into_string() {
|
||||
if let Ok(fd_num) = filename.parse::<RawFd>() {
|
||||
// SAFETY:
|
||||
// We are trying to open the file descriptor we found listed
|
||||
// in the filesystem, but there's a risk that it's not the same one
|
||||
// that we enumerated or that it's since been closed. For the purposes
|
||||
// of this function, either of those outcomes are acceptable. We
|
||||
// simply skip any fds that we can't open, and the function's purpose
|
||||
// is to make a best-effort attempt to open all available fds.
|
||||
if let Ok(file) = unsafe { open_file_by_fd(fd_num) } {
|
||||
opened_entries.push((fd_num, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opened_entries.into_iter()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
pub fn iter_fds() -> Result<impl Iterator<Item = (ShellFd, openfiles::OpenFile)>, error::Error> {
|
||||
Ok(std::iter::empty())
|
||||
}
|
||||
|
||||
/// Attempts to retrieve an `OpenFile` representation for the given already-open file descriptor.
|
||||
///
|
||||
/// If the file descriptor cannot be opened, `None` is returned. Note that there is no guarantee
|
||||
/// that the returned file matches the original file descriptor, as the fd may have been closed
|
||||
/// and potentially re-used in the meantime.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - The file descriptor to open.
|
||||
pub fn try_get_file_for_open_fd(fd: RawFd) -> Option<openfiles::OpenFile> {
|
||||
// SAFETY:
|
||||
// We are trying to open the file descriptor provided by the caller. There's a risk that the fd
|
||||
// is invalid or has been closed since it was enumerated. For the purposes of this function,
|
||||
// we simply return None if we can't open it. There's also a risk that the fd has been closed
|
||||
// and re-used for a different file; again, for the purposes of this function, we accept that
|
||||
// risk and document it as part of the function's contract.
|
||||
unsafe { open_file_by_fd(fd).ok() }
|
||||
}
|
||||
|
||||
unsafe fn open_file_by_fd(fd: RawFd) -> Result<openfiles::OpenFile, error::Error> {
|
||||
// SAFETY: We are creating a BorrowedFd from a file descriptor. Callers typically
|
||||
// enumerate available file descriptors from procfs, devfs, or similar, but there's
|
||||
// still a risk that the fd has become invalid or closed since then -- or that this
|
||||
// function gets used incorrectly.
|
||||
let borrowed_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
|
||||
let owned_fd = borrowed_fd.try_clone_to_owned()?;
|
||||
Ok(std::fs::File::from(owned_fd).into())
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//! Filesystem utilities.
|
||||
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::error;
|
||||
|
||||
pub use std::os::unix::fs::MetadataExt;
|
||||
|
||||
const DEFAULT_EXECUTABLE_SEARCH_PATHS: &[&str] = &[
|
||||
"/usr/local/sbin",
|
||||
"/usr/local/bin",
|
||||
"/usr/sbin",
|
||||
"/usr/bin",
|
||||
"/sbin",
|
||||
"/bin",
|
||||
];
|
||||
|
||||
const DEFAULT_STANDARD_UTILS_PATHS: &[&str] =
|
||||
&["/bin", "/usr/bin", "/sbin", "/usr/sbin", "/etc", "/usr/etc"];
|
||||
|
||||
impl crate::sys::fs::PathExt for Path {
|
||||
fn readable(&self) -> bool {
|
||||
nix::unistd::access(self, nix::unistd::AccessFlags::R_OK).is_ok()
|
||||
}
|
||||
|
||||
fn writable(&self) -> bool {
|
||||
nix::unistd::access(self, nix::unistd::AccessFlags::W_OK).is_ok()
|
||||
}
|
||||
|
||||
fn executable(&self) -> bool {
|
||||
nix::unistd::access(self, nix::unistd::AccessFlags::X_OK).is_ok()
|
||||
}
|
||||
|
||||
fn exists_and_is_block_device(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft| ft.is_block_device())
|
||||
}
|
||||
|
||||
fn exists_and_is_char_device(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft| ft.is_char_device())
|
||||
}
|
||||
|
||||
fn exists_and_is_fifo(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft: std::fs::FileType| ft.is_fifo())
|
||||
}
|
||||
|
||||
fn exists_and_is_socket(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft| ft.is_socket())
|
||||
}
|
||||
|
||||
fn exists_and_is_setgid(&self) -> bool {
|
||||
const S_ISGID: u32 = 0o2000;
|
||||
let file_mode = try_get_file_mode(self);
|
||||
file_mode.is_some_and(|mode| mode & S_ISGID != 0)
|
||||
}
|
||||
|
||||
fn exists_and_is_setuid(&self) -> bool {
|
||||
const S_ISUID: u32 = 0o4000;
|
||||
let file_mode = try_get_file_mode(self);
|
||||
file_mode.is_some_and(|mode| mode & S_ISUID != 0)
|
||||
}
|
||||
|
||||
fn exists_and_is_sticky_bit(&self) -> bool {
|
||||
const S_ISVTX: u32 = 0o1000;
|
||||
let file_mode = try_get_file_mode(self);
|
||||
file_mode.is_some_and(|mode| mode & S_ISVTX != 0)
|
||||
}
|
||||
|
||||
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
|
||||
let metadata = self.metadata()?;
|
||||
Ok((metadata.dev(), metadata.ino()))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_get_file_type(path: &Path) -> Option<std::fs::FileType> {
|
||||
path.metadata().map(|metadata| metadata.file_type()).ok()
|
||||
}
|
||||
|
||||
fn try_get_file_mode(path: &Path) -> Option<u32> {
|
||||
path.metadata().map(|metadata| metadata.mode()).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn get_default_executable_search_paths() -> Vec<String> {
|
||||
DEFAULT_EXECUTABLE_SEARCH_PATHS
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Retrieves the platform-specific set of paths that should contain standard system
|
||||
/// utilities. Used by `command -p`, for example.
|
||||
pub fn get_default_standard_utils_paths() -> Vec<String> {
|
||||
//
|
||||
// Try to call confstr(_CS_PATH). If that fails, can't find a string value, or
|
||||
// finds an empty string, then we'll fall back to hard-coded defaults.
|
||||
//
|
||||
|
||||
if let Ok(Some(cs_path)) = confstr_cs_path() {
|
||||
if !cs_path.is_empty() {
|
||||
return cs_path.split(':').map(|s| s.to_string()).collect();
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_STANDARD_UTILS_PATHS
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn confstr_cs_path() -> Result<Option<String>, std::io::Error> {
|
||||
let value = confstr(nix::libc::_CS_PATH)?;
|
||||
|
||||
if let Some(value) = value {
|
||||
let value_str = value
|
||||
.into_string()
|
||||
.map_err(|_err| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid data"))?;
|
||||
Ok(Some(value_str))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH variable which
|
||||
/// indicates where all the POSIX.2 standard utilities can be found.
|
||||
///
|
||||
/// N.B. We would strongly prefer to use a safe API exposed (in an idiomatic way) by nix
|
||||
/// or similar. Until that exists, we accept the need to make the unsafe call directly.
|
||||
fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
|
||||
// SAFETY:
|
||||
// Calling `confstr` with a null pointer and size 0 is a documented way to query
|
||||
// the required size of the buffer to hold the value associated with `name`. It
|
||||
// should not end up causing any undefined behavior.
|
||||
let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
|
||||
|
||||
// When confstr returns 0, it either means there's no value associated with _CS_PATH, or
|
||||
// _CS_PATH is considered invalid (and not present) on this platform. In both cases, we
|
||||
// treat it as a non-existent value and return None.
|
||||
if required_size == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut buffer = Vec::<u8>::with_capacity(required_size);
|
||||
|
||||
// SAFETY:
|
||||
// We are calling `confstr` with a valid pointer and size that we obtained from the
|
||||
// allocated buffer. Writing `c_char` (i8 or u8 depending on the platform) into
|
||||
// `Vec<u8>` is fine, as i8 and u8 have compatible representations, and Rust does
|
||||
// not support platforms where `c_char` is not 8-bit wide.
|
||||
let final_size =
|
||||
unsafe { nix::libc::confstr(name, buffer.as_mut_ptr().cast(), buffer.capacity()) };
|
||||
|
||||
if final_size == 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// Per the docs on `confstr`, it *may* return a size larger than the provided buffer.
|
||||
// In our usage we wouldn't expect to see this, as we've first queried the required size.
|
||||
// However, we defensively check for this case and return an error if it happens.
|
||||
if final_size > buffer.capacity() {
|
||||
return Err(std::io::Error::other(
|
||||
"confstr needed more space than advertised",
|
||||
));
|
||||
}
|
||||
|
||||
// SAFETY:
|
||||
// We are trusting `confstr` to have written exactly `final_size` bytes into the buffer.
|
||||
// We have checked above that it didn't return a value *larger* than the capacity of
|
||||
// the buffer, and also checked for known error cases. Note that the returned length
|
||||
// should include the null terminator.
|
||||
unsafe { buffer.set_len(final_size) };
|
||||
|
||||
// The last byte is a null terminator. We assert that it is.
|
||||
if !matches!(buffer.pop(), Some(0)) {
|
||||
return Err(std::io::Error::other(
|
||||
"confstr did not null-terminate the returned string",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(std::ffi::OsString::from_vec(buffer)))
|
||||
}
|
||||
|
||||
/// Opens a null file that will discard all I/O.
|
||||
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
|
||||
let f = std::fs::File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/null")?;
|
||||
|
||||
Ok(f)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Terminal input utilities
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
use terminfo::capability as cap;
|
||||
|
||||
use crate::{error, interfaces};
|
||||
|
||||
macro_rules! key {
|
||||
( $terminfo:expr , $our_key:expr, $terminfo_key:ty ) => {{
|
||||
(
|
||||
$our_key,
|
||||
$terminfo
|
||||
.get::<$terminfo_key>()
|
||||
.map(|k| k.expand().to_vec()),
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
fn build_terminfo_key_map() -> HashMap<Vec<u8>, interfaces::Key> {
|
||||
let mut map: HashMap<Vec<u8>, interfaces::Key> = HashMap::new();
|
||||
|
||||
if let Ok(ti) = terminfo::Database::from_env() {
|
||||
// Iterate over key capabilities and populate the map
|
||||
let key_capabilities = [
|
||||
key!(ti, interfaces::Key::F(1), cap::KeyF1<'_>),
|
||||
key!(ti, interfaces::Key::F(2), cap::KeyF2<'_>),
|
||||
key!(ti, interfaces::Key::F(3), cap::KeyF3<'_>),
|
||||
key!(ti, interfaces::Key::F(4), cap::KeyF4<'_>),
|
||||
key!(ti, interfaces::Key::F(5), cap::KeyF5<'_>),
|
||||
key!(ti, interfaces::Key::F(6), cap::KeyF6<'_>),
|
||||
key!(ti, interfaces::Key::F(7), cap::KeyF7<'_>),
|
||||
key!(ti, interfaces::Key::F(8), cap::KeyF8<'_>),
|
||||
key!(ti, interfaces::Key::F(9), cap::KeyF9<'_>),
|
||||
key!(ti, interfaces::Key::F(10), cap::KeyF10<'_>),
|
||||
key!(ti, interfaces::Key::F(11), cap::KeyF11<'_>),
|
||||
key!(ti, interfaces::Key::F(12), cap::KeyF12<'_>),
|
||||
key!(ti, interfaces::Key::Backspace, cap::KeyBackspace<'_>),
|
||||
key!(ti, interfaces::Key::Enter, cap::KeyEnter<'_>),
|
||||
key!(ti, interfaces::Key::Left, cap::KeyLeft<'_>),
|
||||
key!(ti, interfaces::Key::Right, cap::KeyRight<'_>),
|
||||
key!(ti, interfaces::Key::Up, cap::KeyUp<'_>),
|
||||
key!(ti, interfaces::Key::Down, cap::KeyDown<'_>),
|
||||
key!(ti, interfaces::Key::Home, cap::KeyHome<'_>),
|
||||
key!(ti, interfaces::Key::End, cap::KeyEnd<'_>),
|
||||
key!(ti, interfaces::Key::PageUp, cap::KeyPPage<'_>),
|
||||
key!(ti, interfaces::Key::PageDown, cap::KeyNPage<'_>),
|
||||
key!(ti, interfaces::Key::BackTab, cap::BackTab<'_>),
|
||||
// It's not clear if these belong here, because they're not
|
||||
// strictly "key" capabilities.
|
||||
key!(ti, interfaces::Key::Up, cap::CursorUp<'_>),
|
||||
key!(ti, interfaces::Key::Down, cap::CursorDown<'_>),
|
||||
key!(ti, interfaces::Key::Left, cap::CursorLeft<'_>),
|
||||
key!(ti, interfaces::Key::Right, cap::CursorRight<'_>),
|
||||
];
|
||||
|
||||
for (key, v) in key_capabilities {
|
||||
if let Some(Ok(v)) = v {
|
||||
map.insert(v.clone(), key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
pub(crate) static TERMINFO_KEY_MAP: LazyLock<HashMap<Vec<u8>, interfaces::Key>> =
|
||||
LazyLock::new(build_terminfo_key_map);
|
||||
|
||||
/// Translates a key code (byte sequence) into a `Key` enum value. Returns `None`
|
||||
/// if the key code is not recognized.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key_code`: The byte sequence representing the key code.
|
||||
pub fn try_get_key_from_key_code(key_code: &[u8]) -> Option<interfaces::Key> {
|
||||
if let Some(key) = TERMINFO_KEY_MAP.get(key_code) {
|
||||
Some(key.clone())
|
||||
} else if key_code.len() == 1 && !key_code[0].is_ascii_control() {
|
||||
Some(interfaces::Key::Character(key_code[0] as char))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
|
||||
crate::sys::hostname::get()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Resource utilities
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Returns the user and system CPU time used by the current process;
|
||||
/// expressed as a tuple containing user time and system time, in that order.
|
||||
pub fn get_self_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF)?;
|
||||
Ok((
|
||||
convert_rusage_time(usage.user_time()),
|
||||
convert_rusage_time(usage.system_time()),
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the user and system CPU time used by child processes; expressed
|
||||
/// as a tuple containing user time and system time, in that order.
|
||||
pub fn get_children_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_CHILDREN)?;
|
||||
Ok((
|
||||
convert_rusage_time(usage.user_time()),
|
||||
convert_rusage_time(usage.system_time()),
|
||||
))
|
||||
}
|
||||
|
||||
const fn convert_rusage_time(time: nix::sys::time::TimeVal) -> std::time::Duration {
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
std::time::Duration::new(time.tv_sec() as u64, time.tv_usec() as u32 * 1000)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//! Signal processing utilities
|
||||
|
||||
use crate::{error, sys, traps};
|
||||
|
||||
pub(crate) use nix::sys::signal::Signal;
|
||||
|
||||
pub(crate) fn continue_process(pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), nix::sys::signal::SIGCONT)
|
||||
.map_err(|_errno| error::ErrorKind::FailedToSendSignal)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a signal to a specific process.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pid` - The process ID to send the signal to
|
||||
/// * `signal` - The signal to send (must be a real signal, not a trap signal)
|
||||
pub fn kill_process(
|
||||
pid: sys::process::ProcessId,
|
||||
signal: traps::TrapSignal,
|
||||
) -> Result<(), error::Error> {
|
||||
let translated_signal = match signal {
|
||||
traps::TrapSignal::Signal(signal) => signal,
|
||||
traps::TrapSignal::Debug
|
||||
| traps::TrapSignal::Err
|
||||
| traps::TrapSignal::Exit
|
||||
| traps::TrapSignal::Return => {
|
||||
return Err(error::ErrorKind::InvalidSignal(signal.to_string()).into());
|
||||
}
|
||||
};
|
||||
|
||||
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), translated_signal)
|
||||
.map_err(|_errno| error::ErrorKind::FailedToSendSignal)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn lead_new_process_group() -> Result<(), error::Error> {
|
||||
nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn tstp_signal_listener() -> Result<tokio::signal::unix::Signal, error::Error> {
|
||||
let signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::from_raw(
|
||||
nix::libc::SIGTSTP,
|
||||
))?;
|
||||
Ok(signal)
|
||||
}
|
||||
|
||||
pub(crate) fn chld_signal_listener() -> Result<tokio::signal::unix::Signal, error::Error> {
|
||||
let signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::child())?;
|
||||
Ok(signal)
|
||||
}
|
||||
|
||||
pub(crate) use tokio::signal::ctrl_c as await_ctrl_c;
|
||||
|
||||
pub(crate) fn mask_sigttou() -> Result<(), error::Error> {
|
||||
let ignore = nix::sys::signal::SigAction::new(
|
||||
nix::sys::signal::SigHandler::SigIgn,
|
||||
nix::sys::signal::SaFlags::empty(),
|
||||
nix::sys::signal::SigSet::empty(),
|
||||
);
|
||||
|
||||
// SAFETY:
|
||||
// Setting the signal action should be safe here. The unsafe concerns
|
||||
// for calling `sigaction` are primarily around ensuring that any provided
|
||||
// signal handler functions are only performing operations that are
|
||||
// safe to do in a signal handler context. Here we are not providing
|
||||
// a custom handler, just asking the OS to ignore the signal.
|
||||
unsafe { nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGTTOU, &ignore) }?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn poll_for_stopped_children() -> Result<bool, error::Error> {
|
||||
let mut found_stopped = false;
|
||||
|
||||
loop {
|
||||
let wait_status = waitid_all(
|
||||
nix::sys::wait::WaitPidFlag::WUNTRACED | nix::sys::wait::WaitPidFlag::WNOHANG,
|
||||
);
|
||||
match wait_status {
|
||||
Ok(nix::sys::wait::WaitStatus::Stopped(_stopped_pid, _signal)) => {
|
||||
found_stopped = true;
|
||||
}
|
||||
Ok(_) => break,
|
||||
Err(nix::errno::Errno::ECHILD) => break,
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(found_stopped)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn waitid_all(
|
||||
flags: nix::sys::wait::WaitPidFlag,
|
||||
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
|
||||
nix::sys::wait::waitid(nix::sys::wait::Id::All, flags)
|
||||
}
|
||||
|
||||
//
|
||||
// N.B. These functions were mostly copied from nix::sys::wait (https://github.com/nix-rust/nix, MIT license)
|
||||
// to enable use of the `waitid` call on macOS. Ideally nix would expose it on macOS and we would
|
||||
// remove this code.
|
||||
//
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn waitid_all(
|
||||
flags: nix::sys::wait::WaitPidFlag,
|
||||
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
|
||||
// SAFETY:
|
||||
// Code copied from nix::sys::wait implementation of waitid for other platforms.
|
||||
let siginfo = unsafe {
|
||||
// Memory is zeroed rather than uninitialized, as not all platforms
|
||||
// initialize the memory in the StillAlive case
|
||||
let mut siginfo: nix::libc::siginfo_t = std::mem::zeroed();
|
||||
nix::errno::Errno::result(nix::libc::waitid(
|
||||
nix::libc::P_ALL,
|
||||
0,
|
||||
&raw mut siginfo,
|
||||
flags.bits(),
|
||||
))?;
|
||||
siginfo
|
||||
};
|
||||
|
||||
siginfo_to_wait_status(siginfo)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn siginfo_to_wait_status(
|
||||
siginfo: nix::libc::siginfo_t,
|
||||
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
|
||||
// SAFETY:
|
||||
// Code copied from nix::sys::wait implementation of waitid for other platforms.
|
||||
let si_pid = unsafe { siginfo.si_pid() };
|
||||
if si_pid == 0 {
|
||||
return Ok(nix::sys::wait::WaitStatus::StillAlive);
|
||||
}
|
||||
|
||||
let pid = nix::unistd::Pid::from_raw(si_pid);
|
||||
|
||||
// SAFETY:
|
||||
// Code copied from nix::sys::wait implementation of waitid for other platforms.
|
||||
let si_status = unsafe { siginfo.si_status() };
|
||||
|
||||
let status = match siginfo.si_code {
|
||||
nix::libc::CLD_EXITED => nix::sys::wait::WaitStatus::Exited(pid, si_status),
|
||||
nix::libc::CLD_KILLED | nix::libc::CLD_DUMPED => nix::sys::wait::WaitStatus::Signaled(
|
||||
pid,
|
||||
nix::sys::signal::Signal::try_from(si_status)?,
|
||||
siginfo.si_code == nix::libc::CLD_DUMPED,
|
||||
),
|
||||
nix::libc::CLD_STOPPED => {
|
||||
nix::sys::wait::WaitStatus::Stopped(pid, nix::sys::signal::Signal::try_from(si_status)?)
|
||||
}
|
||||
nix::libc::CLD_CONTINUED => nix::sys::wait::WaitStatus::Continued(pid),
|
||||
_ => return Err(nix::errno::Errno::EINVAL),
|
||||
};
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Terminal utilities.
|
||||
|
||||
use crate::{error, sys, terminal};
|
||||
use std::{io::IsTerminal, os::fd::AsFd};
|
||||
|
||||
/// Terminal configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
termios: nix::sys::termios::Termios,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Creates a new `Config` from the actual terminal attributes of the terminal associated
|
||||
/// with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - The file descriptor of the terminal.
|
||||
pub fn from_term(fd: impl AsFd) -> Result<Self, error::Error> {
|
||||
let termios = nix::sys::termios::tcgetattr(fd)?;
|
||||
Ok(Self { termios })
|
||||
}
|
||||
|
||||
/// Applies the terminal settings to the terminal associated with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - The file descriptor of the terminal.
|
||||
pub fn apply_to_term(&self, fd: impl AsFd) -> Result<(), error::Error> {
|
||||
nix::sys::termios::tcsetattr(fd, nix::sys::termios::SetArg::TCSANOW, &self.termios)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies the given high-level terminal settings to this configuration. Does not modify any
|
||||
/// terminal itself.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `settings` - The high-level terminal settings to apply to this configuration.
|
||||
pub fn update(&mut self, settings: &terminal::Settings) {
|
||||
if let Some(echo_input) = &settings.echo_input {
|
||||
if *echo_input {
|
||||
self.termios.local_flags |= nix::sys::termios::LocalFlags::ECHO;
|
||||
} else {
|
||||
self.termios.local_flags -= nix::sys::termios::LocalFlags::ECHO;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(line_input) = &settings.line_input {
|
||||
if *line_input {
|
||||
self.termios.local_flags |= nix::sys::termios::LocalFlags::ICANON;
|
||||
} else {
|
||||
self.termios.local_flags -= nix::sys::termios::LocalFlags::ICANON;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(interrupt_signals) = &settings.interrupt_signals {
|
||||
if *interrupt_signals {
|
||||
self.termios.local_flags |= nix::sys::termios::LocalFlags::ISIG;
|
||||
} else {
|
||||
self.termios.local_flags -= nix::sys::termios::LocalFlags::ISIG;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output_nl_as_nlcr) = &settings.output_nl_as_nlcr {
|
||||
if *output_nl_as_nlcr {
|
||||
self.termios.output_flags |=
|
||||
nix::sys::termios::OutputFlags::OPOST | nix::sys::termios::OutputFlags::ONLCR;
|
||||
} else {
|
||||
self.termios.output_flags -= nix::sys::termios::OutputFlags::ONLCR;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the process ID of this process's parent.
|
||||
pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
|
||||
Some(nix::unistd::getppid().as_raw())
|
||||
}
|
||||
|
||||
/// Get the process group ID for this process's process group.
|
||||
pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
|
||||
Some(nix::unistd::getpgrp().as_raw())
|
||||
}
|
||||
|
||||
/// Get the foreground process ID of the attached terminal.
|
||||
pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
|
||||
nix::unistd::tcgetpgrp(std::io::stdin())
|
||||
.ok()
|
||||
.map(|pgid| pgid.as_raw())
|
||||
}
|
||||
|
||||
/// Move the specified process to the foreground of the attached terminal.
|
||||
pub fn move_to_foreground(pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves the current process to the foreground of the attached terminal.
|
||||
pub fn move_self_to_foreground() -> Result<(), error::Error> {
|
||||
if std::io::stdin().is_terminal() {
|
||||
let pgid = nix::unistd::getpgid(None)?;
|
||||
|
||||
// TODO: jobs: This sometimes fails with ENOTTY even though we checked that stdin is a
|
||||
// terminal. We should investigate why this is happening.
|
||||
let _ = nix::unistd::tcsetpgrp(std::io::stdin(), pgid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use crate::{error, trace_categories};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use uzers::os::unix::UserExt;
|
||||
|
||||
pub(crate) fn is_root() -> bool {
|
||||
uzers::get_current_uid() == 0
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_home_dir(username: &str) -> Option<PathBuf> {
|
||||
if let Some(user_info) = uzers::get_user_by_name(username) {
|
||||
return Some(user_info.home_dir().to_path_buf());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
|
||||
if let Some(username) = uzers::get_current_username() {
|
||||
if let Some(user_info) = uzers::get_user_by_name(&username) {
|
||||
return Some(user_info.home_dir().to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_current_uid())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_current_gid())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_effective_uid())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_effective_gid())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_username() -> Result<String, error::Error> {
|
||||
let username = uzers::get_current_username().ok_or_else(|| error::ErrorKind::NoCurrentUser)?;
|
||||
Ok(username.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
|
||||
let username = uzers::get_current_username().ok_or_else(|| error::ErrorKind::NoCurrentUser)?;
|
||||
let gid = uzers::get_current_gid();
|
||||
let groups = uzers::get_user_groups(&username, gid).unwrap_or_default();
|
||||
Ok(groups.into_iter().map(|g| g.gid()).collect())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
|
||||
// TODO(#475): uzers::all_users() is available but unsafe
|
||||
tracing::debug!(target: trace_categories::UNIMPLEMENTED, "get_all_users");
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
|
||||
// TODO(#475): uzers::all_groups() is available but unsafe
|
||||
tracing::debug!(target: trace_categories::UNIMPLEMENTED, "get_all_groups");
|
||||
Ok(vec![])
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pub use crate::sys::stubs::commands;
|
||||
pub use crate::sys::stubs::fd;
|
||||
pub use crate::sys::stubs::fs;
|
||||
pub use crate::sys::stubs::input;
|
||||
pub(crate) use crate::sys::stubs::network;
|
||||
pub(crate) use crate::sys::stubs::pipes;
|
||||
pub use crate::sys::stubs::process;
|
||||
pub use crate::sys::stubs::resource;
|
||||
pub use crate::sys::stubs::signal;
|
||||
pub use crate::sys::stubs::terminal;
|
||||
pub(crate) use crate::sys::stubs::users;
|
||||
|
||||
/// Platform-specific errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PlatformError {}
|
||||
@@ -0,0 +1,20 @@
|
||||
pub use crate::sys::stubs::commands;
|
||||
pub use crate::sys::stubs::fd;
|
||||
pub use crate::sys::stubs::fs;
|
||||
pub use crate::sys::stubs::input;
|
||||
pub(crate) mod network;
|
||||
pub use crate::sys::stubs::resource;
|
||||
|
||||
/// Signal processing utilities
|
||||
pub mod signal {
|
||||
pub(crate) use crate::sys::stubs::signal::*;
|
||||
pub(crate) use tokio::signal::ctrl_c as await_ctrl_c;
|
||||
}
|
||||
|
||||
pub use crate::sys::stubs::terminal;
|
||||
pub use crate::sys::tokio_process as process;
|
||||
pub(crate) mod users;
|
||||
|
||||
/// Platform-specific errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PlatformError {}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
|
||||
crate::sys::hostname::get()
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#![allow(clippy::missing_const_for_fn)]
|
||||
|
||||
use crate::error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
//
|
||||
// Non-Unix implementation
|
||||
//
|
||||
|
||||
pub(crate) fn get_user_home_dir(username: &str) -> Option<PathBuf> {
|
||||
homedir::home(username).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
|
||||
homedir::my_home().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn is_root() -> bool {
|
||||
// TODO: implement some version of this for Windows
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current uid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current gid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective uid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective gid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_username() -> Result<String, error::Error> {
|
||||
let username = whoami::fallible::username()?;
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
|
||||
// TODO: implement some version of this for Windows
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
|
||||
// TODO: implement some version of this for Windows
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
|
||||
// TODO: implement some version of this for Windows
|
||||
Ok(vec![])
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Terminal control utilities.
|
||||
|
||||
use crate::{error, openfiles, sys};
|
||||
|
||||
/// Encapsulates the state of a controlled terminal.
|
||||
pub struct TerminalControl {
|
||||
prev_fg_pid: Option<sys::process::ProcessId>,
|
||||
}
|
||||
|
||||
impl TerminalControl {
|
||||
/// Acquire the terminal for the shell.
|
||||
pub fn acquire() -> Result<Self, error::Error> {
|
||||
let prev_fg_pid = sys::terminal::get_foreground_pid();
|
||||
|
||||
// Break out into new process group.
|
||||
// TODO: jobs: Investigate why this sometimes fails with EPERM.
|
||||
let _ = sys::signal::lead_new_process_group();
|
||||
|
||||
// Take ownership.
|
||||
sys::terminal::move_self_to_foreground()?;
|
||||
|
||||
// Mask out SIGTTOU.
|
||||
sys::signal::mask_sigttou()?;
|
||||
|
||||
Ok(Self { prev_fg_pid })
|
||||
}
|
||||
|
||||
fn try_release(&mut self) {
|
||||
// Restore the previous foreground process group.
|
||||
if let Some(pid) = self.prev_fg_pid {
|
||||
if sys::terminal::move_to_foreground(pid).is_ok() {
|
||||
self.prev_fg_pid = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalControl {
|
||||
fn drop(&mut self) {
|
||||
self.try_release();
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes high-level terminal settings that can be requested.
|
||||
#[derive(Default, bon::Builder)]
|
||||
pub struct Settings {
|
||||
/// Whether to enable input echoing.
|
||||
pub echo_input: Option<bool>,
|
||||
/// Whether to enable line input (sometimes known as canonical mode).
|
||||
pub line_input: Option<bool>,
|
||||
/// Whether to disable interrupt signals and instead yield the control characters.
|
||||
pub interrupt_signals: Option<bool>,
|
||||
/// Whether to output newline characters as CRLF pairs.
|
||||
pub output_nl_as_nlcr: Option<bool>,
|
||||
}
|
||||
|
||||
/// Guard that automatically restores terminal settings on drop.
|
||||
pub struct AutoModeGuard {
|
||||
initial: sys::terminal::Config,
|
||||
file: openfiles::OpenFile,
|
||||
}
|
||||
|
||||
impl AutoModeGuard {
|
||||
/// Creates a new `AutoModeGuard` for the given file.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file` - The file representing the terminal to control.
|
||||
pub fn new(file: openfiles::OpenFile) -> Result<Self, error::Error> {
|
||||
let initial = sys::terminal::Config::from_term(&file)?;
|
||||
Ok(Self { initial, file })
|
||||
}
|
||||
|
||||
/// Applies the given terminal settings.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `settings` - The terminal settings to apply.
|
||||
pub fn apply_settings(&self, settings: &Settings) -> Result<(), error::Error> {
|
||||
let mut config = sys::terminal::Config::from_term(&self.file)?;
|
||||
config.update(settings);
|
||||
config.apply_to_term(&self.file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AutoModeGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.initial.apply_to_term(&self.file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! Shell test conditional expressions
|
||||
|
||||
use crate::{ExecutionParameters, Shell, error, extendedtests};
|
||||
|
||||
/// Evaluate the given test expression within the provided shell and
|
||||
/// execution context. Returns true if the expression evaluates to true,
|
||||
/// false otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `expr` - The test expression to evaluate.
|
||||
/// * `shell` - The shell context in which to evaluate the expression.
|
||||
/// * `params` - The execution parameters to use during evaluation.
|
||||
pub fn eval_expr(
|
||||
expr: &brush_parser::ast::TestExpr,
|
||||
shell: &mut Shell,
|
||||
params: &ExecutionParameters,
|
||||
) -> Result<bool, error::Error> {
|
||||
match expr {
|
||||
brush_parser::ast::TestExpr::False => Ok(false),
|
||||
brush_parser::ast::TestExpr::Literal(s) => Ok(!s.is_empty()),
|
||||
brush_parser::ast::TestExpr::And(left, right) => {
|
||||
Ok(eval_expr(left, shell, params)? && eval_expr(right, shell, params)?)
|
||||
}
|
||||
brush_parser::ast::TestExpr::Or(left, right) => {
|
||||
Ok(eval_expr(left, shell, params)? || eval_expr(right, shell, params)?)
|
||||
}
|
||||
brush_parser::ast::TestExpr::Not(expr) => Ok(!eval_expr(expr, shell, params)?),
|
||||
brush_parser::ast::TestExpr::Parenthesized(expr) => eval_expr(expr, shell, params),
|
||||
brush_parser::ast::TestExpr::UnaryTest(op, operand) => {
|
||||
extendedtests::apply_unary_predicate_to_str(op, operand, shell, params)
|
||||
}
|
||||
brush_parser::ast::TestExpr::BinaryTest(op, left, right) => {
|
||||
extendedtests::apply_binary_predicate_to_strs(op, left.as_str(), right.as_str(), shell)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Command timing
|
||||
|
||||
use crate::error;
|
||||
|
||||
struct StopwatchTime {
|
||||
now: std::time::SystemTime,
|
||||
self_user: std::time::Duration,
|
||||
self_system: std::time::Duration,
|
||||
children_user: std::time::Duration,
|
||||
children_system: std::time::Duration,
|
||||
}
|
||||
|
||||
impl StopwatchTime {
|
||||
#[allow(clippy::unchecked_time_subtraction)]
|
||||
fn minus(&self, other: &Self) -> Result<StopwatchTiming, error::Error> {
|
||||
let user = (self.self_user - other.self_user) + (self.children_user - other.children_user);
|
||||
let system =
|
||||
(self.self_system - other.self_system) + (self.children_system - other.children_system);
|
||||
|
||||
Ok(StopwatchTiming {
|
||||
wall: self.now.duration_since(other.now)?,
|
||||
user,
|
||||
system,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Stopwatch {
|
||||
start: StopwatchTime,
|
||||
}
|
||||
|
||||
impl Stopwatch {
|
||||
pub fn stop(&self) -> Result<StopwatchTiming, error::Error> {
|
||||
let end = get_current_stopwatch_time()?;
|
||||
end.minus(&self.start)
|
||||
}
|
||||
}
|
||||
pub(crate) struct StopwatchTiming {
|
||||
pub wall: std::time::Duration,
|
||||
pub user: std::time::Duration,
|
||||
pub system: std::time::Duration,
|
||||
}
|
||||
|
||||
pub(crate) fn start_timing() -> Result<Stopwatch, error::Error> {
|
||||
Ok(Stopwatch {
|
||||
start: get_current_stopwatch_time()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_current_stopwatch_time() -> Result<StopwatchTime, error::Error> {
|
||||
let now = std::time::SystemTime::now();
|
||||
let (self_user, self_system) = crate::sys::resource::get_self_user_and_system_time()?;
|
||||
let (children_user, children_system) =
|
||||
crate::sys::resource::get_children_user_and_system_time()?;
|
||||
|
||||
Ok(StopwatchTime {
|
||||
now,
|
||||
self_user,
|
||||
self_system,
|
||||
children_user,
|
||||
children_system,
|
||||
})
|
||||
}
|
||||
|
||||
/// Format the given duration in a non-POSIX-y way.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - The duration to format.
|
||||
pub fn format_duration_non_posixly(duration: &std::time::Duration) -> String {
|
||||
let minutes = duration.as_secs() / 60;
|
||||
let seconds = duration.as_secs() % 60;
|
||||
let millis = duration.subsec_millis();
|
||||
format!("{minutes}m{seconds}.{millis:03}s")
|
||||
}
|
||||
|
||||
/// Format the given duration in a POSIX-y way.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - The duration to format.
|
||||
pub fn format_duration_posixly(duration: &std::time::Duration) -> String {
|
||||
let seconds = duration.as_secs();
|
||||
let ten_millis = duration.subsec_millis() / 10;
|
||||
format!("{seconds}.{ten_millis:02}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_format_time() {
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_millis(0)),
|
||||
"0m0.000s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_millis(1)),
|
||||
"0m0.001s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_millis(123)),
|
||||
"0m0.123s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_millis(1234)),
|
||||
"0m1.234s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_millis(12345)),
|
||||
"0m12.345s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_millis(123_456)),
|
||||
"2m3.456s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_millis(1_234_567)),
|
||||
"20m34.567s"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_micros(1)),
|
||||
"0m0.000s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_micros(999)),
|
||||
"0m0.000s"
|
||||
);
|
||||
assert_eq!(
|
||||
format_duration_non_posixly(&Duration::from_micros(1000)),
|
||||
"0m0.001s"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! Trace utilities
|
||||
|
||||
/// Trace category for command execution.
|
||||
pub const COMMANDS: &str = "commands";
|
||||
/// Trace category for completion.
|
||||
pub const COMPLETION: &str = "completion";
|
||||
/// Trace category for word expansion.
|
||||
pub const EXPANSION: &str = "expansion";
|
||||
/// Trace category for function calls.
|
||||
pub const FUNCTIONS: &str = "functions";
|
||||
/// Trace category for user input.
|
||||
pub const INPUT: &str = "input";
|
||||
/// Trace category for job control.
|
||||
pub const JOBS: &str = "jobs";
|
||||
/// Trace category for parsing.
|
||||
pub const PARSE: &str = "parse";
|
||||
/// Trace category for shell patterns.
|
||||
pub const PATTERN: &str = "pattern";
|
||||
/// Trace category for unimplemented behavior.
|
||||
pub const UNIMPLEMENTED: &str = "unimplemented";
|
||||
@@ -0,0 +1,188 @@
|
||||
//! Facilities for configuring trap handlers.
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
use itertools::Itertools as _;
|
||||
|
||||
use crate::{error, sys};
|
||||
|
||||
/// Type of signal that can be trapped in the shell.
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum TrapSignal {
|
||||
/// A system signal.
|
||||
Signal(sys::signal::Signal),
|
||||
/// The `DEBUG` trap.
|
||||
Debug,
|
||||
/// The `ERR` trap.
|
||||
Err,
|
||||
/// The `EXIT` trap.
|
||||
Exit,
|
||||
/// The `RETURN` trp.
|
||||
Return,
|
||||
}
|
||||
|
||||
impl Display for TrapSignal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TrapSignal {
|
||||
/// Returns all possible values of [`TrapSignal`].
|
||||
pub fn iterator() -> impl Iterator<Item = Self> {
|
||||
const SIGNALS: &[TrapSignal] = &[TrapSignal::Debug, TrapSignal::Err, TrapSignal::Exit];
|
||||
|
||||
let iter = itertools::chain!(
|
||||
SIGNALS.iter().copied(),
|
||||
sys::signal::Signal::iterator().map(TrapSignal::Signal)
|
||||
);
|
||||
|
||||
iter
|
||||
}
|
||||
|
||||
/// Converts [`TrapSignal`] into its corresponding signal name as a [`&'static str`](str)
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Signal(s) => s.as_str(),
|
||||
Self::Debug => "DEBUG",
|
||||
Self::Err => "ERR",
|
||||
Self::Exit => "EXIT",
|
||||
Self::Return => "RETURN",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats [`Iterator<Item = TrapSignal>`](TrapSignal) to the provided writer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `f` - Any type that implements [`std::io::Write`].
|
||||
/// * `it` - An iterator over the signals that will be formatted into the `f`.
|
||||
pub fn format_signals(
|
||||
mut f: impl std::io::Write,
|
||||
it: impl Iterator<Item = TrapSignal>,
|
||||
) -> Result<(), error::Error> {
|
||||
let it = it
|
||||
.filter_map(|s| i32::try_from(s).ok().map(|n| (s, n)))
|
||||
.sorted_by(|a, b| Ord::cmp(&a.1, &b.1))
|
||||
.format_with("\n", |s, f| f(&format_args!("{}) {}", s.1, s.0)));
|
||||
write!(f, "{it}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// implement s.parse::<TrapSignal>()
|
||||
impl FromStr for TrapSignal {
|
||||
type Err = error::Error;
|
||||
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
|
||||
if let Ok(n) = s.parse::<i32>() {
|
||||
Self::try_from(n)
|
||||
} else {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// from a signal number
|
||||
impl TryFrom<i32> for TrapSignal {
|
||||
type Error = error::Error;
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
// NOTE: DEBUG and ERR are real-time signals, defined based on NSIG or SIGRTMAX (is not
|
||||
// available on bsd-like systems),
|
||||
// and don't have persistent numbers across platforms, so we skip them here.
|
||||
Ok(match value {
|
||||
0 => Self::Exit,
|
||||
value => Self::Signal(
|
||||
sys::signal::Signal::try_from(value)
|
||||
.map_err(|_| error::ErrorKind::InvalidSignal(value.to_string()))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// from a signal name
|
||||
impl TryFrom<&str> for TrapSignal {
|
||||
type Error = error::Error;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
#[allow(unused_mut, reason = "only mutated on some platforms")]
|
||||
let mut s = value.to_ascii_uppercase();
|
||||
|
||||
Ok(match s.as_str() {
|
||||
"DEBUG" => Self::Debug,
|
||||
"ERR" => Self::Err,
|
||||
"EXIT" => Self::Exit,
|
||||
"RETURN" => Self::Return,
|
||||
_ => {
|
||||
// Bash compatibility:
|
||||
// support for signal names without the `SIG` prefix, for example `HUP` -> `SIGHUP`
|
||||
if !s.starts_with("SIG") {
|
||||
s.insert_str(0, "SIG");
|
||||
}
|
||||
sys::signal::Signal::from_str(s.as_str())
|
||||
.map(TrapSignal::Signal)
|
||||
.map_err(|_| error::ErrorKind::InvalidSignal(value.into()))?
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type used when failing to convert a `TrapSignal` to a number.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TrapSignalNumberError;
|
||||
|
||||
impl TryFrom<TrapSignal> for i32 {
|
||||
type Error = TrapSignalNumberError;
|
||||
fn try_from(value: TrapSignal) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
TrapSignal::Signal(s) => s as Self,
|
||||
TrapSignal::Exit => 0,
|
||||
_ => return Err(TrapSignalNumberError),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for trap handlers in the shell.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TrapHandlerConfig {
|
||||
/// Registered handlers for traps; maps signal type to command.
|
||||
pub(crate) handlers: HashMap<TrapSignal, String>,
|
||||
/// Current depth of the handler stack.
|
||||
pub(crate) handler_depth: i32,
|
||||
}
|
||||
|
||||
impl TrapHandlerConfig {
|
||||
/// Iterates over the registered handlers for trap signals.
|
||||
pub fn iter_handlers(&self) -> impl Iterator<Item = (TrapSignal, &str)> {
|
||||
self.handlers
|
||||
.iter()
|
||||
.map(|(signal, cmd)| (*signal, cmd.as_str()))
|
||||
}
|
||||
|
||||
/// Tries to find the handler associated with the given signal.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `signal_type` - The type of signal to get the handler for.
|
||||
pub fn get_handler(&self, signal_type: TrapSignal) -> Option<&str> {
|
||||
self.handlers.get(&signal_type).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Registers a handler for a trap signal.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `signal_type` - The type of signal to register a handler for.
|
||||
/// * `command` - The command to execute when the signal is trapped.
|
||||
pub fn register_handler(&mut self, signal_type: TrapSignal, command: String) {
|
||||
let _ = self.handlers.insert(signal_type, command);
|
||||
}
|
||||
|
||||
/// Removes handlers for a trap signal.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `signal_type` - The type of signal to remove handlers for.
|
||||
pub fn remove_handlers(&mut self, signal_type: TrapSignal) {
|
||||
self.handlers.remove(&signal_type);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,514 @@
|
||||
use rand::Rng;
|
||||
|
||||
use crate::{Shell, ShellValue, ShellVariable, error, sys, variables};
|
||||
|
||||
const BASH_MAJOR: u32 = 5;
|
||||
const BASH_MINOR: u32 = 2;
|
||||
const BASH_PATCH: u32 = 15;
|
||||
const BASH_BUILD: u32 = 1;
|
||||
const BASH_RELEASE: &str = "release";
|
||||
const BASH_MACHINE: &str = "unknown";
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub(crate) fn initialize_vars(
|
||||
shell: &mut Shell,
|
||||
do_not_inherit_env: bool,
|
||||
) -> Result<(), error::Error> {
|
||||
// Seed parameters from environment (unless requested not to do so).
|
||||
if !do_not_inherit_env {
|
||||
for (k, v) in std::env::vars() {
|
||||
// See if it's a function exported by an ancestor process.
|
||||
if let Some(func_name) = k.strip_prefix("BASH_FUNC_") {
|
||||
if let Some(func_name) = func_name.strip_suffix("%%") {
|
||||
// Intentionally best-effort; don't fail out of the shell if we can't
|
||||
// parse an incoming function.
|
||||
if shell.define_func_from_str(func_name, v.as_str()).is_ok() {
|
||||
shell.func_mut(func_name).unwrap().export();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut var = ShellVariable::new(ShellValue::String(v));
|
||||
var.export();
|
||||
shell.env.set_global(k, var)?;
|
||||
}
|
||||
}
|
||||
|
||||
let shell_version = shell.version().clone();
|
||||
shell.env.set_global(
|
||||
"BRUSH_VERSION",
|
||||
ShellVariable::new(shell_version.unwrap_or_default()),
|
||||
)?;
|
||||
|
||||
// TODO(#479): implement $_
|
||||
|
||||
// BASH
|
||||
if let Some(shell_name) = &shell.shell_name {
|
||||
shell
|
||||
.env
|
||||
.set_global("BASH", ShellVariable::new(shell_name))?;
|
||||
}
|
||||
|
||||
// BASHOPTS
|
||||
let mut bashopts_var = ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| shell.options.shopt_optstr().into(),
|
||||
setter: |_| (),
|
||||
});
|
||||
bashopts_var.set_readonly();
|
||||
shell.env.set_global("BASHOPTS", bashopts_var)?;
|
||||
|
||||
// BASHPID
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut bashpid_var =
|
||||
ShellVariable::new(ShellValue::String(std::process::id().to_string()));
|
||||
bashpid_var.treat_as_integer();
|
||||
shell.env.set_global("BASHPID", bashpid_var)?;
|
||||
}
|
||||
|
||||
// BASH_ALIASES
|
||||
shell.env.set_global(
|
||||
"BASH_ALIASES",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| {
|
||||
let values = variables::ArrayLiteral(
|
||||
shell
|
||||
.aliases
|
||||
.iter()
|
||||
.map(|(k, v)| (Some(k.to_owned()), v.to_owned()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
ShellValue::associative_array_from_literals(values).unwrap()
|
||||
},
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// TODO(vars): when extdebug is enabled, BASH_ARGC and BASH_ARGV are set to valid values
|
||||
// TODO(vars): implement BASH_ARGC
|
||||
// TODO(vars): implement BASH_ARGV
|
||||
|
||||
// BASH_ARGV0
|
||||
shell.env.set_global(
|
||||
"BASH_ARGV0",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| {
|
||||
let argv0 = shell.shell_name.as_deref().unwrap_or_default();
|
||||
argv0.to_string().into()
|
||||
},
|
||||
// TODO(vars): implement updating BASH_ARGV0
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// TODO(vars): implement mutation of BASH_CMDS
|
||||
shell.env.set_global(
|
||||
"BASH_CMDS",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| shell.program_location_cache.to_value().unwrap(),
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// TODO(vars): implement BASH_COMMAND
|
||||
// TODO(vars): implement BASH_EXECUTIION_STRING
|
||||
// TODO(vars): implement BASH_LINENO
|
||||
|
||||
// BASH_SOURCE
|
||||
shell.env.set_global(
|
||||
"BASH_SOURCE",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| get_bash_source_value(shell),
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// BASH_SUBSHELL
|
||||
shell.env.set_global(
|
||||
"BASH_SUBSHELL",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| shell.depth().to_string().into(),
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// BASH_VERSINFO
|
||||
let mut bash_versinfo_var = ShellVariable::new(ShellValue::indexed_array_from_strs(
|
||||
[
|
||||
BASH_MAJOR.to_string().as_str(),
|
||||
BASH_MINOR.to_string().as_str(),
|
||||
BASH_PATCH.to_string().as_str(),
|
||||
BASH_BUILD.to_string().as_str(),
|
||||
BASH_RELEASE,
|
||||
BASH_MACHINE,
|
||||
]
|
||||
.as_slice(),
|
||||
));
|
||||
bash_versinfo_var.set_readonly();
|
||||
shell.env.set_global("BASH_VERSINFO", bash_versinfo_var)?;
|
||||
|
||||
// BASH_VERSION
|
||||
// This is the Bash interface version. See BRUSH_VERSION for its implementation version.
|
||||
shell.env.set_global(
|
||||
"BASH_VERSION",
|
||||
ShellVariable::new(std::format!(
|
||||
"{BASH_MAJOR}.{BASH_MINOR}.{BASH_PATCH}({BASH_BUILD})-{BASH_RELEASE}"
|
||||
)),
|
||||
)?;
|
||||
|
||||
// COMP_WORDBREAKS
|
||||
shell
|
||||
.env
|
||||
.set_global("COMP_WORDBREAKS", ShellVariable::new(" \t\n\"\'@><=;|&(:"))?;
|
||||
|
||||
// DIRSTACK
|
||||
shell.env.set_global(
|
||||
"DIRSTACK",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| {
|
||||
shell
|
||||
.directory_stack
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
},
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// EPOCHREALTIME
|
||||
shell.env.set_global(
|
||||
"EPOCHREALTIME",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |_shell| {
|
||||
let now = std::time::SystemTime::now();
|
||||
let since_epoch = now
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
since_epoch.as_secs_f64().to_string().into()
|
||||
},
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// EPOCHSECONDS
|
||||
shell.env.set_global(
|
||||
"EPOCHSECONDS",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |_shell| {
|
||||
let now = std::time::SystemTime::now();
|
||||
let since_epoch = now
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
since_epoch.as_secs().to_string().into()
|
||||
},
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// EUID
|
||||
if let Ok(euid) = sys::users::get_effective_uid() {
|
||||
let mut euid_var = ShellVariable::new(ShellValue::String(format!("{euid}")));
|
||||
euid_var.treat_as_integer().set_readonly();
|
||||
shell.env.set_global("EUID", euid_var)?;
|
||||
}
|
||||
|
||||
// FUNCNAME
|
||||
shell.env.set_global(
|
||||
"FUNCNAME",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| get_funcname_value(shell),
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// GROUPS
|
||||
// N.B. We could compute this up front, but we choose to make it dynamic so that we
|
||||
// don't have to make costly system calls if the user never accesses it.
|
||||
shell.env.set_global(
|
||||
"GROUPS",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |_shell| {
|
||||
let groups = get_current_user_gids();
|
||||
ShellValue::indexed_array_from_strings(
|
||||
groups.into_iter().map(|gid| gid.to_string()),
|
||||
)
|
||||
},
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// HISTCMD
|
||||
let mut histcmd_var = ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| {
|
||||
shell
|
||||
.history()
|
||||
.map_or_else(|| "0".into(), |h| h.count().to_string().into())
|
||||
},
|
||||
setter: |_| (),
|
||||
});
|
||||
histcmd_var.treat_as_integer();
|
||||
shell.env.set_global("HISTCMD", histcmd_var)?;
|
||||
|
||||
// HISTFILE (if not already set)
|
||||
if !shell.env.is_set("HISTFILE") {
|
||||
if let Some(home_dir) = shell.home_dir() {
|
||||
let histfile = home_dir.join(".brush_history");
|
||||
shell.env.set_global(
|
||||
"HISTFILE",
|
||||
ShellVariable::new(ShellValue::String(histfile.to_string_lossy().to_string())),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// HOSTNAME
|
||||
shell.env.set_global(
|
||||
"HOSTNAME",
|
||||
ShellVariable::new(
|
||||
sys::network::get_hostname()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
// HOSTTYPE
|
||||
shell.env.set_global(
|
||||
"HOSTTYPE",
|
||||
ShellVariable::new(std::env::consts::ARCH.to_string()),
|
||||
)?;
|
||||
|
||||
// IFS
|
||||
shell.env.set_global("IFS", ShellVariable::new(" \t\n"))?;
|
||||
|
||||
// LINENO
|
||||
shell.env.set_global(
|
||||
"LINENO",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| shell.current_line_number().to_string().into(),
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// MACHTYPE
|
||||
shell
|
||||
.env
|
||||
.set_global("MACHTYPE", ShellVariable::new(BASH_MACHINE))?;
|
||||
|
||||
// OLDPWD (initialization)
|
||||
if !shell.env.is_set("OLDPWD") {
|
||||
let mut oldpwd_var =
|
||||
ShellVariable::new(ShellValue::Unset(variables::ShellValueUnsetType::Untyped));
|
||||
oldpwd_var.export();
|
||||
shell.env.set_global("OLDPWD", oldpwd_var)?;
|
||||
}
|
||||
|
||||
// OPTERR
|
||||
shell.env.set_global("OPTERR", ShellVariable::new("1"))?;
|
||||
|
||||
// OPTIND
|
||||
let mut optind_var = ShellVariable::new("1");
|
||||
optind_var.treat_as_integer();
|
||||
shell.env.set_global("OPTIND", optind_var)?;
|
||||
|
||||
// OSTYPE
|
||||
let os_type = match std::env::consts::OS {
|
||||
"linux" => "linux-gnu",
|
||||
"windows" => "windows",
|
||||
_ => "unknown",
|
||||
};
|
||||
shell
|
||||
.env
|
||||
.set_global("OSTYPE", ShellVariable::new(os_type))?;
|
||||
|
||||
// PATH (if not already set)
|
||||
if !shell.env.is_set("PATH") {
|
||||
let default_path_str = sys::fs::get_default_executable_search_paths().join(":");
|
||||
shell
|
||||
.env
|
||||
.set_global("PATH", ShellVariable::new(default_path_str))?;
|
||||
}
|
||||
|
||||
// PIPESTATUS
|
||||
// TODO: Investigate what happens if this gets unset.
|
||||
// TODO: Investigate if this needs to be saved/preserved across prompt display.
|
||||
shell.env.set_global(
|
||||
"PIPESTATUS",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| {
|
||||
ShellValue::indexed_array_from_strings(
|
||||
shell.last_pipeline_statuses.iter().map(|s| s.to_string()),
|
||||
)
|
||||
},
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// PPID
|
||||
if let Some(ppid) = sys::terminal::get_parent_process_id() {
|
||||
let mut ppid_var = ShellVariable::new(ppid.to_string());
|
||||
ppid_var.treat_as_integer().set_readonly();
|
||||
shell.env.set_global("PPID", ppid_var)?;
|
||||
}
|
||||
|
||||
// RANDOM
|
||||
let mut random_var = ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: get_random_value,
|
||||
setter: |_| (),
|
||||
});
|
||||
random_var.treat_as_integer();
|
||||
shell.env.set_global("RANDOM", random_var)?;
|
||||
|
||||
// SECONDS
|
||||
shell.env.set_global(
|
||||
"SECONDS",
|
||||
ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| {
|
||||
let now = std::time::SystemTime::now();
|
||||
let since_last = now
|
||||
.duration_since(shell.last_stopwatch_time())
|
||||
.unwrap_or_default();
|
||||
let total_seconds = since_last.as_secs() + u64::from(shell.last_stopwatch_offset());
|
||||
total_seconds.to_string().into()
|
||||
},
|
||||
// TODO(vars): implement updating SECONDS
|
||||
setter: |_| (),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// SHELL
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
shell.env.set_global(
|
||||
"SHELL",
|
||||
ShellVariable::new(exe_path.to_string_lossy().to_string()),
|
||||
)?;
|
||||
}
|
||||
|
||||
// SHELLOPTS
|
||||
let mut shellopts_var = ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: |shell| shell.options.seto_optstr().into(),
|
||||
setter: |_| (),
|
||||
});
|
||||
shellopts_var.set_readonly();
|
||||
shell.env.set_global("SHELLOPTS", shellopts_var)?;
|
||||
|
||||
// SHLVL
|
||||
let input_shlvl = shell.env_str("SHLVL").unwrap_or_else(|| "0".into());
|
||||
let updated_shlvl = input_shlvl.as_ref().parse::<u32>().unwrap_or(0) + 1;
|
||||
let mut shlvl_var = ShellVariable::new(updated_shlvl.to_string());
|
||||
shlvl_var.export();
|
||||
shell.env.set_global("SHLVL", shlvl_var)?;
|
||||
|
||||
// SRANDOM
|
||||
let mut random_var = ShellVariable::new(ShellValue::Dynamic {
|
||||
getter: get_srandom_value,
|
||||
setter: |_| (),
|
||||
});
|
||||
random_var.treat_as_integer();
|
||||
shell.env.set_global("SRANDOM", random_var)?;
|
||||
|
||||
// PS1 / PS2
|
||||
if shell.options.interactive {
|
||||
if !shell.env.is_set("PS1") {
|
||||
shell
|
||||
.env
|
||||
.set_global("PS1", ShellVariable::new(r"\s-\v\$ "))?;
|
||||
}
|
||||
|
||||
if !shell.env.is_set("PS2") {
|
||||
shell.env.set_global("PS2", ShellVariable::new("> "))?;
|
||||
}
|
||||
}
|
||||
|
||||
// PS4
|
||||
if !shell.env.is_set("PS4") {
|
||||
shell.env.set_global("PS4", ShellVariable::new("+ "))?;
|
||||
}
|
||||
|
||||
//
|
||||
// PWD
|
||||
//
|
||||
// Reflect our actual working directory. There's a chance
|
||||
// we inherited an out-of-sync version of the variable. Future updates
|
||||
// will be handled by set_working_dir().
|
||||
//
|
||||
let pwd = shell.working_dir().to_string_lossy().to_string();
|
||||
let mut pwd_var = ShellVariable::new(pwd);
|
||||
pwd_var.export();
|
||||
shell.env.set_global("PWD", pwd_var)?;
|
||||
|
||||
// UID
|
||||
if let Ok(uid) = sys::users::get_current_uid() {
|
||||
let mut uid_var = ShellVariable::new(ShellValue::String(format!("{uid}")));
|
||||
uid_var.treat_as_integer().set_readonly();
|
||||
shell.env.set_global("UID", uid_var)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a list of the current user's group IDs, with the effective GID at the front.
|
||||
fn get_current_user_gids() -> Vec<u32> {
|
||||
let mut groups = sys::users::get_user_group_ids().unwrap_or_default();
|
||||
|
||||
// If the effective GID is present but not in the first position in the list, then move
|
||||
// it there.
|
||||
if let Ok(gid) = sys::users::get_effective_gid() {
|
||||
if let Some(index) = groups.iter().position(|&g| g == gid) {
|
||||
if index > 0 {
|
||||
// Move it to the front.
|
||||
groups.remove(index);
|
||||
groups.insert(0, gid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
fn get_random_value(_shell: &Shell) -> ShellValue {
|
||||
let mut rng = rand::rng();
|
||||
let num = rng.random_range(0..32768);
|
||||
let str = num.to_string();
|
||||
str.into()
|
||||
}
|
||||
|
||||
fn get_srandom_value(_shell: &Shell) -> ShellValue {
|
||||
let mut rng = rand::rng();
|
||||
let num: u32 = rng.random();
|
||||
let str = num.to_string();
|
||||
str.into()
|
||||
}
|
||||
|
||||
fn get_funcname_value(shell: &Shell) -> variables::ShellValue {
|
||||
if shell.function_call_stack().is_empty() {
|
||||
ShellValue::Unset(variables::ShellValueUnsetType::IndexedArray)
|
||||
} else {
|
||||
shell
|
||||
.function_call_stack()
|
||||
.iter()
|
||||
.map(|s| s.function_name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bash_source_value(shell: &Shell) -> variables::ShellValue {
|
||||
if shell.function_call_stack().is_empty() {
|
||||
let top_frame = shell.script_call_stack().iter().next();
|
||||
top_frame
|
||||
.map_or_else(Vec::new, |frame| vec![frame.source.as_ref()])
|
||||
.into()
|
||||
} else {
|
||||
shell
|
||||
.function_call_stack()
|
||||
.iter()
|
||||
.map(|s| s.function_definition.source.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ workspace = true
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
napi = { version = "3", features = ["napi10", "tokio_rt", "tokio_time"] }
|
||||
napi-derive = "3"
|
||||
brush-core = "0.4.0"
|
||||
brush-core = { version = "0.4.0", path = "../brush-core-vendored" }
|
||||
brush-builtins = "0.1.0"
|
||||
parking_lot = "0.12.5"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { executeBash } from "@oh-my-pi/pi-coding-agent/exec/bash-executor";
|
||||
import { DEFAULT_MAX_BYTES } from "@oh-my-pi/pi-coding-agent/tools/truncate";
|
||||
import * as shellSnapshot from "@oh-my-pi/pi-coding-agent/utils/shell-snapshot";
|
||||
|
||||
function makeTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "omp-bash-exec-"));
|
||||
@@ -20,6 +22,7 @@ describe("executeBash", () => {
|
||||
|
||||
afterEach(() => {
|
||||
_resetSettingsForTest();
|
||||
vi.restoreAllMocks();
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
@@ -45,6 +48,22 @@ describe("executeBash", () => {
|
||||
expect(result.output.trim()).toBe("hello");
|
||||
});
|
||||
|
||||
it("invokes onChunk with command output", async () => {
|
||||
let seenChunk: string | null = null;
|
||||
const result = await executeBash("echo hello", {
|
||||
cwd: tempDir,
|
||||
timeout: 5000,
|
||||
onChunk: chunk => {
|
||||
if (seenChunk === null) {
|
||||
seenChunk = chunk;
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(result.output.trim()).toBe("hello");
|
||||
expect(seenChunk).not.toBeNull();
|
||||
expect(seenChunk ?? "").toContain("hello");
|
||||
});
|
||||
|
||||
it("times out commands", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
@@ -54,6 +73,16 @@ describe("executeBash", () => {
|
||||
expect(result.output).toContain("timed out");
|
||||
});
|
||||
|
||||
it("times out before follow-up output", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const result = await executeBash("sleep 10; echo done", { cwd: tempDir, timeout: 50 });
|
||||
expect(result.cancelled).toBe(true);
|
||||
expect(result.output).toContain("timed out");
|
||||
expect(result.output).not.toContain("done");
|
||||
});
|
||||
|
||||
it("aborts commands", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
@@ -71,12 +100,33 @@ describe("executeBash", () => {
|
||||
expect(result.output).toContain("Command cancelled");
|
||||
});
|
||||
|
||||
it("aborts before follow-up output", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const promise = executeBash("sleep 10; echo done", {
|
||||
cwd: tempDir,
|
||||
timeout: 5000,
|
||||
signal: controller.signal,
|
||||
});
|
||||
await Bun.sleep(100);
|
||||
controller.abort();
|
||||
const result = await promise;
|
||||
expect(result.cancelled).toBe(true);
|
||||
expect(result.output).toContain("Command cancelled");
|
||||
expect(result.output).not.toContain("done");
|
||||
});
|
||||
|
||||
it("streams output chunks", async () => {
|
||||
const chunks: string[] = [];
|
||||
const result = await executeBash("i=1; while [ $i -le 20 ]; do echo line$i; i=$((i+1)); done", {
|
||||
cwd: tempDir,
|
||||
timeout: 5000,
|
||||
onChunk: chunk => chunks.push(chunk),
|
||||
onChunk: chunk => {
|
||||
expect(chunk.length).toBeGreaterThan(0);
|
||||
chunks.push(chunk);
|
||||
},
|
||||
});
|
||||
const combined = chunks.join("");
|
||||
expect(chunks.length).toBeGreaterThan(0);
|
||||
@@ -86,6 +136,53 @@ describe("executeBash", () => {
|
||||
expect(result.output).toContain("line20");
|
||||
});
|
||||
|
||||
it("streams large output without exhausting memory", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
let totalBytes = 0;
|
||||
let sawChunk = false;
|
||||
const result = await executeBash("awk 'BEGIN { for (i = 0; i < 100000; i++) printf \"a\" }'", {
|
||||
cwd: tempDir,
|
||||
timeout: 5000,
|
||||
onChunk: chunk => {
|
||||
sawChunk = true;
|
||||
totalBytes += Buffer.byteLength(chunk, "utf-8");
|
||||
},
|
||||
});
|
||||
expect(sawChunk).toBe(true);
|
||||
expect(totalBytes).toBe(100000);
|
||||
expect(result.totalBytes).toBe(100000);
|
||||
expect(result.outputBytes).toBeLessThanOrEqual(DEFAULT_MAX_BYTES);
|
||||
expect(result.output).toContain("a");
|
||||
});
|
||||
|
||||
it("sources snapshot env vars across session commands", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const bashPath = process.env.SHELL?.includes("bash") ? process.env.SHELL : "/bin/bash";
|
||||
if (!fs.existsSync(bashPath)) {
|
||||
return;
|
||||
}
|
||||
const snapshotPath = path.join(tempDir, "snapshot.sh");
|
||||
fs.writeFileSync(snapshotPath, "export OMP_SNAPSHOT_TEST=from_snapshot\n");
|
||||
vi.spyOn(Settings.prototype, "getShellConfig").mockReturnValue({
|
||||
shell: bashPath,
|
||||
args: ["-l", "-c"],
|
||||
env: {
|
||||
PATH: process.env.PATH ?? "",
|
||||
HOME: process.env.HOME ?? tempDir,
|
||||
},
|
||||
prefix: undefined,
|
||||
});
|
||||
vi.spyOn(shellSnapshot, "getOrCreateSnapshot").mockResolvedValue(snapshotPath);
|
||||
const sessionKey = "snapshot-test";
|
||||
await executeBash("true", { cwd: tempDir, timeout: 5000, sessionKey });
|
||||
const result = await executeBash("echo $OMP_SNAPSHOT_TEST", { cwd: tempDir, timeout: 5000, sessionKey });
|
||||
expect(result.output.trim()).toBe("from_snapshot");
|
||||
});
|
||||
|
||||
it("does not allow exec to replace the host", async () => {
|
||||
const result = await executeBash("exec echo hi", { cwd: tempDir, timeout: 5000 });
|
||||
expect(result.cancelled).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user