From 9bff77613e50ffea01b670cfb012e7d7ce6fd71c Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 28 May 2026 20:19:53 +0200 Subject: [PATCH] Initial import --- .cargo/config.toml | 8 + .gitignore | 1 + CODE_OF_CONDUCT.md | 128 +++++ CONTRIBUTING.md | 260 +++++++++ Cargo.lock | 1276 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 31 + LICENSE | 18 + README.md | 37 ++ SECURITY.md | 44 ++ src/context_buffer.rs | 102 ++++ src/lib.rs | 920 +++++++++++++++++++++++++++++ src/line_buffer.rs | 109 ++++ src/main.rs | 1 + src/matcher.rs | 271 +++++++++ src/output.rs | 271 +++++++++ src/searcher.rs | 467 +++++++++++++++ tests/test_grep.rs | 1143 ++++++++++++++++++++++++++++++++++++ 17 files changed, 5087 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 src/context_buffer.rs create mode 100644 src/lib.rs create mode 100644 src/line_buffer.rs create mode 100644 src/main.rs create mode 100644 src/matcher.rs create mode 100644 src/output.rs create mode 100644 src/searcher.rs create mode 100644 tests/test_grep.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..7965fae --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.x86_64-unknown-redox] +linker = "x86_64-unknown-redox-gcc" +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +[target.riscv64gc-unknown-linux-musl] +rustflags = ["-C", "target-feature=+crt-static"] +[target.'cfg(target_env = "msvc")'] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..39474f7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socioeconomic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +sylvestre@debian.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..54c509d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,260 @@ + + +# Contributing to grep + +Hi! Welcome to uutils/grep! + +Thanks for wanting to contribute to this project! This document explains +everything you need to know to contribute. Before you start make sure to also +check out these documents: + +- Our community's [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). +- [DEVELOPMENT.md](./DEVELOPMENT.md) for setting up your development + environment. + +Now follows a very important warning: + +> [!WARNING] +> uutils is original code and cannot contain any code from GNU or +> other implementations. This means that **we cannot accept any changes based on +> the GNU source code**. To make sure that cannot happen, **you cannot link to +> the GNU source code** either. It is however possible to look at other implementations +> under a BSD or MIT license like [Apple's implementation](https://opensource.apple.com/source/file_cmds/) +> or [OpenBSD](https://github.com/openbsd/src/tree/master/bin). + +Finally, feel free to join our [Discord](https://discord.gg/wQVJbvJ)! + + + +## Design Goals + +We have the following goals with our development: + +- **Compatible**: The utilities should be a drop-in replacement for the GNU + coreutils. +- **Cross-platform**: All utilities should run on as many of the supported + platforms as possible. +- **Reliable**: The utilities should never unexpectedly fail. +- **Performant**: Our utilities should be written in fast idiomatic Rust. We aim + to match or exceed the performance of the GNU utilities. +- **Well-tested**: We should have a lot of tests to be able to guarantee + reliability and compatibility. + +## How to Help + +There are several ways to help and writing code is just one of them. Reporting +issues and writing documentation are just as important as writing code. + +### Reporting Issues + +We can't fix bugs we don't know about, so good issues are super helpful! Here +are some tips for writing good issues: + +- If you find a bug, make sure it's still a problem on the `main` branch. +- Search through the existing issues to see whether it has already been + reported. +- Make sure to include all relevant information, such as: + - Which version of uutils did you check? + - Which version of GNU coreutils are you comparing with? + - What platform are you on? +- Provide a way to reliably reproduce the issue. +- Be as specific as possible! + +### Writing Documentation + +There's never enough documentation. If you come across any documentation that +could be improved, feel free to submit a PR for it! + +### Writing Code + +If you want to submit a PR, make sure that you've discussed the solution with +the maintainers beforehand. We want to avoid situations where you put a lot of +work into a fix that we can't merge! If there's no issue for what you're trying +to fix yet, make one _before_ you start working on the PR. + +Generally, we try to follow what GNU is doing in terms of options and behavior. +It is recommended to look at the GNU Grep manual +([on the web](https://www.gnu.org/software/grep/manual/html_node/index.html), +or locally using `info `). It is more in depth than the man pages and +provides a good description of available features and their implementation +details. But remember, you cannot look at the GNU source code! + +Also remember that we can only merge PRs which pass our test suite, follow +rustfmt, and do not have any warnings from clippy. See +[DEVELOPMENT.md](./DEVELOPMENT.md) for more information. Be sure to also read +about our [Rust style](#our-rust-style). + +## Our Rust Style + +We want uutils to be written in idiomatic Rust, so here are some guidelines to +follow. Some of these are aspirational, meaning that we don't do them correctly +everywhere in the code. If you find violations of the advice below, feel free to +submit a patch! + +### Don't `panic!` + +The coreutils should be very reliable. This means that we should never `panic!`. +Therefore, you should avoid using `.unwrap()` and `panic!`. Sometimes the use of +`unreachable!` can be justified with a comment explaining why that code is +unreachable. + +### Don't `exit` + +We want uutils to be embeddable in other programs. This means that no function +in uutils should exit the program. Doing so would also lead to code with more +confusing control flow. Avoid therefore `std::process::exit` and similar +functions which exit the program early. + +### `unsafe` + +uutils cannot be entirely safe, because we have to call out to `libc` and do +syscalls. However, we still want to limit our use of `unsafe`. We generally only +accept `unsafe` for FFI, with very few exceptions. Note that performance is very +rarely a valid argument for using `unsafe`. + +If you still need to write code with `unsafe`, make sure to read the +[Rustonomicon](https://doc.rust-lang.org/nomicon/intro.html) and annotate the +calls with `// SAFETY:` comments explaining why the use of `unsafe` is sound. + +### Macros + +Macros can be a great tool, but they are also usually hard to understand. They +should be used sparingly. Make sure to explore simpler options before you reach +for a solution involving macros. + +### `str`, `OsStr` & `Path` + +Rust has many string-like types, and sometimes it's hard to choose the right +one. It's tempting to use `str` (and `String`) for everything, but that is not +always the right choice for uutils, because we need to support invalid UTF-8, +just like the GNU coreutils. For example, paths on Linux might not be valid +UTF-8! Whenever we are dealing with paths, we should therefore stick with +`OsStr` and `Path`. Make sure that you only convert to `str`/`String` if you +know that something is always valid UTF-8. If you need more operations on +`OsStr`, you can use the [`bstr`](https://docs.rs/bstr/latest/bstr/) crate. + +### Doc-comments + +We use rustdoc for our documentation, so it's best to follow +[rustdoc's guidelines](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html#documenting-components). +Make sure that your documentation is not just repeating the name of the +function, but actually giving more useful information. Rustdoc recommends the +following structure: + +``` +[short sentence explaining what it is] + +[more detailed explanation] + +[at least one code example that users can copy/paste to try it] + +[even more advanced explanations if necessary] +``` + +### Other comments + +Comments should be written to _explain_ the code, not to _describe_ the code. +Try to focus on explaining _why_ the code is the way it is. If you feel like you +have to describe the code, that's usually a sign that you could improve the +naming of variables and functions. + +If you edit a piece of code, make sure to update any comments that need to +change as a result. The only thing worse than having no comments is having +outdated comments! + +## Git Etiquette + +To ensure easy collaboration, we have guidelines for using Git and GitHub. + +### Commits + +- Make small and atomic commits. +- Keep a clean history of commits. +- Write informative commit messages. +- Annotate your commit message with the component you're editing. For example: + `cp: do not overwrite on with -i` or `uucore: add support for FreeBSD`. +- Do not unnecessarily move items around in the code. This makes the changes + much harder to review. If you do need to move things around, do that in a + separate commit. + +### Commit messages + +You can read this section in the Git book to learn how to write good commit +messages: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project. + +In addition, here are a few examples for a summary line when committing to +uutils: + +- commit for a single utility + +``` +nohup: cleanup and refactor +``` + +- commit for a utility's tests + +``` +tests/rm: test new feature +``` + +Beyond changes to an individual utility or its tests, other summary lines for +non-utility modules include: + +``` +README: add help +uucore: add new modules +uutils: add new utility +gitignore: add temporary files +``` + +### PRs + +- Make the titles of PRs descriptive. + - This means describing the problem you solve. For example, do not write + `Fix #1234`, but `ls: fix version sort order`. + - You can prefix the title with the utility the PR concerns. +- Keep PRs small and self-contained. A set of small PRs is much more likely to + get merged quickly than one large PR. +- Make sure the CI passes (up to intermittently failing tests). +- You know your code best, that's why it's best if you can solve merge conflicts + on your branch yourself. + - It's up to you whether you want to use `git merge main` or + `git rebase main`. + - Feel free to ask for help with merge conflicts. +- You do not need to ping maintainers to request a review, but it's fine to do + so if you don't get a response within a few days. + +## Platforms + +We take pride in supporting many operating systems and architectures. Any code +you contribute must at least compile without warnings for all platforms in the +CI. However, you can use `#[cfg(...)]` attributes to create platform dependent +features. + +## Licensing + +uutils is distributed under the terms of the MIT License; see the `LICENSE` file +for details. This is a permissive license, which allows the software to be used +with few restrictions. + +Copyrights in the uutils project are retained by their contributors, and no +copyright assignment is required to contribute. + +If you wish to add or change dependencies as part of a contribution to the +project, a tool like `cargo-license` can be used to show their license details. +The following types of license are acceptable: + +- MIT License +- Dual- or tri-license with an MIT License option ("Apache-2.0 or MIT" is a + popular combination) +- "MIT equivalent" license (2-clause BSD, 3-clause BSD, ISC) +- License less restrictive than the MIT License (CC0 1.0 Universal) +- Apache License version 2.0 + +Licenses we will not use: + +- An ambiguous license, or no license +- Strongly reciprocal licenses (GNU GPL, GNU LGPL) + +If you wish to add a reference but it doesn't meet these requirements, please +raise an issue to describe the dependency. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4d21e03 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1276 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dns-lookup" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e39034cee21a2f5bbb66ba0e3689819c4bb5d00382a282006e802a7ffa6c41d" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "windows-sys 0.60.2", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-static" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "os_display" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rlimit" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3" +dependencies = [ + "libc", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uu_grep" +version = "0.1.0" +dependencies = [ + "clap", + "glob", + "memchr", + "onig", + "onig_sys", + "uucore", + "uutests", + "walkdir", +] + +[[package]] +name = "uucore" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d779636d827cde4100f0e65ff3fd23b0b1f1195055475c6e6813d425f30c8e" +dependencies = [ + "clap", + "dns-lookup", + "fluent", + "fluent-bundle", + "fluent-syntax", + "jiff", + "libc", + "nix", + "os_display", + "rustc-hash", + "rustix", + "thiserror", + "time", + "unic-langid", + "uucore_procs", + "wild", +] + +[[package]] +name = "uucore_procs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da865e8a648b260dbd89fca76d0801995877ab27a4e259b6dec30ba9743b86ab" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "uutests" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d93dba8eceb0bebb686b07571b214e94be8cc739c929d0ea773c83c06d46df5" +dependencies = [ + "ctor", + "libc", + "nix", + "pretty_assertions", + "rand", + "regex", + "rlimit", + "tempfile", + "uucore", + "xattr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wild" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" +dependencies = [ + "glob", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "serde", + "zerofrom", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5540713 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "uu_grep" +description = "A Rust implementation of GNU Grep" +repository = "https://github.com/microsoft/uutils-grep" +edition = "2024" +rust-version = "1.88.0" +version = "0.1.0" +license = "MIT" +homepage = "https://github.com/microsoft/uutils-grep" +keywords = ["grep", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + +[[bin]] +name = "grep" +path = "src/main.rs" + +[lib] +name = "uu_grep" +path = "src/lib.rs" + +[dependencies] +clap = { version = "4.5", features = ["wrap_help", "cargo", "color"] } +glob = "0.3.1" +memchr = "2.7.2" +onig = { version = "~6.5.1", default-features = false } +onig_sys = { version = "*", default-features = false } +uucore = "0.8.0" +walkdir = "2.5" + +[dev-dependencies] +uutests = "0.8.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21bd444 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) uutils developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f51f12 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Grep, now in Rust + +A Rust implementation of [GNU Grep](https://www.gnu.org/software/grep/). +This project is an initial release and may contain bugs. + +## Building + +Download Rust at: https://rustup.rs/ + +```shell +# Check out this repository +git clone https://github.com/uutils/grep +cd grep + +# Build a release version +cargo build --release + +# Run! +./target/release/grep --help + +# Run tests (if needed; after making changes) +cargo test +``` + +## Known Issues + +* Does not take `LANG`, etc., into account for handling file encodings (non-UTF8 matches are treated as binary) + +## Contributing + +To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). + +## License + +uutils is licensed under the MIT License - see the `LICENSE` file for details + +GNU Grep is licensed under the GPL 3.0 or later. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f9936b6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,44 @@ +# Security Policy + +## Supported Versions + +We provide security updates only for the latest released version of `uutils/grep`. +Older versions may not receive patches. +If you are using a version packaged by your Linux distribution, please check with your distribution maintainers for their update policy. + +--- + +## Reporting a Vulnerability + +**Do not open public GitHub issues for security vulnerabilities.** +This prevents accidental disclosure before a fix is available. + +Instead, please use the following method: + +- **Email:** [sylvestre@debian.org](mailto:Sylvestre@debian.org) +- **Encryption (optional):** You may encrypt your report using our PGP key: +Fingerprint: B60D B599 4D39 BEC4 D1A9 5CCF 7E65 28DA 752F 1BE1 +--- + +### What to Include in Your Report + +To help us investigate and resolve the issue quickly, please include as much detail as possible: + +- **Type of issue:** e.g. privilege escalation, information disclosure. +- **Location in the source:** file path, commit hash, branch, or tag. +- **Steps to reproduce:** exact commands, test cases, or scripts. +- **Special configuration:** any flags, environment variables, or system setup required. +- **Affected systems:** OS/distribution and version(s) where the issue occurs. +- **Impact:** your assessment of the potential severity (DoS, RCE, data leak, etc.). + +--- + +## Disclosure Policy + +We follow a **Coordinated Vulnerability Disclosure (CVD)** process: + +1. We will acknowledge receipt of your report within **10 days**. +2. We will investigate, reproduce, and assess the issue. +3. We will provide a timeline for developing and releasing a fix. +4. Once a fix is available, we will publish a GitHub Security Advisory. +5. You will be credited in the advisory unless you request anonymity. diff --git a/src/context_buffer.rs b/src/context_buffer.rs new file mode 100644 index 0000000..9e9b0d8 --- /dev/null +++ b/src/context_buffer.rs @@ -0,0 +1,102 @@ +pub struct LineView<'a> { + /// Line content (without the terminator). + pub line: &'a [u8], + /// 1-based line number. + pub line_number: u64, + /// Byte offset of this line in the input stream. + pub byte_offset: u64, + /// Whether this line matched (vs. being a context line). + pub is_match: bool, + /// Match positions within the line (start, end). + /// Empty for context lines or for matching lines we don't need to highlight. + pub match_positions: &'a [(usize, usize)], +} + +#[derive(Clone)] +pub struct BufferedLine { + pub line: Vec, + pub line_number: u64, + pub byte_offset: u64, +} + +impl BufferedLine { + pub fn view(&self) -> LineView<'_> { + LineView { + line: &self.line, + line_number: self.line_number, + byte_offset: self.byte_offset, + is_match: false, + match_positions: &[], + } + } +} + +/// A fixed-capacity ring buffer of context lines. +/// +/// TODO: Ideally, this would be integrated into `LineBuffer`, which can then +/// provide a more optimized `ContextBuffer` when `mmap()` is available. +pub struct ContextBuffer { + slots: Vec, + head: usize, + len: usize, +} + +impl ContextBuffer { + pub fn new(capacity: usize) -> Self { + let len = if capacity == 0 { + 0 + } else { + capacity.next_power_of_two() + }; + Self { + slots: vec![ + BufferedLine { + line: Vec::new(), + line_number: 0, + byte_offset: 0, + }; + len + ], + head: 0, + len: 0, + } + } + + pub fn clear(&mut self) { + self.head = 0; + self.len = 0; + } + + pub fn push(&mut self, line: &[u8], line_number: u64, byte_offset: u64) { + debug_assert!( + !self.slots.is_empty(), + "push on zero-capacity ContextBuffer" + ); + + let mask = self.slots.len() - 1; + let slot = &mut self.slots[self.head & mask]; + + slot.line.clear(); + if slot.line.capacity() / 2 > line.len() { + slot.line.shrink_to(line.len()); + } + slot.line.extend_from_slice(line); + slot.line_number = line_number; + slot.byte_offset = byte_offset; + + self.head = self.head.wrapping_add(1); + self.len = (self.len + 1).min(self.slots.len()); + } + + pub fn drain_iter(&mut self) -> impl Iterator { + let len = self.len; + let slots = &self.slots[..]; + let mask = self.slots.len().wrapping_sub(1); + let start = self.head.wrapping_sub(len); + + self.head = 0; + self.len = 0; + + (0..len).map(move |i| &slots[start.wrapping_add(i) & mask]) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..863c040 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,920 @@ +mod context_buffer; +mod line_buffer; +mod matcher; +mod output; +mod searcher; + +use crate::line_buffer::LineBuffer; +use crate::matcher::Matcher; +use crate::output::OutputWriter; +use crate::searcher::Searcher; +use clap::{Arg, ArgAction, Command}; +use std::ffi::{OsStr, OsString}; +use std::io::{IsTerminal as _, Read}; +use std::path::Path; +use uucore::error::{FromIo, UResult, USimpleError}; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum RegexMode { + Fixed, + Basic, + Extended, + Perl, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BinaryMode { + Binary, + Text, + WithoutMatch, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ColorMode { + Always, + Never, + Auto, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum DirectoryMode { + Read, + Skip, + Recurse, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum DeviceMode { + Default, + Read, + Skip, +} + +struct ColorConfig<'a> { + matched_selected: &'a str, + matched_context: &'a str, + filename: &'a str, + line_number: &'a str, + byte_offset: &'a str, + separator: &'a str, + selected_line: &'a str, + context_line: &'a str, + + reverse_video: bool, + no_erase: bool, +} + +struct GlobSet { + patterns: Vec, +} + +struct Config<'a> { + // Searcher + directory_mode: DirectoryMode, + device_mode: DeviceMode, + follow_symlinks: bool, + include_globs: GlobSet, + exclude_globs: GlobSet, + exclude_dir_globs: GlobSet, + label: &'a str, + #[cfg(windows)] + strip_cr: bool, + binary_mode: BinaryMode, + max_count: Option, + before_context: usize, + after_context: usize, + has_context: bool, + + // Matcher + patterns: &'a [&'a str], + regex_mode: RegexMode, + ignore_case: bool, + invert_match: bool, + word_regexp: bool, + line_regexp: bool, + + // Output + quiet: bool, + count: bool, + show_filename: bool, + files_with_matches: bool, + files_without_match: bool, + only_matching: bool, + byte_offset: bool, + line_number: bool, + initial_tab: bool, + null_separator: bool, + null_data: bool, + line_buffered: bool, + no_messages: bool, + group_separator: Option<&'a str>, + use_color: bool, + color_config: ColorConfig<'a>, +} + +#[uucore::main(no_signals)] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = expand_num_shorthand(args); + let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; + + let grep_color = std::env::var("GREP_COLOR").unwrap_or_default(); + let grep_colors = std::env::var("GREP_COLORS").unwrap_or_default(); + + let patterns_or_files: Vec<_> = matches + .get_many::("patterns_or_files") + .map_or(Default::default(), |v| v.collect()); + let extended_regexp = matches.get_flag("extended_regexp"); + let fixed_strings = matches.get_flag("fixed_strings"); + let perl_regexp = matches.get_flag("perl_regexp"); + let regexp = matches.get_many::("regexp").unwrap_or_default(); + let file_pattern = matches + .get_many::("file_pattern") + .unwrap_or_default(); + let ignore_case = matches.get_flag("ignore_case"); + let word_regexp = matches.get_flag("word_regexp"); + let line_regexp = matches.get_flag("line_regexp"); + let null_data = matches.get_flag("null_data"); + let no_messages = matches.get_flag("no_messages"); + let invert_match = matches.get_flag("invert_match"); + let max_count = matches.get_one::("max_count").copied(); + let byte_offset = matches.get_flag("byte_offset"); + let line_number = matches.get_flag("line_number"); + let line_buffered = matches.get_flag("line_buffered"); + let with_filename = matches.get_flag("with_filename"); + let no_filename = matches.get_flag("no_filename"); + let label = matches + .get_one::("label") + .map_or("(standard input)", |s| s.as_str()); + let only_matching = matches.get_flag("only_matching"); + let quiet = matches.get_flag("quiet"); + let binary_files = matches + .get_one::("binary_files") + .map(String::as_str); + let text = matches.get_flag("text"); + let skip_binary = matches.get_flag("skip_binary"); + let directories = matches.get_one::("directories").map(String::as_str); + let devices = matches.get_one::("devices").map(String::as_str); + let recursive = matches.get_flag("recursive"); + let dereference_recursive = matches.get_flag("dereference_recursive"); + let include = matches.get_many::("include").unwrap_or_default(); + let exclude = matches.get_many::("exclude").unwrap_or_default(); + let exclude_from = matches + .get_many::("exclude_from") + .unwrap_or_default(); + let exclude_dir = matches + .get_many::("exclude_dir") + .unwrap_or_default(); + let files_without_match = matches.get_flag("files_without_match"); + let files_with_matches = matches.get_flag("files_with_matches"); + let count = matches.get_flag("count"); + let initial_tab = matches.get_flag("initial_tab"); + let null = matches.get_flag("null"); + let before_context = matches.get_one::("before_context").copied(); + let after_context = matches.get_one::("after_context").copied(); + let context = matches.get_one::("context").copied(); + let group_separator = matches + .get_one::("group_separator") + .map_or("--", |s| s.as_str()); + let no_group_separator = matches.get_flag("no_group_separator"); + let color = matches + .get_one::("color") + .map_or("", |s| s.as_str()); + #[cfg(windows)] + let binary = matches.get_flag("binary"); + + // With -e/-f given, ALL positionals are files. + let has_explicit_patterns = regexp.len() != 0 || file_pattern.len() != 0; + let (positional_pattern, file_args) = if has_explicit_patterns { + (None, &patterns_or_files[..]) + } else { + patterns_or_files + .split_first() + .map_or((None, &[][..]), |(p, rest)| (Some(*p), rest)) + }; + + // An empty pattern set is a usage error only when no explicit pattern source was + // given (`-e` / `-f`). An empty `-f` file is legitimate and simply matches nothing. + let mut pattern_strings = Vec::new(); + let mut patterns = Vec::new(); + { + for expr in regexp { + for line in expr.split('\n') { + patterns.push(line); + } + } + + for path in file_pattern { + let contents = if *path == "-" { + let mut buf = String::new(); + std::io::stdin() + .read_to_string(&mut buf) + .map_err_context(|| "(standard input)".to_string())?; + buf + } else { + std::fs::read_to_string(path).map_err_context(|| path.to_string())? + }; + pattern_strings.push(contents); + } + for contents in &pattern_strings { + if !contents.is_empty() { + let body = contents.strip_suffix('\n').unwrap_or(contents); + for line in body.split('\n') { + patterns.push(line); + } + } + } + + if let Some(pos) = positional_pattern { + let pat = pos + .to_str() + .ok_or_else(|| USimpleError::new(2, "pattern must be valid UTF-8".to_string()))?; + for line in pat.split('\n') { + patterns.push(line); + } + } + } + if patterns.is_empty() && !has_explicit_patterns { + return Err(USimpleError::new( + 2, + "no PATTERN specified. Try 'grep --help' for more information.".to_string(), + )); + } + + // Decoded options into enums + let regex_mode = if fixed_strings { + RegexMode::Fixed + } else if extended_regexp { + RegexMode::Extended + } else if perl_regexp { + RegexMode::Perl + } else { + RegexMode::Basic + }; + let directory_mode = if recursive || dereference_recursive { + DirectoryMode::Recurse + } else { + match directories { + Some("skip") => DirectoryMode::Skip, + Some("recurse") => DirectoryMode::Recurse, + _ => DirectoryMode::Read, + } + }; + let binary_mode = if text { + BinaryMode::Text + } else if skip_binary { + BinaryMode::WithoutMatch + } else { + match binary_files { + Some("text") => BinaryMode::Text, + Some("without-match") => BinaryMode::WithoutMatch, + _ => BinaryMode::Binary, + } + }; + let device_mode = match devices { + Some("read") => DeviceMode::Read, + Some("skip") => DeviceMode::Skip, + _ => DeviceMode::Default, + }; + let color = match color { + "always" => ColorMode::Always, + "never" => ColorMode::Never, + _ => ColorMode::Auto, + }; + let (before_context, after_context, has_context) = { + let fallback = context.unwrap_or(0); + let before = before_context.unwrap_or(fallback); + let after = after_context.unwrap_or(fallback); + let has = context.is_some() || before_context.is_some() || after_context.is_some(); + (before, after, has) + }; + let include_globs = { + let mut patterns = GlobSet::with_capacity(include.len()); + for pattern in include { + patterns.add(pattern)?; + } + patterns + }; + let exclude_globs = { + let mut patterns = GlobSet::with_capacity(exclude.len()); + for pattern in exclude { + patterns.add(pattern)?; + } + for path in exclude_from { + let contents = std::fs::read_to_string(path).map_err_context(|| path.to_string())?; + for line in contents.lines() { + let trimmed = line.trim(); + if !trimmed.is_empty() { + patterns.add(trimmed)?; + } + } + } + patterns + }; + let exclude_dir_globs = { + let mut patterns = GlobSet::with_capacity(exclude_dir.len()); + for pattern in exclude_dir { + patterns.add(pattern)?; + } + patterns + }; + let show_filename = if with_filename { + true + } else if no_filename { + false + } else { + match file_args { + [] => directory_mode == DirectoryMode::Recurse, + [one] if one.to_str() != Some("-") => { + directory_mode == DirectoryMode::Recurse && Path::new(one).is_dir() + } + [_] => false, + _ => true, + } + }; + let group_separator = (!no_group_separator).then_some(group_separator); + let use_color = match color { + ColorMode::Always => true, + ColorMode::Never => false, + ColorMode::Auto => std::io::stdout().is_terminal(), + }; + let color_config = ColorConfig::from_env(&grep_color, &grep_colors); + + let config = Config { + // Searcher + directory_mode, + device_mode, + follow_symlinks: dereference_recursive, + include_globs, + exclude_globs, + exclude_dir_globs, + label, + #[cfg(windows)] + strip_cr: !binary, + binary_mode, + max_count, + before_context, + after_context, + has_context, + + // Matcher + patterns: &patterns, + regex_mode, + ignore_case, + invert_match, + word_regexp, + line_regexp, + + // Output + quiet, + count, + show_filename, + files_with_matches, + files_without_match, + only_matching, + byte_offset, + line_number, + initial_tab, + null_separator: null, + null_data, + line_buffered, + no_messages, + group_separator, + use_color, + color_config, + }; + + let matcher = Matcher::compile(&config)?; + let writer = OutputWriter::new(&config); + let mut searcher = Searcher::new(&config, matcher, writer); + let mut lb = LineBuffer::new(if config.null_data { b'\0' } else { b'\n' }); + + if file_args.is_empty() { + if directory_mode != DirectoryMode::Recurse { + _ = searcher.process_stdin(&mut lb); + } else { + _ = searcher.process_implicit_cwd(&mut lb); + } + } else { + for f in file_args { + let cf = if f.to_str() == Some("-") { + searcher.process_stdin(&mut lb) + } else { + searcher.process_path(&mut lb, Path::new(f)) + }; + if cf.is_break() { + break; + } + } + } + + searcher.finish() +} + +pub fn uu_app() -> Command { + Command::new("grep") + .version(env!("CARGO_PKG_VERSION")) + .about("Search for PATTERNS in each FILE.") + .disable_help_flag(true) + .disable_version_flag(true) + .after_help( + "When FILE is '-', read standard input. If no FILE is given, read standard \ + input, but with -r, recursively search the working directory instead. With \ + fewer than two FILEs, assume -h. Exit status is 0 if any line is selected, \ + 1 otherwise; if any error occurs and -q is not given, the exit status is 2.", + ) + .arg( + Arg::new("patterns_or_files") + .help("Pattern (if no -e/-f) followed by files to search") + .index(1) + .num_args(0..) + .value_parser(clap::value_parser!(OsString)), + ) + .arg( + Arg::new("extended_regexp") + .short('E') + .long("extended-regexp") + .help("PATTERNS are extended regular expressions") + .action(ArgAction::SetTrue) + .overrides_with_all(["basic_regexp", "fixed_strings", "perl_regexp"]), + ) + .arg( + Arg::new("fixed_strings") + .short('F') + .long("fixed-strings") + .help("PATTERNS are strings") + .action(ArgAction::SetTrue) + .overrides_with_all(["basic_regexp", "extended_regexp", "perl_regexp"]), + ) + .arg( + Arg::new("basic_regexp") + .short('G') + .long("basic-regexp") + .help("PATTERNS are basic regular expressions") + .action(ArgAction::SetTrue) + .overrides_with_all(["extended_regexp", "fixed_strings", "perl_regexp"]), + ) + .arg( + Arg::new("perl_regexp") + .short('P') + .long("perl-regexp") + .help("PATTERNS are Perl regular expressions") + .action(ArgAction::SetTrue) + .overrides_with_all(["basic_regexp", "extended_regexp", "fixed_strings"]), + ) + .arg( + Arg::new("regexp") + .short('e') + .long("regexp") + .value_name("PATTERNS") + .help("use PATTERNS for matching") + .action(ArgAction::Append) + .allow_hyphen_values(true), + ) + .arg( + Arg::new("file_pattern") + .short('f') + .long("file") + .value_name("FILE") + .help("take PATTERNS from FILE") + .action(ArgAction::Append), + ) + .arg( + Arg::new("ignore_case") + .short('i') + .long("ignore-case") + .short_alias('y') + .help("ignore case distinctions in patterns and data") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no_ignore_case") + .long("no-ignore-case") + .help("do not ignore case distinctions (default)") + .action(ArgAction::SetTrue) + .overrides_with("ignore_case"), + ) + .arg( + Arg::new("word_regexp") + .short('w') + .long("word-regexp") + .help("match only whole words") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("line_regexp") + .short('x') + .long("line-regexp") + .help("match only whole lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("null_data") + .short('z') + .long("null-data") + .help("a data line ends in 0 byte, not newline") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no_messages") + .short('s') + .long("no-messages") + .help("suppress error messages") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("invert_match") + .short('v') + .long("invert-match") + .help("select non-matching lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("version") + .short('V') + .long("version") + .help("display version information and exit") + .action(ArgAction::Version), + ) + .arg( + Arg::new("help") + .long("help") + .help("display this help text and exit") + .action(ArgAction::Help), + ) + .arg( + Arg::new("max_count") + .short('m') + .long("max-count") + .value_name("NUM") + .help("stop after NUM selected lines") + .value_parser(clap::value_parser!(u64)), + ) + .arg( + Arg::new("byte_offset") + .short('b') + .long("byte-offset") + .help("print the byte offset with output lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("line_number") + .short('n') + .long("line-number") + .help("print line number with output lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("line_buffered") + .long("line-buffered") + .help("flush output on every line") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("with_filename") + .short('H') + .long("with-filename") + .help("print file name with output lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no_filename") + .short('h') + .long("no-filename") + .help("suppress the file name prefix on output") + .action(ArgAction::SetTrue) + .overrides_with("with_filename"), + ) + .arg( + Arg::new("label") + .long("label") + .value_name("LABEL") + .help("use LABEL as the standard input file name prefix"), + ) + .arg( + Arg::new("only_matching") + .short('o') + .long("only-matching") + .help("show only nonempty parts of lines that match") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("quiet") + .short('q') + .long("quiet") + .alias("silent") + .help("suppress all normal output") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("binary_files") + .long("binary-files") + .value_name("TYPE") + .help("assume that binary files are TYPE; TYPE is 'binary', 'text', or 'without-match'") + .value_parser(["binary", "text", "without-match"]), + ) + .arg( + Arg::new("text") + .short('a') + .long("text") + .help("equivalent to --binary-files=text") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("skip_binary") + .short('I') + .help("equivalent to --binary-files=without-match") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("directories") + .short('d') + .long("directories") + .value_name("ACTION") + .help("how to handle directories; ACTION is 'read', 'recurse', or 'skip'") + .value_parser(["read", "skip", "recurse"]), + ) + .arg( + Arg::new("devices") + .short('D') + .long("devices") + .value_name("ACTION") + .help("how to handle devices, FIFOs and sockets; ACTION is 'read' or 'skip'") + .value_parser(["read", "skip"]), + ) + .arg( + Arg::new("recursive") + .short('r') + .long("recursive") + .help("like --directories=recurse") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("dereference_recursive") + .short('R') + .long("dereference-recursive") + .help("likewise, but follow all symlinks") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("include") + .long("include") + .value_name("GLOB") + .help("search only files that match GLOB (a file pattern)") + .action(ArgAction::Append), + ) + .arg( + Arg::new("exclude") + .long("exclude") + .value_name("GLOB") + .help("skip files that match GLOB") + .action(ArgAction::Append), + ) + .arg( + Arg::new("exclude_from") + .long("exclude-from") + .value_name("FILE") + .help("skip files that match any file pattern from FILE") + .action(ArgAction::Append), + ) + .arg( + Arg::new("exclude_dir") + .long("exclude-dir") + .value_name("GLOB") + .help("skip directories that match GLOB") + .action(ArgAction::Append), + ) + .arg( + Arg::new("files_without_match") + .short('L') + .long("files-without-match") + .help("print only names of FILEs with no selected lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("files_with_matches") + .short('l') + .long("files-with-matches") + .help("print only names of FILEs with selected lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("count") + .short('c') + .long("count") + .help("print only a count of selected lines per FILE") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("initial_tab") + .short('T') + .long("initial-tab") + .help("make tabs line up (if needed)") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("null") + .short('Z') + .long("null") + .help("print 0 byte after FILE name") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("before_context") + .short('B') + .long("before-context") + .value_name("NUM") + .help("print NUM lines of leading context") + .value_parser(clap::value_parser!(usize)), + ) + .arg( + Arg::new("after_context") + .short('A') + .long("after-context") + .value_name("NUM") + .help("print NUM lines of trailing context") + .value_parser(clap::value_parser!(usize)), + ) + .arg( + Arg::new("context") + .short('C') + .long("context") + .value_name("NUM") + .help("print NUM lines of output context") + .value_parser(clap::value_parser!(usize)), + ) + .arg( + Arg::new("group_separator") + .long("group-separator") + .value_name("SEP") + .help("print SEP on line between matches with context") + .default_value("--"), + ) + .arg( + Arg::new("no_group_separator") + .long("no-group-separator") + .help("do not print separator for matches with context") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("color") + .long("color") + .alias("colour") + .value_name("WHEN") + .help("use markers to highlight the matching strings; WHEN is 'always', 'never', or 'auto'") + .value_parser(["always", "never", "auto"]) + .default_missing_value("auto") + .num_args(0..=1) + .require_equals(true), + ) + .arg( + Arg::new("binary") + .short('U') + .long("binary") + .help("do not strip CR characters at EOL (MSDOS/Windows)") + .action(ArgAction::SetTrue), + ) +} + +/// Expand GNU grep's `-NUM` shorthand to `-C NUM`. +fn expand_num_shorthand(args: impl Iterator) -> Vec { + const SHORT_OPTS_WITH_VALUE: &[u8] = b"efmABCDd"; + const LONG_OPTS_WITH_VALUE: &[&[u8]] = &[ + b"regexp", + b"file", + b"max-count", + b"label", + b"after-context", + b"before-context", + b"context", + b"devices", + b"directories", + b"binary-files", + b"include", + b"exclude", + b"exclude-from", + b"exclude-dir", + b"group-separator", + ]; + + fn consumes_next_arg(arg: &[u8]) -> bool { + if let Some(name) = arg.strip_prefix(b"-") { + LONG_OPTS_WITH_VALUE.contains(&name) + } else { + arg.last() + .is_some_and(|b| SHORT_OPTS_WITH_VALUE.contains(b)) + } + } + + let mut out: Vec = args.collect(); + let mut i = 1; // argv[0] is the executable name + + while i < out.len() { + let arg = out[i].as_encoded_bytes(); + + // Narrow down to `-flags` (and `--options`) and strip the leading `-` for easier matching. + let Some(arg) = arg.strip_prefix(b"-") else { + // Not a flag, skip. + i += 1; + continue; + }; + + // No more options after `--`. + if arg == b"-" { + break; + } + + // This flag consumes two args. Skip both. + if consumes_next_arg(arg) { + i += 2; + continue; + } + + // Translate -NUM to -C NUM. + if !arg.is_empty() && arg.iter().all(u8::is_ascii_digit) { + // SAFETY: We know that all `arg` bytes are valid ASCII (digits). + let digits = unsafe { OsStr::from_encoded_bytes_unchecked(arg) }.to_owned(); + out[i] = OsString::from("-C"); + out.insert(i + 1, digits); + i += 2; + continue; + } + + i += 1; + } + + out +} + +impl GlobSet { + fn with_capacity(capacity: usize) -> Self { + Self { + patterns: Vec::with_capacity(capacity), + } + } + + fn add(&mut self, pattern: &str) -> UResult<()> { + let compiled_pattern = glob::Pattern::new(pattern) + .map_err(|e| USimpleError::new(2, format!("invalid glob '{pattern}': {e}")))?; + self.patterns.push(compiled_pattern); + Ok(()) + } + + fn is_empty(&self) -> bool { + self.patterns.is_empty() + } + + fn matches(&self, name: &OsStr) -> bool { + let Some(name) = name.to_str() else { + return false; + }; + self.patterns.iter().any(|pattern| pattern.matches(name)) + } +} + +impl<'a> ColorConfig<'a> { + fn from_env(grep_color: &'a str, grep_colors: &'a str) -> Self { + let mut config = Self { + matched_selected: "01;31", + matched_context: "01;31", + filename: "35", + line_number: "32", + byte_offset: "32", + separator: "36", + selected_line: "", + context_line: "", + reverse_video: false, + no_erase: false, + }; + + if !grep_color.is_empty() { + config.matched_selected = grep_color; + config.matched_context = grep_color; + } + + for item in grep_colors.split(':') { + if let Some((key, value)) = item.split_once('=') { + match key { + "ms" => config.matched_selected = value, + "mc" => config.matched_context = value, + "fn" => config.filename = value, + "ln" => config.line_number = value, + "bn" => config.byte_offset = value, + "se" => config.separator = value, + "sl" => config.selected_line = value, + "cx" => config.context_line = value, + "rv" => config.reverse_video = true, + "ne" => config.no_erase = true, + _ => {} + } + } else { + match item { + "rv" => config.reverse_video = true, + "ne" => config.no_erase = true, + _ => {} + } + } + } + + config + } +} diff --git a/src/line_buffer.rs b/src/line_buffer.rs new file mode 100644 index 0000000..878afe7 --- /dev/null +++ b/src/line_buffer.rs @@ -0,0 +1,109 @@ +use memchr::memchr; +use std::fs::File; +use std::io::{self, Read as _}; + +pub struct LineBuffer { + buffer: Vec, + + /// Start of the valid data and current (incomplete) line in `buffer`. + beg: usize, + /// Offset of where the next `memchr` should continue scanning from. + /// This helps us avoid re-scanning bytes we've already confirmed don't contain a terminator. + scan: usize, + /// End of the current valid data in `buffer`. + end: usize, + + /// Absolute file offset of `buffer[pos]`. + next_line_start: u64, + /// Set once a file read has returned EOF. + eof: bool, + /// The line terminator is typically NUL or LF. + line_terminator: u8, +} + +impl LineBuffer { + pub fn new(line_terminator: u8) -> Self { + Self { + buffer: vec![0; 128 * 1024], + + beg: 0, + scan: 0, + end: 0, + + next_line_start: 0, + eof: false, + line_terminator, + } + } + + /// Reset the buffer to an empty state. + pub fn reset(&mut self) { + self.beg = 0; + self.scan = 0; + self.end = 0; + self.next_line_start = 0; + self.eof = false; + } + + /// Read the next line from the given reader. + /// Returns `Ok(None)` if the end of the reader is reached. + /// Otherwise, returns `Ok(Some((line, line_start)))`, where `line` is the line read (without the + /// `line_terminator`), and `line_start` is the absolute byte offset of the start of the line. + pub fn read_line(&mut self, file: &mut File) -> io::Result> { + if self.eof { + return Ok(None); + } + + loop { + // Look for a line terminator and if found, yield that line. + if let Some(off) = memchr(self.line_terminator, &self.buffer[self.scan..self.end]) { + let line_start = self.next_line_start; + let beg = self.beg; + let end = self.scan + off; + let line = &self.buffer[beg..end]; + self.beg = end + 1; + self.scan = self.beg; + self.next_line_start += (self.beg - beg) as u64; + return Ok(Some((line, line_start))); + } + + // `buffer[pos..end]` has no terminator. Remember that for the next scan. + self.scan = self.end; + + // Move the partial line to the beginning of the buffer. + // The idea is that read() calls will either be very slow, and this doesn't matter, + // or they'll be very fast with big chunks and we want to maximize the amount of space per syscall. + if self.beg > 0 { + self.buffer.copy_within(self.beg..self.end, 0); + self.end -= self.beg; + self.scan -= self.beg; + self.beg = 0; + } + if self.end == self.buffer.len() { + // A single line exceeds the current buffer; grow. + self.buffer.resize(self.buffer.len() * 2, 0); + } + + // Read more data! + let n = loop { + match file.read(&mut self.buffer[self.end..]) { + Ok(n) => break n, + Err(e) if e.kind() == io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + }; + if n == 0 { + // EOF: Yield the last line, if any. + return if self.beg == self.end { + Ok(None) + } else { + let line = &self.buffer[self.beg..self.end]; + let line_start = self.next_line_start; + self.eof = true; // shortcut the next call to read_line() + Ok(Some((line, line_start))) + }; + } + self.end += n; + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..288f967 --- /dev/null +++ b/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_grep); diff --git a/src/matcher.rs b/src/matcher.rs new file mode 100644 index 0000000..2416679 --- /dev/null +++ b/src/matcher.rs @@ -0,0 +1,271 @@ +use crate::{Config, RegexMode}; +use onig::{EncodedBytes, Regex, RegexOptions, Region, SearchOptions, Syntax, SyntaxBehavior}; +use onig_sys::{OnigEncCtype_ONIGENC_CTYPE_WORD, OnigEncodingUTF8}; +use uucore::error::{UResult, USimpleError}; + +pub struct Matcher<'a> { + config: &'a Config<'a>, + patterns: Vec, +} + +impl<'a> Matcher<'a> { + pub fn compile(config: &'a Config<'a>) -> UResult { + let mut patterns = Vec::with_capacity(config.patterns.len()); + for raw in config.patterns { + patterns.push(CompiledPattern::compile(raw, config)?); + } + Ok(Self { config, patterns }) + } + + /// Decide whether `line` matches and return the positions to highlight. + pub fn match_line(&self, line: &[u8]) -> Option> { + let mut any_seen = false; + let positions: Vec<_> = MatchIter::new(&self.patterns, line) + .filter(|&(start, end)| { + any_seen = true; + // Drop zero-length matches from the output. + if start == end { + return false; + } + // Drop matches that don't span the whole line if `-x` was requested. + if self.config.line_regexp && !(start == 0 && end == line.len()) { + return false; + } + // Drop matches that aren't word matches if `-w` was requested. + if self.config.word_regexp && !Self::is_word_match(line, start, end) { + return false; + } + true + }) + .collect(); + + let raw_matched = if self.config.line_regexp || self.config.word_regexp { + // -w / -x are authoritative once positions are filtered. + !positions.is_empty() + } else { + any_seen + }; + + if raw_matched != self.config.invert_match { + Some(positions) + } else { + None + } + } + + /// Cheap match check that doesn't enumerate positions. + pub fn is_match(&self, line: &[u8]) -> Option> { + // `-w` / `-x` need positions to filter, so we fall back to `match_line`. + let matched = if self.config.line_regexp || self.config.word_regexp { + self.match_line(line).is_some() + } else { + let raw_matched = self.patterns.iter().any(|p| p.is_match(line)); + raw_matched != self.config.invert_match + }; + matched.then(Vec::new) + } + + /// Word-boundary check `-w`. + /// NOTE that `-w` does not check both sides, unlike `\b` in a regex. + /// Start/End-of-line count as non-words. + fn is_word_match(line: &[u8], start: usize, end: usize) -> bool { + // SAFETY: This code uses OnigEncodingType such that it can support other types of encodings in the future. + unsafe { + let mbc_to_code = OnigEncodingUTF8.mbc_to_code.unwrap_unchecked(); + let is_code_ctype = OnigEncodingUTF8.is_code_ctype.unwrap_unchecked(); + let line_end = line.as_ptr().add(line.len()); + + if end < line.len() { + let cp = mbc_to_code(line.as_ptr().add(end), line_end); + if is_code_ctype(cp, OnigEncCtype_ONIGENC_CTYPE_WORD) != 0 { + return false; + } + } + + if start > 0 { + let left_adjust = OnigEncodingUTF8.left_adjust_char_head.unwrap_unchecked(); + let head = left_adjust(line.as_ptr(), line.as_ptr().add(start - 1)); + let cp = mbc_to_code(head, line_end); + if is_code_ctype(cp, OnigEncCtype_ONIGENC_CTYPE_WORD) != 0 { + return false; + } + } + + true + } + } +} + +/// Streaming k-way merge over compiled patterns +struct MatchIter<'a> { + cursors: Vec>, + /// End of the last emitted match. + last_end: usize, +} + +impl<'a> MatchIter<'a> { + fn new(patterns: &'a [CompiledPattern], line: &'a [u8]) -> Self { + Self { + cursors: patterns + .iter() + .map(|pattern| { + let mut c = Cursor { + pattern, + line, + offset: 0, + pending: None, + }; + c.refill(); + c + }) + .collect(), + last_end: 0, + } + } +} + +impl<'a> Iterator for MatchIter<'a> { + type Item = (usize, usize); + + fn next(&mut self) -> Option { + // Discard stale pendings that fall before the last emit. + for cursor in &mut self.cursors { + if matches!(cursor.pending, Some((s, _)) if s < self.last_end) { + cursor.offset = self.last_end; + cursor.refill(); + } + } + + // Pick the leftmost pending. + // Tie-break by largest end so POSIX leftmost-longest holds across + // patterns too (e.g. `-e a -e ab` against `ab` emits `ab`). + let best_idx = self + .cursors + .iter() + .enumerate() + .filter_map(|(i, c)| c.pending.map(|p| (i, p))) + .min_by_key(|&(_, (s, e))| (s, std::cmp::Reverse(e))) + .map(|(i, _)| i)?; + + let (start, end) = self.cursors[best_idx].pending.unwrap(); + self.cursors[best_idx].refill(); + self.last_end = end; + Some((start, end)) + } +} + +struct Cursor<'a> { + pattern: &'a CompiledPattern, + line: &'a [u8], + /// Where the next `search_leftmost` call should start. + offset: usize, + /// Pre-fetched next match for this pattern. + /// `None` once the pattern is exhausted. + pending: Option<(usize, usize)>, +} + +impl Cursor<'_> { + fn refill(&mut self) { + if self.offset >= self.line.len() { + self.pending = None; + return; + } + let Some((start, leftmost_end)) = self.pattern.search_leftmost(self.line, self.offset) + else { + self.pending = None; + return; + }; + let end = self + .pattern + .longest_end_at(self.line, start) + .unwrap_or(leftmost_end); + // Advance the next search past the match we just found. + // Zero-length matches need a +1 nudge to avoid spinning forever. + self.offset = end.max(start + 1); + self.pending = Some((start, end)); + } +} + +struct CompiledPattern { + /// Default semantics. It's decently fast and used for searching. + leftmost: Regex, + /// Compiled with `FIND_LONGEST`. If used for a search, it'll search the + /// entire haystack to find the longest. This makes it unsuitable for searching, + /// but it's perfect for a second, anchored match pass for POSIX semantics. + longest_anchored: Regex, +} + +impl CompiledPattern { + fn compile(pattern: &str, config: &Config) -> UResult { + let mut syntax = *match config.regex_mode { + RegexMode::Fixed => Syntax::asis(), + RegexMode::Basic => Syntax::grep(), + RegexMode::Extended => Syntax::gnu_regex(), + RegexMode::Perl => Syntax::perl(), + }; + if !matches!(config.regex_mode, RegexMode::Fixed) { + syntax.enable_behavior(SyntaxBehavior::SYNTAX_BEHAVIOR_ALLOW_INTERVAL_LOW_ABBREV); + } + + let mut options = RegexOptions::REGEX_OPTION_NONE; + if config.ignore_case { + options |= RegexOptions::REGEX_OPTION_IGNORECASE; + } + + fn compile_with(pattern: &str, syntax: &Syntax, options: RegexOptions) -> UResult { + Regex::with_options_and_encoding(pattern, options, syntax).map_err(|err| { + USimpleError::new(2, format!("invalid pattern \"{pattern}\": {err}")) + }) + } + + let leftmost = compile_with(pattern, &syntax, options)?; + let longest_anchored = compile_with( + pattern, + &syntax, + options | RegexOptions::REGEX_OPTION_FIND_LONGEST, + )?; + Ok(Self { + leftmost, + longest_anchored, + }) + } + + /// Find the leftmost match starting at or after `offset`. + fn search_leftmost(&self, line: &[u8], offset: usize) -> Option<(usize, usize)> { + let mut region = Region::new(); + self.leftmost.search_with_encoding( + EncodedBytes::from_parts(line, &raw mut OnigEncodingUTF8), + offset, + line.len(), + SearchOptions::SEARCH_OPTION_NONE, + Some(&mut region), + )?; + region.pos(0) + } + + /// Given a known leftmost start `start`, return the longest extent + /// of a match anchored exactly there = POSIX leftmost-longest end. + fn longest_end_at(&self, line: &[u8], start: usize) -> Option { + let mut region = Region::new(); + self.longest_anchored.match_with_encoding( + EncodedBytes::from_parts(line, &raw mut OnigEncodingUTF8), + start, + SearchOptions::SEARCH_OPTION_NONE, + Some(&mut region), + ); + region.pos(0).map(|(_, end)| end) + } + + /// True if any match exists in `line` (including zero-length). + fn is_match(&self, line: &[u8]) -> bool { + self.leftmost + .search_with_encoding( + EncodedBytes::from_parts(line, &raw mut OnigEncodingUTF8), + 0, + line.len(), + SearchOptions::SEARCH_OPTION_NONE, + None, + ) + .is_some() + } +} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..f5672f2 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,271 @@ +use crate::Config; +use crate::context_buffer::LineView; +use std::ffi::OsStr; +use std::io::{self, BufWriter, StdoutLock, Write}; +use std::path::Path; + +#[cfg(target_pointer_width = "64")] +const BUF_SIZE: usize = 128 * 1024; +#[cfg(target_pointer_width = "32")] +const BUF_SIZE: usize = 16 * 1024; + +pub struct OutputWriter<'a> { + config: &'a Config<'a>, + out: BufWriter>, + line_number_width: usize, +} + +impl<'a> OutputWriter<'a> { + pub fn new(config: &'a Config<'a>) -> Self { + Self { + config, + out: BufWriter::with_capacity(BUF_SIZE, io::stdout().lock()), + line_number_width: 0, + } + } + + /// Returns true if line numbers will be padded with whitespace (`-T`), + /// which means that you should call `set_line_number_width`. + pub fn wants_padded_line_numbers(&self) -> bool { + self.config.initial_tab && self.config.line_number + } + + /// Set the minimum field width for line numbers (used with `-T`). + /// + /// It's sort of wrong for this to be a setter, because it's technically + /// per-session state in searcher.rs, but it's convenient and it's cheap. + pub fn set_line_number_width(&mut self, width: usize) { + debug_assert!(self.wants_padded_line_numbers(), "don't call this function"); + self.line_number_width = width; + } + + /// Flush stdout. + pub fn flush(&mut self) -> io::Result<()> { + self.out.flush() + } + + /// Write a matching or context line. + pub fn write_line(&mut self, view: &LineView<'_>, filename: &Path) -> io::Result<()> { + if self.config.only_matching && view.is_match { + self.write_only_matching(view, filename) + } else { + self.write_line_with_matches(view, filename) + } + } + + /// Write only the matching portions of a line (`-o` mode). + fn write_only_matching(&mut self, view: &LineView<'_>, filename: &Path) -> io::Result<()> { + for &(start, end) in view.match_positions { + if start == end { + continue; // Skip zero-length matches + } + + self.write_prefix( + filename, + view.line_number, + view.byte_offset + start as u64, + b':', + )?; + + self.write_colored_bytes( + self.config.color_config.matched_selected, + &view.line[start..end], + )?; + self.write_terminator()?; + } + + self.maybe_flush() + } + + /// Otherwise, write the whole line, with optional color highlighting of matches. + fn write_line_with_matches(&mut self, view: &LineView<'_>, filename: &Path) -> io::Result<()> { + self.write_prefix( + filename, + view.line_number, + view.byte_offset, + if view.is_match { b':' } else { b'-' }, + )?; + + let mut last_end = 0; + + if self.config.use_color && view.is_match && !view.match_positions.is_empty() { + for &(start, end) in view.match_positions { + if start > last_end { + self.out.write_all(&view.line[last_end..start])?; + } + if start < end { + let match_bytes = &view.line[start..end]; + self.write_colored_bytes( + self.config.color_config.matched_selected, + match_bytes, + )?; + } + last_end = end; + } + } + + if last_end < view.line.len() { + self.out.write_all(&view.line[last_end..])?; + } + + self.write_terminator()?; + self.maybe_flush() + } + + /// Write a line prefix. + fn write_prefix( + &mut self, + filename: &Path, + line_number: u64, + byte_offset: u64, + sep_char: u8, + ) -> io::Result<()> { + if self.config.show_filename { + self.write_colored_fmt( + self.config.color_config.filename, + format_args!("{}", filename.display()), + )?; + if self.config.null_separator { + self.out.write_all(b"\0")?; + } else { + self.write_separator(sep_char)?; + } + } + + if self.config.line_number { + let width = self.line_number_width; + self.write_colored_fmt( + self.config.color_config.line_number, + format_args!("{:>width$}", line_number, width = width), + )?; + self.write_separator(sep_char)?; + } + + if self.config.byte_offset { + self.write_colored_fmt( + self.config.color_config.byte_offset, + format_args!("{}", byte_offset), + )?; + self.write_separator(sep_char)?; + } + + if self.config.initial_tab + && (self.config.line_number || self.config.byte_offset || self.config.show_filename) + { + self.out.write_all(b"\t")?; + } + + Ok(()) + } + + /// Write the count line for `-c` mode. + pub fn write_count(&mut self, count: u64, filename: &Path) -> io::Result<()> { + if self.config.show_filename { + let sep = if self.config.null_separator { + b'\0' + } else { + b':' + }; + write!(self.out, "{}{}", filename.display(), sep as char)?; + } + + writeln!(self.out, "{}", count)?; + self.maybe_flush() + } + + /// Write filename for `-l` / `-L` mode. + pub fn write_filename(&mut self, path: &Path) -> io::Result<()> { + self.write_colored_fmt( + self.config.color_config.filename, + format_args!("{}", path.display()), + )?; + self.out.write_all(if self.config.null_separator { + b"\0" + } else { + b"\n" + })?; + self.maybe_flush() + } + + /// Write an IO error to stderr. + pub fn report_io_error(&self, label: &OsStr, err: &io::Error) { + if !self.config.no_messages && !self.config.quiet { + eprintln!("grep: {label}: {err}", label = label.to_string_lossy()); + } + } + + /// Write the "binary file matches" message to stderr. + pub fn report_binary_match(&self, path: &Path) { + eprintln!("grep: {}: binary file matches", path.display()); + } + + /// Write the group separator between context groups. + pub fn write_group_separator(&mut self) -> io::Result<()> { + if let Some(sep) = self.config.group_separator { + self.write_colored_bytes(self.config.color_config.separator, sep.as_bytes())?; + self.out.write_all(b"\n")?; + self.maybe_flush() + } else { + Ok(()) + } + } + + /// Flush in `--line-buffered` mode. + fn maybe_flush(&mut self) -> io::Result<()> { + if self.config.line_buffered { + self.out.flush() + } else { + Ok(()) + } + } + + /// Write the line/record terminator (`\n`, or `\0` under `-z`). + fn write_terminator(&mut self) -> io::Result<()> { + self.out + .write_all(if self.config.null_data { b"\0" } else { b"\n" }) + } + + fn write_separator(&mut self, ch: u8) -> io::Result<()> { + self.write_colored_bytes(self.config.color_config.separator, &[ch]) + } + + fn write_colored_bytes(&mut self, color: &str, text: &[u8]) -> io::Result<()> { + let colored = self.config.use_color && !color.is_empty(); + if colored { + self.write_colored_prefix(color)?; + } + self.out.write_all(text)?; + if colored { + self.write_colored_suffix()?; + } + Ok(()) + } + + fn write_colored_fmt(&mut self, color: &str, args: std::fmt::Arguments<'_>) -> io::Result<()> { + let colored = self.config.use_color && !color.is_empty(); + if colored { + self.write_colored_prefix(color)?; + } + self.out.write_fmt(args)?; + if colored { + self.write_colored_suffix()?; + } + Ok(()) + } + + fn write_colored_prefix(&mut self, color: &str) -> io::Result<()> { + write!(self.out, "\x1b[{}m", color)?; + if !self.config.color_config.no_erase { + self.out.write_all(b"\x1b[K")?; + } + Ok(()) + } + + fn write_colored_suffix(&mut self) -> io::Result<()> { + if self.config.color_config.no_erase { + self.out.write_all(b"\x1b[m") + } else { + self.out.write_all(b"\x1b[m\x1b[K") + } + } +} diff --git a/src/searcher.rs b/src/searcher.rs new file mode 100644 index 0000000..35b05d3 --- /dev/null +++ b/src/searcher.rs @@ -0,0 +1,467 @@ +use crate::context_buffer::{ContextBuffer, LineView}; +use crate::line_buffer::LineBuffer; +use crate::matcher::Matcher; +use crate::output::OutputWriter; +use crate::{BinaryMode, Config, DeviceMode, DirectoryMode}; +use memchr::memchr; +use std::ffi::OsStr; +use std::fs::File; +use std::io; +use std::mem::ManuallyDrop; +use std::ops::ControlFlow; +use std::path::Path; +use uucore::error::{ExitCode, FromIo, UResult}; +use walkdir::WalkDir; + +pub struct Searcher<'a> { + config: &'a Config<'a>, + writer: OutputWriter<'a>, + matcher: Matcher<'a>, + any_match: bool, + had_error: bool, + binary_notice_enabled: bool, + + // Per-session state + session_context_buf: ContextBuffer, + session_match_count: u64, + session_after_remaining: usize, + session_last_printed_line: u64, // 0 = nothing yet + session_binary_detected: bool, +} + +impl<'a> Searcher<'a> { + pub fn new(config: &'a Config<'a>, matcher: Matcher<'a>, writer: OutputWriter<'a>) -> Self { + Self { + config, + writer, + matcher, + any_match: false, + had_error: false, + binary_notice_enabled: config.binary_mode == BinaryMode::Binary + && !config.quiet + && !config.count + && !config.files_with_matches + && !config.files_without_match, + + session_context_buf: ContextBuffer::new(config.before_context), + session_match_count: 0, + session_after_remaining: 0, + session_last_printed_line: 0, + session_binary_detected: false, + } + } + + /// Search the given path. + pub fn process_path(&mut self, lb: &mut LineBuffer, path: &Path) -> ControlFlow<()> { + if path.is_dir() { + match self.config.directory_mode { + DirectoryMode::Skip => ControlFlow::Continue(()), + // Yield the directory path itself; `process_file` will + // report the OS error from trying to open it as a file. + DirectoryMode::Read => self.process_file(lb, path), + DirectoryMode::Recurse => self.process_dir(lb, path, false), + } + } else { + // Top-level FIFOs/sockets/devices: read by default, drop only on `-D skip`. + if self.config.device_mode == DeviceMode::Skip && Self::is_special_file(path) { + return ControlFlow::Continue(()); + } + self.process_file(lb, path) + } + } + + /// Recursive search of the current directory. This is used when `-r` + /// is used without a path. In that case, GNU grep emits bare paths + /// for which we need special handling. + pub fn process_implicit_cwd(&mut self, lb: &mut LineBuffer) -> ControlFlow<()> { + self.process_dir(lb, Path::new("."), true) + } + + /// Search on standard input. + pub fn process_stdin(&mut self, lb: &mut LineBuffer) -> ControlFlow<()> { + // Turn the Stdin struct into a File to avoid monomorphization just for files VS stdin. + // The cast is "unsafe" because we package a borrowed handle into an owned File. + // SAFETY: To make that safe, we simply use ManuallyDrop. + #[cfg(windows)] + let mut stdin = { + use std::os::windows::io::{AsRawHandle, FromRawHandle}; + let handle = io::stdin().as_raw_handle(); + ManuallyDrop::new(unsafe { File::from_raw_handle(handle) }) + }; + #[cfg(not(windows))] + let mut stdin = { + use std::os::fd::FromRawFd; + ManuallyDrop::new(unsafe { File::from_raw_fd(0) }) + }; + + let path = Path::new(&self.config.label); + + // Stdin is not seekable; use fixed width. + if self.writer.wants_padded_line_numbers() { + self.writer.set_line_number_width(19); + } + + let result = self.session_run(lb, path, &mut stdin); + self.record_result(OsStr::new(&self.config.label), result) + } + + /// Flush output and produce the overall result. + /// Returns true if any input matched. + pub fn finish(mut self) -> UResult<()> { + self.writer + .flush() + .map_err_context(|| "(standard output)".to_string())?; + + if self.had_error { + Err(ExitCode::new(2)) + } else if self.any_match { + Ok(()) + } else { + Err(ExitCode::new(1)) // aka: no match + } + } + + fn process_dir( + &mut self, + lb: &mut LineBuffer, + start: &Path, + strip_root: bool, + ) -> ControlFlow<()> { + let mut walker = WalkDir::new(start) + .follow_links(self.config.follow_symlinks) + .into_iter(); + + while let Some(entry) = walker.next() { + let Ok(entry) = entry else { + continue; + }; + + let file_type = entry.file_type(); + + // We're only interested in files, so skip dirs. + // If we have a --exclude-dir pattern, skip matching directories entirely. + if file_type.is_dir() { + if self.config.exclude_dir_globs.matches(entry.file_name()) { + walker.skip_current_dir(); + } + continue; + } + + // GNU `-r` doesn't follow symlinks. + // With `-R` we already had walkdir resolve them. + if file_type.is_symlink() { + continue; + } + + // Skip non-regular files unless `-D read` was given explicitly. + if !file_type.is_file() && self.config.device_mode != DeviceMode::Read { + continue; + } + + // Handle include/exclude globs. + let name = entry.file_name(); + if (!self.config.include_globs.is_empty() && !self.config.include_globs.matches(name)) + || self.config.exclude_globs.matches(name) + { + continue; + } + + let mut path = entry.path(); + if strip_root { + path = Self::strip_dot_prefix(path); + } + + self.process_file(lb, path)?; + } + + ControlFlow::Continue(()) + } + + fn process_file(&mut self, lb: &mut LineBuffer, path: &Path) -> ControlFlow<()> { + let result = File::open(path).and_then(|mut file| { + if self.writer.wants_padded_line_numbers() { + let file_size = file.metadata().map_or(0, |m| m.len()); + self.writer + .set_line_number_width(file_size.max(1).ilog10() as usize + 1); + } + self.session_run(lb, path, &mut file) + }); + self.record_result(path.as_os_str(), result) + } + + fn record_result(&mut self, label: &OsStr, result: io::Result) -> ControlFlow<()> { + match result { + Ok(true) => { + self.any_match = true; + + // In quiet mode, all we want is the exit code, which means + // we can stop searching as soon as we see the first match. + if self.config.quiet { + return ControlFlow::Break(()); + } + } + Ok(false) => {} + Err(err) => { + self.had_error = true; + self.writer.report_io_error(label, &err); + } + } + + ControlFlow::Continue(()) + } + + fn session_any_match(&self) -> bool { + self.session_match_count > 0 + } + + fn session_can_match(&self) -> bool { + self.config + .max_count + .is_none_or(|max| self.session_match_count < max) + } + + fn session_should_continue(&self) -> bool { + self.session_can_match() || self.session_after_remaining > 0 + } + + fn session_suppress_normal_output(&self) -> bool { + self.config.count || self.config.files_without_match || self.session_binary_detected + } + + fn session_needs_match_positions(&self) -> bool { + (self.config.word_regexp + || self.config.line_regexp + || self.config.only_matching + || self.config.use_color) + && !self.session_suppress_normal_output() + } + + /// Should the trailing `Binary file ... matches` notice be emitted? + /// Suppressed by `-c`, `-l`, `-L`, `-q` (all folded into + /// [`Self::binary_notice_enabled`] at construction time). + fn session_should_emit_binary_notice(&self) -> bool { + self.binary_notice_enabled && self.session_binary_detected && self.session_any_match() + } + + fn session_run( + &mut self, + lb: &mut LineBuffer, + path: &Path, + reader: &mut File, + ) -> io::Result { + // Reset all session (per-file) state. + self.session_context_buf.clear(); + self.session_match_count = 0; + self.session_after_remaining = 0; + self.session_last_printed_line = 0; + self.session_binary_detected = false; + lb.reset(); + + let mut line_number: u64 = 0; + + while let Some((line, line_start)) = lb.read_line(reader)? { + line_number += 1; + + // Handle `-U, --binary` On Windows. + #[cfg(windows)] + let line = + if self.config.strip_cr && !self.config.null_data && line.last() == Some(&b'\r') { + &line[..line.len() - 1] + } else { + line + }; + + // Any null byte flips us into binary mode. + if !self.session_mark_binary_if(|| memchr(0, line).is_some()) { + return Ok(false); + } + + if let Some(positions) = self.session_match_line(line) { + // TODO: GNU grep respects LANG. Here, I'm always checking for valid UTF-8. + if !self.session_mark_binary_if(|| std::str::from_utf8(line).is_err()) { + return Ok(false); + } + + // Print the match and context, and update session state accordingly. + if !self.session_handle_match(path, line_number, line_start, line, &positions)? { + return Ok(true); + } + + if self.session_should_emit_binary_notice() { + self.writer.report_binary_match(path); + return Ok(true); + } + } else { + self.session_handle_nonmatch(path, line_number, line_start, line)?; + } + + if !self.session_should_continue() { + break; + } + } + + self.session_finalize(path) + } + + /// Mark the file as binary when `predicate` returns true. + #[inline(always)] + fn session_mark_binary_if(&mut self, predicate: impl FnOnce() -> bool) -> bool { + if self.session_binary_detected || self.config.binary_mode == BinaryMode::Text { + return true; + } + if !predicate() { + return true; + } + self.session_binary_detected = true; + self.config.binary_mode != BinaryMode::WithoutMatch + } + + fn session_match_line(&self, line: &[u8]) -> Option> { + if !self.session_can_match() { + None + } else if self.session_needs_match_positions() { + self.matcher.match_line(line) + } else { + self.matcher.is_match(line) + } + } + + /// Returns false if this file's search is complete (e.g. `-q`, `-l`, `-L`). + fn session_handle_match( + &mut self, + path: &Path, + line_number: u64, + byte_offset: u64, + line: &[u8], + positions: &[(usize, usize)], + ) -> io::Result { + self.session_match_count += 1; + + if self.config.quiet { + return Ok(false); + } + if self.config.files_with_matches { + self.writer.write_filename(path)?; + return Ok(false); + } + if self.config.files_without_match { + return Ok(false); + } + + if !self.session_suppress_normal_output() { + self.session_write_match_with_context( + path, + &LineView { + line, + line_number, + byte_offset, + is_match: true, + match_positions: positions, + }, + )?; + } + + self.session_after_remaining = self.config.after_context; + Ok(true) + } + + fn session_handle_nonmatch( + &mut self, + path: &Path, + line_number: u64, + byte_offset: u64, + line: &[u8], + ) -> io::Result<()> { + if self.session_after_remaining > 0 { + if !self.session_suppress_normal_output() { + self.writer.write_line( + &LineView { + line, + line_number, + byte_offset, + is_match: false, + match_positions: &[], + }, + path, + )?; + self.session_last_printed_line = line_number; + } + self.session_after_remaining -= 1; + } else if self.config.before_context > 0 + && self.session_can_match() + && !self.session_suppress_normal_output() + { + self.session_context_buf + .push(line, line_number, byte_offset); + } + Ok(()) + } + + fn session_write_match_with_context( + &mut self, + path: &Path, + view: &LineView<'_>, + ) -> io::Result<()> { + // Group separator between non-adjacent groups. + // `last_printed_line == 0` means we haven't printed anything yet. + // = first group = skip the separator + if self.config.has_context + && self.session_last_printed_line > 0 + && view.line_number > self.session_last_printed_line + 1 + { + self.writer.write_group_separator()?; + } + + for ctx in self.session_context_buf.drain_iter() { + if ctx.line_number > self.session_last_printed_line { + self.writer.write_line(&ctx.view(), path)?; + self.session_last_printed_line = ctx.line_number; + } + } + + self.writer.write_line(view, path)?; + self.session_last_printed_line = view.line_number; + Ok(()) + } + + /// End-of-file bookkeeping: count / `-L` / binary notice. + fn session_finalize(&mut self, path: &Path) -> io::Result { + if self.config.count && !self.config.files_with_matches && !self.config.files_without_match + { + self.writer.write_count(self.session_match_count, path)?; + } + if self.config.files_without_match && !self.session_any_match() { + self.writer.write_filename(path)?; + } + if self.session_should_emit_binary_notice() { + self.writer.report_binary_match(path); + } + Ok(self.session_any_match()) + } + + fn strip_dot_prefix(path: &Path) -> &Path { + let bytes = path.as_os_str().as_encoded_bytes(); + + #[cfg(windows)] + let bytes = bytes + .strip_prefix(b".\\") + .or_else(|| bytes.strip_prefix(b"./")) + .unwrap_or(bytes); + + #[cfg(not(windows))] + let bytes = bytes.strip_prefix(b"./").unwrap_or(bytes); + + // SAFETY: We sliced off a pure ASCII prefix off of `path`. + Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }) + } + + fn is_special_file(path: &Path) -> bool { + match std::fs::metadata(path) { + Ok(m) => { + let ft = m.file_type(); + !ft.is_file() && !ft.is_dir() + } + Err(_) => false, + } + } +} diff --git a/tests/test_grep.rs b/tests/test_grep.rs new file mode 100644 index 0000000..0de181a --- /dev/null +++ b/tests/test_grep.rs @@ -0,0 +1,1143 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use uutests::util::{TestScenario, UCommand}; + +static UCMD_INIT: AtomicBool = AtomicBool::new(false); + +fn ucmd() -> (TestScenario, UCommand) { + if !UCMD_INIT.swap(true, Ordering::Relaxed) { + unsafe { std::env::set_var("UUTESTS_BINARY_PATH", env!("CARGO_BIN_EXE_grep")) }; + } + + let scene = TestScenario::new("grep"); + let cmd = scene.cmd(env!("CARGO_BIN_EXE_grep")); + (scene, cmd) +} + +#[test] +fn bre_default_metacharacters() { + // BRE: . * ^ $ [] [^] and literal +, |, (, ) + let cases: &[(&str, &str, &str)] = &[ + ("a.c", "abc\nadc\nac\n", "abc\nadc\n"), + ("fo*", "f\nfo\nfoo\nbar\n", "f\nfo\nfoo\n"), + ("^foo", "foo\nbarfoo\nfoo\n", "foo\nfoo\n"), + ("bar$", "bar\nfoobar\nbarx\n", "bar\nfoobar\n"), + ("[Hh]i", "Hi\nhi\nHI\n", "Hi\nhi\n"), + ("[^a-z]X", "aX\nbX\n.X\n", ".X\n"), + // `+`, `|`, `(`, `)` are literals in BRE + ("a+b", "a+b\nab\n", "a+b\n"), + ("a|b", "a|b\na\n", "a|b\n"), + ("(x)", "(x)\nx\n", "(x)\n"), + ]; + for (pat, input, expected) in cases { + let (_s, mut c) = ucmd(); + c.args(&[pat]) + .pipe_in(*input) + .succeeds() + .stdout_only(*expected); + } +} + +#[test] +fn bre_gnu_extensions() { + // \+ \? \| \{m,n\} \< \> \b \w plus backreferences and leading `*`. + let (_s, mut c) = ucmd(); + c.args(&[r"o\+"]) + .pipe_in("o\noo\nx\n") + .succeeds() + .stdout_only("o\noo\n"); + + let (_s, mut c) = ucmd(); + c.args(&[r"Hi\|HI"]) + .pipe_in("Hi\nHI\nhi\n") + .succeeds() + .stdout_only("Hi\nHI\n"); + + let (_s, mut c) = ucmd(); + c.args(&[r"a\{2,3\}"]) + .pipe_in("a\naa\naaaa\n") + .succeeds() + .stdout_only("aa\naaaa\n"); + + let (_s, mut c) = ucmd(); + c.args(&[r"\"]) + .pipe_in("word\nwording\nthe word here\n") + .succeeds() + .stdout_only("word\nthe word here\n"); + + let (_s, mut c) = ucmd(); + c.args(&[r"\bcontain\b"]) + .pipe_in("contain\ncontainer\ncontained\n") + .succeeds() + .stdout_only("contain\n"); + + // BRE backreference: repeated adjacent word. + let (_s, mut c) = ucmd(); + c.args(&[r"\(\b\w\+\b\) \1"]) + .pipe_in("the the cat\nfoo bar\nis is great\n") + .succeeds() + .stdout_only("the the cat\nis is great\n"); + + // Leading `*` is literal in BRE. + let (_s, mut c) = ucmd(); + c.args(&["*foo"]) + .pipe_in("*foo\nfoo\n**foo\n") + .succeeds() + .stdout_only("*foo\n**foo\n"); +} + +#[test] +fn ere_metacharacters() { + let cases: &[(&[&str], &str, &str)] = &[ + (&["-E", "Hi|HI"], "Hi\nHI\nhi\n", "Hi\nHI\n"), + (&["-E", "o+"], "o\noo\nx\n", "o\noo\n"), + ( + &["-E", "colou?r"], + "colour\ncolor\ncolouur\n", + "colour\ncolor\n", + ), + (&["-E", "(foo|bar)x"], "foox\nbarx\nbaz\n", "foox\nbarx\n"), + (&["-E", "o{2}"], "o\noo\nooo\n", "oo\nooo\n"), + ( + &["-E", "a{,2}b"], + "b\nab\naab\naaab\n", + "b\nab\naab\naaab\n", + ), + // ERE backreference works on the Oniguruma path. + ( + &["-E", r"(....).*\1"], + "beriberi\nhelloworld\n", + "beriberi\n", + ), + ]; + for (args, input, expected) in cases { + let (_s, mut c) = ucmd(); + c.args(args) + .pipe_in(*input) + .succeeds() + .stdout_only(*expected); + } +} + +#[test] +fn ere_invalid_pattern_is_error() { + let (_s, mut c) = ucmd(); + c.args(&["-E", "["]) + .pipe_in("x\n") + .fails_with_code(2) + .stderr_contains("invalid pattern"); +} + +#[test] +fn fixed_string_is_literal() { + // Metacharacters are not interpreted. + let (_s, mut c) = ucmd(); + c.args(&["-F", ".*+?"]) + .pipe_in("a.*+?b\n.*\n") + .succeeds() + .stdout_only("a.*+?b\n"); + + // `o+` is the two characters `o+`, so doesn't match `foo`. + let (_s, mut c) = ucmd(); + c.args(&["-F", "o+"]).pipe_in("foo\n").fails_with_code(1); + + // Multiple -e patterns. + let (_s, mut c) = ucmd(); + c.args(&["-F", "-e", "hi", "-e", "HI"]) + .pipe_in("hi\nHI\nlo\n") + .succeeds() + .stdout_only("hi\nHI\n"); +} + +#[test] +fn pcre_features() { + let (_s, mut c) = ucmd(); + c.args(&["-P", r"\d+"]) + .pipe_in("abc\n123\nfoo42bar\n") + .succeeds() + .stdout_only("123\nfoo42bar\n"); + + // Lookahead. + let (_s, mut c) = ucmd(); + c.args(&["-P", "-o", r"foo(?=\d)"]) + .pipe_in("foo123\nfooBar\n") + .succeeds() + .stdout_only("foo\n"); + + // Lookbehind. + let (_s, mut c) = ucmd(); + c.args(&["-P", "-o", r"(?<=v=)\d+"]) + .pipe_in("v=42\nv=7x\n") + .succeeds() + .stdout_only("42\n7\n"); +} + +#[test] +fn posix_character_classes() { + let (_s, mut c) = ucmd(); + c.args(&["-E", "[[:digit:]]+"]) + .pipe_in("abc\n42\nx9y\n") + .succeeds() + .stdout_only("42\nx9y\n"); + + let (_s, mut c) = ucmd(); + c.args(&["-E", "[[:notdef:]]"]) + .pipe_in("x\n") + .fails_with_code(2); +} + +#[test] +fn longest_match_semantics() { + // -F with overlapping alternatives must return the longest. + let (_s, mut c) = ucmd(); + c.args(&["-F", "-o", "-e", "sam", "-e", "samwise"]) + .pipe_in("samwise\n") + .succeeds() + .stdout_only("samwise\n"); + + // ERE alternation: longest wins, regardless of branch order. + let (_s, mut c) = ucmd(); + c.args(&["-E", "-o", "foo|foobar|foobarbaz"]) + .pipe_in("foobarbaz\n") + .succeeds() + .stdout_only("foobarbaz\n"); + + // Regression: `REGEX_OPTION_FIND_LONGEST` must be re-anchored at each + // match start. A naive line-anchored search would swallow `"x" "y"` + // as a single match. + let (_s, mut c) = ucmd(); + c.args(&["-o", r#""[^"]*""#]) + .pipe_in("\"x\" \"y\"\n") + .succeeds() + .stdout_only("\"x\"\n\"y\"\n"); +} + +#[test] +fn ignore_case_and_override() { + let input = "Hello\nhELLO\nHELLO\nworld\n"; + + // -i: all three case variants match the lowercase pattern. + let (_s, mut c) = ucmd(); + c.args(&["-i", "hello"]) + .pipe_in(input) + .succeeds() + .stdout_only("Hello\nhELLO\nHELLO\n"); + + // --no-ignore-case undoes an earlier -i. Searching for `Hello` now + // matches only the exactly-cased line. + let (_s, mut c) = ucmd(); + c.args(&["-i", "--no-ignore-case", "Hello"]) + .pipe_in(input) + .succeeds() + .stdout_only("Hello\n"); +} + +#[test] +fn invert_match() { + let (_s, mut c) = ucmd(); + c.args(&["-v", "foo"]) + .pipe_in("foo\nbar\nfoobar\nbaz\n") + .succeeds() + .stdout_only("bar\nbaz\n"); +} + +#[test] +fn word_regexp() { + let (_s, mut c) = ucmd(); + c.args(&["-w", "foo"]) + .pipe_in("foo\nfoobar\nfoo bar\nxfoox\n") + .succeeds() + .stdout_only("foo\nfoo bar\n"); + + // Also works with -F. + let (_s, mut c) = ucmd(); + c.args(&["-F", "-w", "foo"]) + .pipe_in("foo bar\nfoobar\n") + .succeeds() + .stdout_only("foo bar\n"); +} + +#[test] +fn line_regexp() { + let (_s, mut c) = ucmd(); + c.args(&["-x", "foo bar"]) + .pipe_in("foo bar\nfoo bar!\nx foo bar\n") + .succeeds() + .stdout_only("foo bar\n"); +} + +#[test] +fn max_count() { + // Basic cap. + let (_s, mut c) = ucmd(); + c.args(&["-m", "2", "a"]) + .pipe_in("a\na\na\na\n") + .succeeds() + .stdout_only("a\na\n"); + + // -m 0 means zero. Exit 1, no output. + let (_s, mut c) = ucmd(); + c.args(&["-m", "0", "a"]) + .pipe_in("a\n") + .fails_with_code(1) + .no_stdout(); + + // -A trailing context still printed after the cutoff. + let (_s, mut c) = ucmd(); + c.args(&["-m", "1", "-A", "2", "match"]) + .pipe_in("noise\nmatch\nctx1\nctx2\ntail\n") + .succeeds() + .stdout_only("match\nctx1\nctx2\n"); + + // -c is capped by -m. + let (_s, mut c) = ucmd(); + c.args(&["-c", "-m", "1", "a"]) + .pipe_in("a\na\na\n") + .succeeds() + .stdout_only("1\n"); +} + +#[test] +fn pattern_sources() { + // Positional. + let (_s, mut c) = ucmd(); + c.args(&["foo"]) + .pipe_in("foo\nbar\n") + .succeeds() + .stdout_only("foo\n"); + + // -e single and repeated. + let (_s, mut c) = ucmd(); + c.args(&["-e", "foo", "-e", "bar"]) + .pipe_in("foo\nbar\nbaz\n") + .succeeds() + .stdout_only("foo\nbar\n"); + + // -e containing a newline is split into multiple patterns. + let (_s, mut c) = ucmd(); + c.args(&["-e", "foo\nbar"]) + .pipe_in("foo\nbaz\nbar\n") + .succeeds() + .stdout_only("foo\nbar\n"); + + // -f reads one pattern per line. + let (scene, mut c) = ucmd(); + scene.fixtures.write("pats", "foo\nbar\n"); + c.args(&["-f", "pats"]) + .pipe_in("foo\nbaz\nbar\n") + .succeeds() + .stdout_only("foo\nbar\n"); + + // Combined -e and -f. + let (scene, mut c) = ucmd(); + scene.fixtures.write("pats", "bar\n"); + c.args(&["-e", "foo", "-f", "pats"]) + .pipe_in("foo\nbar\nbaz\n") + .succeeds() + .stdout_only("foo\nbar\n"); + + // -f from stdin via `-`. + let (_s, mut c) = ucmd(); + c.args(&["-f", "-", "-e", "literal"]) + .pipe_in("foo\nbar\n") + .fails_with_code(1); + + // `-e EXPR` splits on `\n`; trailing newline = empty pattern = matches all. + let (_s, mut c) = ucmd(); + c.args(&["-e", "foo\n"]) + .pipe_in("foo\nbar\nbaz\n") + .succeeds() + .stdout_only("foo\nbar\nbaz\n"); + + // `-f` strips one trailing `\n` as the file terminator, so a sole-`\n` + // file is one empty pattern (vs. zero for a truly empty file). + let (scene, mut c) = ucmd(); + scene.fixtures.write("pat_just_nl", "\n"); + c.args(&["-f", "pat_just_nl"]) + .pipe_in("a\nb\n") + .succeeds() + .stdout_only("a\nb\n"); +} + +#[test] +fn empty_pattern_file_matches_nothing() { + let (scene, mut c) = ucmd(); + scene.fixtures.write("empty", ""); + c.args(&["-f", "empty"]) + .pipe_in("anything\n") + .fails_with_code(1) + .no_stdout(); +} + +#[test] +fn empty_pattern_matches_every_line() { + let (_s, mut c) = ucmd(); + c.args(&["-E", ""]) + .pipe_in("a\nb\nc\n") + .succeeds() + .stdout_only("a\nb\nc\n"); +} + +#[test] +fn pattern_starting_with_dash_needs_double_dash() { + let (_s, mut c) = ucmd(); + c.args(&["--", "-foo-"]) + .pipe_in("x -foo- y\nplain\n") + .succeeds() + .stdout_only("x -foo- y\n"); + + // Or via -e. + let (_s, mut c) = ucmd(); + c.args(&["-e", "-foo-"]) + .pipe_in("x -foo- y\n") + .succeeds() + .stdout_only("x -foo- y\n"); +} + +#[test] +fn no_pattern_is_usage_error() { + let (_s, mut c) = ucmd(); + c.fails_with_code(2); +} + +#[test] +fn count_modes() { + // Count for single stdin. + let (_s, mut c) = ucmd(); + c.args(&["-c", "a"]) + .pipe_in("a\nb\na\n") + .succeeds() + .stdout_only("2\n"); + + // Count with -v counts non-matching lines. + let (_s, mut c) = ucmd(); + c.args(&["-c", "-v", "a"]) + .pipe_in("a\nb\na\nc\n") + .succeeds() + .stdout_only("2\n"); + + // Count per file with multiple files. + let (scene, mut c) = ucmd(); + scene.fixtures.write("f1", "a\nb\na\n"); + scene.fixtures.write("f2", "x\ny\n"); + c.args(&["-c", "a", "f1", "f2"]) + .succeeds() + .stdout_only("f1:2\nf2:0\n"); +} + +#[test] +fn files_with_and_without_matches() { + let (scene, mut c) = ucmd(); + scene.fixtures.write("hit", "yes\n"); + scene.fixtures.write("miss", "no\n"); + + c.args(&["-l", "yes", "hit", "miss"]) + .succeeds() + .stdout_only("hit\n"); + + // -L: list files with no match. Exit code follows match semantics. + // Since "missing" never matched anywhere, exit is 1. + let (scene, mut c) = ucmd(); + scene.fixtures.write("hit", "yes\n"); + scene.fixtures.write("miss", "no\n"); + c.args(&["-L", "missing", "hit", "miss"]) + .fails_with_code(1) + .stdout_is("hit\nmiss\n"); + + // -l early-exits after the first match. Verify it doesn't print twice. + // when the file has many. + let (scene, mut c) = ucmd(); + scene.fixtures.write("many", "x\nx\nx\nx\nx\n"); + c.args(&["-l", "x", "many"]) + .succeeds() + .stdout_only("many\n"); +} + +#[test] +fn count_combined_with_listing_flags() { + let (scene, _) = ucmd(); + scene.fixtures.write("hit", "yes\n"); + scene.fixtures.write("miss", "no\n"); + + // -c + -l: -l wins, only filenames printed. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-c", "-l", "yes", "hit", "miss"]) + .succeeds() + .stdout_only("hit\n"); + + // -c + -o: -c wins (count, not matches). + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-c", "-o", "x"]) + .pipe_in("xxx\n") + .succeeds() + .stdout_only("1\n"); +} + +#[test] +fn only_matching() { + // Multiple matches per line. + let (_s, mut c) = ucmd(); + c.args(&["-o", "-E", "foo"]) + .pipe_in("foo bar foo\nbaz\n") + .succeeds() + .stdout_only("foo\nfoo\n"); + + // -o -n shows line number per match, not per line. + let (_s, mut c) = ucmd(); + c.args(&["-o", "-n", "-E", "x"]) + .pipe_in("xx\ny\nxx\n") + .succeeds() + .stdout_only("1:x\n1:x\n3:x\n3:x\n"); + + // -o -b uses the byte offset of the match itself. + let (_s, mut c) = ucmd(); + c.args(&["-o", "-b", "-E", "ab"]) + .pipe_in("xxab yyab\n") + .succeeds() + .stdout_only("2:ab\n7:ab\n"); + + // -o -i preserves the matched text's original case. + let (_s, mut c) = ucmd(); + c.args(&["-o", "-i", "hello"]) + .pipe_in("Hello\nhELLO\nHELLO!\n") + .succeeds() + .stdout_only("Hello\nhELLO\nHELLO\n"); + + // After a match ends, ^ must not re-match at that position. + let (_s, mut c) = ucmd(); + c.args(&["-o", "^hello*"]) + .pipe_in("hellooo_hello\n") + .succeeds() + .stdout_only("hellooo\n"); +} + +#[test] +fn quiet_modes() { + // Match: exit 0, no output. + let (_s, mut c) = ucmd(); + c.args(&["-q", "a"]).pipe_in("a\n").succeeds().no_output(); + + // No match: exit 1, no output. + let (_s, mut c) = ucmd(); + c.args(&["-q", "z"]) + .pipe_in("a\n") + .fails_with_code(1) + .no_output(); +} + +#[test] +fn auto_filename_prefix() { + let (scene, _) = ucmd(); + scene.fixtures.write("a", "hit\n"); + scene.fixtures.write("b", "hit\n"); + + // Single file: no prefix. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["hit", "a"]).succeeds().stdout_only("hit\n"); + + // Multiple files: prefix shown. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["hit", "a", "b"]) + .succeeds() + .stdout_only("a:hit\nb:hit\n"); +} + +#[test] +fn force_filename_flags() { + let (scene, _) = ucmd(); + scene.fixtures.write("a", "hit\n"); + + // -H forces prefix on a single file. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-H", "hit", "a"]) + .succeeds() + .stdout_only("a:hit\n"); + + // -h suppresses it even with multiple files. + scene.fixtures.write("b", "hit\n"); + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-h", "hit", "a", "b"]) + .succeeds() + .stdout_only("hit\nhit\n"); + + // Last-one-wins between -H and -h (clap overrides_with). + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-H", "-h", "hit", "a"]) + .succeeds() + .stdout_only("hit\n"); + + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-h", "-H", "hit", "a"]) + .succeeds() + .stdout_only("a:hit\n"); +} + +#[test] +fn label_only_applies_to_stdin() { + // --label replaces "(standard input)". + let (_s, mut c) = ucmd(); + c.args(&["--label=IN", "-H", "x"]) + .pipe_in("x\n") + .succeeds() + .stdout_only("IN:x\n"); + + // For a real file, --label is ignored. + let (scene, mut c) = ucmd(); + scene.fixtures.write("real", "x\n"); + c.args(&["--label=IN", "-H", "x", "real"]) + .succeeds() + .stdout_only("real:x\n"); +} + +#[test] +fn line_number_and_byte_offset_prefixes() { + let (_s, mut c) = ucmd(); + c.args(&["-n", "b"]) + .pipe_in("a\nb\nc\nb\n") + .succeeds() + .stdout_only("2:b\n4:b\n"); + + // Byte offset alone. + let (_s, mut c) = ucmd(); + c.args(&["-b", "world"]) + .pipe_in("hello\nworld\n") + .succeeds() + .stdout_only("6:world\n"); + + // Combined -n -b -H. + let (scene, mut c) = ucmd(); + scene.fixtures.write("f", "hello world\n"); + c.args(&["-H", "-n", "-b", "world", "f"]) + .succeeds() + .stdout_only("f:1:0:hello world\n"); + + // -T inserts a tab between the prefix and the content for alignment. + // The amount of leading padding is implementation-defined (GNU pads + // generously, this impl pads tightly), so we only assert the suffix. + let (_s, mut c) = ucmd(); + c.args(&["-T", "-n", "x"]) + .pipe_in("x\n") + .succeeds() + .stdout_contains("1:\tx\n"); +} + +#[test] +fn null_filename_separator() { + let (scene, mut c) = ucmd(); + scene.fixtures.write("f", "x\n"); + c.args(&["-Z", "-l", "x", "f"]) + .succeeds() + .stdout_only("f\0"); +} + +#[test] +fn after_before_combined_context() { + let input = "a\nb\nMATCH\nc\nd\n"; + + let (_s, mut c) = ucmd(); + c.args(&["-A", "1", "MATCH"]) + .pipe_in(input) + .succeeds() + .stdout_only("MATCH\nc\n"); + + let (_s, mut c) = ucmd(); + c.args(&["-B", "1", "MATCH"]) + .pipe_in(input) + .succeeds() + .stdout_only("b\nMATCH\n"); + + let (_s, mut c) = ucmd(); + c.args(&["-C", "1", "MATCH"]) + .pipe_in(input) + .succeeds() + .stdout_only("b\nMATCH\nc\n"); +} + +#[test] +fn num_shorthand_is_context() { + // `-2` is shorthand for `-C 2`. + let (_s, mut c) = ucmd(); + c.args(&["-2", "MATCH"]) + .pipe_in("a\nb\nMATCH\nc\nd\n") + .succeeds() + .stdout_only("a\nb\nMATCH\nc\nd\n"); +} + +#[test] +fn context_line_prefixes_use_dash() { + // Match line uses `:`, context lines use `-`. + let (_s, mut c) = ucmd(); + c.args(&["-n", "-A", "1", "MATCH"]) + .pipe_in("a\nMATCH\nb\n") + .succeeds() + .stdout_only("2:MATCH\n3-b\n"); +} + +#[test] +fn group_separator_behavior() { + let input = "M\nx\nx\nx\nM\n"; + + // Default `--` between non-adjacent match groups. + let (_s, mut c) = ucmd(); + c.args(&["-C", "0", "M"]) + .pipe_in(input) + .succeeds() + .stdout_only("M\n--\nM\n"); + + // --no-group-separator suppresses it. + let (_s, mut c) = ucmd(); + c.args(&["--no-group-separator", "-C", "0", "M"]) + .pipe_in(input) + .succeeds() + .stdout_only("M\nM\n"); + + // Custom separator. + let (_s, mut c) = ucmd(); + c.args(&["--group-separator=***", "-C", "0", "M"]) + .pipe_in(input) + .succeeds() + .stdout_only("M\n***\nM\n"); +} + +#[test] +fn overlapping_context_not_duplicated() { + let (_s, mut c) = ucmd(); + c.args(&["-n", "-C", "1", "-E", "b|d"]) + .pipe_in("a\nb\nc\nd\ne\n") + .succeeds() + .stdout_only("1-a\n2:b\n3-c\n4:d\n5-e\n"); +} + +#[test] +fn color_always_emits_sgr_el_sequence() { + let (_s, mut c) = ucmd(); + c.args(&["--color=always", "foo"]) + .pipe_in("foo\n") + .succeeds() + .stdout_contains("\x1b[01;31m\x1b[Kfoo\x1b[m\x1b[K"); +} + +#[test] +fn color_never_emits_no_escapes() { + let (_s, mut c) = ucmd(); + c.args(&["--color=never", "foo"]) + .pipe_in("foo\n") + .succeeds() + .stdout_only("foo\n"); +} + +#[test] +fn color_line_number_uses_green() { + let (_s, mut c) = ucmd(); + c.args(&["--color=always", "-n", "foo"]) + .pipe_in("foo\n") + .succeeds() + .stdout_contains("\x1b[32m\x1b[K1\x1b[m\x1b[K"); +} + +#[test] +fn color_with_ignore_case_preserves_original_text() { + let (_s, mut c) = ucmd(); + c.args(&["--color=always", "-i", "word"]) + .pipe_in("Word\nwORD\n") + .succeeds() + .stdout_contains("Word") + .stdout_contains("wORD") + .stdout_contains("\x1b[01;31m\x1b[KWord\x1b[m\x1b[K") + .stdout_contains("\x1b[01;31m\x1b[KwORD\x1b[m\x1b[K"); +} + +#[test] +fn color_anchored_pattern_no_rematch_at_match_end() { + let (_s, mut c) = ucmd(); + c.args(&["--color=always", "^word_*"]) + .pipe_in("word_word\n") + .succeeds() + .stdout_contains("\x1b[01;31m\x1b[Kword_\x1b[m\x1b[K"); +} + +#[test] +fn grep_colors_env_overrides() { + let (_s, mut c) = ucmd(); + c.env("GREP_COLORS", "ms=33:ln=34") + .args(&["--color=always", "-n", "foo"]) + .pipe_in("foo\n") + .succeeds() + .stdout_contains("\x1b[33m\x1b[Kfoo\x1b[m\x1b[K") + .stdout_contains("\x1b[34m\x1b[K1\x1b[m\x1b[K"); +} + +#[test] +fn legacy_grep_color_env() { + // uutests clears the environment by default, + // so we need to set GREP_COLORS explicitly. + let (_s, mut c) = ucmd(); + c.env("GREP_COLOR", "44") + .args(&["--color=always", "foo"]) + .pipe_in("foo\n") + .succeeds() + .stdout_contains("\x1b[44m\x1b[Kfoo\x1b[m\x1b[K"); +} + +#[test] +fn binary_detection_via_nul_byte() { + let (scene, mut c) = ucmd(); + scene.fixtures.write_bytes("b", b"hit\0\n"); + c.args(&["hit", "b"]) + .succeeds() + .no_stdout() + .stderr_contains("binary file matches"); +} + +#[test] +fn binary_detection_via_invalid_utf8() { + let (scene, mut c) = ucmd(); + scene.fixtures.write_bytes("b", b"a\x9db\n"); + c.args(&["a", "b"]) + .succeeds() + .no_stdout() + .stderr_contains("binary file matches"); +} + +#[test] +fn lone_control_byte_is_still_text() { + let (scene, mut c) = ucmd(); + scene.fixtures.write_bytes("ctl", b"a\x01b\n"); + c.args(&["a", "ctl"]) + .succeeds() + .stdout_is_bytes(b"a\x01b\n") + .no_stderr(); +} + +#[test] +fn invalid_utf8_on_nonmatching_line_does_not_poison_file() { + // Whether the bad byte appears before or after the match, the matching + // line must still be returned and the file must not be reported as binary. + let (scene, _) = ucmd(); + scene.fixtures.write_bytes("before", b"x\x9dy\nhit\n"); + scene.fixtures.write_bytes("after", b"hit\nx\x9dy\n"); + + for name in ["before", "after"] { + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["hit", name]) + .succeeds() + .stdout_is("hit\n") + .no_stderr(); + } +} + +#[test] +fn binary_files_text_forces_text_mode() { + let (scene, _) = ucmd(); + scene.fixtures.write_bytes("b", b"hit\0more\n"); + + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-a", "hit", "b"]) + .succeeds() + .stdout_contains("hit"); + + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["--binary-files=text", "hit", "b"]) + .succeeds() + .stdout_contains("hit"); +} + +#[test] +fn binary_files_without_match_skips() { + let (scene, _) = ucmd(); + scene.fixtures.write_bytes("b", b"hit\0more\n"); + + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-I", "hit", "b"]).fails_with_code(1).no_output(); + + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["--binary-files=without-match", "hit", "b"]) + .fails_with_code(1) + .no_output(); +} + +fn build_tree(scene: &TestScenario) { + scene.fixtures.mkdir_all("tree"); + scene.fixtures.mkdir_all("tree/sub"); + scene.fixtures.write("tree/a.txt", "grep me\n"); + scene.fixtures.write("tree/b.log", "grep me\n"); + scene.fixtures.write("tree/sub/c.txt", "grep me\n"); +} + +#[test] +fn recursive_default() { + let (scene, mut c) = ucmd(); + build_tree(&scene); + c.args(&["-r", "-l", "grep", "tree"]) + .succeeds() + .stdout_contains("a.txt") + .stdout_contains("b.log") + .stdout_contains("c.txt"); +} + +#[test] +fn recursive_no_file_defaults_to_cwd_not_stdin() { + let (scene, mut c) = ucmd(); + build_tree(&scene); + c.current_dir(scene.fixtures.plus("tree")) + .args(&["-r", "-l", "grep"]) + // We did NOT pipe in anything; if the impl tried to read stdin we'd + // hang or fail. cwd should be searched instead. + .succeeds() + .stdout_contains("a.txt"); +} + +#[test] +fn recursive_with_include_exclude() { + let (scene, _) = ucmd(); + build_tree(&scene); + + // include filters in. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-r", "-l", "--include=*.txt", "grep", "tree"]) + .succeeds() + .stdout_contains("a.txt") + .stdout_does_not_contain("b.log"); + + // exclude filters out. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-r", "-l", "--exclude=*.log", "grep", "tree"]) + .succeeds() + .stdout_contains("a.txt") + .stdout_does_not_contain("b.log"); + + // exclude-dir filters whole directories. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-r", "-l", "--exclude-dir=sub", "grep", "tree"]) + .succeeds() + .stdout_does_not_contain("c.txt"); + + // include + exclude both apply (exclude wins on conflict). + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&[ + "-r", + "-l", + "--include=*.txt", + "--include=*.log", + "--exclude=*.log", + "grep", + "tree", + ]) + .succeeds() + .stdout_contains("a.txt") + .stdout_does_not_contain("b.log"); +} + +#[test] +fn recursive_exclude_from_file() { + let (scene, mut c) = ucmd(); + build_tree(&scene); + scene.fixtures.write("excludes", "*.log\n"); + c.args(&["-r", "-l", "--exclude-from=excludes", "grep", "tree"]) + .succeeds() + .stdout_contains("a.txt") + .stdout_does_not_contain("b.log"); +} + +#[cfg(unix)] +#[test] +fn dereference_recursive_follows_symlinks() { + use std::os::unix::fs::symlink; + + let (scene, _) = ucmd(); + scene.fixtures.mkdir_all("tree"); + scene.fixtures.mkdir_all("target"); + scene.fixtures.write("target/hit.txt", "grep me\n"); + symlink( + scene.fixtures.plus("target"), + scene.fixtures.plus("tree/link"), + ) + .unwrap(); + + // -R must follow the symlink. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-R", "-l", "grep", "tree"]) + .succeeds() + .stdout_contains("hit.txt"); + + // -r must NOT follow it. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-r", "-l", "grep", "tree"]) + .fails_with_code(1) + .no_output(); +} + +#[test] +fn directories_skip_silently() { + let (scene, mut c) = ucmd(); + build_tree(&scene); + c.args(&["-d", "skip", "grep", "tree"]) + .fails_with_code(1) + .no_output(); +} + +#[test] +fn directories_read_errors() { + let (scene, mut c) = ucmd(); + build_tree(&scene); + c.args(&["-d", "read", "grep", "tree"]) + .fails_with_code(2) + .stderr_contains("tree"); +} + +#[test] +fn directories_recurse_equivalent_to_dash_r() { + let (scene, mut c) = ucmd(); + build_tree(&scene); + c.args(&["-d", "recurse", "-l", "grep", "tree"]) + .succeeds() + .stdout_contains("a.txt"); +} + +#[cfg(unix)] +#[test] +fn recursive_skips_fifos_by_default() { + use std::process::Command; + + let (scene, _) = ucmd(); + build_tree(&scene); + + // Reading this FIFO would block forever, so the test + // implicitly proves it was skipped if grep returns. + let fifo_path = scene.fixtures.plus("tree/fifo"); + let status = Command::new("mkfifo") + .arg(&fifo_path) + .status() + .expect("mkfifo failed"); + assert!(status.success(), "could not create FIFO"); + + // Default (no -D): FIFO is skipped, regular file matches. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-r", "grep", "tree"]) + .succeeds() + .stdout_contains("a.txt") + .stdout_does_not_contain("fifo"); + + // Explicit -D skip: same behavior. + let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep")); + c.args(&["-r", "-D", "skip", "grep", "tree"]) + .succeeds() + .stdout_contains("a.txt") + .stdout_does_not_contain("fifo"); +} + +#[test] +fn nonexistent_file_is_error() { + let (_s, mut c) = ucmd(); + c.args(&["x", "does-not-exist"]) + .fails_with_code(2) + .stderr_contains("does-not-exist"); +} + +#[test] +fn dash_argument_means_stdin() { + let (_s, mut c) = ucmd(); + c.args(&["x", "-"]) + .pipe_in("x\ny\n") + .succeeds() + .stdout_only("x\n"); +} + +#[test] +fn empty_file_exits_one() { + let (scene, mut c) = ucmd(); + scene.fixtures.write("empty", ""); + c.args(&["x", "empty"]).fails_with_code(1).no_output(); +} + +#[test] +fn empty_stdin_exits_one() { + let (_s, mut c) = ucmd(); + c.args(&["x"]).pipe_in("").fails_with_code(1).no_output(); +} + +#[test] +fn missing_trailing_newline_still_matched() { + let (_s, mut c) = ucmd(); + c.args(&["x"]).pipe_in("x").succeeds().stdout_only("x\n"); +} + +#[test] +fn crlf_line_endings_are_stripped_on_output() { + let (_s, mut c) = ucmd(); + c.args(&["x"]) + .pipe_in("x\r\ny\r\n") + .succeeds() + .stdout_only(if cfg!(windows) { "x\n" } else { "x\r\n" }); +} + +#[test] +fn empty_line_matches_anchored_empty_pattern() { + let (_s, mut c) = ucmd(); + c.args(&["^$"]) + .pipe_in("a\n\nb\n") + .succeeds() + .stdout_only("\n"); +} + +#[test] +fn unicode_literal_matches() { + let (_s, mut c) = ucmd(); + c.args(&["café"]) + .pipe_in("café au lait\ntea\n") + .succeeds() + .stdout_only("café au lait\n"); +} + +#[test] +fn null_data_mode_records() { + // Records are delimited by NUL on input and output alike. + let (_s, mut c) = ucmd(); + c.args(&["-z", "hello"]) + .pipe_in(&b"hello\0world\0"[..]) + .succeeds() + .stdout_is_bytes(b"hello\0"); + + // Counting works under -z. + let (_s, mut c) = ucmd(); + c.args(&["-z", "-c", "hello"]) + .pipe_in(&b"hello\0world\0"[..]) + .succeeds() + .stdout_only("1\n"); +} + +#[test] +fn exit_codes_basic_triad() { + // 0: any match. + let (_s, mut c) = ucmd(); + c.args(&["x"]).pipe_in("x\n").succeeds(); + + // 1: no match. + let (_s, mut c) = ucmd(); + c.args(&["x"]).pipe_in("y\n").fails_with_code(1); + + // 2: error (missing file). + let (_s, mut c) = ucmd(); + c.args(&["x", "missing"]).fails_with_code(2); +} + +#[test] +fn error_outranks_match_in_exit_code() { + let (scene, mut c) = ucmd(); + scene.fixtures.write("real", "x\n"); + // -s suppresses the message but not the exit code. + c.args(&["-s", "x", "real", "missing"]) + .fails_with_code(2) + .stdout_contains("x") + .no_stderr(); +} + +#[test] +fn help_and_version() { + let (_s, mut c) = ucmd(); + c.args(&["--help"]).succeeds(); + + let (_s, mut c) = ucmd(); + c.args(&["--version"]) + .succeeds() + .stdout_contains(env!("CARGO_PKG_VERSION")); +}