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)
iflag
、oflag
、cflag
、lflag
是位掩码整数
在 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
就像我说的,有很多代码需要阅读,这只是我的快速理解。
我正在尝试做一些非常具体的事情,包括将控制字符发送到标准输出并从标准输入中读取。
我在 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)
iflag
、oflag
、cflag
、lflag
是位掩码整数
在 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
就像我说的,有很多代码需要阅读,这只是我的快速理解。