SBCL编译可执行文件中read-line为何读取为符号而非字符串?
我尝试用Common Lisp过滤bash的locate命令输出,仅保留目录,为此创建了dirfilter.lisp脚本,代码如下:
#!/usr/bin/env -S sbcl --script (let (line) (loop while (setq line (read-line)) when (not (pathname-name (probe-file line))) do (princ line) (terpri)))
使用locate -i hello | ./dirfilter.lisp运行时一切正常。但移除shebang行后,通过SBCL REPL执行以下代码编译为独立可执行文件:
(load (compile-file "dirfilter.lisp")) (save-lisp-and-die "dirfilter" :executable t)
运行locate -i hello | ./dirfilter时出现错误:
debugger invoked on a UNBOUND-VARIABLE @52BBE314 in thread
#<THREAD tid=203029 "main thread" RUNNING {1001448003}>:
The variable /HOME/DALANICOLAI/HELLO.PNG is unbound.
我判断该错误是因为可执行文件中read-line未将输入读取为字符串而是符号,请问这是什么原因,该如何修复?
原因与修复方法
错误原因
当用save-lisp-and-die生成独立可执行文件时,SBCL默认会进入交互REPL模式,此时它会将标准输入的内容当作Lisp代码解析,而非纯文本字符串。像/HOME/DALANICOLAI/HELLO.PNG这种以斜杠开头的路径,会被Lisp读取器解析为符号,而非字符串,自然会触发"未绑定变量"的错误——因为程序试图引用这个不存在的符号,而不是处理路径字符串。
另外,原代码没有指定可执行文件的入口逻辑,导致SBCL启动后默认进入交互读取模式,完全没执行你写的过滤循环。
修复方案
修改代码,明确指定程序入口函数,让可执行文件启动时直接执行过滤逻辑,而非进入交互REPL。
修正后的代码
(defun main () (let (line) (handler-case (loop while (setq line (read-line *standard-input* nil nil)) when (not (pathname-name (probe-file line))) do (princ line) (terpri)) (end-of-file () nil)))) (save-lisp-and-die "dirfilter" :executable t :toplevel #'main)
修改说明
- 封装入口函数:把过滤逻辑放到
main函数中,清晰定义程序执行入口 - 指定顶层函数:通过
:toplevel #'main参数告诉SBCL,可执行文件启动时直接调用main函数 - 处理文件结束:添加
handler-case捕获end-of-file异常,避免输入流结束时抛出错误 - 明确输入流:指定
*standard-input*作为read-line的输入源,消除歧义
编译与运行
- 移除原代码中的shebang行,将上述代码保存为
dirfilter.lisp - 在SBCL REPL中加载代码并执行编译:
(load "dirfilter.lisp") - 生成的
dirfilter可执行文件即可正常处理管道输入:locate -i hello | ./dirfilter
兼容脚本与编译模式的版本
如果需要同时支持shebang脚本运行和编译为可执行文件,可以用条件编译:
#!/usr/bin/env -S sbcl --script (defun main () (let (line) (handler-case (loop while (setq line (read-line *standard-input* nil nil)) when (not (pathname-name (probe-file line))) do (princ line) (terpri)) (end-of-file () nil)))) #-script (save-lisp-and-die "dirfilter" :executable t :toplevel #'main) #+script (main)
- 用shebang运行时(
./dirfilter.lisp),#+script分支触发,直接执行main - 编译时,
#-script分支触发,生成指定入口的可执行文件
内容的提问来源于stack exchange,提问作者dalanicolai

