SBCL 程序 I/O 互操作性

SBCL Program I/O interoperability

我有一个我想阅读的程序,可以说是 python。所以我有这两个功能:



(defun start-python ()
  (let ((process 
     (sb-ext:run-program "/usr/bin/python" nil
                 :output :stream
                 :input :stream
                 :wait nil
                 :search t
                 :error *standard-output*)))
    process))

(defun test-process-stream ()
  (let ((process (start-python)))
    (format (sb-ext:process-input process) "print 'hello world!'~%")
    (finish-output (sb-ext:process-input process))
    ;;I do not want to call "close" because I will be still reading from the input
    (close (sb-ext:process-input process))
    (print (read-line (sb-ext:process-output process)))
    (when (listen (sb-ext:process-output process))
      (print (read-line (sb-ext:process-output process))))

    (close (sb-ext:process-output process))
    (sb-ext:process-close process)
    ))

我希望能够从 python 进程的输出中增量读取,同时向其提供输入。 我尝试了几种方法,甚至是这里提到的方法:

但我在 SBCL 中无法做到这一点。 在示例代码中,我调用 close 因为这是我获得任何输出的唯一方式。否则它只会挂起。

我非常感谢任何指点,因为我坚持这一点。我什至尝试使用 (listen ...)(finish-output ...),但它仍然挂在 (read-line ...) 上。 (listen ...) 的唯一区别是它 returns false 并且不打印任何内容。在尝试阅读之前,我什至尝试了 (sleep 2)。仍然没有。

编辑:最终我的目标是 运行 swipl 即 SWI-Prolog。我在这里以 python 为例。我想实现 lisp 和 prolog 之间的互操作性,这样我就可以向 prolog 发出查询并读回响应。目前我找不到任何适合我需要的项目或库,所以这就是我尝试这个的原因。

[这个答案的大部分内容并不有趣,因为发问者询问 Python 结果发现他们指的是 Prolog,所以我浪费了我的时间来解决他们所说的问题而不是他们实际遇到的问题.我把它留在这里以防它对其他人有用。]

我认为这不是 SBCL 的问题。给定以下代码:

(defun call-with-process (f program args &rest keys &key &allow-other-keys)
  (let ((process (apply #'sb-ext:run-program program args keys)))
    (unwind-protect
        (funcall f process)
      (sb-ext:process-close process))))

(defmacro with-process ((process program args &rest keys &key &allow-other-keys)
                        &body forms)
  `(call-with-process
    (lambda (,process)
      ,@forms)
    ,program ,args ,@keys))

(defun test-echo-process (&rest strings-to-send)
  (with-process (p "/bin/cat" '()
                   :wait nil
                   :input ':stream
                   :output ':stream)
    (let ((i (sb-ext:process-input p))
          (o (sb-ext:process-output p)))
      (dolist (s strings-to-send)
        (format i "~A~%" s)
        (finish-output i)
        (format t "Sent ~A, got ~A~%" s (read-line o))))))

然后 test-echo-process 工作正常。

但是这个函数(等同于你的)挂起:

(defun test-python-process ()
  (with-process (p "/usr/bin/python" '()
                   :wait nil
                   :input ':stream
                   :output ':stream)
    (let ((i (sb-ext:process-input p))
          (o (sb-ext:process-output p)))
      (format i "print 'here'~%")
      (finish-output i)
      (format t "~A~%" (read-line o)))))

所以,实际上问题是 Python 的行为方式。事实上,你可以证明这一点。这是终端的一些输出:

$ cat | python
print "hi"
print "there"
hi
there
$

没有显示的是,在我输入第二个 print 命令后,我发送了一个 EOF(即 Unix 上的 ^D)。

所以 Python 是,我认为完全合理,缓冲它的输入,可能只有在它不是终端的情况下。

所以你需要做点什么来阻止这种情况的发生。作为最初的事情,我会把你想要 Python 到 运行 的程序放在一个文件中,这样标准输入就只做一件事。但你会发现自己身处痛苦的世界。

如果你实现这个功能

(defun test-python-script (args &rest strings-to-send)
  (with-process (p "/usr/bin/python" args
                   :wait nil
                   :input ':stream
                   :output ':stream)
    (let ((i (sb-ext:process-input p))
          (o (sb-ext:process-output p)))
      (dolist (s strings-to-send)
        (format i "~A~%" s)
        (finish-output i)
        (format t "sent ~A, got ~A~%" s (read-line o))))))

那么你可能会认为 echo.py 中的 Python 是这样的:

from sys import stdin, exit

if __name__ == '__main__':
    for l in stdin:
        print "got " + l.strip()
    exit(0)

& 然后 运行ning (test-python-script '("echo.py") "foo" "bar") 就可以了。但是你至少在两个方面是错误的(你可以通过在命令行上 运行ning python echo.py 来检查它并看到它仍然缓冲。

第一个错误是在 Python 中使用文件作为迭代器内置了缓冲,您似乎无法避免。您可以通过编写一个无缓冲的迭代器来处理这个问题,所以 echo.py 现在是

from sys import stdin, exit

class UnbufferedLineIterator(object):
    # I take back what I said about 'perfectly reasonably'
    def __init__(self, stream):
        self.stream = stream

    def __iter__(self):
        return self

    def next(self):
        line = self.stream.readline()
        if len(line) > 0:
            return line
        else:
            raise StopIteration

if __name__ == '__main__':
    for l in UnbufferedLineIterator(stdin):
        print "got " + l.strip()
    exit(0)

这可能行得通,但行不通,因为 仍然 在 Python 一侧的某处进行缓冲。您可以通过 运行ning Python 和 -u 参数以非常粗略的方式摆脱它。所以,终于

* (test-python-script '("-u" "echo.py") "foo" "bar")
sent foo, got got foo
sent bar, got got bar

但是我认为真正的答案是去问 Python 人们这是怎么回事,因为我不相信 -u 是正确的答案或者它可以是这么难。

我设法使用以下代码使其工作:

    (defun start-python ()
      (let ((process 
         (sb-ext:run-program "/usr/bin/python3" nil
                     :output :stream
                     :input :stream
                     :wait nil
                     :pty t
                     :error *standard-output*)))
        process))

    (defun read-until-newline (process)
      (let ((r ""))
        (loop for c = (read-char-no-hang (sb-ext:process-pty process))
           do (progn
            (if (or (not c) (char= c #\newline))
            (return-from read-until-newline r)
            (setf r (concatenate 'string r (format nil "~c" c))))))))

    (defun print-all-output (process &key (discard nil))
      (sleep 0.1)
      (loop 
         do (progn
          (if (listen (sb-ext:process-pty process))
              (if (not discard)
                  (print (read-until-newline process))
                  (read-until-newline process))
              (return)))))

    (defun send-to-python (process str)
      (format (sb-ext:process-pty process) str)
      (finish-output (sb-ext:process-pty process)))

    (defun test-process-stream ()
      (let* ((process (start-python)))
        (print-all-output process :discard t) ;;discard banner message
        (send-to-python process "X=[1,2,3,4,5]~%print(X[:2],X[2:])~%X~%")
        (print-all-output process)
        (sb-ext:process-close process)
        ))

非常感谢@jkiiski 帮助我调试这段代码。 诀窍是使用 :pty 作为 run-program 的参数,然后使用流 (sb-ext:process-pty process) 与进程通信。在那之后做 (finish-output (sb-ext:process-pty process)) 刷新我们对程序的输入。然后等待一点时间很重要,这样子流程才能积累输出。在 (sleep 0.1) 之后, (listen ...) 将能够判断是否有输出等待。然后它只是一个循环 (read-char-no-hang) 来读取字符,直到剩下 none 为止。如 (read-until-newline) 所示,我用换行符分隔输出 上面的代码产生以下输出:

">>> [1, 2] [3, 4, 5]^M" 
">>> [1, 2, 3, 4, 5]^M" 
">>> "

(print-all-output process) 的任何后续调用都将逐步打印程序的输出。