类似grep
的工具,它理解源代码语法并允许除搜索之外的操作。
与grep
一样,正则表达式是核心原语。与grep
不同,附加功能可实现更高的精度,并提供操作选项。这允许srgn
沿着正则表达式维度进行操作,而 IDE 工具(重命名全部、查找所有引用...)单独无法操作,这是对它们的补充。
srgn
围绕要采取的操作(如果有)进行组织,仅在精确的、可选的语言语法感知范围内起作用。就现有工具而言,将其视为tr
、 sed
、 ripgrep 和tree-sitter
的混合体,其设计目标是简单性:如果您了解正则表达式和您正在使用的语言的基础知识,那么您就可以开始使用。
提示
此处显示的所有代码片段均作为单元测试的一部分使用实际的srgn
二进制文件进行验证。这里展示的内容保证有效。
最简单的srgn
用法与tr
类似:
$ echo ' Hello World! ' | srgn ' [wW]orld ' ' there ' # replacement
Hello there !
正则表达式模式'[wW]orld'
(范围)的匹配项被第二个位置参数替换(操作)。可以指定零个或多个操作:
$ echo ' Hello World! ' | srgn ' [wW]orld ' # zero actions: input returned unchanged
Hello World !
$ echo ' Hello World! ' | srgn --upper ' [wW]orld ' ' you ' # two actions: replacement, afterwards uppercasing
Hello YOU !
替换始终首先执行并按位置指定。任何其他操作都会在命令行标志之后应用并作为命令行标志给出。
类似地,可以指定多个范围:除了正则表达式模式之外,还可以指定语言语法感知范围,其范围为源代码的语法元素(例如,“Python 中class
定义的所有主体”) )。如果两者都给定,则正则表达式模式仅在第一个语言范围内应用。这使得使用普通正则表达式通常不可能实现的精确搜索和操作,并提供与 IDE 中的“重命名全部”等工具不同的维度。
例如,考虑这个(毫无意义的)Python 源文件:
"""Module for watching birds and their age."""
from dataclasses import dataclass
@ dataclass
class Bird :
"""A bird!"""
name : str
age : int
def celebrate_birthday ( self ):
print ( "?" )
self . age += 1
@ classmethod
def from_egg ( egg ):
"""Create a bird from an egg."""
pass # No bird here yet!
def register_bird ( bird : Bird , db : Db ) -> None :
assert bird . age >= 0
with db . tx () as tx :
tx . insert ( bird )
可以使用以下方式进行搜索:
$ cat birds.py | srgn --python ' class ' ' age '
11: age: int
15: self.age += 1
字符串age
仅在Python class
定义中查找和找到(而不是在诸如register_bird
之类的函数体中,其中age
也出现,并且在普通grep
中几乎不可能排除在考虑范围之外)。默认情况下,此“搜索模式”还会打印行号。如果未指定任何操作,则进入搜索模式,并且给定诸如--python
之类的语言1 — 将其想象为“ripgrep,但具有句法语言元素”。
搜索也可以跨行执行,例如查找缺少文档字符串的方法(又名class
内的def
):
$ cat birds.py | srgn --python ' class ' ' def .+:ns+[^"s]{3} ' # do not try this pattern at home
13: def celebrate_birthday(self):
14: print("?")
请注意,它不会显示from_egg
(具有文档字符串)或register_bird
(不是方法, def
external class
)。
语言范围本身也可以指定多次。例如,在 Rust 代码片段中
pub enum Genre {
Rock ( Subgenre ) ,
Jazz ,
}
const MOST_POPULAR_SUBGENRE : Subgenre = Subgenre :: Something ;
pub struct Musician {
name : String ,
genres : Vec < Subgenre > ,
}
可以通过手术将多个项目钻入
$ cat music.rs | srgn --rust ' pub-enum ' --rust ' type-identifier ' ' Subgenre ' # AND'ed together
2: Rock(Subgenre),
其中仅返回匹配所有条件的行,其作用类似于所有条件之间的逻辑与。请注意,条件是从左到右计算的,从而排除了某些组合的意义:例如,在Python doc-strings
中搜索 Python class
主体通常不会返回任何内容。然而,逆过程却按预期工作:
$ cat birds.py | srgn --py ' class ' --py ' doc-strings '
8: """A bird!"""
19: """Create a bird from an egg."""
class
之外没有任何文档字符串出现!
-j
标志改变了这种行为:从从左到右交叉,到独立运行所有查询并连接它们的结果,允许您一次搜索多种方式:
$ cat birds.py | srgn -j --python ' comments ' --python ' doc-strings ' ' bird[^s] '
8: """A bird!"""
19: """Create a bird from an egg."""
20: pass # No bird here yet!
同样, bird[^s]
模式也可以在注释或文档字符串中找到,而不仅仅是“注释中的文档字符串”。
如果未给出标准输入, srgn
知道如何自动查找相关源文件,例如在此存储库中:
$ srgn --python ' class ' ' age '
docs/samples/birds
11: age: int
15: self.age += 1
docs/samples/birds.py
9: age: int
13: self.age += 1
它递归地遍历当前目录,根据文件扩展名和 shebang 行查找文件,并以非常高的速度进行处理。例如, srgn --go strings 'd+'
可在 3 秒内在 M3 的 12 个核心上查找并打印约 3,000,000 行 Go 代码的 Kubernetes 代码库中 Go 字符串中的所有约 140,000 个数字。有关处理多个文件的更多信息,请参见下文。
范围和操作几乎可以任意组合(尽管许多组合不会有用,甚至没有意义)。例如,考虑这个 Python 片段(有关使用其他支持的语言的示例,请参见下文):
"""GNU module."""
def GNU_says_moo ():
"""The GNU function -> say moo -> ✅"""
GNU = """
GNU
""" # the GNU...
print ( GNU + " says moo" ) # ...says moo
运行以下命令:
cat gnu.py | srgn --titlecase --python ' doc-strings ' ' (?' ' $1: GNU ? is not Unix '
该调用的剖析是:
--titlecase
(一个动作)将在范围内找到的所有内容命名为标题
--python 'doc-strings'
(范围)将根据 Python 语言语法将范围限定为(即,仅考虑)文档字符串
'(? (范围)仅看到前一个选项已经确定的范围,并将进一步缩小范围。它永远不能扩大以前的范围。正则表达式范围应用于任何语言范围之后。
(?是否定后向语法,演示了如何使用此高级功能。不考虑以
The
为前缀的GNU
字符串。
'$1: GNU ? is not Unix'
(一个操作)将用该字符串替换每个匹配的事件(即,发现在范围内的每个输入部分)。匹配的出现是仅在 Python 文档字符串中的'(?模式。值得注意的是,这个替换字符串表明:
$1
进行动态变量绑定和替换,它携带第一个捕获正则表达式组捕获的内容。那是([az]+)
,因为(?没有捕获。
该命令使用多个范围(语言和正则表达式模式)和多个操作(替换和标题大小写)。然后结果显示为
"""Module: GNU ? Is Not Unix."""
def GNU_says_moo ():
"""The GNU function -> say moo -> ✅"""
GNU = """
GNU
""" # the GNU...
print ( GNU + " says moo" ) # ...says moo
其中更改仅限于:
- """GNU module."""
+ """Module: GNU ? Is Not Unix."""
def GNU_says_moo():
"""The GNU -> say moo -> ✅"""
警告
虽然srgn
处于测试版(主要版本 0),但请确保仅(递归地)处理可以安全恢复的文件。
搜索模式不会覆盖文件,因此始终安全。
有关该工具的完整帮助输出,请参阅下文。
笔记
支持的语言有
从版本中下载预构建的二进制文件。
该板条箱以与cargo-binstall
兼容的格式提供其二进制文件:
cargo install cargo-binstall
(可能需要一段时间)cargo binstall srgn
(几秒钟,因为它从 GitHub 下载预构建的二进制文件)这些步骤保证有效™,因为它们在 CI 中进行了测试。如果您的平台没有可用的预构建二进制文件,它们也可以工作,因为该工具将退回到从源代码进行编译。
可以通过以下方式获得公式:
brew install srgn
通过不稳定可用:
nix-shell -p srgn
可通过 AUR 获取。
端口可用:
sudo port install srgn
所有 GitHub Actions 运行器镜像都预装了cargo
,并且cargo-binstall
提供了方便的 GitHub Action:
jobs :
srgn :
name : Install srgn in CI
# All three major OSes work
runs-on : ubuntu-latest
steps :
- uses : cargo-bins/cargo-binstall@main
- name : Install binary
run : >
cargo binstall
--no-confirm
srgn
- name : Use binary
run : srgn --version
上面的内容总共只用了 5 秒就结束了,因为不需要编译。有关更多上下文,请参阅cargo-binstall
关于 CI 的建议。
在 Linux 上, gcc
可以工作。
在 macOS 上,使用clang
。
在 Windows 上,MSVC 可以工作。
安装时选择“使用 C++ 进行桌面开发”。
cargo install srgn
cargo add srgn
请参阅此处了解更多信息。
shell 完成脚本支持各种 shell。例如,将eval "$(srgn --completions zsh)"
附加到~/.zshrc
以在 ZSH 中完成补全。交互式会话可以如下所示:
该工具是围绕范围和操作设计的。范围缩小了要处理的输入部分。然后操作执行处理。通常,作用域和操作都是可组合的,因此可以传递其中的多个。两者都是可选的(但不采取任何行动是没有意义的);不指定范围意味着整个输入都在范围内。
同时,与普通tr
存在相当大的重叠:该工具被设计为在最常见的用例中具有密切的对应性,并且仅在需要时才进行超越。
最简单的动作就是替换。它是为了与tr
和一般人体工程学兼容而专门访问的(作为参数,而不是选项)。所有其他操作都作为标志或选项(如果它们具有值)给出。
例如,简单的单字符替换如tr
中所示:
$ echo ' Hello, World! ' | srgn ' H ' ' J '
Jello, World!
第一个参数是范围(在本例中为文字H
)。与它匹配的任何内容都将受到处理(在本例中替换为J
,即第二个参数)。但是,没有像tr
中那样直接的字符类概念。相反,默认情况下,范围是正则表达式模式,因此它的类可以用于类似的效果:
$ echo ' Hello, World! ' | srgn ' [a-z] ' ' _ '
H____, W____!
默认情况下,替换会在整个匹配中贪婪地发生(注意 UTS 字符类,让人想起tr
的[:alnum:]
):
$ echo ' ghp_oHn0As3cr3T!! ' | srgn ' ghp_[[:alnum:]]+ ' ' * ' # A GitHub token
*!!
支持高级正则表达式功能,例如环视:
$ echo ' ghp_oHn0As3cr3T ' | srgn ' (?<=ghp_)[[:alnum:]]+ ' ' * '
ghp_*
安全使用这些模式时要小心,因为高级模式没有一定的安全和性能保证。如果不使用它们,性能不会受到影响。
替换不限于单个字符。它可以是任何字符串,例如修复此引用:
$ echo ' "Using regex, I now have no issues." ' | srgn ' no issues ' ' 2 problems '
"Using regex, I now have 2 problems."
该工具完全支持 Unicode,对某些高级字符类提供有用的支持:
$ echo ' Mood: ? ' | srgn ' ? ' ' ? '
Mood: ?
$ echo ' Mood: ???? :( ' | srgn ' p{Emoji_Presentation} ' ' ? '
Mood: ???? :(
替换可以识别变量,可以通过正则表达式捕获组访问这些变量。捕获组可以编号,也可以选择命名。第零个捕获组对应于整个比赛。
$ echo ' Swap It ' | srgn ' (w+) (w+) ' ' $2 $1 ' # Regular, numbered
It Swap
$ echo ' Swap It ' | srgn ' (w+) (w+) ' ' $2 $1$1$1 ' # Use as many times as you'd like
It SwapSwapSwap
$ echo ' Call +1-206-555-0100! ' | srgn ' Call (+?d-d{3}-d{3}-d{4}).+ ' ' The phone number in "$0" is: $1. ' # Variable `0` is the entire match
The phone number in "Call +1-206-555-0100!" is: +1-206-555-0100.
例如,一个更高级的用例是使用命名捕获组进行代码重构(也许您可以想出一个更有用的...):
$ echo ' let x = 3; ' | srgn ' let (?[a-z]+) = (?.+); ' ' const $var$var = $expr + $expr; '
const xx = 3 + 3;
与 bash 中一样,使用花括号来消除紧邻内容中的变量的歧义:
$ echo ' 12 ' | srgn ' (d)(d) ' ' $2${1}1 '
211
$ echo ' 12 ' | srgn ' (d)(d) ' ' $2$11 ' # will fail (`11` is unknown)
$ echo ' 12 ' | srgn ' (d)(d) ' ' $2${11 ' # will fail (brace was not closed)
看到替换只是一个静态字符串,它的用处是有限的。这就是tr
的秘密武器通常发挥作用的地方:使用它的字符类,这些字符类在第二个位置也有效,巧妙地从第一个位置的成员转换为第二个位置的成员。在这里,这些类是正则表达式,并且仅在第一个位置(范围)有效。正则表达式是一个状态机,不可能匹配到“字符列表”,而“字符列表”在tr
中是第二个(可选)参数。这个概念已经被抛之脑后,并且失去了灵活性。
相反,使用所提供的全部固定的操作。浏览一下tr
最常见的用例就会发现,所提供的一组操作几乎涵盖了所有这些!如果您的用例未涵盖,请随时提出问题。
进行下一步行动。
删除从输入中找到的所有内容。与tr
中的标志名称相同。
$ echo ' Hello, World! ' | srgn -d ' (H|W|!) '
ello, orld
笔记
由于默认范围是匹配整个输入,因此在没有范围的情况下指定删除是错误的。
将与范围匹配的重复字符压缩为单个出现的字符。与tr
中的标志名称相同。
$ echo ' Helloooo Woooorld!!! ' | srgn -s ' (o|!) '
Hello World!
如果传递了一个字符类,则该类的所有成员都将被挤入首先遇到的任何类成员中:
$ echo ' The number is: 3490834 ' | srgn -s ' d '
The number is: 3
匹配中的贪婪性不会被修改,所以要小心:
$ echo ' Winter is coming... ??? ' | srgn -s ' ?+ '
Winter is coming... ???
笔记
该图案与整个太阳的运行相匹配,因此没有什么可以挤压的。夏季盛行。
如果用例需要,则反转贪婪:
$ echo ' Winter is coming... ??? ' | srgn -s ' ?+? ' ' ☃️ '
Winter is coming... ☃️
笔记
同样,与删除一样,在没有明确范围的情况下指定压缩也是一个错误。否则,整个输入都会被压缩。
tr
使用的很大一部分属于这一类。这非常简单。
$ echo ' Hello, World! ' | srgn --lower
hello, world!
$ echo ' Hello, World! ' | srgn --upper
HELLO, WORLD!
$ echo ' hello, world! ' | srgn --titlecase
Hello, World!
根据规范化形式 D 分解输入,然后丢弃标记类别的代码点(请参阅示例)。这大致意味着:选择花哨的角色,撕掉悬挂的部分,然后扔掉它们。
$ echo ' Naïve jalapeño ärgert mgła ' | srgn -d ' P{ASCII} ' # Naive approach
Nave jalapeo rgert mga
$ echo ' Naïve jalapeño ärgert mgła ' | srgn --normalize # Normalize is smarter
Naive jalapeno argert mgła
请注意, mgła
超出了 NFD 的范围,因为它是“原子的”,因此不可分解(至少这是 ChatGPT 在我耳边低声说的)。
此操作将多字符 ASCII 符号替换为适当的单代码点、本机 Unicode 对应项。
$ echo ' (A --> B) != C --- obviously ' | srgn --symbols
(A ⟶ B) ≠ C — obviously
或者,如果您只对数学感兴趣,请使用范围界定:
$ echo ' A <= B --- More is--obviously--possible ' | srgn --symbols ' <= '
A ≤ B --- More is--obviously--possible
由于 ASCII 符号与其替换符号之间存在 1:1 对应关系,因此效果是可逆的2 :
$ echo ' A ⇒ B ' | srgn --symbols --invert
A => B
目前仅支持有限的符号集,但可以添加更多符号。
此操作将德语特殊字符 (ae, oe, ue, ss) 的替代拼写替换为其本机版本 (ä, ö, ü, ß) 3 。
$ echo ' Gruess Gott, Neueroeffnungen, Poeten und Abenteuergruetze! ' | srgn --german
Grüß Gott, Neueröffnungen, Poeten und Abenteuergrütze!
此操作基于单词列表(如果这使您的二进制文件过于膨胀,则编译时不使用german
功能)。请注意上述示例的以下特点:
Poeten
保持原样,而不是天真地错误地转换为Pöten
Abenteuergrütze
不会出现在任何合理的单词列表中,但仍然得到了正确的处理Abenteuer
也保持原样,而不是错误地转换为Abenteür
Neueroeffnungen
偷偷地形成了一个组成词( neu
、 Eröffnungen
)都不具备的ue
元素,但仍然被正确处理(尽管大小写也不匹配)根据要求,可能会强制进行替换,这对于名称可能有用:
$ echo ' Frau Loetter steht ueber der Mauer. ' | srgn --german-naive ' (?<=Frau )w+ '
Frau Lötter steht ueber der Mauer.
通过积极的前瞻,除了称呼之外什么都没有被限定,因此也发生了变化。 Mauer
正确地保持原样,但ueber
未经过处理。第二遍解决了这个问题:
$ echo ' Frau Loetter steht ueber der Mauer. ' | srgn --german-naive ' (?<=Frau )w+ ' | srgn --german
Frau Lötter steht über der Mauer.
笔记
与某些“父级”相关的选项和标志以其父级名称为前缀,并且在给出时将暗示其父级,以便后者不需要显式传递。这就是为什么--german-naive
被命名为 --german-naive,而--german
不需要被传递。
一旦clap
支持子命令链接,这种行为可能会改变。
对于这个不起眼的工具来说,某些分支是无法确定的,因为它在没有语言上下文的情况下运行。例如, Busse
(公共汽车)和Buße
(忏悔)都是合法词。默认情况下,如果合法,替换就会贪婪地执行(毕竟这就是srgn
的全部意义),但是有一个标志用于切换此行为:
$ echo ' Busse und Geluebte ' | srgn --german
Buße und Gelübte
$ echo ' Busse ? und Fussgaenger ?♀️ ' | srgn --german-prefer-original
Busse ? und Fußgänger ?♀️
大多数操作都是可组合的,除非这样做是无意义的(例如删除)。它们的应用顺序是固定的,因此给定标志的顺序没有影响(如果需要,管道多次运行是一种替代方案)。替换总是首先发生。一般来说,CLI 的设计是为了防止误用和意外:它宁愿崩溃也不愿做一些意想不到的事情(当然,这是主观的)。请注意,许多组合在技术上都是可行的,但可能会产生无意义的结果。
组合操作可能如下所示:
$ echo ' Koeffizienten != Bruecken... ' | srgn -Sgu
KOEFFIZIENTEN ≠ BRÜCKEN...
可以指定更窄的范围,并将同样适用于所有操作:
$ echo ' Koeffizienten != Bruecken... ' | srgn -Sgu ' bw{1,8}b '
Koeffizienten != BRÜCKEN...
需要单词边界,否则Koeffizienten
会匹配为Koeffizi
和enten
。请注意尾随期间如何不能被压缩等。 .
会干扰给定的。常规管道解决了这个问题:
$ echo ' Koeffizienten != Bruecken... ' | srgn -Sgu ' bw{1,8}b ' | srgn -s ' . '
Koeffizienten != BRÜCKEN.
注意:可以使用文字范围来规避正则表达式转义( .
)。经过特殊处理的替换动作也是可组合的:
$ echo ' Mooood: ????!!! ' | srgn -s ' p{Emoji} ' ' ? '
Mooood: ?!!!
表情符号首先被全部替换,然后被挤压。注意没有其他东西是如何被挤压的。
范围是srgn
的第二个驱动概念。在默认情况下,主要范围是正则表达式。操作部分详细展示了此用例,因此此处不再重复。它作为第一个位置参数给出。
srgn
通过准备好的、语言语法感知的范围扩展了这一点,这通过优秀的tree-sitter
库成为可能。它提供了查询功能,其工作原理非常类似于针对树数据结构的模式匹配。
srgn
与这些查询中的一些最有用的查询捆绑在一起。通过其可发现的 API(作为库或通过 CLI, srgn --help
),人们可以了解支持的语言和可用的、准备好的查询。每种支持的语言都带有逃生舱口,允许您运行自己的自定义即席查询。该阴影以--lang-query
的形式出现,其中lang
是一种语言,例如python
。有关此高级主题的更多信息,请参阅下文。
笔记
首先应用语言范围,因此无论您传递什么正则表达式(又称主范围),它都会单独对每个匹配的语言构造进行操作。
本节显示了一些准备好的查询的示例。
unsafe
代码(Rust) Rust 中unsafe
关键字的优点之一是它的“grepability”。然而, rg 'unsafe'
当然会显示所有字符串匹配( rg 'bunsafeb'
在一定程度上有帮助),而不仅仅是实际 Rust 语言关键字中的那些。 srgn
有助于使这一点更加精确。例如:
// Oh no, an unsafe module!
mod scary_unsafe_operations {
pub unsafe fn unsafe_array_access ( arr : & [ i32 ] , index : usize ) -> i32 {
// UNSAFE: This function performs unsafe array access without bounds checking
* arr . get_unchecked ( index )
}
pub fn call_unsafe_function ( ) {
let unsafe_numbers = vec ! [ 1 , 2 , 3 , 4 , 5 ] ;
println ! ( "About to perform an unsafe operation!" ) ;
let result = unsafe {
// Calling an unsafe function
unsafe_array_access ( & unsafe_numbers , 10 )
} ;
println ! ( "Result of unsafe operation: {}" , result ) ;
}
}
可以搜索为