pipe2arg.sh - Turn pipe input to file argument
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 \ ;
將很臭很長的 LastChangedDate、LastChangedRevision、LastChangedBy 與 HeadURL 等舊式的特殊 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;
裡面有幾個重點:
- 對於真正要跑的指令的處理,必須要在用來存放 pipe input 的暫存檔建立,路徑確定之後,才好開始處理。因此,參數處理的迴圈,要拆成兩個來跑。且當跑到要用來代換成檔名的位置時,要以暫存檔路徑替代之。
- 第一行要對
sh加上-f的參數,抑制程式裡的 pathname expansion,如此才能夠順利將給定的參數儲存。 - 使用
read讀取 stdin 並存入暫存檔。但由於read本身會對反斜線作特殊處理,因此read要加-r抑制此行為。 - 另外,
read時是以 token 為單位,而預設的 delimiters 包含空白、tab 與換行等,原輸入內容會受影響,因此在跑迴圈用read讀資料前,需先將用來規範 delimiters 的特殊變數IFS備份並清空,讓read可以完整讀入原始內容,之後再回復成備份值。
目前這個 script 用起來還不錯,不過還沒碰到如含空白的路徑的情況,還不曉得有沒有問題就是了。不過,將 stdin 先全部存到暫存檔,可能效率不會太好。因此,理論上應該可以用 named pipe 來取代暫存檔,這樣也可以防止 stdin 資料太多的問題。不過,詳細作法就要再研究了。
Post a Comment