前言
在撰寫命令列程式時,處理命令列參數是常見的任務。程式設計者在剛學程式設計時,寫的程式也都是命令列程式。所以,處理命令列參數是程式設計者在學習初期就會碰到的任務。
然而,在 Common Lisp 實作品中,取得命令列參數的方式並不一致。與其在每個命令列程式中重覆解決這項無法避開的議題,還不如將這個問題封裝成跨平台的函式,日後就以相同的方式來解決。本文介紹在常見的 Common Lisp 實作品中處理命令列參數的方式。
以跨平台的方式取得命令列參數
在 Common Lisp 程式中,命令列參數的型態是列表 (list)。由於列表本身即儲存長度和符號的資訊,不需要再用額外的變數來取得命令列參數相關的資訊。 (註)
(註) 在 C 語言中,命令列參數儲在 argc
(長度) 和 argv
(字串陣列) 兩個變數中。但 Common Lisp 的列表即同時具有這兩項資訊。
但不同 Common Lisp 實作品取得命令列參數列表的變數相異。故我們把呼叫命令列參數的過程封裝在 argument-vector
函式中。其程式碼如下:
(defun argument-vector ()
(declare (ftype (function () list) argument-vector))
"Unprocessed argv (argument vector)"
#+sbcl sb-ext:*posix-argv*
#+ccl ccl:*command-line-argument-list*
#+clisp ext:*args*
#+abcl ext:*command-line-argument-list*
#+ecl (ext:command-args)
#-(or sbcl ccl clisp abcl ecl)
(error "Unsupported Common Lisp implementation"))
其實這個函式的動作很簡單,只是在呼叫 Common Lisp 實作品的內建變數而已。比較關鍵的點在於這個函式會因應不同 Common Lisp 實作品呼叫不同的變數。
呼叫這個函式後,即可取得命令列參數。但這並沒有解決本文所討論的議題,因為每個 Common Lisp 實作品回傳的命令列參數的內容也不一致。我們會在下一節繼續討論這個議題。
初步處理命令列參數的 Common Lisp 程式
承上節,由於每種 Common Lisp 實作品回傳的命令列參數的內容不一致,我們得先把多餘的部分去除。參考以下函式:
(defun argument-script ()
(declare (ftype (function () list) argument-vector))
"Processed command-line argument(s) in scripting mode."
(let* ((args (argument-vector))
#+sbcl (args (rest args))
#+ccl (args (rest (rest (rest (rest (rest (rest args)))))))
#+abcl (args (rest args))
#+ecl (args (rest (rest (rest args))))
;; In CLISP, no loading script in argument(s).
)
(cons *load-truename* args)))
這個函式根據不同的 Common Lisp 實作品,多次使用 rest 函式去除多餘的參數。最後使用 cons 將 Common Lisp 命令稿名稱 (*load-truename* 變數) 和參數接起來後回傳,就可以得到標準化的命令列參數。
請讀者不要死背這個函式。這個函式的操作是根據實測而撰寫的。以下是用來實測的範例程式:
(load "cl-yautils.lisp")
(use-package :cl-yautils)
(defun main ()
; Print out unprocessed argument vector.
(write-line "Unprocessed argument vector:")
(puts (argument-vector))
(write-line "") ; Separator.
; Print out processed argument(s).
(write-line "Processed argument(s) in scripting mode:")
(puts (argument-script))
(finish-output)
(quit-with-status))
(main)
在這個範例中,argument-vector
函式和 argument-script
函式都已經實作好了。這兩個函式的實作如同上文所列。
我們假定該 Common Lisp 程式以腳本模式 (scripting mode) 來使用。以下是假想的指令:
$ interpreter --load args.lisp a b c
實際的指令會根據 Common Lisp 實作品略有差異。
使用 SBCL (Steel Bank CL) 時,處理參數的程式會壓縮成單一參數 sbcl
。所以,只要去除一個參數即可:
> sbcl --noinform --script examples\args.lisp a b c
Unprocessed argument(s):
(sbcl a b c)
Processed argument(s):
(C:/Users/user/Documents/cl-yautils/examples/args.lisp a b c)
使用 Clozure CL 時,處理參數的程式會如實印出。所以,要去除六個參數:
> wx86cl64.exe --load examples\args.lisp -- a b c
Unprocessed argument(s):
(wx86cl64.exe --load examples\args.lisp -- a b c)
Processed argument(s):
(C:/Users/user/Documents/cl-yautils/examples/args.lisp a b c)
在命令列參數中,--
是有意義的。詳見下文。
使用 ECL (Embeddable CL) 時,不需加 --
。所以,會有三個額外的參數:
> ecl -shell examples\args.lisp a b c
;;; Loading "C:/Users/user/Documents/cl-yautils/cl-yautils.lisp"
Unprocessed argument(s):
(ecl -shell examples\args.lisp a b c)
Processed argument(s):
(C:/Users/user/Documents/cl-yautils/examples/args.lisp a b c)
使用 CLISP 時,不需在參數加 --
。此外,CLISP 會自動吞吃掉多餘的參數,在我們的函式中就不需處理了:
> clisp examples\args.lisp a b c
Unprocessed argument(s):
(a b c)
Processed argument(s):
(C:\Users\user\Documents\cl-yautils\examples\args.lisp a b c)
使用 ABCL (Armed Bear CL) 時,需要在參數加 --
。但 ABCL 會自動吞吃掉多餘的參數,包括 --
:
> abclrun.bat examples\args.lisp -- a b c
Unprocessed argument(s):
(a b c)
Processed argument(s):
(C:/Users/user/Documents/cl-yautils/examples/args.lisp a b c)
在本節中,我們展示了常見的五種 Common Lisp 實作品處理參數的方式。讀者可藉此了解不同實作品在處理參數上的差異。在實作函式時,就不需死背函式的指令,可以用合理的方式撰寫函式。
若把 Common Lisp 命令稿編譯成執行檔,處理命令列參數的方式會改變。所以,我們保留原始的命令列參數列表,以因應不同情境的變化。
細心的讀者會發現,有時候我們得在參數中加 --
,有時候則不需要。這牽涉到 Common Lisp 實作品的行為。詳見下一節。
是誰在處理命令列參數?
當 Common Lisp 原始碼當成命令稿來呼叫時,實際上會有兩隻程式來處理參數。一個是 Common Lisp 實作品本身,另一個則是 Common Lisp 命令稿。由於 Common Lisp 實作品位於指令的第一個位置,命令列參數處理的方式是由 Common Lisp 實作品來決定。
以 SBCL 來說,當呼叫 Common Lisp 命令稿後,後續的參數會自動轉由該命令稿執行。所以,不需要加 --
:
> sbcl --noinform --load examples\args.lisp a b c
然而,使用 Clozure CL 呼叫 Common Lisp 命令稿時,一律由 Clozure CL 來處理參數。為了要把參數導給 Common Lisp 命令稿,得用 --
做為分隔線:
> wx86cl64.exe --load examples\args.lisp -- a b c
對於撰寫 Common Lisp 命令稿的程式設計者來說,會希望命令列參數由該命令稿來處理,而非由 Common Lisp 實作品來處理。但對該 Common Lisp 命令稿的使用者來說,加入 --
不是自然的使用方式。此外,這樣的指令太長了,不容易記憶。
著眼於這個議題,可以用命令列腳本 (command-line script) 做為 wrapper 來封裝呼叫指令的方式。Common Lisp 命令稿的使用者就不需記憶參數的特殊使用方式。由於 Unix 和 Windows 的命令列腳本語言相異。我們會分別對這兩種腳本語言撰寫 wrapper。
Unix 平台適用的 POSIX Shell 腳本
在 Unix (註) 上,最常見的 shell 是 Bash。但 POSIX shell 的可攜性較好,且 Bash 向後相容 POSIX shell。此外,有些 BSD 或商用 Unix 預設不一定會用 Bash,但至少會有 POSIX shell。所以,我們仍然使用 POSIX shell 來寫 wrapper。
(註) 包括 macOS、GNU/Linux、BSD、商用 Unix 等。
以下是此腳本的使用範例:
$ args sbcl a b c
以下是該 wrapper 的參考實作:
#!/bin/sh
usage () {
local stream=$1;
if [ -z "$stream" ];
then
stream="1";
fi
echo "Usage: $0 [lisp] [param] ..." >&"$stream";
echo "" >&"$stream";
echo "Valid Common Lisp implementation:" >&"$stream";
echo " sbcl" >&"$stream";
echo " ccl" >&"$stream";
echo " clisp" >&"$stream";
echo " ecl" >&"$stream";
echo " abcl" >&"$stream";
}
# Constants
SBCL="sbcl"
CCL="ccl"
CLISP="clisp"
ECL="ecl"
ABCL="abcl"
lisp=$1;
if [ "-h" = "$lisp" ] || [ "--help" = "$lisp" ];
then
echo "Use \`$0 usage\` or \`$0 help\` to show help info";
exit;
fi
if [ "usage" = "$lisp" ] || [ "help" = "$lisp" ];
then
usage;
exit;
fi
case $lisp in
"$SBCL")
shift;
;;
"$CCL")
shift;
;;
"$CLISP")
shift;
;;
"$ECL")
shift;
;;
"$ABCL")
shift;
;;
*)
echo "Unsupported Common Lisp implementation" >&2;
usage 2;
exit 1;
esac
# Locate the path of the script itself.
root=$(dirname "$0");
if [ "$SBCL" = "$lisp" ];
then
# Check whether SBCL and its wrapper exist.
if ! command -v sbclrun 2>/dev/null 1>&2;
then
echo "No SBCL or its wrapper on the system" >&2;
exit 1;
fi
# Invoke SBCL wrapper.
sbclrun "$root/args.lisp" "$@";
elif [ "$CCL" = "$lisp" ];
then
# Check whether Clozure CL and its wrapper exist.
if ! command -v ccl 2>/dev/null 1>&2;
then
echo "No Clozure CL or its wrapper on the system" >&2;
exit 1;
fi
# Invoke Clozure CL wrapper.
cclrun "$root/args.lisp" -- "$@";
elif [ "$CLISP" = "$lisp" ];
then
# Check whether CLISP exists.
if ! command -v clisp --version 2>/dev/null 1>&2;
then
echo "No CLISP on the system" >&2;
exit 1;
fi
# Invoke CLISP directly.
clisp "$root/args.lisp" "$@";
elif [ "$ECL" = "$lisp" ];
then
# Check whether ECL exists.
if ! command -v ecl --help 2>/dev/null 1>&2;
then
echo "No ECL on the system" >&2;
exit 1;
fi
# Invoke ECL directly.
if [ "Darwin" = $(uname) ];
then
ecl --shell "$root/args.lisp" "$@";
else
ecl -shell "$root/args.lisp" "$@";
fi
elif [ "$ABCL" = "$lisp" ];
then
# Check whether ABCL and its wrapper exist.
if ! command -v abcl 2>/dev/null 1>&2;
then
echo "No ABCL or its wrapper on the system" >&2;
exit 1;
fi
# Invoke ABCL wrapper.
abclrun "$root/args.lisp" -- "$@";
fi
一開始是簡易的說明文件。我們希望腳本使用者可在不閱讀腳本原始碼的前提下就會用該腳本。
在呼叫特定 Common Lisp 實作品前,要先以 POSIX shell 腳本簡單地處理一下命令列參數,篩出 lisp
旗標,才能知道要呼叫那一個 Common Lisp 實作品。
由於第一個參數已經被 wrapper 用掉了,所以要用 shift
吞吃掉該參數。剩下的參數則會原封不動地傳到 Common Lisp 命令稿上。Wrapper 只是過水的小程式,實際上參數還是要帶給 Common Lisp 命令稿。
可以注意一下我們會針對特定的 Common Lisp 實作品加上 --
來分隔參數。由於此 wrapper 已經封裝呼叫 Common Lisp 命令稿的指令,使用者在使用 wrapper 時不需要針對不同 Common Lisp 實作品使用不同的參數。
Windows 平台適用的 Batch 腳本
Windows 原生的腳本語言有四種 (註) 。在這四者之中,Batch 腳本是最古老的,相容性也最好。能夠用 Batch 腳本完成的任務,就不要用新的腳本語言來寫,以得到最佳的相容性。
(註) 有 Batch、VBScript、JScript、PowerShell。
使用此腳本的範例如下:
> args.bat sbcl a b c
此 wrapper 的參考實作如下:
@echo off
rem Constants
set sbcl="sbcl"
set ccl="ccl"
set clisp="clisp"
set ecl="ecl"
set abcl="abcl"
rem Get the name of the script itself.
set self=%~n0%~x0
rem Get the root path of current batch script.
set rootdir=%~dp0
rem Get first argument.
set lisp=%1
if "" == "%lisp%" goto hint
if "/?" == "%lisp%" goto hint
if "usage" == "%lisp%" goto usage
if "help" == "%lisp%" goto usage
if %sbcl% == "%lisp%" goto runlisp
if %ccl% == "%lisp%" goto runlisp
if %clisp% == "%lisp%" goto runlisp
if %ecl% == "%lisp%" goto runlisp
if %abcl% == "%lisp%" goto runlisp
echo Unsupported Common Lisp implementation
exit /B 1
:hint
echo Use `%self% usage` or `%self% help` to show help info
exit /B 0
:usage
echo Usage: %self% [lisp] [param] ...
echo.
echo Valid Common Lisp implementation:
echo sbcl
echo ccl
echo clisp
echo ecl
echo abcl
exit /B 0
:runlisp
rem Consume first argument.
shift
rem Collect remaining argument(s).
set args=
:collect_args
set arg=%1
shift
if "" neq "%arg%" set args=%args% %arg% && goto collect_args
rem Run specific Common Lisp implementation.
if %sbcl% == "%lisp%" goto runsbcl
if %ccl% == "%lisp%" goto runccl
if %clisp% == "%lisp%" goto runclisp
if %ecl% == "%lisp%" goto runecl
if %abcl% == "%lisp%" goto runabcl
rem Fallback as some error message.
echo Unknown Common Lisp implementation
exit /B 1
:runsbcl
sbclrun %rootdir%args.lisp %args%
exit /B 0
:runccl
cclrun %rootdir%args.lisp -- %args%
exit /B 0
:runclisp
clisp %rootdir%args.lisp %args%
exit /B 0
:runecl
ecl -shell %rootdir%args.lisp %args%
exit /B 0
:runabcl
abclrun %rootdir%args.lisp -- %args%
exit /B 0
一開始是此腳本的簡易說明文件。我們希望腳本使用者可在不看腳本原始碼的前提下就會用這個腳本。
此 wrapper 使用變數 lisp
做為旗標。再根據該旗標的設定來呼叫相對應的 Common Lisp 實作品。由於 Common Lisp 實作品本身是跨平台軟體,在 Unix 和 Windows 上使用相同的參數,所以呼叫的方式是雷同的。
由於 Batch 的語法所帶來的限制,我們在程式中使用較多的 goto
。在一般性的程式設計中,不會常使用 goto
。但 Batch 為了相容性因素,不會改語法。我們只能把程式碼儘量寫得結構化一點。
本腳本的中段使用 goto
模擬迴圈以收集剩餘的命令列參數。網路上比較少見這種用法,有興趣的讀者可以看一下。
繼續處理命令列參數
在收集到標準化的命令列參數後,只能算是完成前半段的任務。後半段的任務就是拆解命令列參數,根據程式使用者輸入的參數來決定程式實際的行為。
拆解命令列參數算是不大不小的任務,其實不一定要使用函式庫,除非是碰到比較複雜的情境。
Common Lisp 的命令列參數的資料型態是列表,可以在 loop
迴圈中利用 pop 巨集逐一吞吃參數,然後再寫 cond
或其他選擇控制結構來控制程式實際的行為。以下是 Common Lisp 虛擬碼:
(loop
(let ((arg (pop args)))
#|Use `arg` here.|#))
由於每個程式所需的參數相異,此處不逐一解說。
附記
本文所用的範例程式和 wrapper 存放在 cl-portable 專案中。