處理命令列參數 (Command Line Arguments)

    前言

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

    然而,在 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 args)))))
             #+ecl    (args (rest (rest (rest args))))
             ; In ABCL and 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 cclrun 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 abclrun 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-yautils 專案中。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter Parler