指定区域的含义
在写代码的过程中我们可能会在源文件中加入一些特定的【段描述信息】。这些段描述信息一般有固定的【开始字符串】与【结束字符串】,这两者之间的内容都属于段描述信息的具体内容。这种段描述信息就是一个指定区域的实例。
上面的文字描述了指定区域的一种描述方法,即用固定的开始字符串与结束字符串来标志一段特定区域的内容。 开始字符串与结束字符串中一般会使用一些【特殊】的字符,这样就能在很大程度上避免其它区域被【错误识别】。
行号指定区间的处理
1. 使用 sed 完成
这个指定区域也可以通过【行号区间】来描述,这样的方式相对简单,难点变为如何【获取行号】。获取成功后直接调用 sed 就能简单的删除开始与结束行之间的内容。
2. 使用高级语言完成
使用高级语言来解决这个问题也是非常简单的。我们需要设定一个【行号计数变量】,每次读入一行就判断此变量是否在指定的行号区间内,在则【忽略】当前行,不在则将当前行【追加】到一个 【buffer】 中,然后对这个计数变量【加一】。循环读入文件,处理完成后将 buffer 内容重新【写回】到文件就完成了任务。不过需要注意行号计数变量的【初始值】,一般是从 【1】 开始。
这种使用行号指定区间的方式不太灵活。当单个文件中有多个区间时,查找多个区间起始与终止行号的任务变得很麻烦,这时我们需要更好的解决方案。
下面是一些删除用字符串描述指定区域的解决方案。
使用 sed 解决
1. 简单的分析
使用 sed 解决这个问题不太容易,需要用到一些不太常用的技巧。你可以想想,sed 读入每一行并判断此行是否是指定区域开始,这一过程并不太难。当没有匹配到就正常处理也没什么,可当匹配到区域开始之后,后续行的处理流程就需要变化,这些行需要被删除,这就与正常的处理流程不同了。要实现这样的行为好像可以通过跳转语句来完成,可你仔细想想可能会发现这个过程不容易实现。
2. 使用地址区间完成
经过研究,我发现其实可以通过使用【地址区间】来完成。对于这个地址区间我之前也没有搞得很懂,我只是经常使用数字区间与单个匹配模式,没有使用过【匹配模式区间】,所以我觉得有点复杂。研究了一下,当文件中只存在一个指定区域时,sed 可以很简单的解决这个问题。示例代码如下:
'/\\*section start/,/section end \*\\/{
/\\* section start/n
/section end \*\\/n
d
}'
测试文件的内容如下:
first line
\* section start
remove component1
section end *\
\* section start
remove component2
section end *\
\* section start
remove component3
section end *\
\* section start
remove component4
section end *\
end line
执行结果如下:
[longyu@debian-10:10:31:45] perl $ sed '/\\*section start/,/section end \*\\/{
> /\\* section start/n
> /section end \*\\/n
> d
> }' test-file
first line
\* section start
section end *\
\* section start
section end *\
\* section start
section end *\
\* section start
section end *\
3. 解决执行异常的问题
细心的读者可以发现,上面的执行结果有点不对头。指定区域内的内容确实被删除了,可指定区域外的一些行也被删掉了。 研究发现上面的脚本中跳过终止字符串所在行的方式存在问题。使用 n 命令来读入新的一行到模式空间并跳转到脚本起始,这样的方式【改变了】模式空间区域的【正常执行流】,导致这个区间没有按照期望的结果正常终止,实际结果就是终止行的下一行也被删除了。
将上面的脚本进行修改就可以避免这个问题,代码如下:
'/\\*section start/,/section end \*\\/{
/\\* section start/n
/section end \*\\/ !{d}
} '
上面使用了 ! 命令,此命令过滤出未匹配到终止字符的行,然后后执行 d 命令【删除】该行。
执行结果如下:
[longyu@debian-10:10:46:25] perl $ sed '/\\*section start/,/section end \*\\/{
/\\* section start/n
/section end \*\\/ !{d}
} ' test-file
first line
\* section start
section end *\
\* section start
section end *\
\* section start
section end *\
\* section start
section end *\
end line
上面这个脚本避免了前一个脚本的问题,正确的解决了问题。
4. 最开始的解决方案
上面这这种解决方法我最开始并没有想到,我最开始想到的是如下脚本:
/\\\* section start/ {
h;d;
}
/section end \*\\/ {
x;G;p;d
}
{
x;
/\\\* section start/ {
x;d;
}
x;
}
将上述脚本保存为 sedsrc 文件,执行 sed 命令,结果如下:
[longyu@debian-10:10:59:13] perl $ sed -f sedsrc test-file
first line
\* section start
section end *\
\* section start
section end *\
\* section start
section end *\
\* section start
section end *\
end line
5. 为什么会这样写
当时写的时候就觉得这个脚本太过【复杂】。虽然能够完成任务,但是不容易懂。尽管如此我觉得上面这个脚本能够说明一些更为深刻的问题。
上面的脚本可以分为三个执行过程:
- 匹配到一个指定区域的起始行,将该行存储到保存空间,删除模式空间内容,
执行新的一次循环 - 匹配到一个指定区域的结束行,在正常情况下,保存空间中已经保存了起始
行,这时需要输出起始行与终止行,将结束行与开始行互换位置,保持空间的
内容变为了结束行内容,这也就意味着退出指定区域 - 对于未匹配到开始与结束行的模式空间,判断当前行是否在一个指定区域内,
在则删除,不在则不进行处理
上面的脚本是在我不了解 sed 的地址空间范围可以指定两个不同的模式时实现的。这个实现的核心在于【保存】匹配到一个起始行的状态,并在后续行中【判断】是否已经在一个指定区域内,在则删除,不在则正常处理,最后当匹配到终止行的时候输出起始行与终止行。如果要匹配到一个开始行就输出该行,则可以使用如下脚本:
/\\\* section start/ {
p;h;d;
}
/section end \*\\/ {
p;x;d;
}
{
x;
/\\\* section start/ {
x;d;n;
}
x;
}
不同中的共同之处
仔细思考上面的处理过程,我发现其中有四个状态:
- 匹配到起始行
- 匹配到结束行
- 匹配到指定区域内的行
- 匹配到指定区域外的行
上面的这些脚本中,都用到了这几个状态,并且在这些状态之间进行切换并执行绑定到不同状态上的操作。 使用 sed 提供的地址区间功能,sed 内部自动处理了第三与第四种状态,使用上面那个复杂的过程,第三与第四种状态由我自己处理,这可能是其中最大的区别。
使用 awk 与 perl 解决
使用 awk 与 perl 解决这个问题的原理类似。首先【读入】行,然后【过滤掉】不需要的内容,然后【写入】到文件中。也可以在从文件读入的过程中直接完成!
注意在 awk 中同时读入与写入数据到相同文件中可能造成文件内容丢失的问题!!
这里我选择使用 perl 来解决这个问题,代码如下:
#!/usr/bin/perl
use utf8;
use strict;
sub err_ret {
my($str, $errno) = @_;
print STDERR "$str\n";
return $errno;
}
sub read_from_file {
my($filename) = @_;
my $str;
if ($filename eq "") {
return &err_ret("non filename", -1);
}
if (! open FILE, "< $filename") {
return &err_ret("open $filename failed\n");
}
while (<FILE>) {
$str .= $_;
}
close(FILE);
return $str;
}
sub write_to_file {
my($str, $filename) = @_;
if ($str eq "" || $filename eq "") {
return &err_ret("invalid parameters in write_to_file", -1);
}
if (! open FILE, '>', $filename) {
return &err_ret("cannot open $filename to write", -1);
}
print FILE $str;
close FILE;
return 0;
}
sub filter_component {
my($str_src, $start_str, $end_str) = @_;
my $flag = 0;
my $newline_pos = 0;
my $outstr;
if ($start_str eq "" || $end_str eq "") {
return &err_ret("invalid parameters in filter_component", -1);
}
while (($newline_pos = index($str_src, "\n")) != -1) {
my $str = substr($str_src, 0, ,$newline_pos + 1);
$str_src = substr($str_src, $newline_pos + 1);
if (index($str, $start_str) != -1) {
$flag = 1;
$outstr .= $str;
} elsif ($flag == 0) {
$outstr .= $str;
} elsif (index($str, $end_str) != -1) {
$outstr .= $str;
$flag = 0;
}
}
# when file is not ended with a '\n', add unmatched line.
return $outstr . $str_src;
}
# Todo: use the command line to get start_str and end_str
foreach my $file (@ARGV) {
my $str = read_from_file($file);
# This is only an example, You can change the start_str and
# end_str to complete your needs
my $outstr = filter_component($str, "\\\* section start", "section end *\\");
write_to_file($outstr, $file);
}
上面的代码的主要逻辑如下:
- 读入文件到 buffer 中
- 按照指定的区域开始与结束字符串来过滤 buffer
- 将过滤后的 buffer 重新写回到文件中
在使用 sed 来完成这个任务也逃不脱这三步,只是大部分的工作由 sed 帮我们做了,使用 perl 来完成则需要多做些工作,从这些多出来的工作中能够发现更具一般性的问题,这反过来也让我对这些工具的工作原理有了更深入的认识。
进一步的思考
在上面的描述中,问题的解决方案在变化,问题本身其实没有变化,变化的应该是我对问题的认识,这种认识的进一步深化让我看到了一些不同的东西。从使用工具迈向理解工具所要达成的需求,这让我对工具的设计萌生了兴趣。使用通用语言来解决问题给我提供了一个很好的契机,让我能够更接近问题的本质,可其实它也为我设置了一个新的障碍,这障碍便是【语言本身】。
在上面解决问题的过程中,我用不同的工具,不同的语言来实现相同的功能,这都是以我建立的思考模型为基础进行的不同表达。不同的工具与语言,其本身所具有的意义与我的表达在某些方面有所重叠。这一重叠在工具中体现的尤为明显!可能设计这些工具的人也曾有过类似的思考,不过他们比我要走的更远!
why 的问题
这一过程也可以说是从学以致用到用以致学转变的实例。我对这些熟练的工具产生了好奇心,对工具所达成的需求有了更深入的了解,对其实现原理也有所领悟。我拥有了一个不同的视角,看到了不同的东西。
我可以说我开始不断的追问“为什么”。诸如为什么要提供这样的功能?为什么要这样设计?这样的问题在我的心中不断涌现,更进一步激发了我的好奇心,让我有极大的动力去研究这些工具的实现以回答心中的疑问,这无疑是一个新的阶段。
将上面的思考应用到我目前正在做的事情之上,我会问为什么硬件要设计成我看到的样子?为什么外设的寄存器包含标志位、功能位、使能位等等?这些问题是我之前不曾想过的问题,现在看来这些问题是非常值得思考的。
解释与执行的模式
更进一步的思考,我可以说我看到了一个更大的模式。这个模式是解释与执行的模式。我曾经阅读过 sed、awk、bash 的部分代码,发现它们都可以说是一种解释器。虽然它们可以归为一类,但是它们的语法基本上没有太大的关联,这也就意味着我们在使用这几个工具时对问题的的思考是不同的,可以说我们是在不同的抽象层次完成任务。
如果抛开这些语法的差异,追踪执行的过程,向更低的层次迈进,也许又会发现许多共同之处。这些共同之处其实隐含着问题本身的意义,看到这些共同之处让我们能够从表达形式的变化回归到问题的本质上,不再受表达方式的束缚。
总结
本文以删除文件中指定区域内的行为引子,通过描述不同的解决方案,一步步向问题的本质靠拢。从解决方法的不同出发看到相同之处,最终以解释与执行的模式来将这一过程上升到一个更高的层次,是一次有意义的思想漫游!