mirror of
https://github.com/uutils/grep.git
synced 2026-06-10 16:15:11 -07:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b62b44dd3 | ||
|
|
7c79cb4e3a | ||
|
|
abcdd7e84d | ||
|
|
d28bf769a1 | ||
|
|
b4980df814 | ||
|
|
e9825e3503 | ||
|
|
f5d5f6c063 | ||
|
|
337b7c704b | ||
|
|
ad7595ffe6 | ||
|
|
e3d80f59e2 | ||
|
|
56d774f576 | ||
|
|
28186e9ec3 | ||
|
|
f4798cb6d0 | ||
|
|
4e6823a8b3 | ||
|
|
b0700b1d78 | ||
|
|
bc416c6d8b | ||
|
|
a7b15320af |
@@ -9,6 +9,9 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write # Publish grep instead of discarding
|
||||
|
||||
# End the current execution if there is a new changeset in the PR.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -47,7 +50,21 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd 'grep'
|
||||
cargo build --release
|
||||
cargo build --release --config=profile.release.strip=true
|
||||
tar -C target/release -cf - grep | zstd -19 -o ../grep-x86_64-unknown-linux-gnu.tar.zst
|
||||
- name: Publish latest commit
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
tag_name: latest-commit
|
||||
body: |
|
||||
commit: ${{ github.sha }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: |
|
||||
grep-x86_64-unknown-linux-gnu.tar.zst
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run GNU grep testsuite
|
||||
shell: bash
|
||||
|
||||
@@ -11,6 +11,23 @@
|
||||
A Rust implementation of [GNU Grep](https://www.gnu.org/software/grep/).
|
||||
This project is an initial release and may contain bugs.
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
cargo install uu_grep
|
||||
```
|
||||
|
||||
## 🚀 Try it online
|
||||
|
||||
You can try `grep` directly in your browser on the [uutils playground](https://uutils.github.io/playground/).
|
||||
Arguments (and a full command) can be passed through the URL via the `cmd` query parameter, for example:
|
||||
|
||||
```shell
|
||||
printf '🚀 rocket\n🛰️ satellite\n🌙 moon\n⭐ star\n' | grep 🌙
|
||||
```
|
||||
|
||||
[Run it in the playground](https://uutils.github.io/playground/?cmd=printf%20%27%F0%9F%9A%80%20rocket%5Cn%F0%9F%9B%B0%EF%B8%8F%20satellite%5Cn%F0%9F%8C%99%20moon%5Cn%E2%AD%90%20star%5Cn%27%20%7C%20grep%20%F0%9F%8C%99)
|
||||
|
||||
## Building
|
||||
|
||||
Download Rust at: https://rustup.rs/
|
||||
|
||||
+12
-2
@@ -255,6 +255,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
));
|
||||
}
|
||||
|
||||
// GNU grep's PCRE backend (-P) supports only a single pattern.
|
||||
if perl_regexp && patterns.len() > 1 {
|
||||
return Err(USimpleError::new(
|
||||
2,
|
||||
"the -P option only supports a single pattern".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Decoded options into enums
|
||||
let regex_mode = if fixed_strings {
|
||||
RegexMode::Fixed
|
||||
@@ -707,14 +715,16 @@ pub fn uu_app() -> Command {
|
||||
.short('L')
|
||||
.long("files-without-match")
|
||||
.help("print only names of FILEs with no selected lines")
|
||||
.action(ArgAction::SetTrue),
|
||||
.action(ArgAction::SetTrue)
|
||||
.overrides_with("files_with_matches"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("files_with_matches")
|
||||
.short('l')
|
||||
.long("files-with-matches")
|
||||
.help("print only names of FILEs with selected lines")
|
||||
.action(ArgAction::SetTrue),
|
||||
.action(ArgAction::SetTrue)
|
||||
.overrides_with("files_without_match"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("count")
|
||||
|
||||
+10
-7
@@ -28,13 +28,10 @@ impl<'a> Matcher<'a> {
|
||||
/// Decide whether `line` matches and return the positions to highlight.
|
||||
pub fn match_line(&self, line: &[u8]) -> Option<Vec<(usize, usize)>> {
|
||||
let mut any_seen = false;
|
||||
let mut any_selected = 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;
|
||||
@@ -43,13 +40,19 @@ impl<'a> Matcher<'a> {
|
||||
if self.config.word_regexp && !Self::is_word_match(line, start, end) {
|
||||
return false;
|
||||
}
|
||||
any_selected = true;
|
||||
// Drop zero-length matches from the output.
|
||||
if 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()
|
||||
// -w / -x are authoritative once matches are filtered. Zero-length
|
||||
// matches can select a line even though there is no span to output.
|
||||
any_selected
|
||||
} else {
|
||||
any_seen
|
||||
};
|
||||
@@ -174,7 +177,7 @@ struct Cursor<'a> {
|
||||
|
||||
impl Cursor<'_> {
|
||||
fn refill(&mut self) {
|
||||
if self.offset >= self.line.len() {
|
||||
if self.offset > self.line.len() {
|
||||
self.pending = None;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ impl<'a> OutputWriter<'a> {
|
||||
view.line_number,
|
||||
view.byte_offset + start as u64,
|
||||
b':',
|
||||
false,
|
||||
)?;
|
||||
|
||||
self.write_colored_bytes(
|
||||
@@ -90,6 +91,7 @@ impl<'a> OutputWriter<'a> {
|
||||
view.line_number,
|
||||
view.byte_offset,
|
||||
if view.is_match { b':' } else { b'-' },
|
||||
view.line.is_empty(),
|
||||
)?;
|
||||
|
||||
let mut last_end = 0;
|
||||
@@ -125,6 +127,7 @@ impl<'a> OutputWriter<'a> {
|
||||
line_number: u64,
|
||||
byte_offset: u64,
|
||||
sep_char: u8,
|
||||
content_empty: bool,
|
||||
) -> io::Result<()> {
|
||||
if self.config.show_filename {
|
||||
self.write_colored_fmt(
|
||||
@@ -155,7 +158,10 @@ impl<'a> OutputWriter<'a> {
|
||||
self.write_separator(sep_char)?;
|
||||
}
|
||||
|
||||
// GNU grep aligns content with a tab under -T, but only when there is
|
||||
// content to align: an empty line keeps just its prefix (no tab).
|
||||
if self.config.initial_tab
|
||||
&& !content_empty
|
||||
&& (self.config.line_number || self.config.byte_offset || self.config.show_filename)
|
||||
{
|
||||
self.out.write_all(b"\t")?;
|
||||
|
||||
+3
-1
@@ -283,7 +283,9 @@ impl<'a> Searcher<'a> {
|
||||
|
||||
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()) {
|
||||
if self.config.binary_mode != BinaryMode::WithoutMatch
|
||||
&& !self.session_mark_binary_if(|| std::str::from_utf8(line).is_err())
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,24 @@ fn ere_invalid_pattern_is_error() {
|
||||
.stderr_contains("invalid pattern");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_tab_skips_empty_lines() {
|
||||
// -T aligns content with a tab, but GNU omits the tab for an empty line
|
||||
// (a whitespace-only line still gets one). -H forces the filename prefix
|
||||
// on, so the tab is exercised.
|
||||
let (s, mut c) = ucmd();
|
||||
s.fixtures.write("in", "x\n\n");
|
||||
c.args(&["-T", "-H", "^", "in"])
|
||||
.succeeds()
|
||||
.stdout_is("in:\tx\nin:\n");
|
||||
|
||||
let (s, mut c) = ucmd();
|
||||
s.fixtures.write("in", "x\n \n");
|
||||
c.args(&["-T", "-H", "^", "in"])
|
||||
.succeeds()
|
||||
.stdout_is("in:\tx\nin:\t \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_string_is_literal() {
|
||||
// Metacharacters are not interpreted.
|
||||
@@ -170,6 +188,34 @@ fn pcre_features() {
|
||||
.stdout_only("42\n7\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perl_regexp_rejects_multiple_patterns() {
|
||||
// GNU grep's PCRE backend (-P) only supports a single pattern.
|
||||
// Multiple -e patterns must produce exit 2 and the canonical error message.
|
||||
// See: https://github.com/uutils/grep/issues/34
|
||||
|
||||
// Two separate -e flags.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-P", "-e", "foo", "-e", "bar"])
|
||||
.pipe_in("foo\nbar\n")
|
||||
.fails_with_code(2)
|
||||
.stderr_contains("the -P option only supports a single pattern");
|
||||
|
||||
// A newline inside the pattern string is split into multiple patterns.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-P", "-e", "foo\nbar"])
|
||||
.pipe_in("foo\nbar\n")
|
||||
.fails_with_code(2)
|
||||
.stderr_contains("the -P option only supports a single pattern");
|
||||
|
||||
// A single pattern with -P must still work normally.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-P", "-e", r"\d+"])
|
||||
.pipe_in("abc\n42\n")
|
||||
.succeeds()
|
||||
.stdout_only("42\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posix_character_classes() {
|
||||
let (_s, mut c) = ucmd();
|
||||
@@ -251,6 +297,12 @@ fn word_regexp() {
|
||||
.pipe_in("foo bar\nfoobar\n")
|
||||
.succeeds()
|
||||
.stdout_only("foo bar\n");
|
||||
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-w", "$"])
|
||||
.pipe_in("abc\n\nx\n")
|
||||
.succeeds()
|
||||
.stdout_only("\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -260,6 +312,12 @@ fn line_regexp() {
|
||||
.pipe_in("foo bar\nfoo bar!\nx foo bar\n")
|
||||
.succeeds()
|
||||
.stdout_only("foo bar\n");
|
||||
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-x", "$"])
|
||||
.pipe_in("abc\n\nx\n")
|
||||
.succeeds()
|
||||
.stdout_only("\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -460,6 +518,25 @@ fn files_with_and_without_matches() {
|
||||
.stdout_only("many\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_with_and_without_matches_mutually_exclusive() {
|
||||
// Test that -l and -L are mutually exclusive with last-one-wins semantics
|
||||
let (scene, mut c) = ucmd();
|
||||
scene.fixtures.write("file", "match\n");
|
||||
|
||||
// -l -L: last flag (-L) wins, so no output (file has match, -L excludes it)
|
||||
c.args(&["-l", "-L", "match", "file"])
|
||||
.succeeds()
|
||||
.stdout_only("");
|
||||
|
||||
// -L -l: last flag (-l) wins, so filename is printed
|
||||
let (scene, mut c) = ucmd();
|
||||
scene.fixtures.write("file", "match\n");
|
||||
c.args(&["-L", "-l", "match", "file"])
|
||||
.succeeds()
|
||||
.stdout_only("file\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_combined_with_listing_flags() {
|
||||
let (scene, _) = ucmd();
|
||||
@@ -862,6 +939,7 @@ fn binary_files_text_forces_text_mode() {
|
||||
fn binary_files_without_match_skips() {
|
||||
let (scene, _) = ucmd();
|
||||
scene.fixtures.write_bytes("b", b"hit\0more\n");
|
||||
scene.fixtures.write_bytes("invalid", b"a\x9db\n");
|
||||
|
||||
let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep"));
|
||||
c.args(&["-I", "hit", "b"]).fails_with_code(1).no_output();
|
||||
@@ -870,6 +948,18 @@ fn binary_files_without_match_skips() {
|
||||
c.args(&["--binary-files=without-match", "hit", "b"])
|
||||
.fails_with_code(1)
|
||||
.no_output();
|
||||
|
||||
let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep"));
|
||||
c.args(&["-I", "a", "invalid"])
|
||||
.succeeds()
|
||||
.stdout_is_bytes(b"a\x9db\n")
|
||||
.no_stderr();
|
||||
|
||||
let mut c = scene.cmd(env!("CARGO_BIN_EXE_grep"));
|
||||
c.args(&["--binary-files=without-match", "a", "invalid"])
|
||||
.succeeds()
|
||||
.stdout_is_bytes(b"a\x9db\n")
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
fn build_tree(scene: &TestScenario) {
|
||||
|
||||
Reference in New Issue
Block a user