这不会很漂亮... =) 这是我作为 sed 脚本的解决方案。请注意,它要求第一行通知 shell 如何调用 sed 来执行我们的脚本。如您所见,使用了“-n”标志,因此我们强制 sed 仅打印我们通过“p”或“P”命令明确命令它的内容。“-f”选项告诉 sed 从文件中读取命令,该文件的名称跟在选项后面。由于脚本的文件名由 shell 连接到最终命令中,它将正确地从脚本中读取命令(即,如果您运行“./myscript.sed”,shell 将执行“/bin/sed -nf myscript .sed”)。
#!/bin/sed -nf
s/[^][{}]//g
t loop
: loop
t dummy
: dummy
s/^\s*[[{]/&/
t open
s/^\s*[]}]/&\
/
t close
d
: open
s/^\(\s*\)[[]\s*[]]/\1[]\
/
s/^\(\s*\)[{]\s*[}]/\1{}\
/
t will_loop
b only_open
: will_loop
P
s/.*\n//
b loop
: only_open
s/^\s*[[{]/&\
/
P
s/.*\n//
s/[][{}]/ &/g
b loop
: close
s/ \([][{}]\)/\1/g
P
s/.*\n//
b loop
在开始之前,我们必须首先将所有内容都剥离成方括号和方括号。这是第一个“s”命令的职责。它告诉 sed 用任何内容替换不是括号或方括号的每个字符,即。去掉它。请注意,匹配中的方括号表示要匹配的一组字符,但是当其中的第一个字符是“^”时,它实际上将匹配除“^”之后指定的字符之外的任何字符。因为我们想要匹配右方括号,并且我们需要用方括号关闭要忽略的字符组,所以我们通过将右方括号设置为“^”之后的第一个字符来告诉组中应该包含右方括号。然后我们可以指定其余的字符:左方括号,打开方括号和右方括号(忽略字符组:“][{}”),然后用右方括号关闭组。我试图在这里详细说明,因为这可能会造成混淆。
现在来看看实际的逻辑。该算法非常简单:
while line isn't empty
if line starts with optional spaces followed by [ or {
if after the [ or { there are optional spaces followed by a respective ] or }
print the respective pair, with only the indentation spaces, followed by a newline
else
print the opening square or normal bracket, followed by a newline
remove what was printed from the pattern space (a.k.a. the buffer)
add a space before every open or close bracket (normal or square)
end-if
else
remove a space before every open or close bracket (normal or square)
print the closing square or normal bracket, followed by a newline
remove what was printed from the pattern space
end-if
end-while
但是有几个怪癖。首先,sed 不直接支持“while”循环或“if”语句。我们能得到的最接近的是“b”和“t”命令。“b”命令分支(跳转)到一个预定义的标签,类似于 C 的 goto 语句。“t”也分支到预定义的标签,但前提是自当前行上运行的脚本开始或自上一个“t”命令以来发生了替换。标签是用“:”命令编写的。
因为很可能第一个命令实际上至少执行了一次替换,所以它后面的第一个“t”命令将导致分支。因为我们需要测试一些其他替换,我们需要确保下一个“t”命令不会因为第一个命令而自动成功。这就是为什么我们从它上面一行的“t”命令开始(即,无论它是否分支,它仍然会在同一点继续),所以我们可以“重置”“t”使用的内部标志命令。
因为“循环”标签会从至少一个“b”命令分支到,所以在执行“b”时可能会设置相同的标志,因为只有“t”命令可以清除它。因此,我们需要执行相同的解决方法来重置标志,这次使用“虚拟”标签。
我们现在通过检查是否存在开方括号或开闭括号来开始算法。因为我们只想测试它们是否存在,所以我们必须将匹配替换为它自己,也就是“&”代表的意思,如果匹配成功,sed 会自动为“t”命令设置内部标志。如果匹配成功,我们使用“t”命令分支到“open”标签。
如果不成功,我们需要看看我们是否匹配一个封闭的正方形或正常的括号。该命令几乎相同,但现在我们在右括号后添加一个换行符。我们通过在放置匹配项的位置(即“&”之后)添加一个转义的换行符(即反斜杠后跟一个实际的换行符)来做到这一点。与上面类似,如果匹配成功,我们使用“t”命令跳转到“close”标签。如果不成功,我们将认为该行无效,并立即清空模式空间(缓冲区)并在下一行重新启动脚本,所有这些都使用单个“d”命令。
输入“open”标签,我们将首先处理一对匹配的开闭括号的情况。如果我们匹配它们,我们将在它们之前打印缩进空格,它们之间没有任何空格,并以换行符结尾。每种类型的括号对(方形或普通)都有一个特定的命令,但它们是类似的。因为我们必须跟踪有多少缩进空间,所以我们必须将它们存储在一个特殊的“变量”中。我们通过使用组捕获来做到这一点,它将存储从“(”之后开始并在“)”之前结束的匹配部分。因此,我们使用它来捕获行首之后和左括号之前的空格。然后我们继续匹配左括号,后跟空格和相应的右括号。当我们编写替换时,我们确保使用特殊变量“\1”重新插入空格,该变量包含匹配中第一个组捕获存储的数据。然后我们编写相应的一对开括号和右括号以及转义的换行符。
如果我们设法进行了任何替换,我们必须打印我们刚刚编写的内容,将其从模式空间中删除并使用该行的剩余字符重新开始循环。因此,我们首先使用“t”命令分支到“will_loop”标签。否则,我们分支到“only_open”标签,它将处理只有一个开括号的情况,没有连续的相应闭括号。
在“will_loop”标签内,我们只需使用“P”命令打印模式空间中的所有内容,直到第一个换行符(我们手动添加)。然后,我们手动删除第一个换行符之前的所有内容,以便我们继续处理该行的其余部分。这类似于“D”命令的作用,但不需要重新启动脚本的执行。最后,我们再次分支到循环的开头。
在“only_open”标签内,我们以与之前类似的方式匹配一个左括号,但现在我们重写它并附加一个换行符。然后我们打印该行并将其从模式空间中删除。现在我们将所有括号(打开或关闭,方括号或普通括号)替换为前面的单个空格字符。这样我们就可以增加缩进。最后,我们再次分支到循环的开头。
最后的标签“close”将处理一个右括号。我们首先删除括号前的每个空格,有效地减少缩进。为此,我们需要使用捕获,因为虽然我们想匹配空格和后面的括号,但我们只想写回括号。最后,将所有内容打印到我们在输入“关闭”标签之前手动添加的换行符,从模式空间中删除我们打印的内容并重新启动循环。
一些观察:
- 这不会检查代码的语法正确性(即 {{[}] 将被接受)
- 它会在遇到括号时添加和删除缩进,无论它们的类型如何。这意味着当它添加缩进时,即使遇到的右括号不是同一类型,它也会删除它。
希望这会有所帮助,很抱歉这篇长文=)