遍历文件中的行时如何在匹配之前和之后输出行?

时间:2019-08-13 00:47:27

标签: loops rust

我正在尝试从Rust中的GNU -A复制-Bgrep参数功能。这会在从文件或stdin读取的匹配行之前和之后打印出文本行。示例:

$ printf '1\n2\n3\nfoo\n4\n5\n6\n7\nfoo\n8\n9' | grep -A 1 -B 1 foo
3
foo
4
--
--
7
foo
8

我想要的输出将在模式匹配之前和/或之后返回n行。

仅以stdin为例,返回匹配行的简单情况很容易实现,如下所示:

use std::io::{self, BufRead, BufReader, Result};
fn main() {
    for line in BufReader::new(io::stdin()).lines() {
        match line {
            Ok(l) => {
                if l.contains("foo"){
                    println!("{}", l);
                }
            }
            Err(e) => println!("error parsing line: {:?}", e),
        }
    }
}

输出:

$ printf '1\n2\n3\nfoo\n4\n5\n6\n7\nfoo\n8\n9' | cargo run
foo
foo

但是,似乎不可能在这样的迭代器中返回周围的行,因为在每次循环迭代时都无法再访问之前的行。

我看到了Windows类型,但是它仅适用于切片。

这个sliding_windows板条箱似乎是我想要的功能,但我无法弄清楚如何使其与文件中的行中的迭代器一起使用。

我也查看了itertools,但没有看到执行此操作的任何窗口迭代器函数。

在我深入研究自己的形式的行缓冲区对象以便缓存n先前见过的行(也许是某种环形缓冲区?)之前,我希望可能已经有了某种方法在Rust中可以轻松实现此目的。

2 个答案:

答案 0 :(得分:3)

有效地实现这一点非常棘手,而使用滚动缓冲区的本能就很大。这就是GNU grep和ripgrep所做的。如果您愿意产生一些依赖关系,则可以依靠ripgrep的某些内部库来实现所需的功能。例如,这是一个利用grep-searcher条板箱执行所需操作的程序:

use std::error::Error;
use std::io;

use grep_regex::RegexMatcher;
use grep_searcher::{Searcher, SearcherBuilder, Sink, SinkContext, SinkMatch};

fn main() -> Result<(), Box<dyn Error>> {
    let re = RegexMatcher::new(r"foo")?;
    let mut searcher = SearcherBuilder::new()
        .before_context(1)
        .after_context(1)
        .build();
    searcher.search_reader(
        &re,
        io::stdin().lock(),
        MySink(io::stdout().lock()),
    )?;
    Ok(())
}

struct MySink<W>(W);

impl<W: io::Write> Sink for MySink<W> {
    type Error = io::Error;

    fn matched(
        &mut self,
        _: &Searcher,
        mat: &SinkMatch,
    ) -> Result<bool, io::Error> {
        self.0.write_all(mat.bytes())?;
        Ok(true)
    }

    fn context(
        &mut self,
        _: &Searcher,
        ctx: &SinkContext,
    ) -> Result<bool, io::Error> {
        self.0.write_all(ctx.bytes())?;
        Ok(true)
    }

    fn context_break(
        &mut self,
        _: &Searcher,
    ) -> Result<bool, io::Error> {
        self.0.write_all(b"--\n")?;
        Ok(true)
    }
}

具有以下依赖性:

[dependencies]
grep-regex = "0.1.5"
grep-searcher = "0.1.6"

其输出是:

$ printf '1\n2\n3\nfoo\n4\n5\n6\n7\nfoo\n8\n9' | ./target/release/grepex
3
foo
4
--
7
foo
8

如果不需要正则表达式,可以删除grep-regex依赖项,但是它需要编写更多代码以提供自己的grep-matcher::Matcher实现(如果看起来不那么容易,您只需要简单的子字符串搜索即可。

如果您确实想自己实现此功能,则可以尝试阅读implementation inside grep-searcher。实际上,它们全都建立在roll buffer之上。

如果您不太关心性能,则可以逐行循环,并保留足够的缓冲区来存储前N行(其中N是您的大小“之前”窗口)。找到匹配项后,从缓冲区中打印前N行。同时,在N处启动一个计数器,该计数器每连续一行将减少1。当计数器在0上方时,打印对应于“之后”窗口的行。 (这与滚动缓冲区的操作并没有什么不同。)

答案 1 :(得分:1)

一种方法是开发一种状态机,该状态机始终保持最后2N + 1行的窗口。当找到匹配行时,它将新条目添加到与将来的行号关联的待打印的未决匹配列表中。当到达该将来的行号时,将打印具有该行号的条目以及从上下文窗口中拉出的上下文行,请记住,靠近输入开头的匹配项少于N个前导上下文行。当输入结束时,将打印所有仍待处理的条目,请记住这些条目的尾随上下文行少于N。

与所有这些突变抗争Rust!