4 个版本 (2 个重大更新)

0.4.0 2021 年 4 月 6 日
0.3.0 2018 年 12 月 10 日
0.1.1 2017 年 4 月 30 日
0.1.0 2017 年 4 月 22 日

#1252 in 解析器实现

MIT 许可证

59KB
1K SLoC

Rust 中的 Lua 字符串模式

Lua 字符串模式 是一种强大而轻量级的正则表达式的替代方案。它们不是正则表达式,因为没有替代操作符(| 运算符),但这通常不是问题。事实上,完整的正则表达式变得 过于强大,而强大可能是有害的或只是让人困惑。这就是为什么 OpenBSD 的 httpd 使用 Lua 模式。使用 % 作为转义字符而不是传统的 \ 是一种令人耳目一新的选择。在 Rust 上下文中,lua-patterns 是一个非常轻量级的依赖项,如果你不需要 regex crate 的全部功能。

这个库重用了 Lua 5.2 的原始源代码 - 只有 400 行经过实战考验的 C 代码。我最初做这个是为了将类似的项目带到 C++ 中。

更多信息可以在 Lua 维基 上找到。酷的地方在于 Lua 是一个 300KB 的下载,如果你想在不通过 Rust 的情况下测试模式,这是一个不错的选择。

我已经将 Rust 接口组织得与原始 Lua 库非常相似,'match'、'gmatch' 和 'gsub',但将它们作为 LuaPattern 结构体的方法。这样做有两个主要原因

  • 尽管字符串模式不编译,但可以在前端进行验证
  • 匹配后,结构体包含结果
let mut m = lua_patterns::LuaPattern::new("one");
let text = "hello one two";
assert!(m.matches(text));
let r = m.range();
assert_eq!(r.start, 6);
assert_eq!(r.end, 9);

这本身并不令人印象深刻,因为它可以用字符串的 find 方法做到。(如果向 new 提供了一个无效的模式,它将引发恐慌,因此如果您想有更多控制权,请使用 new_try。)

一旦我们开始使用模式,事情就变得更加有趣,特别是与 捕获 相关

let mut m = lua_patterns::LuaPattern::new("(%a+) one");
let text = " hello one two";
assert!(m.matches(text));
assert_eq!(m.capture(0),1..10); // "hello one"
assert_eq!(m.capture(1),1..6); // "hello"

Lua 模式(如正则表达式)默认情况下不锚定,所以它会找到第一个匹配项并从这里开始工作。0 捕获始终存在(完整匹配),在这里 1 捕获只是选择了第一个单词。

有一个明显的限制:"%a" 仅指代表字母的单一字节的 C 本地化。Lua 用户通常会寻找“非空格序列”("%S+")等 - 也就是说,使用周围的标点符号或空格来识别可能是 UTF-8 的序列。

如果您希望将捕获的内容作为字符串,那么有几种选择。如果只有一个,那么 match_maybe 非常有用。

let mut m = lua_patterns::LuaPattern::new("OK%s+(%d+)");
let res = m.match_maybe("and that's OK 400 to you");
assert_eq!(res, Some("400"));

您可以将其作为向量抓取(如果匹配失败,则向量将为空)。

let mut m = lua_patterns::LuaPattern::new("(%a+) one");
let text = " hello one two";
let v = m.captures(text);
assert_eq!(v, &["hello one","hello"]);

这将创建一个向量。您可以使用 capture_into 来避免过多的分配。

let mut m = lua_patterns::LuaPattern::new("(%a+) one");
let text = " hello one two";
let mut v = Vec::new();
if m.capture_into(text,&mut v) {
    assert_eq!(v, &["hello one","hello"]);
}

想象一下这个操作在一个循环中发生——向量只会在第一次填充时分配,之后就没有分配了。如果您正在检查文本与多个模式,这是一个方便的方法,实际上比使用 Lua 的 string.match 更直观。(我个人更喜欢使用那些被称为“if 语句”的奇妙事物,而不是复杂的正则表达式。)

gmatch 方法创建了一个遍历所有匹配字符串的迭代器。

let mut m = lua_patterns::LuaPattern::new("%S+");
let split: Vec<_> = m.gmatch("dog  cat leopard wolf  ").collect();
assert_eq!(split,&["dog","cat","leopard","wolf"]);

返回单个匹配;如果没有捕获,则返回整个匹配,否则返回第一个匹配。所以 "(%S+)" 会得到相同的结果。

更通用版本是 gmatch_captures,它创建一个对捕获的 流式 迭代器。您需要对此稍加小心;特别是,如果您尝试在返回的捕获上使用 collect,您将得到无意义的结果:不要尝试保留这些值。如果涉及 get 方法,则可以安全地收集!

let mut m = lua_patterns::LuaPattern::new("(%S)%S+");
let split: Vec<_> = m.gmatch_captures("dog  cat leopard wolf")
       .map(|cc| cc.get(1)).collect();
assert_eq!(split,&["d","c","l","w"]);

文本替换是我最喜欢的一个功能,所以这里提供 gsub_with

let mut m = lua_patterns::LuaPattern::new("%$(%S+)");
let res = m.gsub_with("hello $dolly you're so $fine",
    |cc| cc.get(1).to_uppercase()
);
assert_eq!(res,"hello DOLLY you're so FINE");

闭包传递一个 Closures 对象,并通过 get 方法访问捕获;它返回一个 String

gsub 的第二种形式在您有一个包含闭包引用的替换字符串时很方便。(为了添加字面量 "%",请将其转义为 "%%")

let mut m = lua_patterns::LuaPattern::new("%s+");
let res = m.gsub("hello dolly you're so fine","");
assert_eq!(res, "hellodollyyou'resofine");

let mut m = lua_patterns::LuaPattern::new("(%S+)%s*=%s*(%S+);%s*");
let res = m.gsub("a=2; b=3; c = 4;", "'%2':%1 ");
assert_eq!(res, "'2':a '3':b '4':c ");

Lua 中 string.gsub 的第三种形式使用表进行查找——也就是说,是一个映射。但对于映射,您确实需要以某种特殊方式处理“未找到”的情况

use std::collections::HashMap;
let mut map = HashMap::new();
// updating old lines for the 21st Century
map.insert("dolly", "baby");
map.insert("fine", "cool");
map.insert("good-looking", "pretty");

let mut m = lua_patterns::LuaPattern::new("%$%((.-)%)");
let res = m.gsub_with("hello $(dolly) you're so $(fine) and $(good-looking)",
    |cc| map.get(cc.get(1)).unwrap_or(&"?").to_string()
);
assert_eq!(res,"hello baby you're so cool and pretty");

(".-" 模式意味着“尽可能少地匹配”——通常称为“懒惰”匹配。)

这相当于替换字符串 "%1:'%2'"。

let mut m = lua_patterns::LuaPattern::new("(%S+)%s*=%s*([^;]+);");
let res = m.gsub_with("alpha=bonzo; beta=felix;",
    |cc| format!("{}:'{}',", cc.get(1), cc.get(2))
);
assert_eq!(res, "alpha:'bonzo', beta:'felix',");

拥有基于字节的模式匹配器可能很有用。例如,这基本上是旧的 strings 工具——我们将所有“二进制”文件读入一个字节的向量,然后使用 gmatch_bytes 遍历所有对应于两个或更多相邻 ASCII 字母的 &[u8] 匹配

let mut words = lua_patterns::LuaPattern::new("%a%a+");
for w in words.gmatch_bytes(b"hello dolly") {
    println!("{}",String::from_utf8_lossy(w));
}

模式本身可能是任意的字节——Lua 的字符串匹配不考虑嵌入的空字节

let patt = &[0xDE,0x00,b'+',0xBE];
let bytes = &[0xFF,0xEE,0x0,0xDE,0x0,0x0,0xBE,0x0,0x0];

let mut m = lua_patterns::LuaPattern::from_bytes(patt);
assert!(m.matches_bytes(bytes));
assert_eq!(&bytes[m.capture(0)], &[0xDE,0x00,0x00,0xBE]);

这里的问题是,当我们的“任意”字节包括特殊匹配字符(如 $(即 0x24)等)时,这并不明显。因此,存在 LuaPatternBuilder

let bytes = &[0xFF,0xEE,0x0,0xDE,0x24,0x24,0xBE,0x0,0x0];

let patt = lua_patterns::LuaPatternBuilder::new()
    .bytes_as_hex("DE24") // less tedious than a byte slice
    .text("+")  // unescaped
    .bytes(&[0xBE]) // byte slice
    .build();

let mut m = lua_patterns::LuaPattern::from_bytes(&patt);
// picks up "DE2424BE"

静态验证:这个版本尝试验证字符串模式。如果您需要错误,请使用 new_tryfrom_bytes_try,否则构造函数会恐慌。如果匹配在成功验证后恐慌,则是一个 BUG - 请报告有问题的模式。

没有运行时依赖