Python 和 OCaml 之间终端标准输入处理的差异

Difference in terminal stdin handling between Python and OCaml

我正在尝试做一些非常具体的事情,包括将控制字符发送到标准输出并从标准输入中读取。

我在 Python 中有一个有效的实现,我正在尝试将它翻译成 OCaml。

令我惊喜的是,可以非常直接地翻译,几乎是逐行翻译。但是当我 运行 它的行为不同并且 OCaml 不起作用。

在我看来,问题一定是 OCaml 和 Python 运行 处理终端的方式之间存在一些模糊的区别,也许特别是标准输入。

首先是工作 Python 代码:

import os, select, sys, time, termios, tty

def query_colours():
  fp = sys.stdin
  fd = fp.fileno()
  if os.isatty(fd):
      old_settings = termios.tcgetattr(fd)
      tty.setraw(fd)
      try:
          print('3]10;?3]11;?')
          r, _, _ = select.select([ fp ], [], [], 0.1)
          if fp in r:
              return fp.read(48)
          else:
              print("no input available")
              return None
      finally:
          termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
  else:
      raise ValueError("Not a tty")

我的 OCaml 翻译看起来像:

let query_colours () =
  let fd = Unix.stdin in
  if Unix.isatty fd then
    let old_settings = Unix.tcgetattr fd in
    set_raw fd;
    Fun.protect
      ~finally:(fun () -> Unix.tcsetattr fd Unix.TCSADRAIN old_settings)
      (fun () ->
          print_string "\o033]10;?\o007\o033]11;?\o007";
          let r, _, _ = Unix.select [fd] [] [] 0.1 in
          let buf = Bytes.create 48 in
          Printf.printf ">> len r: %d\n" (List.length r);  (* debugging *)
          ignore @@ (
            match List.exists (fun (el) -> el == fd) r with
            | true -> Unix.read fd buf 0 48
            | false -> failwith "No input available"
          );
          Bytes.to_string buf
        )
  else
    invalid_arg "Not a tty"

请注意,我们必须制作 tty.setraw 的 OCaml 实现。首先,这是来自 Python stdlib:

的来源
def setraw(fd, when=TCSAFLUSH):
    """Put terminal into a raw mode."""
    mode = tcgetattr(fd)
    mode[IFLAG] = mode[IFLAG] & ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
    mode[OFLAG] = mode[OFLAG] & ~(OPOST)
    mode[CFLAG] = mode[CFLAG] & ~(CSIZE | PARENB)
    mode[CFLAG] = mode[CFLAG] | CS8
    mode[LFLAG] = mode[LFLAG] & ~(ECHO | ICANON | IEXTEN | ISIG)
    mode[CC][VMIN] = 1
    mode[CC][VTIME] = 0
    tcsetattr(fd, when, mode)

iflagoflagcflaglflag 是位掩码整数

在 OCaml 方面,Stdlib 提供了一个包含所有布尔值的记录,而不是四个位掩码整数:https://ocaml.org/api/Unix.html#TYPEterminal_io

我对 tty.setraw 的 OCaml 翻译看起来像:

let set_raw ?(set_when=Unix.TCSAFLUSH) fd =
  let mode : Unix.terminal_io = {
    (Unix.tcgetattr fd) with
    c_brkint = false;
    c_icrnl = false;
    c_inpck = false;
    c_istrip = false;
    c_ixon = false;
    c_opost = false;
    c_csize = 8;
    c_parenb = false;
    c_echo = false;
    c_icanon = false;
    (* c_iexten = false; ...does not exist on Unix.terminal_io  *)
    c_ixoff = false; (* IEXTEN and IXOFF appear to set the same bit *)
    c_isig = false;
    c_vmin = 1;
    c_vtime = 0;
  } in
  Unix.tcsetattr fd set_when mode

好的,现在问题...

当我 运行 Python 版本时,它只是 returns 一个字符串,如:

'\x1b]10;rgb:c7f1/c7f1/c7f1\x07\x1b]11;rgb:0000/0000/0000\x07'

这是预期的行为。我没有听到 BEL 声音或屏幕上打印的任何其他内容。

当我 运行 我的 OCaml 版本时,我听到 BEL 声音并且我看到:

╰─ dune exec -- ./bin/cli.exe
>> len r: 0
Fatal error: exception Failure("No input available")
^[]10;rgb:c7f1/c7f1/c7f1^G^[]11;rgb:0000/0000/0000^G%

╭─    ~/Documents/Dev/ *5 !4 ?4       2 ✘  18:20:26 
╰─ 10;rgb:c7f1/c7f1/c7f1

╭─    ~/Documents/Dev/ *5 !4 ?4       2 ✘  18:20:26 
╰─ 11;rgb:0000/0000/0000

我们从打印调试len r: 0可以看出,select调用没有发现stdin准备好读取。

相反,我们在 我的程序退出后 看到发送到 stdin 的结果。

FWIW 如果我通过 Unix.open_process_in 运行 来自 OCaml 程序内部的 Python 脚本,那么我从 Python 脚本中得到相同的(损坏的)行为:

utop # run "bin/query.py";;
- : string list = ["7]10;?[=17=]77]11;?[=17=]7"; "no input available"]

我意识到这可能有点晦涩难懂,但如果有人有任何见解,我将不胜感激。

需要阅读的代码很多,但仅从描述来看,您似乎在刷新输出之前将终端返回到其旧状态。

这对 OCaml 来说并不是什么特别奇怪的事情,但是 OCaml 确实比其他一些语言更倾向于保留缓冲输出。

您可以尝试在 print_string:

之后添加这个
flush stdout

就像我说的,有很多代码需要阅读,这只是我的快速理解。