刪除文件中指定區域內的行

指定區域的含義

在寫代碼的過程中我們可能會在源文件中加入一些特定的段描述信息。這些段描述信息一般有固定的開始字符串與結束字符串,這兩者之間的內容都屬於段描述信息的具體內容。這種段描述信息就是一個指定區域的實例。

上面的文字描述了指定區域的一種描述方法,即用固定的開始字符串與結束字符串來標誌一段特定區域的內容。開始字符串與結束字符串中一般會使用一些特殊的字符,這樣就能在很大程度上避免其它區域被錯誤識別

行號指定區間的處理

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
	next end line
	next end line

5. 爲什麼會這樣寫

當時寫的時候就覺得這個腳本太過複雜,雖然能夠完成任務,但是不容易懂。儘管如此我覺得上面這個腳本能夠說明一些更爲深刻的問題。

上面的腳本可以分爲三個執行過程:

  1. 匹配到一個指定區域的起始行,將該行存儲到保存空間,刪除模式空間內容,
    執行新的一次循環
  2. 匹配到一個指定區域的結束行,在正常情況下,保存空間中已經保存了起始
    行,這時需要輸出起始行與終止行,將結束行與開始行互換位置,保持空間的
    內容變爲了結束行內容,這也就意味着退出指定區域
  3. 對於未匹配到開始與結束行的模式空間,判斷當前行是否在一個指定區域內,
    在則刪除,不在則不進行處理

上面的腳本是在我不瞭解 sed 的地址空間範圍可以指定兩個不同的模式時實現的。這個實現的核心在於保存匹配到一個起始行的狀態,並在後續行中判斷是否已經在一個指定區域內,在則刪除,不在則正常處理,最後當匹配到終止行的時候輸出起始行與終止行。如果要匹配到一個開始行就輸出該行,則可以使用如下腳本:

	/\\\* section start/ {
		p;h;d;
	}
	/section end \*\\/ {
		p;x;d;
	}
	{
		x;
		/\\\* section start/ {
			x;d;n;
		}
		x;
	}

不同中的共同之處

仔細思考上面的處理過程,我發現其中有四個狀態:

  1. 匹配到起始行
  2. 匹配到結束行
  3. 匹配到指定區域內的行
  4. 匹配到指定區域外的行

上面的這些腳本中,都用到了這幾個狀態,並且在這些狀態之間進行切換並執行綁定到不同狀態上的操作。 使用 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);
}

上面的代碼的主要邏輯如下:

  1. 讀入文件到 buffer 中
  2. 按照指定的區域開始與結束字符串來過濾 buffer
  3. 將過濾後的 buffer 重新寫回到文件中

在使用 sed 來完成這個任務也逃不脫這三步,只是大部分的工作由 sed 幫我們做了,使用 perl 來完成則需要多做些工作,從這些多出來的工作中能夠發現更具一般性的問題,這反過來也讓我對這些工具的工作原理有了更深入的認識

進一步的思考

在上面的描述中,問題的解決方案在變化,問題本身其實沒有變化,變化的應該是我對問題的認識,這種認識的進一步深化讓我看到了一些不同的東西。從使用工具邁向理解工具所要達成的需求,這讓我對工具的設計萌生了興趣。使用通用語言來解決問題給我提供了一個很好的契機,讓我能夠更接近問題的本質,可其實它也爲我設置了一個新的障礙,這障礙便是語言本身。

在上面解決問題的過程中,我用不同的工具,不同的語言來實現相同的功能,這都是以我建立的思考模型爲基礎進行的不同表達不同的工具與語言,其本身所具有的意義與我的表達在某些方面有所重疊。這一重疊在工具中體現的尤爲明顯!可能設計這些工具的人也曾有過類似的思考,不過他們比我要走的更遠!

why 的問題

這一過程也可以說是從學以致用到用以致學轉變的實例。我對這些熟練的工具產生了好奇心,對工具所達成的需求有了更深入的瞭解,對其實現原理也有所領悟。我擁有了一個不同的視角,看到了不同的東西。

我可以說我開始不斷的追問“爲什麼”。諸如爲什麼要提供這樣的功能?爲什麼要這樣設計?這樣的問題在我的心中不斷涌現,更進一步激發了我的好奇心,讓我有極大的動力去研究這些工具的實現以回答心中的疑問,這無疑是一個新的階段

將上面的思考應用到我目前正在做的事情之上,我會問爲什麼硬件要設計成我看到的樣子?爲什麼外設的寄存器包含標誌位、功能位、使能位等等?這些問題是我之前不曾想過的問題,現在看來這些問題是非常值得思考的。

解釋與執行的模式

我可以說我看到了一個更大的模式。這個模式是解釋與執行的模式。我曾經閱讀過 sed、awk、bash 的部分代碼,發現它們都可以說是一種解釋器。雖然它們可以歸爲一類,但是它們的語法基本上沒有太大的關聯,這也就意味着我們在使用這幾個工具時對問題的的思考是不同的,可以說我們是在不同的抽象層次完成任務

如果拋開這些語法的差異,追蹤執行的過程,向更低的層次邁進,也許又會發現許多共同之處。這些共同之處其實隱含着問題本身的意義,看到這些共同之處讓我們能夠從表達形式的變化迴歸到問題的本質上,不再受表達方式的束縛

總結

本文以刪除文件中指定區域內的行爲引子,通過描述不同的解決方案,一步步向問題的本質靠攏。從解決方法的不同出發看到相同之處,最終以解釋與執行的模式來將這一過程上升到一個更高的層次,是一次有意義的思想漫遊!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章