ANSI Common Lisp 标准中没有涉及到此话题。(值得注意的是,ANSI Common Lisp 标准制定时,Lisp Machines 属于热门话题。在当时的情况下,Lisp 就是操作系统)。所以,基本上以下提到的相关函数和宏都依赖于运行的操作系统和具体实现。
当然咯,也有常见的或是可以用 quicklisp 安装的一些 Common Lisp 库。其中有:
- ASDF3:几乎包含了所有的功能,包括 Utilities for Implementation- and OS- Portability (UIOP).
- osicat
- unix-opts:命令行参数解析库,与 Python 的
argparse
相似。
获取环境变量
UIOP 中有个与众不同的函数,这个函数可以获取 Unix/Linux 的环境变量。
* (uiop:getenv "HOME")
"/home/edi"
以下是 getenv
的实现:
* (defun my-getenv (name &optional default)
"Obtains the current value of the POSIX environment variable NAME."
(declare (type (or string symbol) name))
(let ((name (string name)))
(or #+abcl (ext:getenv name)
#+ccl (ccl:getenv name)
#+clisp (ext:getenv name)
#+cmu (unix:unix-getenv name) ; since CMUCL 20b
#+ecl (si:getenv name)
#+gcl (si:getenv name)
#+mkcl (mkcl:getenv name)
#+sbcl (sb-ext:posix-getenv name)
default)))
MY-GETENV
* (my-getenv "HOME")
"/home/edi"
* (my-getenv "HOM")
NIL
* (my-getenv "HOM" "huh?")
"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*
变量中:
$ sbcl my-command-line-arg
….
* sb-ext:*posix-argv*
("sbcl" "my-command-line-arg")
*
详细的使用方法参见:SBCL 手册。
其中 LispWorks 与之类似的是 system:*line-arguments-list*
变量。
* system:*line-arguments-list*
("/Users/cbrown/Projects/lisptty/tty-lispworks" "-init" "/Users/cbrown/Desktop/lisp/lispworks-init.lisp")
以下是返回参数列表函数的多个实现。
(defun my-command-line ()
(or
#+SBCL *posix-argv*
#+LISPWORKS system:*line-arguments-list*
#+CMU extensions:*command-line-words*
nil))
现在,就可以很方便的访问参数并通过表定义来解析这些参数了。
解析命令行参数
快速的浏览下 Awesome CL list#scripting
章节,然后就是 unix-opts 的使用介绍了。
(ql:quickload "unix-opts")
可以将 unix-opts
库定义别名为 opts
。
(rename-package :unix-opts :unix-opts '(opts))
首先,使用 opts:define-opts
来定义参数:
(opts:define-opts
(:name :help
:description "print this help text"
:short #\h
:long "help")
(:name :level
:description "The level of something (integer)."
:short #\l
:long "level"
:arg-parser #'parse-integer))
以上参数的定义都是要有意义(self-explanatory)的。其中 #'parse-integer
是 CL 的内建函数。
现在就可以调用 opts:get-opts
来解析获取这些参数了, get-opts
返回两个值:
第一个返回值是包含有效的参数及其绑定值的列表(即被 define-opts
定义过了的参数),第二个返回值是其他的自由参数(未被定义和绑定的)。可以通过 multiple-value-bind
来获取返回值。
(multiple-value-bind (options free-args)
;; There is no error handling yet (specially for options not having their argument).
(opts:get-opts)
(multiple-value-bind (options free-args)
(opts:get-opts '("hello" "-h" "-l" "1"))
(format t "Options: ~a~&" options)
(format t "free args: ~a~&" free-args))
Options: (HELP T LEVEL 1)
free args: (hello)
NIL
当输入为定义的参数时,会得到异常,进入 debugger。下面将介绍 unix-opts 的文档和处理错误参数及其他的异常。
用 getf
来访问 options
值重的参数,然后通过 opts:exit
来退出。
(multiple-value-bind (options free-args)
;; No error handling.
(opts:get-opts)
(if (getf options :help)
(progn
(opts:describe
:prefix "My app. Usage:"
:args "[keywords]")
(exit))) ;; <= exit takes an optional return status.
...
到现在为止,必要的只是已经讲解完成。详细的代码示例可以在其文档中找到,文档中也介绍了一些在终端中很酷的包(标准的ansi 颜色,打印表格和进度条,读取参数的接口等)。
运行其他程序
接下来要讲的是 uiop 库。
同步
[uiop:run-program](https://common-lisp.net/project/asdf/uiop.html#UIOP_002fRUN_002dPROGRAM)
有两种使用方法:一种是接受需要执行程序的程序名,另一种是在程序名后添加程序的参数。
(uiop:run-program "firefox")
或
(uiop:run-program (list "firefox" "http:url"))
上面的代码会运行制定的程序(操作系统上可以已经安装了的并能通过对应的命令调用),当程序执行完成后,返回程序执行的结果。
:output t
将打印标准输出。
run-program
有以下可选的参数:
run-program (command &rest keys &key
ignore-error-status
(force-shell nil force-shell-suppliedp)
input
(if-input-does-not-exist :error)
output
(if-output-exists :supersede)
error-output
(if-error-output-exists :supersede)
(element-type #-clozure *default-stream-element-type* #+clozure 'character)
(external-format *utf-8-external-format*)
&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。 当 output
为 t
时,输出是当前的 *standard-output*
否则,output
将会是个值,这个值是适应的 slurp-input-stream
函数的第一个参数。
或者是键值对的列表。在这种情况下, run-program
会为程序输出创建个临时的输出流。而这个临时的输出流会被 slurp-input-stream
处理,slurp-input-stream
将 output
( 或是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-output
与 output
相似,唯一不同的是产生的结果是 run-program
返回的第二个值。t
值代表 *error-output*
。同时 :output
表示将错误重定向的输出流,并返回 nil
。
if-error-output-exists
与 if-output-exist
类似,只不过它取决于 error-output
而不是 output
。
input
与 output
类似,它使用的是 vomit-output-stream
,无返回值,t
表示 *standard-input*
。
if-input-does-not-exist
与 if-output-exists
类似,只在 input
的值为字符串或路径时起作用,接受的值为 :create
、:error
(默认值)。
当 element-type
、 external-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)
定义原型如下:
launch-program (command &rest keys
&key
input
(if-input-does-not-exist :error)
output
(if-output-exists :supersede)
error-output
(if-error-output-exists :supersede)
(element-type #-clozure *default-stream-element-type*
#+clozure 'character)
(external-format *utf-8-external-format*)
directory
#+allegro separate-streams
&allow-other-keys)
运行的程序输出也是通过设置 output
关键词参数:
- 当
output
值为路径名、路径名的字符串或者nil
(默认值)指向空设备,程序的输出会写入到该指定路径的文件中。 - 当参数是
:interactive
时,输出是继承当前的进程的输出;注意这个输出和*standard-output*
可能会有差别。在slime
中,输出是*inferior-lis*
这个 buffer。 - 当
output
为t
时,输出是当前的*standard-output*
。 - 当其值为
:stream
, 会创建一个新的流,可以通过process-info-output
访问和读取 - 以上都不是的话,
output
将是个能够被该 Lisp 解释器处理的值。
if-output-exists
参数只有在 output
是一个字符串或路径时才生效,接受三个值: :error
、:append
和:supersede
(默认)。这些参数值只在 output
存在的情况下才会生效,和 open
函数中的 if-exists
参数与 :direction
:output
的情况类似。
error-output
与 output
相似。t
值代表 *error-output*
。同时 :output
表示将错误重定向的输出流,:stream
表示由 process-info-error-output
产生一个流。
launch-program
返回值是 process-info
的对象,其结构如下(来源):
(defclass process-info ()
(
;; The advantage of dealing with streams instead of PID is the
;; availability of functions like `sys:pipe-kill-process`.
(process :initform nil)
(input-stream :initform nil)
(output-stream :initform nil)
(bidir-stream :initform nil)
(error-output-stream :initform nil)
;; For backward-compatibility, to maintain the property (zerop
;; exit-code) <-> success, an exit in response to a signal is
;; encoded as 128+signum.
(exit-code :initform nil)
;; If the platform allows it, distinguish exiting with a code
;; >128 from exiting in response to a signal by setting this code
(signal-code :initform nil)))
参见docstrings.
检验子进程是否存活
由于是异步运行,所以需要对子进程时候运行结束进行判断,uiop:process-alive-p
就是用来检验子进程是否存活,其参数类型是由 launch-program
返回的 process-info
对象。
* (defparameter *shell* (uiop:launch-program "bash" :input :stream :output :stream))
;; inferior shell process now running
* (uiop:process-alive-p *shell*)
T
;; Close input and output streams
* (uiop:close-streams *shell*)
* (uiop:process-alive-p *shell*)
NIL
获取返回码
uiop:wait-process
的作用是当进程结束后,会立即返回其退出码,如果进程还在运行,将会进行等待知道进程结束。类似与阻塞。
(uiop:process-alive-p *process*)
NIL
(uiop:wait-process *process*)
0
0 表示正常退出(可以使用 zerop
进行验证)。
退出码同时也存储在 process-info
对象中的 exit-code
属性中。在上面 process-info
类的定义中,没有访问方法,所以可以使用 slot-value
来获取。其默认值为 nil
,所以不用去检查这个属性是否绑定了值。
(slot-value *my-process* 'uiop/launch-program::exit-code)
0
这里有个小技巧,就是必须使用 wait-process
,否则结果将会是 nil
。
基于 wait-process
是阻塞的,可以创建个新线程:
(bt:make-thread
(lambda ()
(let ((exit-code (uiop:wait-process
(uiop:launch-program (list "of" "commands"))))
(if (zerop exit-code)
(print :success)
(print :failure)))))
:name "Waiting for <program>")
注意,run-program
的第三个返回值才是程序的退出码。
子进程的输入/输出
当 input
关键字设为 :stream
时,将会创建一个和文件一样可被写入的流,这个流可通过 uiop:process-info-input
进行访问。
;; Start the inferior shell, with input and output streams
* (defparameter *shell* (uiop:launch-program "bash" :input :stream :output :stream))
;; Write a line to the shell
* (write-line "find . -name '*.md'" (uiop:process-info-input *shell*))
;; Flush stream
* (force-output (uiop:process-info-input *shell*))
其中 write-line 会将字符串写入给定的流中,同时在最后添加新行。
force-output 会尝试去刷新流,但不会等流完全完成。
读取输出流也一样,可以调用 uiop:process-info-output
,其返回一个输入流
* (read-line (uiop:process-info-output *shell*))
在某些情况下,需要读取的数据的大小是已知的,或者说由分界符。如果不是这种情况的话,调用 read-line 会一直停在那里等待数据。为了避免这种情况,可以使用 listen 来检测是否有字符可读取:
* (let ((stream (uiop:process-info-output *shell*)))
(loop while (listen stream) do
;; Characters are immediately available
(princ (read-line stream))
(terpri)))
同时,也有 read-char-no-hang 读取单个字符,当没有字符时返回 nil
。请注意,由于缓冲区和其他进程执行的时间等问题,不能保证在 listen
或 read-char-no-hang
返回 nil
之前收到所有发送的数据。
管道
下面的例子等价于在 Shell 中执行 ls | sort
。注意 “ls” 是通过 launch-program
(异步)的方式调用,同属将输到一个流,管道的最后一个命令 “sort” 是由 run-program
调用,输出到一个字符串.
(uiop:run-program "sort"
:input
(uiop:process-info-output
(uiop:launch-program "ls"
:output :stream))
: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)
。
(defparameter *sigchld* 0)
(defparameter *compile-files-debug* 2)
(defun sigchld-handler (p1 p2 p3)
(when (> 0 *compile-files-debug*)
(print (list "returned" p1 p2 p3))
(force-output))
(decf *sigchld*))
(defun compile-files (files &key (load nil))
(setq *sigchld* 0)
(system:enable-interrupt unix:sigchld #'sigchld-handler)
(do ((f files (cdr f)))
((not f))
(format t "~&process ~d diving for ~a" (unix:unix-getpid)
`(compile-file ,(car f)))
(force-output)
(let ((pid (unix:unix-fork)))
(if (/= 0 pid)
;; parent
(incf *sigchld*)
;; child
(progn
(compile-file (car f) :verbose nil :print nil)
(unix:unix-exit 0)))))
(do () ((= 0 *sigchld*))
(sleep 1)
(when (> 0 *compile-files-debug*)
(format t "~&process ~d still waiting for ~d childs"
(unix:unix-getpid) *sigchld*)))
(when (> 0 *compile-files-debug*)
(format t "~&finished"))
(when load
(do ((f files (cdr f)))
((not f))
(load (compile-file-pathname (car f))))))