SBCLの出力をパイプして使う場合の注意点

SBCLの出力をパイプして使う場合の注意点。
何も気にせずに書いていると大量のエラー情報が出力されて戸惑うことがある。
例として以下のSBCLスクリプトを用いる。

;;;; ファイル名: loop.lisp
;;;; sbcl-1.0.48
 (loop FOR n FROM 0
       DO
       (format t "loop: ~a~%" n))

実行結果。
上のスクリプトだと、Ctrl-Cを使って停止した場合も、大量のエラー出力がでる。

$ sbcl --script loop.lisp
loop: 0
loop: 1
loop: 2
loop: 3
loop: 4
loop: 5
loop: 6
loop: 7
Ctrl-C   ;; 終了!
unhandled SB-SYS:INTERACTIVE-INTERRUPT in thread #<SB-THREAD:THREAD
                                                   "initial thread" RUNNING
                                                   {1002961281}>:
  Interactive interrupt at #x7FFFF79CA8BE.

0: (SB-DEBUG::MAP-BACKTRACE
    #<CLOSURE (LAMBDA #) {10029F8839}>
    :START
    0
    :COUNT
    128)
1: (BACKTRACE 128 #<SYNONYM-STREAM :SYMBOL SB-SYS:*STDERR* {1000168001}>)
2: (SB-DEBUG::DEBUGGER-DISABLED-HOOK
    #<SB-SYS:INTERACTIVE-INTERRUPT {10029F66A1}>
    #<unavailable argument>)
3: (SB-DEBUG::RUN-HOOK
    *INVOKE-DEBUGGER-HOOK*
    #<SB-SYS:INTERACTIVE-INTERRUPT {10029F66A1}>)
4: (INVOKE-DEBUGGER #<SB-SYS:INTERACTIVE-INTERRUPT {10029F66A1}>)
5: (SB-INT:%BREAK SB-UNIX:SIGINT #<SB-SYS:INTERACTIVE-INTERRUPT {10029F66A1}>)
6: ((FLET SB-UNIX::INTERRUPT-IT))
;; 省略
31: ((FLET SB-FASL::LOAD-STREAM)
     #<SB-SYS:FD-STREAM for "file /tmp/loop.lisp" {10029699D1}>
     NIL)
32: (LOAD
     #<SB-SYS:FD-STREAM for "file /tmp/loop.lisp" {10029699D1}>
     :VERBOSE
     NIL
     :PRINT
     NIL
     :IF-DOES-NOT-EXIST
     T
     :EXTERNAL-FORMAT
     :DEFAULT)
33: (SB-IMPL::PROCESS-SCRIPT "/tmp/loop.lisp")
34: (SB-IMPL::TOPLEVEL-INIT)
35: ((LABELS SB-IMPL::RESTART-LISP))

unhandled condition in --disable-debugger mode, quitting

これは、スクリプトを次のように修正すれば解決する。

;; デバッガフックを設定
(setf sb-ext:*invoke-debugger-hook*  
      (lambda (condition hook) 
        (declare (ignore conditoin hook))
        ;; デバッガが呼ばれたら、単にプログラムを終了する
        (sb-ext:quit)))

(loop FOR n FROM 0
      DO
      (format t "loop: ~a~%" n))

#|
;; 以下の方法でも良い
(handler-case
  (loop FOR n FROM 0
        DO
        (format t "loop: ~a~%" n))
  (condition ()
    (sb-ext:quit)))
|#

実行結果。
エラーは出力されない。

$ sbcl --script loop.lisp
loop: 0
loop: 1
loop: 2
loop: 3
loop: 4
loop: 5
loop: 6
loop: 7
Ctrl-C   ;; 終了!

ただ、これでもまだ問題がある。
エラーが出力される典型的なケースは、出力をパイプでheadコマンドに繋いだ場合。

$ sbcl --script loop.lisp | head -5
loop: 0
loop: 1
loop: 2
loop: 3
loop: 4
Help! 11 nested errors. SB-KERNEL:*MAXIMUM-ERROR-DEPTH* exceeded.
0: (SB-DEBUG::MAP-BACKTRACE #<CLOSURE (LAMBDA (SB-DEBUG::FRAME)) {1002A1BC39}> :START 0 :COUNT 1152921504606846975)
1: (BACKTRACE 1152921504606846975 #<SYNONYM-STREAM :SYMBOL SB-SYS:*TTY* {10001A6831}>)
2: ((LAMBDA NIL))
3: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #<CLOSURE (LAMBDA NIL) {1002A1BBF9}>)
4: (SB-IMPL::ERROR-ERROR "Help! " 11 " nested errors. " "SB-KERNEL:*MAXIMUM-ERROR-DEPTH* exceeded.")
5: (SB-IMPL::INFINITE-ERROR-PROTECTOR)
6: (ERROR SB-INT:SIMPLE-STREAM-ERROR :STREAM #<SB-SYS:FD-STREAM for "standard output" {1002961B11}> :FORMAT-CONTROL "~@<~?: ~2I~_~A~:>" :FORMAT-ARGUMENTS ("Couldn't write to ~s" (#<SB-SYS:FD-STREAM for "standard output" {1002961B11}>) "Broken pipe"))
7: (SB-IMPL::SIMPLE-STREAM-PERROR "Couldn't write to ~s" #<SB-SYS:FD-STREAM for "standard output" {1002961B11}> 32)
;; 略
82: (SB-IMPL::SIMPLE-STREAM-PERROR "Couldn't write to ~s" #<SB-SYS:FD-STREAM for "standard output" {1002961B11}> 32)
83: (SB-IMPL::FLUSH-OUTPUT-BUFFER #<SB-SYS:FD-STREAM for "standard output" {1002961B11}>)
84: (FORCE-OUTPUT #<SYNONYM-STREAM :SYMBOL SB-SYS:*STDOUT* {10001E4BB1}>)
85: (SB-INT:FLUSH-STANDARD-OUTPUT-STREAMS)
86: ((LABELS SB-IMPL::RESTART-LISP))

debugger invoked on a SIMPLE-ERROR in thread #<THREAD "initial thread" RUNNING {1002961281}>: Maximum error nesting depth exceeded

Type HELP for debugger help, or (SB-EXT:QUIT) to exit from SBCL.

(no restarts: If you didn't do this on purpose, please report it as a bug.)

("no debug information for frame")
0] abort  ;; デバッグモードに入ってしまうので abort を入力して抜ける

詳しく調べてはいないので原因は不明だが、headコマンドがsbclからの入力を閉じた後も、そのストリームに(sbclが)出力を試みているために、エラーが生じているように見える。
スクリプトをさらに次のように修正すると、この問題も解決する。

;; デバッガフックを設定
(setf sb-ext:*invoke-debugger-hook*  
      (lambda (condition hook) 
        (declare (ignore conditoin hook))
        ;; デバッガが呼ばれたら、単にプログラムを終了する
        ;; recklessly-p に t を指定して、後始末(標準出力ストリームのフラッシュ等)が行われないようにする
        (sb-ext:quit :recklessly-p t)))

(loop FOR n FROM 0
      DO
      (format t "loop: ~a~%" n))

実行結果。
headコマンドにパイプした場合もエラーは出力されない。

$ sbcl --script loop.lisp | head -n
loop: 0
loop: 1
loop: 2
loop: 3
loop: 4

sb-ext:quit関数のrecklessly-pをnilにすると、スクリプト終了時に後始末のための処理が走って、その中で(既にheadコマンドによって閉じられている)標準出力ストリームへの出力処理が入っていたことが原因の模様(かなり推測)