Arlo 問我有沒有辦法讓 Editplus 2 預設使用 UTF-8 encoding?很可惜地,雖然 Editplus 2 可以預設檔案的行尾要用 PC (CR-LF)、UNIX (LF) 還是 Mac (CR) 格式,但卻沒有辦法預設新檔案要使用的 encoding,一律使用 ANSI codepage。

Arlo 問這個問題,想必是因為常常忘了把原始碼檔案的 encoding 設成 UTF-8,導致程式執行時出錯。我以前也有碰過這樣的困擾,於是從某 trac 資料庫裡把下面這段挖出來:

Here's a command to check which file has incorrect utf8 encoding:

SHELL> find . -type f -name '*.php' -or -name '*.html' -or -name '*.htm' \
       | xargs -n 1 -t iconv -f UTF-8 > /dev/null

If there's any file showing as follows, this file is non-utf8 encoded:

iconv: ./manage_categories.php: cannot convert

想了一想,還是寫成 script 放進 ADE 好了,程式碼是我最佳的知識儲存方法。可以有的變化有:

  • 指定要檢查哪些延伸檔名的檔案。以上例來說,就是只檢查 .php.html.htm 的檔案。
  • 可以選擇是否要略過 CVS 或 Subversion 的 working copy 用 meta 目錄。對 CVS working copy 來說,就是 CVS/ 目錄,對 Subversion working copy 來說,就是 .svn/ 目錄。當然預設是都要略過。

本來還想要寫成可以任意指定要檢查的 encoding。但因為我是用 iconv 轉換作為檢查的方法。若 iconv 可以將成功地該檔自 UTF-8 轉換成目前 locale 指定的 encoding,則是該檔為正確的 UTF-8 檔案。

這裡的關鍵在於 UTF-8 是 unicode,理論上 unicode 包容萬物,可以轉換到任何一種 encoding,所以跑起來沒問題。但若是遇上非 unicode 的 encoding,比如說 GB2312,就沒辦法保證一定能夠轉換成目前 locale 指定的 encoding,而產生一堆 false positive 了。

當然,我還是可以山不轉路轉,改試著從該 encoding 轉換成 unicode,但,算了,懶了,就檢查 UTF-8 就好,以後有需要再說。

於是,有了如下的 usage 設計:

Usage: list_non_utf8.sh [ <option> ... ] <directory> [ <file-ext> ... ]

List all non-utf8 encoded files recursively for those files with file extension
<file-ext>.

Options:

  -h/--help   Show this help message.
  --no-svn    Skip subversion directories.  (default)
  --with-svn  Do not skip subversion directories.
  --no-cvs    Skip CVS directories.  (default)
  --with-cvs  Do not skip CVS directories.

執行起來應該像這個樣子:

SHELL> list_non_utf8.sh . sh
./taipeilink-with_backup.sh
./bbsclient.sh
./taipeilink.sh

程式很好寫,除了一個「redirection 與 pipeline 處理」的問題:怎樣拋棄 iconv 的 stdout,把 stderr 撿起來 pipe 給另外一個程式處理?因為 iconv 會把轉換後的檔案丟到 stdout,這個是我們不需要的,要轉到 /dev/null 丟棄。而當 iconv 無法成功轉換檔案時,就會在 stderr 上吐出含有檔名的錯誤訊息,而這個檔名,就是我們所需要的。所以我希望達到這個效果:stdout 出來的東西轉到 /dev/null 丟棄,然後 stderr 出來的東西轉給 sed 把非檔名的部分去掉,然後輸出到 stdout。

自己測了半天,寫不出來。寫 shell script 最麻煩的就是 redirection + pipeline 與 quoting + escaption 的問題了。最後拜了一下咕狗大神後找到這個網頁:《KSB's sh redirection and pipeline notes》,聖經啊!在詳細的解說之下,終於用下面的方法解決了我的問題:

iconv -f UTF-8 2>&1 > /dev/null | sed -e ...

所以,雖然 Editplus 2 沒有辦法預設新檔案要使用的 encoding,防止原始碼檔案 encoding 設錯的問題,但我們可以事後用這個 list_non_utf8.sh,檢查出非 UTF-8 編碼的原始碼檔案。UNIX 環境果然才是王道啊,需要什麼非現成的功能,隨時都可以拼拼湊湊兜一個出來用。

Update 2005-10-27

剛剛順便逛了一下 Editplus 2 網站,結果發現新的 2.20 版,剛加上了 Arlo 要的功能,另外還有等很久的「Supports multiple languages in single Unicode or UTF-8 file」的功能,真是不錯。約五年前我就開始在用 Editplus 2 了。Editplus 2 是一個從以前就一直比 UltraEdit 功能還要強大好用的 editor,雖然改版緩慢,但沒有什麼缺憾的合理設計,一直是我的最愛之一。

Update 2008-04-24

還是把 list_non_utf8.sh 列在下面好了:

#!/bin/sh

usage()
{
    echo "Usage: list_non_utf8.sh [ <option> ... ] <directory> [ <file-ext> ... ]";
    echo "";
    echo "List all non-utf8 encoded files recursively for those files with file extension";
    echo "<file-ext>.";
    echo;
    echo "Options:";
    echo;
    echo "  -h/--help   Show this help message.";
    echo "  --no-svn    Skip subversion directories.  (default)";
    echo "  --with-svn  Do not skip subversion directories.";
    echo "  --no-cvs    Skip CVS directories.  (default)";
    echo "  --with-cvs  Do not skip CVS directories.";
    echo;
    exit 0;
}

exit_with_msg()
{
    local ex=0;
    local msg='';
    if [ $# -gt 0 ]; then
        ex=$1;
        shift;
    fi;
    if [ $# -gt 0 ]; then
        msg="$1";
        shift;
    fi;
    if [ ! -z "$msg" ]; then
        echo "ERROR: $msg";
    fi;
    echo "Type 'list_non_utf8.sh --help' for usage.";
    exit $ex;
}

__opt_directory='';
__opt_file_exts='';
__opt_no_svn='yes';
__opt_no_cvs='yes';
while [ $# -gt 0 ]; do
    __arg="$1"; shift;
    case "$__arg" in
    --no-svn)
        __opt_no_svn='yes';
        ;;
    --with-svn)
        __opt_no_svn='no';
        ;;
    --no-cvs)
        __opt_no_cvs='yes';
        ;;
    --with-cvs)
        __opt_no_cvs='no';
        ;;
    -h|--help)
        usage;
        ;;
    -*) # argument.
        exit_with_msg 1 "Invalid option: $__arg";
        ;;
    *)
        if [ -z $__directory ]; then
            __opt_directory="$__arg";
        else
            if [ ! -z $__opt_file_exts ]; then
                __opt_file_exts="$__opt_file_exts -or ";
            fi;
            __opt_file_exts="$__opt_file_exts -name '*.$__arg'";
        fi;
        ;;
    esac;
done;
if [ -z "$__opt_directory" ]; then
    exit_with_msg 1 "Missing <directory>.";
fi;

if [ \( $__opt_no_svn = 'no' \) -a \( $__opt_no_cvs = 'no' \) ]; then
    find $__opt_directory -type f $__opt_file_exts \
    | xargs -n 1 iconv -f UTF-8 \
    2>&1 > /dev/null \
    | sed -e 's/^iconv: //' -e 's/: cannot convert$//'
else
    __grep_cmd="grep -v ";
    if [ $__opt_no_svn = 'yes' ]; then
        __grep_cmd="$__grep_cmd -e .svn/";
    fi;
    if [ $__opt_no_cvs = 'yes' ]; then
        __grep_cmd="$__grep_cmd -e CVS/";
    fi;
    find $__opt_directory -type f $__opt_file_exts \
    | $__grep_cmd \
    | xargs -n 1 iconv -f UTF-8 \
    2>&1 > /dev/null \
    | sed -e 's/^iconv: //' -e 's/: cannot convert$//'
fi;

exit 0;