美思 [Common Lisp] 程式設計教學:處理命令列參數 (Command Line Arguments)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在撰寫命令列程式時,處理命令列參數是常見的任務。程式設計者在剛學程式設計時,寫的程式也都是命令列程式。所以,處理命令列參數是程式設計者在學習初期就會碰到的任務。

然而,在 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 專案中。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。