ANSI Common Lisp 标准中没有涉及到此话题。(值得注意的是,ANSI Common Lisp 标准制定时,Lisp Machines 属于热门话题。在当时的情况下,Lisp 就是操作系统)。所以,基本上以下提到的相关函数和宏都依赖于运行的操作系统和具体实现。
当然咯,也有常见的或是可以用 quicklisp 安装的一些 Common Lisp 库。其中有:

获取环境变量

UIOP 中有个与众不同的函数,这个函数可以获取 Unix/Linux 的环境变量。

  1. * (uiop:getenv "HOME")
  2. "/home/edi"

以下是 getenv 的实现:

  1. * (defun my-getenv (name &optional default)
  2. "Obtains the current value of the POSIX environment variable NAME."
  3. (declare (type (or string symbol) name))
  4. (let ((name (string name)))
  5. (or #+abcl (ext:getenv name)
  6. #+ccl (ccl:getenv name)
  7. #+clisp (ext:getenv name)
  8. #+cmu (unix:unix-getenv name) ; since CMUCL 20b
  9. #+ecl (si:getenv name)
  10. #+gcl (si:getenv name)
  11. #+mkcl (mkcl:getenv name)
  12. #+sbcl (sb-ext:posix-getenv name)
  13. default)))
  14. MY-GETENV
  15. * (my-getenv "HOME")
  16. "/home/edi"
  17. * (my-getenv "HOM")
  18. NIL
  19. * (my-getenv "HOM" "huh?")
  20. "huh?"

值得注意的是,上面的一些实现中也能设置环境变量。其中有 ECL(si:setenv)、AllegroCL、LispWorks 和 CLISP。在这些解释器中,可以将 [setf](http://www.lispworks.com/documentation/HyperSpec/Body/m_setf_.htm)getenv 函数配合使用。这个特性在 Lisp 环境中启动个子进程时很有用。

同时,Osicat
库中有个 (environment-variable "name") 的方法,可以在类POSIX系统上运行,其中包括Windows。当然,这个方法也可以和 setf 配合使用。

访问命令行参数

基础

访问命令行的参数依赖于具体实现,但大部分实现都有这个功能。 Roswell 和其他的库让访问命令行参数更加方便。

SBCL 将参数列表保存在 sb-ext:*posix-argv* 变量中:

  1. $ sbcl my-command-line-arg

….

  1. * sb-ext:*posix-argv*
  2. ("sbcl" "my-command-line-arg")
  3. *

详细的使用方法参见:SBCL 手册
其中 LispWorks 与之类似的是 system:*line-arguments-list* 变量。

  1. * system:*line-arguments-list*
  2. ("/Users/cbrown/Projects/lisptty/tty-lispworks" "-init" "/Users/cbrown/Desktop/lisp/lispworks-init.lisp")

CMUCL 有个有趣的拓展用来 操作参数

以下是返回参数列表函数的多个实现。

  1. (defun my-command-line ()
  2. (or
  3. #+SBCL *posix-argv*
  4. #+LISPWORKS system:*line-arguments-list*
  5. #+CMU extensions:*command-line-words*
  6. nil))

现在,就可以很方便的访问参数并通过表定义来解析这些参数了。

解析命令行参数

快速的浏览下 Awesome CL list#scripting
章节,然后就是 unix-opts 的使用介绍了。

  1. (ql:quickload "unix-opts")

可以将 unix-opts 库定义别名为 opts

  1. (rename-package :unix-opts :unix-opts '(opts))

首先,使用 opts:define-opts 来定义参数:

  1. (opts:define-opts
  2. (:name :help
  3. :description "print this help text"
  4. :short #\h
  5. :long "help")
  6. (:name :level
  7. :description "The level of something (integer)."
  8. :short #\l
  9. :long "level"
  10. :arg-parser #'parse-integer))

以上参数的定义都是要有意义(self-explanatory)的。其中 #'parse-integer 是 CL 的内建函数。

现在就可以调用 opts:get-opts 来解析获取这些参数了, get-opts 返回两个值:
第一个返回值是包含有效的参数及其绑定值的列表(即被 define-opts 定义过了的参数),第二个返回值是其他的自由参数(未被定义和绑定的)。可以通过 multiple-value-bind 来获取返回值。

  1. (multiple-value-bind (options free-args)
  2. ;; There is no error handling yet (specially for options not having their argument).
  3. (opts:get-opts)
  1. (multiple-value-bind (options free-args)
  2. (opts:get-opts '("hello" "-h" "-l" "1"))
  3. (format t "Options: ~a~&" options)
  4. (format t "free args: ~a~&" free-args))
  5. Options: (HELP T LEVEL 1)
  6. free args: (hello)
  7. NIL

当输入为定义的参数时,会得到异常,进入 debugger。下面将介绍 unix-opts 的文档和处理错误参数及其他的异常。

getf 来访问 options 值重的参数,然后通过 opts:exit 来退出。

  1. (multiple-value-bind (options free-args)
  2. ;; No error handling.
  3. (opts:get-opts)
  4. (if (getf options :help)
  5. (progn
  6. (opts:describe
  7. :prefix "My app. Usage:"
  8. :args "[keywords]")
  9. (exit))) ;; <= exit takes an optional return status.
  10. ...

到现在为止,必要的只是已经讲解完成。详细的代码示例可以在其文档中找到,文档中也介绍了一些在终端中很酷的包(标准的ansi 颜色,打印表格和进度条,读取参数的接口等)。

运行其他程序

接下来要讲的是 uiop 库。

同步

[uiop:run-program](https://common-lisp.net/project/asdf/uiop.html#UIOP_002fRUN_002dPROGRAM) 有两种使用方法:一种是接受需要执行程序的程序名,另一种是在程序名后添加程序的参数。

  1. (uiop:run-program "firefox")

  1. (uiop:run-program (list "firefox" "http:url"))

上面的代码会运行制定的程序(操作系统上可以已经安装了的并能通过对应的命令调用),当程序执行完成后,返回程序执行的结果。

:output t 将打印标准输出。

run-program 有以下可选的参数:

  1. run-program (command &rest keys &key
  2. ignore-error-status
  3. (force-shell nil force-shell-suppliedp)
  4. input
  5. (if-input-does-not-exist :error)
  6. output
  7. (if-output-exists :supersede)
  8. error-output
  9. (if-error-output-exists :supersede)
  10. (element-type #-clozure *default-stream-element-type* #+clozure 'character)
  11. (external-format *utf-8-external-format*)
  12. &allow-other-keys)

force-shell 参数设为 t 时,lisp 总会调用一个 shell 来执行程序,而不是直接执行程序。同样的,当force-shell 设为 nil 时将永不调用 shell。

除非 ignore-error-status 指定了,否则当程序执行失败时会抛出 subprocess-error 的异常。

output 值为路径名、路径名的字符串或者 nil(默认值)指向空设备,程序的输出会写入到该指定路径的文件中。当 output 的值为 :interactive 时,输出是继承当前的进程的输出;注意这个输出和 *standard-output* 可能会有差别。在 slime 中,输出是 *inferior-lis* 这个 buffer。 当 outputt 时,输出是当前的 *standard-output* 否则,output 将会是个值,这个值是适应的 slurp-input-stream 函数的第一个参数。
或者是键值对的列表。在这种情况下, run-program 会为程序输出创建个临时的输出流。而这个临时的输出流会被 slurp-input-stream 处理,slurp-input-streamoutput( 或是output 的第一个元素,其他的元素作为关键字参数处理)作为第一个参数。调用 slurp-input-stream 产生的主值(如果不被调用则为 nil) 将是 run-program 返回的第一个值。
例如,:output :string 将以字符串的形式返回整个输出流。
:output '(:string :stripped t) 将返回去去除了换行符的字符串。

if-output-exists 参数只有在 output 是一个字符串或路径时才生效,接受三个值: :error:append:supersede(默认)。这些参数值只在 output 存在的情况下才会生效,和 open 函数中的 if-exists 参数与 :direction :output的情况类似。

error-outputoutput 相似,唯一不同的是产生的结果是 run-program 返回的第二个值。t 值代表 *error-output*。同时 :output 表示将错误重定向的输出流,并返回 nil

if-error-output-existsif-output-exist类似,只不过它取决于 error-output 而不是 output

inputoutput 类似,它使用的是 vomit-output-stream,无返回值,t 表示 *standard-input*

if-input-does-not-existif-output-exists 类似,只在 input 的值为字符串或路径时起作用,接受的值为 :create:error(默认值)。

element-typeexternal-format 适用时,将传递到解释器中,然后创建输出流。

在子进程并行运行时,有且仅有一个流会出现 slurping 或 vomiting,这取决于选项和实现,并且优先考虑输出处理。其他流在创建子进程之前或之后通过临时文件处理或者说是消耗掉。

run-program 返回 3 个值:

  • output 的处理后的结果或是 nil
  • error-output处理后的结果或是 nil
  • 程序的返回码(exit-code),0 表示正常退出, 其他的错误码需参考其 exit-code 中的定义。

异步

[uiop:launch-program](https://common-lisp.net/project/asdf/uiop.html#UIOP_002fLAUNCH_002dPROGRAM) 定义原型如下:

  1. launch-program (command &rest keys
  2. &key
  3. input
  4. (if-input-does-not-exist :error)
  5. output
  6. (if-output-exists :supersede)
  7. error-output
  8. (if-error-output-exists :supersede)
  9. (element-type #-clozure *default-stream-element-type*
  10. #+clozure 'character)
  11. (external-format *utf-8-external-format*)
  12. directory
  13. #+allegro separate-streams
  14. &allow-other-keys)

运行的程序输出也是通过设置 output 关键词参数:

  • output 值为路径名、路径名的字符串或者 nil(默认值)指向空设备,程序的输出会写入到该指定路径的文件中。
  • 当参数是 :interactive 时,输出是继承当前的进程的输出;注意这个输出和 *standard-output* 可能会有差别。在 slime 中,输出是 *inferior-lis* 这个 buffer。
  • outputt 时,输出是当前的 *standard-output*
  • 当其值为 :stream, 会创建一个新的流,可以通过 process-info-output 访问和读取
  • 以上都不是的话, output 将是个能够被该 Lisp 解释器处理的值。

if-output-exists 参数只有在 output 是一个字符串或路径时才生效,接受三个值: :error:append:supersede(默认)。这些参数值只在 output 存在的情况下才会生效,和 open 函数中的 if-exists 参数与 :direction :output的情况类似。

error-outputoutput 相似。t 值代表 *error-output*。同时 :output 表示将错误重定向的输出流,:stream 表示由 process-info-error-output 产生一个流。

launch-program 返回值是 process-info 的对象,其结构如下(来源):

  1. (defclass process-info ()
  2. (
  3. ;; The advantage of dealing with streams instead of PID is the
  4. ;; availability of functions like `sys:pipe-kill-process`.
  5. (process :initform nil)
  6. (input-stream :initform nil)
  7. (output-stream :initform nil)
  8. (bidir-stream :initform nil)
  9. (error-output-stream :initform nil)
  10. ;; For backward-compatibility, to maintain the property (zerop
  11. ;; exit-code) <-> success, an exit in response to a signal is
  12. ;; encoded as 128+signum.
  13. (exit-code :initform nil)
  14. ;; If the platform allows it, distinguish exiting with a code
  15. ;; >128 from exiting in response to a signal by setting this code
  16. (signal-code :initform nil)))

参见docstrings.

检验子进程是否存活

由于是异步运行,所以需要对子进程时候运行结束进行判断,uiop:process-alive-p 就是用来检验子进程是否存活,其参数类型是由 launch-program 返回的 process-info 对象。

  1. * (defparameter *shell* (uiop:launch-program "bash" :input :stream :output :stream))
  2. ;; inferior shell process now running
  3. * (uiop:process-alive-p *shell*)
  4. T
  5. ;; Close input and output streams
  6. * (uiop:close-streams *shell*)
  7. * (uiop:process-alive-p *shell*)
  8. NIL

获取返回码

uiop:wait-process 的作用是当进程结束后,会立即返回其退出码,如果进程还在运行,将会进行等待知道进程结束。类似与阻塞。

  1. (uiop:process-alive-p *process*)
  2. NIL
  3. (uiop:wait-process *process*)
  4. 0

0 表示正常退出(可以使用 zerop 进行验证)。

退出码同时也存储在 process-info 对象中的 exit-code 属性中。在上面 process-info 类的定义中,没有访问方法,所以可以使用 slot-value 来获取。其默认值为 nil,所以不用去检查这个属性是否绑定了值。

  1. (slot-value *my-process* 'uiop/launch-program::exit-code)
  2. 0

这里有个小技巧,就是必须使用 wait-process,否则结果将会是 nil

基于 wait-process 是阻塞的,可以创建个新线程:

  1. (bt:make-thread
  2. (lambda ()
  3. (let ((exit-code (uiop:wait-process
  4. (uiop:launch-program (list "of" "commands"))))
  5. (if (zerop exit-code)
  6. (print :success)
  7. (print :failure)))))
  8. :name "Waiting for <program>")

注意,run-program 的第三个返回值才是程序的退出码。

子进程的输入/输出

input 关键字设为 :stream 时,将会创建一个和文件一样可被写入的流,这个流可通过 uiop:process-info-input 进行访问。

  1. ;; Start the inferior shell, with input and output streams
  2. * (defparameter *shell* (uiop:launch-program "bash" :input :stream :output :stream))
  3. ;; Write a line to the shell
  4. * (write-line "find . -name '*.md'" (uiop:process-info-input *shell*))
  5. ;; Flush stream
  6. * (force-output (uiop:process-info-input *shell*))

其中 write-line 会将字符串写入给定的流中,同时在最后添加新行。
force-output 会尝试去刷新流,但不会等流完全完成。

读取输出流也一样,可以调用 uiop:process-info-output,其返回一个输入流

  1. * (read-line (uiop:process-info-output *shell*))

在某些情况下,需要读取的数据的大小是已知的,或者说由分界符。如果不是这种情况的话,调用 read-line 会一直停在那里等待数据。为了避免这种情况,可以使用 listen 来检测是否有字符可读取:

  1. * (let ((stream (uiop:process-info-output *shell*)))
  2. (loop while (listen stream) do
  3. ;; Characters are immediately available
  4. (princ (read-line stream))
  5. (terpri)))

同时,也有 read-char-no-hang 读取单个字符,当没有字符时返回 nil 。请注意,由于缓冲区和其他进程执行的时间等问题,不能保证在 listenread-char-no-hang 返回 nil 之前收到所有发送的数据。

管道

下面的例子等价于在 Shell 中执行 ls | sort。注意 “ls” 是通过 launch-program(异步)的方式调用,同属将输到一个流,管道的最后一个命令 “sort” 是由 run-program 调用,输出到一个字符串.

  1. (uiop:run-program "sort"
  2. :input
  3. (uiop:process-info-output
  4. (uiop:launch-program "ls"
  5. :output :stream))
  6. :output :string)

复刻 CMUCL

Martin Cracauer 写了个 CMUCL 的函数,可以并行的编译多个文件。这个函数展示了怎么在 CL 实现 Unix 的系统函数 [fork](http://www.freebsd.org/cgi/man.cgi?query=fork&apropos=0&sektion=0&manpath=FreeBSD+4.5-RELEASE&format=html)

  1. (defparameter *sigchld* 0)
  2. (defparameter *compile-files-debug* 2)
  3. (defun sigchld-handler (p1 p2 p3)
  4. (when (> 0 *compile-files-debug*)
  5. (print (list "returned" p1 p2 p3))
  6. (force-output))
  7. (decf *sigchld*))
  8. (defun compile-files (files &key (load nil))
  9. (setq *sigchld* 0)
  10. (system:enable-interrupt unix:sigchld #'sigchld-handler)
  11. (do ((f files (cdr f)))
  12. ((not f))
  13. (format t "~&process ~d diving for ~a" (unix:unix-getpid)
  14. `(compile-file ,(car f)))
  15. (force-output)
  16. (let ((pid (unix:unix-fork)))
  17. (if (/= 0 pid)
  18. ;; parent
  19. (incf *sigchld*)
  20. ;; child
  21. (progn
  22. (compile-file (car f) :verbose nil :print nil)
  23. (unix:unix-exit 0)))))
  24. (do () ((= 0 *sigchld*))
  25. (sleep 1)
  26. (when (> 0 *compile-files-debug*)
  27. (format t "~&process ~d still waiting for ~d childs"
  28. (unix:unix-getpid) *sigchld*)))
  29. (when (> 0 *compile-files-debug*)
  30. (format t "~&finished"))
  31. (when load
  32. (do ((f files (cdr f)))
  33. ((not f))
  34. (load (compile-file-pathname (car f))))))