Subversion (SVN) 的 --file 選項有個問題就是,不能像一般的指令一樣,當為路徑為 - 時,就從 stdin 讀資料。舉個例子來說:

SHELL> echo value | svn propset foo:bar --file - pipe2arg.shsvn: Reading from stdin is currently broken, so disabledExit 1

理論上,我會希望 foo:bar 這個 property 的值,從 stdin 讀進來,但 SVN 卻跟我說,現在這個功能壞掉了。因此,有些特殊的玩法,就沒辦法用。例如我們就沒辦法這麼下指令:

SHELL> svn propget svn:keywords pipe2arg.sh \       | sed -e 's/LastChangedDate/Date/' \             -e 's/LastChangedRevision/Rev/' \             -e 's/LastChangedBy/Author/' \             -e 's/HeadURL/URL/' \       | svn propset svn:keywords --file - pipe2arg.sh \       ;

將很臭很長的 LastChangedDateLastChangedRevisionLastChangedByHeadURL 等舊式的特殊 keywords,轉換成與 CVS 相容的新式 keywords。

原本為了解決這個問題,我曾寫了一個叫 svn-set-prop.sh 的 script,可以對 SVN 的 properties 做一些處理,如對某 property 原本的值,append 一行新值進去,或刪去某值等等。但後來發覺,功能就算做得再多,也還是永遠做不玩。最好的辦法,還是利用 filter 的原理,讓 properties 的編輯,可以如上例一樣,這樣子基本上就可以做到任何事了。所以,最終我還是得回來解決「SVN 從 stdin 讀資料的功能壞掉了」的這個問題。

要解這個問題,很直覺的想法,當然就是把 stdin 先存到暫存檔裡,然後讓有問題的程式,改從該暫存檔讀資料。然而,因為程式的參數格式千百種,為了方便起見,我參照了 xargs-I 這個參數的設計。當沒有指定 -I 參數時,存放 stdin 內容的暫存檔路徑,就會 append 到真正要執行的指令的最後面。而如果有指定 -I 參數的話,就可以用一個代換字串,來指定這個暫存檔路徑,要放在指令的哪一個地方。例如,要將目前目錄下所有的 .sh 檔,複製到一個目錄下時,因為目標目錄是 cp 的最後一個參數,所以我們必須要用 xargs 的 -I,將要複製的 *.sh 檔,插入到 cp 指令的目標目錄之前。指令如下:

SHELL> find . -name '*.sh' | xargs -I @ cp @ /tmp/foo/;

其中,我用了 @ 作為代換字串,目前目錄下的各個 *.sh 檔的路徑,會代換到 @ 所在的那一個位置。

因此,設計的 pipe2arg.sh 的用法如下:

SHELL> pipe2arg.sh --helpUsage: pipe2arg.sh [ <option> ... ] <utility> [ <arguments> ... ]

Save pipe input as a temporary file, then pass the path of the temporary fileto <utility> as supplymental arguments.  Note that input must be text.  Binaryinput will not work.

Options:

  -h,--help               Show this help messsage.  -I,--replace <replstr>  Replace <replstr> to the path of the temporary file,                          instead of append as last argument.  -t,--echo-cmd           Echo the command to be executed to standard error                          immediately before it is executed.

Version: r878 (2006-09-01)

希望達到的效果如:

SHELL> cat ~/.profile | pipe2arg.sh -t cat -b+ cat -b /tmp/pipe2arg.GBpIkU     1  BLOCKSIZE=K;    export BLOCKSIZE     2  EDITOR=vi;      export EDITOR     3  PAGER=more;     export PAGER 

程式碼如下:

#!/bin/sh -ef

exit_usage(){    local ex="$1"; shift;    echo >&2 "\Usage: `basename $0` [ <option> ... ] <utility> [ <arguments> ... ]

Save pipe input as a temporary file, then pass the path of the temporary fileto <utility> as supplymental arguments.  Note that input must be text.  Binaryinput will not work.

Options:

  -h,--help               Show this help messsage.  -I,--replace <replstr>  Replace <replstr> to the path of the temporary file,                          instead of append as last argument.  -t,--echo-cmd           Echo the command to be executed to standard error                          immediately before it is executed.

Version: r${__revision} (${__rev_date})";    while [ $# -gt 0 ]; do        echo >&2 "ERROR: $1";        shift;    done;    exit $ex;}

opt_replace='';opt_echo_cmd='no';opt_utility='';opt_arguments='';while [ $# -gt 0 ]; do    arg="$1"; shift;    case "$arg" in    -h|--help)        exit_usage 0;        ;;    -I|--replace)        if [ $# -gt 0 ]; then            opt_replace="$1"; shift;        else            exit_usage 1 'Missing <replstr>.';        fi;        ;;    -t|--echo-cmd)        opt_echo_cmd='yes';        ;;    -*)        exit_usage 1 "Unknown option: $arg";        ;;    *)        opt_utility="$arg";        break;        ;;    esac;done;if [ -z "$opt_utility" ]; then    exit_usage 1 'Missing <utility>.';fi;

temp_file=`mktemp /tmp/pipe2arg.XXXXXX`;while [ $# -gt 0 ]; do    arg="$1"; shift;    if [ \( -n "$opt_replace" \) -a \         \( "x$opt_replace" = "x$arg" \) ]; then        arg="$temp_file";    fi;    opt_arguments="$opt_arguments $arg";done;if [ -z "$opt_replace" ]; then    opt_arguments="$opt_arguments $temp_file";fi;

# Dump stdin to $temp_file.xIFS=$IFS;IFS='';while read -r line; do    echo $line >> $temp_file;done;IFS=$xIFS;

if [ "x$opt_echo_cmd" = 'xyes' ]; then    echo + $opt_utility $opt_arguments;fi;

# execute <utility>$opt_utility $opt_arguments;

rm -f $temp_file;

裡面有幾個重點:

  1. 對於真正要跑的指令的處理,必須要在用來存放 pipe input 的暫存檔建立,路徑確定之後,才好開始處理。因此,參數處理的迴圈,要拆成兩個來跑。且當跑到要用來代換成檔名的位置時,要以暫存檔路徑替代之。
  2. 第一行要對 sh 加上 -f 的參數,抑制程式裡的 pathname expansion,如此才能夠順利將給定的參數儲存。
  3. 使用 read 讀取 stdin 並存入暫存檔。但由於 read 本身會對反斜線作特殊處理,因此 read 要加 -r 抑制此行為。
  4. 另外,read 時是以 token 為單位,而預設的 delimiters 包含空白、tab 與換行等,原輸入內容會受影響,因此在跑迴圈用 read 讀資料前,需先將用來規範 delimiters 的特殊變數 IFS 備份並清空,讓 read 可以完整讀入原始內容,之後再回復成備份值。

目前這個 script 用起來還不錯,不過還沒碰到如含空白的路徑的情況,還不曉得有沒有問題就是了。不過,將 stdin 先全部存到暫存檔,可能效率不會太好。因此,理論上應該可以用 named pipe 來取代暫存檔,這樣也可以防止 stdin 資料太多的問題。不過,詳細作法就要再研究了。