Thursday, November 04, 2004

我是苦命 patcher

我是苦命 patcher,我想來說說 VSS 的壞話。

在我進公司之前,老闆選了 Microsoft Visual SourceSafe (VSS) 作為部門的 version control system。這,是苦命的開始。理論上,使用 version control system,可以幫助程式員們,彼此協同開發。我以前就有過這樣子的經驗,和學弟兩人,使用 CVS 一起開發一套社群軟體。兩人卯足了勁拼命 commit,彼此分工合作,絲毫不會有程式互相「蓋到」的情況發生。可是,現在在公司上班,與同事協同開發的感覺,完全變了一個樣。在我們部門裡, VSS 根本就只是被當作一個 source deposit,而沒有發揮 source control 應該要有的功能。

在我們部門裡,典型的協同開發的流程,就如同下面這個故事一般:

  1. A 君寫了一套軟體的初版,程式已經可以動作,初版的功能皆已齊備了,然後他趕緊在 VSS 裡建一個專案,或是找一個相對應的位置,把程式放進去。
  2. 然 後,老闆找了 B 君來做一個新功能,於是 B 君就找 A 君要程式,A 君說,放在 VSS 裡,於是 B 君就回去自己的電腦前,想要把程式 checkout 出來,卻發現他沒有權限。於是,B 君再回去找 A 君,然後 A 君就打開他的 VSS,加了 B 君的帳號。就在 B 君要回自己位子上去工作前,A 君拉住了 B 君,對 B 君說:「你不要亂 commit 喔,程式寫好了之後,拿給我看看,沒問題再 commit。」
  3. 於是,B 君便辛勤地與新功能奮鬥。其間,歷經了若干次因當機或其他不明原因,系統 crash,導致程式流失。還好,B 君平時有做備份的習慣,因此在 B 君的電腦裡,最後一共存了十幾份程式的備份檔,其中有約 1/4,因為在備份的時候,忘了標註日期時間,最後變成了讓人根本搞不清楚到底內含了什麼東西的一沱廢物。
  4. 由 於新功能的解法未定,在開發過程中,為了嘗試各種解 法,B 君的電腦裡的程式目錄,又分成了好幾個不同的目錄,好比 FooProj-Method1、FooProj-Method1+Method2、FooProj-Solution7、FooProj- Method1+Method3、FooProj-Mess、FooProj-HalfSolved 等等讓人有點明瞭又有點模糊的奇怪目錄名。
  5. 另 外,B 君覺得,有些程式可以抽離出來,成為一個 utility library,好比對檔名的分析與處理等。因此,B 君便將這些程式抽離出來,另外做成了一個 library。然後在原本的程式裡,呼叫這個 library。當然,這個 library,又是放在另外一個目錄裡。
  6. 很不幸地,可以被抽離出來的程式,其種類越來越多,於是這個 utility library 就越來越龐大。更麻煩的是,這個 utility library 裡,有些功能種類已經寫完了,有些功能種類還沒有寫完,另外還有些功能種類,B 君一改再改,卻仍是還沒有改出覺得可以滿意的版本。也因此,FooProj-Method1 目錄的程式,必須搭配 UtilLib-Initial 目錄下的 utility library,然後 FooProj-Method1+Method2 目錄必須搭配 UtilLib-StrOK 目錄,FooProj-Solution7 目錄必須搭配 UtilLib-StrOK+SQLTemp 目錄,而 FooProj-Method1+Method3 所對應的 utility library 的目錄,已於上次當機而遺失了,FooProj-Mess 搭配 UtilLib-Good1、FooProj-HalfSolved 搭配 UtilLib-Good1+I18nTemp+SQLBad。老實說,連 B 君都快搞不清楚這些目錄彼此之間,錯綜複雜的關係了。
  7. 總 算,B 君改出了一個可以跑的版本了,於是 B 君將 FooProj-OK1 與 UtilLib-Good2 包成兩個 zip 檔,寄給 A 君,然後來到 A 君的電腦前,與 A 君討論。A 君打開信件,把 FooProj-OK1 裡的所有 .cpp 檔,全部 copy 到自己專案目錄下的 src/ 目錄下,然後 compile 程式,瞬間指著上千個 error message 對 B 君發飆。B 君只好很委屈地對 A 君解釋說,還需要加上 UtilLib-Good2 才行。於是 A 君與 B 君,手忙腳亂地花了好一陣子,才順利地把 UtilLib-Good2 裝進 A 君的電腦裡,並把相關設定都設好。此時,B 君的程式,才能正確地在 A 君的電腦上跑。
  8. 看 完程式之後,A 君對 B 君的程式,尚稱滿意。不過,因為 B 君的程式,是用 C++ 寫的,A 君對 C++ 並不怎麼熟悉,再加上,A 君不太願意,在他偶爾還會改進的這套程式裡,加上 UtilLib-Good2 這個拖油瓶,因此,A 君對 B 君說:「嗯,不錯,不過你還是先不要 commit 好了,等這個新功能穩定之後,我們再來弄個最終版本。喔,對,這幾天我又稍微改了一下程式,現在程式應該會快了一些,你看是不是要把新的程式 merge 一下。」由於 A 君較為資深,因此 B 君便無異議地答應了。
  9. 回到自己的電腦前,B 君開始手動 merge 兩份程式。很快地,B 君發現,光用 diff 找出相異處,根本不可行,因為兩人的程式排版風格差異太多了。好比,A 君習慣用 i++,而 B 君則習慣用 ++i;A 君習慣用 if (p) s();,B 君習慣用 if (p) { s(); }。類似這種不算差異的差異,不勝枚舉,B 君改的煩不勝煩,幾乎每一個 diff 回報的差異,都必須用人眼仔細檢視,才能確定該怎麼修改。這次 merge,B 君花了三天才完成,因為第一次 merge 完之後,發現程式跑了會當,而又找不出到底是哪裡漏了,所以 B 君只好又再重新 merge 了一次。
  10. 這樣的故事,週而復始,不斷地發生……

當然,上面的故事,點出了許多的問題。不過做人處事嘛,最好還是對事不對人。因此,咱們來罵罵 VSS 吧。

Alan De Smet 的「Visual SourceSafe: Microsoft's Source Destruction System」這篇文章裡,整理出了許多 VSS 的邪惡之處。另外,Alan De Smet 也介紹了這一篇「Visual SourceSafe Version Control: Unsafe at any Speed?」,不過這篇的排版比較差,看起來很辛苦,所以我就懶得看了。以上當然不是事情的全部,以下,則是我的抱怨:

夜了,剛總算把 2/3 的專案給人工 merge 完畢,剩下一個全域變數不曉得跑到那個檔案去了。我是苦命 patcher,我覺得我比 VSS 還要厲害。-.-||


[Thunderbird]怎樣知道IMAP Server上的信件夾,有哪些有新信?

個人習慣自己架 IMAP Server,透過 fetchmail + procmail + spamassassin 來收信、分信以及擋信。把信全放在 IMAP Server 有一個好處是,到哪裡都可以灌個 MUA 來看信,至不濟還可以用 webmail 遷就一下。

可是,由於信件已經在 server 上事先用 procmail 自動分到不同的信件夾去了,因此,我在看信的時候,便需要知道,哪一個信件夾有新信件。由於會被自動分信的,多半是屬於電子報或郵遞論壇類型的信件,不需 急著看其內容,僅需依據標題,再來決定是否需要花費時間下載觀看。另外,廣告信也是一種其實並不需要下載的信種,不過為了避免誤判,所以還是需要稍微瀏覽 一下標題,因此也屬於這類僅需要下載標題的信種。

在 Outlook Express 裡面就有這樣子的一個功能,可以對每一個 IMAP 信件夾,分別指定其下載的方式,可以為不下載、只下載信頭,以及全部下載等三種方式。透過將有可能會有新信的信件夾,設定成只下載信頭,其他的信件夾,設 定成不下載,然後 INBOX 設定成全部下載,我便可以達到我所希望的效果,在 INBOX 裡的信件,多是確定要看得信件,已經下載完畢直接可以閱讀,而其他的信件夾裡,則是新的電子報或郵遞論壇信件,等我去決定是否要觀看,歸類用的信件夾,則根本不需要下載。

然而,Thunderbird 似乎並無法達到這樣「細緻」的功能。請問有任何網友有類似的經驗與需求,或知道有任何的 extensions 或 extensions 的組合,甚至搭配 server-side 設定的辦法,可以達到我上述的需求嗎?懇請賜教。

--
我的微笑,堅持要有鼻子。:-)


有關此問題的討論:Mozilla@Taiwan


Thursday, October 28, 2004

Anti-idle by Screen and Expect

目標

使用 Screen + Expect,取代 Windows 上一般的 Enhanced Telnet Client 如 PacketSiTE 之流,達到在 server 上真正「一直掛站」的效果。

方法

  1. Client 端不限,PuTTY 是不錯的選擇。

  2. Server 端需要 screen、expect,以及可設定 prompt 的 shell,下面只介紹 tcsh 的作法。

  3. 首先,在 screenrc 裡面加上這一行:

    hardstatus alwayslastline '%{.bY}[%H]%{.bR} %?%-LW  %?%{..W}%n%f %t%{.bR}%?%+LW%?%=%{.bY}%0c'

    這 是用來在 terminal 上,使用最底下一行,顯示 screen 的 status line,會顯示的東西,除了 screen 裡各個「window」的 number、flag 及 title等資訊。額外左端加上 server 的 hostname,右端加上 server 的時間。這樣子長的就會有點像 Windows 的工作列,右端擺上 server 時間還有個好處是,由於每分鐘會變動一次,遇到那種 idle 太久會斷線的網路環境裡,從 local 到 server 端的連線就不會斷線,因為每分鐘至少會傳一次資料。

    以下統一一下 terminology 好了:使用 PuTTY 這端叫 local 端,跑 screen + expect 的 server 那端叫 server 端,然後連到的 bbs 站叫 remote 端。

    另 外要注意的是,目前的 window 會用高亮度顯示,上一個 window,在數字右邊會有一個 - 號。背景 window 有「嗶」聲發生,但我們還沒「轉台」過去看的 window,數字旁邊會有一個 ! 號。這個 ! 號,有助於我們掌握是哪一個 window 的 BBS 有訊息傳來。另外,您最好額外在 screenrc 裡設定:

    bell_msg 'Bell in window %n^G'

    這 樣 screen 還會在背景 window 有「嗶」發聲時,顯示短暫兩、三秒的訊息,告知是那一個窗有「嗶」聲。這個設定裡頭的 ^G 表示,然後要真的發出「嗶」聲。因為 screen 預設背景 window 的「嗶」是不發出聲音的,故利用 bell_msg 我們可以迫使其發出聲音來。

    另外要注意的是,我的 screen 版本為:

    SHEEL> screen -v
    Screen version 3.09.11 (FAU) 14-Feb-02
    
    SHEEL> uname -sr
    FreeBSD 4.10-PRERELEASE

    我 在另一台 Linux(RedHat)上設 hardstatus,底下會顯示亂碼一行。該機器的 screen 版本比我稍舊,詳細版本忘記了。目前還不確定是該版本的 screen 有問題,亦或是 screenrc 編寫的有問題(RedHat 預設了一個複雜的 screenrc)。

    由於 hardstatus line 會佔去一行的空間,而一般的 BBS 都至少要是 80 x 24,故原始的 PuTTY 設定便無法容納這額外需要的一行。所以請在 PuTTY 的 Window 設定頁裡,將 Rows 改成 25,以容納這額外多出來的 hardstatus line。

  4. 接著,利用 screen 的 dynamic title 機制,送出 title-escape-sequence 以根據 remote bbs 名稱修改 screen window title。為了要在 remote bbs 斷線之後,能夠把 screen window title 修改回來,所以一併參照 screen 的文件,替 shell 也應用上這個 dynamic title 機制。

    Screen 的 dynamic title 的原理為:當畫面顯示了一個 title-escape-sequence ( k ) 時,就會於這個 sequence 後面,尋找某個指定的字串。從這個字串的後面一個字元開始,到第一個空白字元出現為止,這中間的字串,就是會被設定為 screen window title 的字串。整個動作,會發生在 title-escape-sequence 後的第一個 \n 出現時。而要讓 screen 照上述這麼做,我們必須要指定這一個「某個指定的字串」,以及若該 screen window title 的字串不存在(指定字串之後馬上碰上 \n) 時,所要顯示的字串。設定方法便是,在 screenrc 裡,設定 shelltitle 為 'SEARCH|NAME',其中,SEARCH 就是那個「某個指定的字串」,而 NAME 就是若該字串不存在時,所要顯示的東西。配合 prompt 這個 shell 指令,反映到 screen window title 上來。tcsh + screen 的設定方法如下:

    在 tcshrc 裡設定:

    set prompt = "%{\ek\e\\%}%% "

    我自己的 prompt 會複雜一點,但總之,在最後面加上上面這個字串就對了。然後在 screenrc 裡設定:

    shelltitle '% |TCSH'

    這樣子,,screen 就會抓取每個 shell 指令,反映到 screen window title來。試打個 top 指令,就可以從剛剛在第 3 步裡所設定的 hardstatus line 上面,體驗到 dynamic title 的效果了。

  5. 接著,我們要編寫 except 檔,來達到「上 BBS 不會斷線」,以及「依據所上的 BBS 站,修改 screen window title」的功能。Expect 程式如下:

    #!/usr/local/bin/expect -f
    #
    # Name: bbs_noidle.expect
    # Description: expect script to run a shell which avoids being
    #              "autologged out"  by sending a  every
    #              specified timeout limit if no other I/O has occurred.
    # Author: Don Libes, December 30, 1991
    # Modified by: Clive Lin  to fit tw BBS.
    #              bbs_noidle.expect telnet bbs.some.where
    #              And send  only
    # Modified by: Jeff Hung  @ 20040829 to add
    #              these features:
    #              - Use zh-telnet to connect to BBS.
    #              - Able to use abbrevation instead of complete hostname.
    #              - Use screen's 'title-escape-sequence' to dynamically
    #                set window title.
    #
    
    set timeout 180
    set anti_idle "\177";
    
    log_user 0
    system stty -echo raw
    
    if {[llength $argv] <= 0} {
       send "Usage: bbs_noidle.expect ( | )\n";
       exit 0;
    }
    
    set bbs_abbr "$argv";
    set bbs_host "$argv";
    switch $bbs_abbr {
       lady     { set bbs_host "bbs.lady.idv.tw"      }
       tkucs    { set bbs_host "bbs.cs.tku.edu.tw"    }
       nctuiim  { set bbs_host "bbs.iim.nctu.edu.tw"  }
       tku      { set bbs_host "bbs.tku.edu.tw"       }
       ptt      { set bbs_host "ptt.cc"               }
       zoo      { set bbs_host "zoo.twbbs.org"        }
    }
    
    # Cause screen to print where we're connecting to.
    send "We're connecting to \033k\033\\% $bbs_abbr %\n\r";
    
    eval spawn -noecho /usr/local/bin/zh-telnet $bbs_host
    
    expect {
       timeout { send $anti_idle; exp_continue }
       -re .+  { send_user -raw -- $expect_out(buffer); exp_continue }
       -i $user_spawn_id -re .+ { send -- $expect_out(buffer); exp_continue }
    }

    傳 給這個 expect 程式的命令列參數,可以是在程式中的 switch {} 裡頭所列的 host name abbrevation,或是正確完整可傳給 telnet 當作命令列參數的的 host name。修個這裡的列表,您就可以編寫您自己的 bbs list。

    其中的秘訣在於這一行:

    send "We're connecting to \033k\033\\% $bbs_abbr %\n\r";

    在 連線前,先送出 title-escape-sequence 修改 screen window title,達到顯示目前所上的 BBS 站的效果。在程式最開頭的 set timeout 可以設定「防呆」的時間為多少秒。 而 set anti_idle 則可以設定,防呆時要送出那個字串。一般來說,送出 \177,也就是 DELETE 字元,差不多就很夠用了。

展望

  1. 將 bbs list 改用檔案設定,而非寫死在 expect 程式裡。

  2. 當真的 idle 太久時,自動修改 BBS 暱稱為「我出門了~」。

補充

補充一下,在 .cshrc 裡頭加上:

if ( ! $?WINDOW ) then
   alias screen 'env LC_CTYPE=en_US.ISO8859-1 screen -x -RR'
else
   alias screen 'env LC_CTYPE=en_US.ISO8859-1 screen'
endif

然後在 .login 裡加上:

if ( ! $?WINDOW ) then
   screen
endif

就可以在 login 後自動進入原先的 screen,進入仍在掛站中的 BBS。當然,這樣子離線就不能真的打 exit 離線,要用 screen 的 detach 離線法:C-a D D。

若 是有要在 shell 裡工作的話,請建立一個同 UID 的帳號,好比叫 jeffbbs,在 PuTTY 裡建立不同名稱的連線,預設使用 jeffbbs 登入,這樣子的話,就可以在用 jeffbbs 登入時,享用 Screen + Expect 掛站大法。而在使用 jeffhung 登入時,則是進入正常的 shell。這是因為 screen 是用 login id 來判定 session 歸屬的。


Simple DB3 C++ Wrapper

為了寫程式方便順手包的 db3++.h,將操作 Berkeley DB ver.3[1]的 C API 稍作包裝,以便在 C++ 裡更好用。因為純粹以快速寫完為原則,所以一些成員函式的 prototype 設計就沒有仔細構思,error handling 也未盡完善,不過以下所展示的已足以說明如何包裝、使用 db3:

#include 
#include 
#include 
#include 


template 
class BerkeleyDB3
{
public:

 typedef K key_type;
 typedef D data_type;
 typedef ::std::pair value_type;

 // 一般使用,type 請傳入 DB_BTREE 或是 DB_HASH,指底層資料儲存方式。然
 // 後 flags 常會傳入 DB_CREATE 和/或 DB_RDONLY,前者指定若 fname 檔案
 // 不在要不要建立新檔,後者指定是否唯讀。
 BerkeleyDB3(const char* fname, DBTYPE type, u_int32_t flags)
 {
   int ret;
   if ((ret = db_create(&db_, NULL, 0)) != 0) {
     ::std::cerr << "db_create: "
                 << db_strerror(ret)
                 << ::std::endl;
     ::std::exit(1);
   }
   if ((ret = db_->open(db_, fname, NULL, type, flags, 0664)) != 0) {
     ::std::cerr << "DB->open(\""
                 << fname << \"") failed: "
                 << db_strerror(ret)
                 << ::std::endl;
     ::std::exit(1);
   }
 }

 ~BerkeleyDB3()
 {
   int ret;
   if ((ret = db_->close(db_, 0)) != 0) {
     ::std::cerr << "db: db->close() failed: "
                 << db_strerror(ret)
                 << ::std::endl;
     ::std::exit(1);
   }
 }

 bool put(K key, D data)
 {
   DBTType dbt_key(key);
   DBTType dbt_data(data);
   int ret;

   if ((ret = db_->put(db_, NULL, dbt_key.dbt(), dbt_data.dbt(), 0)) != 0) {
     ::std::cerr << "DB->put("
                 << key
                 << ", "
                 << data
                 << ") failed: "
                 << db_strerror(ret)
                 << ::std::endl;
     exit(1);
   }

   return true;
 }

 bool get(K key, D& data)
 {
   DBTType dbt_key(key);
   DBTType dbt_data;
   int ret;

   if ((ret = db_->get(db_, NULL, dbt_key.dbt(), dbt_data.dbt(), 0)) != 0) {
     switch (ret) {
     case DB_NOTFOUND:
       return false;
       break;
     default:
       ::std::cerr << "DB->get("
                   << key
                   << ") failed: "
                   << db_strerror(ret)
                   << ::std::endl;
       exit(1);
     }
   }
   data = dbt_data.get();

   return true;
 }


private:

 // 這裡應該利用 template specialization 技術,針對幾種常見的 type 作偏
 // 特化。好比說若 T 是 char*,很顯然地我們要存進資料庫的不是那個指標。
 template 
 class DBTType
 {
 friend BerkeleyDB3;
 public:

   DBTType()
   {
     memset(&dbt_, 0, sizeof(DBT));
   }

   DBTType(const T& data)
     : data_(data)
   {
     memset(&dbt_, 0, sizeof(DBT));
     dbt_.data = &data_;
     dbt_.size = sizeof(data_);
   }

   void set(const T& data)
   {
     data_ = data;
     dbt_.data = &data_;
     dbt_.size = sizeof(data_);
   }

   const T get()
   {
     return *((T*)(dbt_.data));
   }

 private:

   DBT* dbt()
   {
     return &dbt_;
   }

   DBT dbt_;
   T   data_; // 用以作為資料的記憶體空間,好處是不必管理底層記憶體,壞
              // 處是要負擔新建物件的成本,且 T 必須是 copyable。

 }

 DB* db_;

};

[1] Berkeley DB 是睡貓公司出的資料庫軟體,網址為 http://www.sleepycat.com/

Wednesday, October 20, 2004

[Perl] Escape Full Big5 Character for R.E. in Perl under 5.8

由於 perl 5.8 以下(不包括 5.8),其字串是 byte 的組成單位是 byte 而非 character,所以碰上內含有 regular expression 特殊字元的 big5 字,就會造成 regular expression 判斷出錯。例如:

print (($s =~ m/會議/) ? 'match' : 'no match');

不管 $s 是否含有「會議」一詞,皆會印出 'match'。這是因為「會」字的第二個 byte,是個 '|',而 '|' 在 perl regular expression 裡,代表著特殊的意義。這個問題的通常解法,是將含有 regular expression 特殊字元的全形 Big5 字元,改寫成以 hex 數字表示的形式。例如,m/會議/ 就可以被改寫成 m/[\xB7][\x7C]議/。不過,如果是程式裡面的 regular expression 那還好,見一個改一個便是。可是,若要 match 的字串是 run-time 才能知道的,好比說 user input 的話,這招就沒效了。

這問題的終極且最好的解法,當然是換用 perl 5.8。然而,換用 perl 5.8,可能代表著整套程式都必須跟著升級,而這通常會是很麻煩的一件事,而且,也不一定每一台機器都有灌 perl 5.8。因此,我們需要另一個方法來迴避這個問題。

要迴避這個問題,其實策略也很簡單:「(只)檢查每一個全形 big5 字元,若其內含有 r.e. 特殊字元,便將之代換成以 hex 數字表示的形式」。因此,我們有了以下的 function:

my $__FULLBIG5CHARS = q{
# Table adopted from http://freebsd.sinica.edu.tw/zh-tut/big5.html
# (http://netlab.cse.yzu.edu.tw/~statue/freebsd/zh-tut/big5.html)
#
#    第一位元組      第二位元組          字區                                                  制訂
# ----------------------------------------------------------------------------------------------------
 ([\xA1-\xA2][\x40-\x7E\xA1-\xFE])| # 各種符號區                                            Big5_1984
 (     [\xA3][\x40-\x7E\xA1-\xBF])| # 各種符號區 (?]括標點符號、ASCII 全形符號、注音符號等) Big5_1984
 (     [\xA3]              [\xE1])| # 歐元符號                                              CP950
 ([\xA4-\xC5][\x40-\x7E\xA1-\xFE])| # 常用字區                                              Big5_1984
 (     [\xC6]         [\x40-\x7E])| # 常用字區                                              Big5_1984
 (     [\xC6]         [\xA1-\xFE])| # 罕用符號區                                            Big5_ETen
 (     [\xC7][\x40-\x7E\xA1-\xFE])| # 罕用符號區 (?]括日文、俄文等)                         Big5_ETen
 (     [\xC8][\x40-\x7E\xA1-\xD3])| # 罕用符號區 (?]括俄文、輸入法特殊符號等)               Big5_ETen
 ([\xC9-\xF8][\x40-\x7E\xA1-\xFE])| # 次常用字區                                            Big5_1984
 (     [\xF9][\x40-\x7E\xA1-\xD5])| # 次常用字區                                            Big5_1984
 (     [\xF9]         [\xD6-\xDC])| # 七個擴充字 (碁, 銹, 裏, 墻, 恒, 粧, 嫺)               Big5_ETen
 (     [\xF9]         [\xDD-\xFE])| # 表格符號區                                            Big5_ETen
};

sub mmi_escape_big5_re
{
 my ($mbcs) = @_;

 die if (!defined($mbcs));

 # One character per list element.
 my @chars = ();
 while ($mbcs =~ /\G($__BIG5CHARS)/gox) {
    push @chars, $&;
 }

 my $str = '';
 for my $char (@chars) {
    if (length($char) > 1) {
       if ($char =~ m/[\[|\^\$]/o) {
          my $c0 = sprintf('[\x%X]', ord(substr($char, 0, 1)));
          my $c1 = sprintf('[\x%X]', ord(substr($char, 1, 1)));
          $char = "$c0$c1";
       }
       else {
       }
    }
    $str .= $char;
 }

 return $str;
}

使用這個 function 很簡單,在用之前,把要用來 match 的變數,用這個 function 先「過濾」一遍就可以了。例如:

$content = 'blah~ blah~ blah~';
while ($input = <>) {
 chomp($input);
 $re = mmi_escape_big5_re($input);
 if ($content =~ m/$re/) {
    print "There are '$input' in \$content.\n";
 }
}

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]