expect-pty 0.6.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/docs/API.md CHANGED
@@ -1,31 +1,42 @@
1
1
  # Ruby API
2
2
 
3
3
  支持 Ruby 3.4+ 和 Linux/macOS。加载入口为 `require "expect/pty"`,独立流过滤器可加载 `expect/redactor`。
4
+ `Expect` 是模块;`Expect::Session` 是公开的真实会话。工厂、回调、链式方法与 `Result#session` 使用同一个 Session。
4
5
 
5
6
  ## 创建与关闭
6
7
 
7
- `Expect.spawn(*command, env: {}, chdir: nil, **configuration)` 创建并启动 PTY;`Expect.new` 可先创建 PTY,再通过实例 `spawn` 启动。
8
- 单字符串遵循 Ruby 的自动 shell 语义(含 shell 元字符时可能交给 shell);多个独立参数按 argv 执行。
9
- 外部输入应作为独立参数传入,避免拼接进单字符串命令。启动失败抛出 `Expect::SpawnError`。
8
+ `Expect.spawn(*command, env: {}, chdir: nil, raw: false, graceful: false, **settings)` 创建并启动 PTY。
9
+ `settings` 是下文列出的六个显式会话关键字的统称,并非任意配置 Hash。需要预设终端时先 `Expect::Session.new(**settings)`,
10
+ 通过 `session.slave` 使用原生终端方法,再调用 `session.spawn(*command, env: {}, chdir: nil, raw: false)`。
11
+ Session 构造器不接收命令或操作参数。同一会话只能启动一次。
10
12
 
11
- `Expect.open(io, writer: io, own: false, **configuration)` 适配真实可 select 的 IO。默认借用;`own: true` 接管关闭责任,
12
- 包括初始化失败。类级 `spawn`、`open` 无块时返回 Expect,有块时返回块结果并确保关闭。非局部返回也清理。
13
+ 单字符串遵循 Ruby 的自动 shell 语义;多个独立参数按 argv 执行。外部输入应作为独立参数传入。
14
+ 启动失败抛出 `Expect::SpawnError`;工厂失败时回收尚未交付的会话。
13
15
 
14
- `close(graceful: graceful_close?)` 最终硬关闭兜底,返回 nil;`soft_close(timeout: 15, term_timeout: 1)` 收取尾部、关闭句柄并最多发送 TERM;
15
- `hard_close(timeout: 0.2)` 关闭句柄后分阶段等待、TERM、KILL。后两者和 `wait(timeout: nil)` 返回真实 `Process::Status` 或 nil。
16
+ `Expect.open(io, writer: io, own: false, graceful: false, **settings)` 适配真实可 select 的 IO,默认借用。
17
+ `own: true` 接管关闭责任,包括初始化中已知设置的非法值导致的失败;未知关键字在 Ruby 方法入口拒绝,此时不接管 IO。
18
+ `spawn`、`open` 无块时返回 Session,有块时返回块结果并确保按 `graceful:` 关闭;非局部返回也清理。
19
+ 清理中的 StandardError 不覆盖本次作用域已有的主异常;没有主异常时照常传播。新发生的 Interrupt/SystemExit 不会被清理包装吞掉。
20
+
21
+ `close(graceful: false)` 最终硬关闭兜底,返回 nil;`soft_close(timeout: 15, term_timeout: 1)` 收取尾部、关闭句柄并最多发送
22
+ TERM;
23
+ `hard_close(timeout: 0.2)` 关闭句柄后分阶段等待、TERM、KILL。后两者和 `wait(timeout: nil)` 返回真实 `Process::Status` 或
24
+ nil。
16
25
  借用 IO 不随会话关闭。输入 EOF、IO 关闭与子进程退出分别由 `eof?`、`closed?`、`process_status` 表示。
17
26
 
18
27
  ## 匹配与回调
19
28
 
20
- `expect(*patterns, timeout:, deadline:)` 统一返回 `Expect::Result`;`number` 为从 1 开始的文本模式序号或 nil。
21
- 使用 `matched?` 判断匹配成功,Result 对象本身始终为真值。
22
- 类方法增加 `from:` 指定默认来源。模式为 String、Regexp、`:eof` 或 `:timeout`,位置模式与声明块不能混用。
29
+ `session.expect(*patterns, timeout: session.timeout, deadline: nil, consume: true, reset_timeout_on_read: false)` 返回不可变
30
+ `Expect::Result`。
31
+ `Expect.expect` 使用相同操作关键字,另加 `from:` 指定默认来源;其 timeout 默认 nil。模式为 String、Regexp、`:eof` 或
32
+ `:timeout`,
33
+ 位置模式与声明块不能混用。`number` 为从 1 开始的文本模式序号或 nil,事件声明也占序号;使用 `matched?` 判断成功。
23
34
 
24
35
  ```ruby
25
36
  result = session.expect(timeout: 3) do |patterns|
26
- patterns.on("name: ") do |connection|
27
- connection.puts("Ruby")
28
- connection.continue
37
+ patterns.on("name: ") do |source|
38
+ source.puts("Ruby")
39
+ Expect.continue
29
40
  end
30
41
  patterns.on(/hello (\w+)/)
31
42
  end
@@ -33,56 +44,70 @@ result => { number:, captures: }
33
44
  ```
34
45
 
35
46
  无参数声明块在 PatternList 上运行,直接调用 `on`、`eof`、`timeout`;有参数块保留调用者 self。
36
- 注册完成后模式与分组冻结,再次注册抛出 FrozenError;借用的会话与回调不被冻结。
37
- `on`、`eof` 的回调接收外层 Expect;`timeout` 接收全部活跃来源的数组。
38
- 只有 `Expect.continue(reset_timeout: true)` 或实例 `continue` 的控制符使等待继续;`reset_timeout: false` 保持原相对期限。
39
- `deadline` 是单调时钟绝对秒数,不被输入或回调延长。IO 期限不会强行中断用户回调或单次正则计算。
47
+ 注册完成后模式与分组冻结,借用的会话与回调不冻结。相邻同来源组按对象身份及顺序合并,优先级为组、会话、模式;不按文本位置重排。
48
+ `on`、`eof` 回调接收 Session;`timeout` 接收全部活跃来源。只有 `Expect.continue(reset_timeout: true)` 的控制符使等待继续,
49
+ `reset_timeout: false` 保持原相对期限。全部 EOF 后直接返回 EOF;已处理的来源不再参与超时。
50
+
51
+ `consume: false` 仅在本轮保留完整匹配缓冲;继续但不改变缓冲的模式暂停到输入变化,避免反复触发。
52
+ `reset_timeout_on_read: true` 使每次读取重置本轮相对期限。`deadline` 是单调时钟绝对秒数,不被输入或回调延长。
53
+ IO 期限不打断用户代码或单次正则;不可信正则应使用 Ruby `Regexp` 自身的 timeout。
40
54
 
41
- Result 字段为 `number`、`error`、`match`、`before`、`after`、`session`、`captures`。对象、字符串及捕获数组均不可变,未参与捕获的组为 nil。
42
- `matched?`、`timeout?`、`eof?` 查询结果。错误为 nil、`:timeout`、`:eof` 或原始 IO 异常;原始异常与来源会话不冻结。
55
+ Result 字段为 `number`、`error`、`match`、`before`、`after`、`session`、`captures`。对象、文本及捕获数组均不可变,未参与捕获为
56
+ nil。
57
+ 错误为 nil、`:timeout`、`:eof` 或原始 IO 异常;来源会话与异常不复制、不冻结。使用 `matched?`、`timeout?`、`eof?` 查询,
43
58
  使用 `to_h`、位置/键模式解构或 `with` 构造新结果;没有字段 writer 或 `to_a`。
44
- `last_result` 以及会话上的 `before`、`after`、`match`、`match_number`、`captures`、`error` 读取最近结果。
59
+ 会话的 `last_result`、`before`、`after`、`match`、`match_number`、`captures`、`error` 读取最近结果。
60
+
61
+ 匹配、捕获按原始字节保存;文本显示时先 `dup` 再 `force_encoding`。固定 UTF-8 正则等待尾部字符收齐,非法编码抛出
62
+ EncodingError。
63
+ 成功默认消费匹配前缀,超时保留缓冲,EOF 将余下内容放入 before 并清空缓冲。EOF 不表示子进程已经退出。
45
64
 
46
- 匹配和捕获按原始字节保存;文本显示时先 `dup` 再 `force_encoding`。固定 UTF-8 正则等待尾部字符收齐,非法编码抛出 EncodingError。
65
+ ## 会话属性与读写
47
66
 
48
- ## 配置与读写
67
+ 不提供全局配置或配置对象。构造器、工厂显式接受以下设置,会话可通过同名 reader/writer 修改;非法更新保留原值。
49
68
 
50
- `Expect.configuration` 返回冻结默认配置,`Expect.configure(**options) { |config| ... }` 发布新快照;失败不发布部分设置。
51
- 每个会话拥有独立配置,可以通过同名 reader/writer 修改。未知配置键抛出 ArgumentError。
69
+ | 设置 | 默认值 | 语义 |
70
+ |------------------------|--------|--------------------------------------|
71
+ | timeout、write_timeout | nil | 非负有限秒数;nil 无限,0 非阻塞尝试 |
72
+ | buffer_limit | nil | 正整数尾部字节上限,nil 无限 |
73
+ | logger | nil | 借用 Ruby Logger 的 add/debug? 协议 |
74
+ | transcript | nil | 借用 write 协议,记录真实接收字节 |
75
+ | outputs | [] | 复制可写目标数组,原样转发接收字节 |
52
76
 
53
- | 属性 | 默认值 | 语义 |
54
- |---|---|---|
55
- | timeout、write_timeout | nil | 非负有限秒数;nil 无限,0 非阻塞尝试 |
56
- | buffer_limit | nil | 正整数尾部字节上限,nil 无限 |
57
- | debug_level | 0 | 0 关闭,1 生命周期,2 收发字节,3 缓冲 |
58
- | raw_pty、preserve_buffer、log_stdout | false | 子终端 raw、匹配后保留缓冲、stdout 转发 |
59
- | log_listeners、raw_terminal | true | 监听器转发、人工接管时设置本地 raw |
60
- | reset_timeout_on_read、graceful_close | false | 输入刷新相对期限、通用 close 先软关闭 |
77
+ 操作参数 `raw`、`consume`、`reset_timeout_on_read`、`graceful` 不持久化为会话设置。
78
+ `buffer` 返回副本,`buffer=` 复制并应用上限,`clear_buffer` 移交并清空缓冲;`buffer_discarded_bytes` 累计窗口裁剪量,匹配消费不计入。
61
79
 
62
- 布尔项只提供 `name?` 和 `name=`,赋值遵循 Ruby 真值规则。`buffer` 返回副本,`buffer=` 复制并应用上限,`clear_buffer` 移交并清空缓冲;
63
- `buffer_discarded_bytes` 累计窗口裁剪量,匹配消费不计入。
80
+ `write(*objects)` 按 to_s 写字节并返回字节数,`<<` 返回 Session,`puts` 遵循 Ruby IO 换行与数组规则并返回 nil。
81
+ `send_slow(*objects, delay:)` 按字符延迟发送并收取回复。背压超时抛出 `Expect::WriteTimeout`,`bytes_written` 仅标识本次调用已确认进度,
82
+ 嵌套写入异常放在 cause。成功短写不受 write_timeout 的总耗时限制。
83
+ `to_io`、`writer`、`slave` 暴露句柄;`pid`、`command`、`tty_name`、`fileno`、`tty?`、`alive?`、`exit_code` 查询会话属性。
84
+ 终端直接使用 `session.to_io.console_mode`、`echo=`、`winsize` 等 Ruby io/console 接口,没有 stty 或窗口尺寸包装方法。
64
85
 
65
- `write(*objects)` 按 to_s 写字节并返回字节数,`<<` 返回 Expect,`puts` 遵循 Ruby IO 换行与数组规则并返回 nil。
66
- `send_slow(*objects, delay:)` 按字符延迟发送并收取回复。背压超时抛出 `Expect::WriteTimeout`,`bytes_written` 标识已确认进度。
67
- `to_io`、`writer`、`slave` 暴露底层句柄;`pid`、`command`、`tty_name`、`fileno`、`tty?`、`alive?`、`exit_code` 查询会话属性。
68
- `stty(*modes)` 查询/设置终端模式;`winsize` 和 `winsize=` 使用 `[行数, 列数]`。
86
+ ## 诊断、脱敏与转接
69
87
 
70
- ## 日志、脱敏与转接
88
+ `logger=` 接收 nil 或支持 `add` / `debug?` 的对象,直接兼容 Ruby Logger 及符合此协议的 ActiveSupport logger;不依赖
89
+ ActiveSupport。
90
+ Logger 自身控制级别和格式。INFO 记录生命周期/匹配,DEBUG 增加收发字节;`add` 接收冻结事件 Hash
91
+ `{ event:, pid:, fd:, message: }` 和 progname `"Expect"`,message 字符串也冻结。
71
92
 
72
- `log_to(target, mode: "a")` 接收路径或可写目标,也可只给块。路径以私有权限打开并取得关闭责任;IO/块只借用。
73
- `log_output=` 更换目标或设 nil 停止;`write_log` 补写日志而不发送到子进程。IO writer 必须返回实际接受的字节数,支持短写;
74
- 日志块的返回值不控制匹配。日志记录真实读取,不因匹配/转接重复记录。
93
+ `transcript=` 接收 nil 或 writer;路径打开、权限和关闭由调用方通过 File.open 管理,不接收路径或 callable。
94
+ `write_transcript(*objects)` 补写接收记录并返回 nil;不发送到子进程。writer 必须返回实际接受的正整数字节数,支持短写。
95
+ logger、transcript、outputs 一律借用,显式关闭冲刷过滤尾部,但不会关闭这些目标。
75
96
 
76
- `diagnostic_output=` 接收 info/debug Logger 协议、可写对象、回调或 nil。回调接收冻结事件 Hash:`event`、`level`、`pid`、`fd`、`message`。
77
- `redact(*secrets)` 注册非空字符串,仅过滤日志和诊断,不更改匹配或协议字节。`Expect::Redactor` 提供 `append`、`finish(partial: true)`、
78
- `patterns=` 和完整文本类方法 `redact`;流尾部默认隐藏疑似秘密前缀。
97
+ `redact(*secrets)` 追加非空字符串,仅过滤 transcript 和诊断,不更改匹配、Result 或 outputs。
98
+ `Expect::Redactor` 提供 `append`、`finish(partial: true)`、`patterns=` 和完整文本类方法 `redact`;流尾部默认隐藏疑似秘密前缀。
79
99
 
80
- `listeners=` 校验并复制可写目标数组,`listeners` 返回副本。`Expect.interconnect(*sources, timeout:)` 按监听关系转接,
81
- 返回停止来源或在总期限到达时返回 nil。`on_sequence(String/Regexp/:eof) { ... }` 注册无参数回调,nil/false 停止,其余值继续。
82
- `pending_output?` 表示尚未交付数据;再次转接同源会话继续发送,不重放成功前缀。同源递归转接抛出 ReentrancyError,转义回调可以嵌套匹配。
100
+ `outputs=` 校验并复制可写目标数组,读取返回副本;`$stdout` 与其他 writer 一样显式加入。
101
+ `Expect.interconnect(*sources, timeout: nil)` 按 outputs 转接,返回停止来源或在总期限到达时返回 nil。
102
+ `on_sequence(String/Regexp/:eof) { ... }` 注册无参数回调,未给块或返回 nil/false 停止,其余值继续。
103
+ `pending_output?` 表示尚未交付数据;再次转接同源会话继续发送,不重放成功前缀;更换 outputs 不改变已排队字节的目标。
104
+ 同源递归转接抛出 ReentrancyError;转义回调可嵌套匹配,所有来源状态按对象身份隔离。
83
105
 
84
- `interact(input: $stdin, escape: nil, output: nil, timeout: nil)` 临时建立双向转接,退出后恢复监听关系和本地终端模式。
85
- 日志、诊断及自定义 writer 回调同步执行,应及时返回。
106
+ `interact(input: $stdin, escape: nil, output: nil, timeout: nil, raw: true)` 临时建立双向转接,退出后恢复
107
+ outputs、转义和本地终端模式。
108
+ `output: nil` 使用默认输出,`escape: nil` 不注册退出序列;显式 false 不是缺省值,非法输出或转义会在转发前报错。
109
+ `raw: false` 将终端设置留给调用方。transcript、logger 及自定义 writer 同步执行,应及时返回。
86
110
 
87
111
  `Expect.monotonic` 读取单调时钟,`Expect.duration` 校验秒数,`Expect.readable_sessions(*sources, timeout: 0)` 返回就绪来源。
88
- 内部 Session、Matcher、Relay 和资源账本不属于用户 API。RBS 覆盖公开声明与协作协议;`rbs validate` 不验证 Ruby 方法体。
112
+ Matcher、Relay、资源账本和标为 `@api private` 的 Session 协作方法不是用户契约。RBS 覆盖公开声明;`rbs validate` 不验证 Ruby
113
+ 方法体。
data/docs/MIGRATION.md CHANGED
@@ -1,15 +1,68 @@
1
- # 0.5.x → 0.6.0 迁移说明
2
-
3
- 0.6.0 包含以下不兼容调整;从 0.5.x 升级时需要同步修改调用代码。
4
-
5
- - 最低运行环境调整为 Ruby 3.4;CI 验证 Ruby 3.4 和 4.0 的 Linux/macOS 组合。
6
- - Result 由可变 Struct 改为不可变 Data。`result.match = ...` 改为 `result.with(match: ...)`;原结果保持不变。
7
- - `result.to_a` 改为位置模式 `result => [number, error, match, before, after, connection, captures]`,或优先使用键模式 `result => { number:, captures: }`。
8
- - 结果文本和捕获值不能原地修改。需要调整编码时使用 `result.match.dup.force_encoding("UTF-8")`,不要直接修改快照。
9
- - 结果的 `session` 与回调参数仍为外层 Expect;内部运行状态移入 Session。外部代码不应访问实例变量或调用私有匹配、转接钩子。
10
- - Gem 不再携带测试、基准、示例及发布脚本;这些开发材料从仓库取得。运行时依赖不包含 RBS、YARD 等开发工具。
11
- - 类级和实例级 `expect` 统一返回 Result,移除 `expect_result`。旧的序号比较改用 `.number`;成功判断使用 `.matched?`,EOF/超时使用 `.eof?` / `.timeout?`。
12
- - 保留会话上的 `before`、`after`、`match`、`match_number`、`captures`、`error`,它们读取最近结果;也可使用 `last_result`。
13
- - 布尔配置只提供 `name?` 与 `name=`,构造关键字和配置 Hash 的键不变。
14
- - 模式在进入匹配器时冻结,运行中不能追加或替换规则。每次等待重新声明所需模式。
15
- - 单字符串命令保留 Ruby 自动 shell 语义;多参数命令逐项传入 argv。
1
+ # 迁移说明
2
+
3
+ ## 0.6.x → 0.7.0
4
+
5
+ 0.7.0 主动移除门面和旧配置接口,不提供兼容别名。最低版本仍为 Ruby 3.4,`require "expect/pty"` 和不可变 Result 保持不变。
6
+
7
+ | 0.6.x | 0.7.0 | 迁移含义 |
8
+ |------------------------------------------------------|--------------------------------------------------|----------------------------------------------------------------|
9
+ | `Expect` 类与外层会话门面 | `Expect` 模块、公开 `Expect::Session` | 工厂、匹配回调、Result.session 和链式返回同一个 Session |
10
+ | `Expect.new(...)` | `Expect::Session.new(**settings)` | 仅预建 PTY;命令及 env/chdir/raw 交给之后的 session.spawn |
11
+ | `Expect.spawn` / `Expect.open` 返回 Expect | 返回 Expect::Session | 类型判断与 RBS 引用改为 Session;有块仍返回块结果 |
12
+ | `Expect.configure`、`configuration`、`Configuration` | 显式会话关键字与属性 | 应用共享默认值自行保存 Hash,调用时展开;不再继承类级配置 |
13
+ | `raw_pty: true` / `raw_pty=` | `spawn(raw: true)` | 每次启动的子终端操作参数 |
14
+ | `preserve_buffer = true` | `expect(consume: false)` | 每轮等待设置;默认仍消费成功匹配前缀 |
15
+ | `reset_timeout_on_read = true` | `expect(reset_timeout_on_read: true)` | 只作用于本轮相对期限,deadline 仍不可延长 |
16
+ | `raw_terminal = false` | `interact(raw: false)` | 只作用于本次人工接管 |
17
+ | `graceful_close = true` | 工厂 `graceful: true` 或 `close(graceful: true)` | 块清理策略与直接关闭均显式指定 |
18
+ | `session.continue(...)` | `Expect.continue(...)` | 继续控制符只由模块提供 |
19
+ | `listeners`、`log_listeners`、`log_stdout` | `outputs` writer 数组 | 把 `$stdout` 显式加入;清空或替换数组控制转发 |
20
+ | `debug_level`、`diagnostic_output` | `logger` / `logger=` | Ruby Logger 的 add/debug? 协议;级别、格式和输出由 logger 管理 |
21
+ | 诊断回调事件中的 `level` | Logger add 的 severity 参数 | 事件 Hash 保留 event/pid/fd/message;不再生成 buffer 诊断 |
22
+ | `log_output`、`log_to` | `transcript` / `transcript=` | 只接收 writer;文件打开、权限、模式和关闭由调用方负责 |
23
+ | `write_log(*objects)` | `write_transcript(*objects)` | 补写记录,不发送字节,固定返回 nil |
24
+ | 路径日志、callable 日志 | `File.open` 或实现 write 的目标 | 库不拥有日志文件;write 必须返回已接受字节数 |
25
+ | `session.stty(...)` | `session.to_io` 的 io/console 方法 | 原生 raw/cooked/echo=/console_mode;不再启动辅助命令 |
26
+ | `session.winsize` / `winsize=` | `session.to_io.winsize` / `winsize=` | 原生窗口尺寸操作 |
27
+
28
+ 显式关键字只接受 timeout、write_timeout、buffer_limit、logger、transcript、outputs 六个会话设置。
29
+ 未知关键字在 Ruby 方法入口抛出 ArgumentError,因此 `Expect.open(..., own: true, unknown: value)` 不接管 IO;
30
+ 已知设置的非法值在初始化中失败,仍清理 own:true 的端点。logger、transcript、outputs 始终借用,关闭会话不关闭目标。
31
+
32
+ ```ruby
33
+ require "expect/pty"
34
+ require "logger"
35
+
36
+ defaults = { timeout: 5, buffer_limit: 65_536 }.freeze
37
+ logger = Logger.new($stderr, level: Logger::INFO)
38
+ File.open("session.log", File::WRONLY | File::CREAT | File::APPEND, 0o600) do |transcript|
39
+ Expect.spawn("/bin/sh", "-i", **defaults, logger: logger, transcript: transcript,
40
+ outputs: [$stdout], raw: true) do |session|
41
+ session.puts("printf 'ready\\n'")
42
+ session.expect("ready", consume: false, reset_timeout_on_read: true)
43
+ session.clear_buffer
44
+ session.puts("exit")
45
+ end
46
+ end
47
+ ```
48
+
49
+ 兼容 `add` / `debug?` 的 ActiveSupport logger 可直接注入,不需要为本库引入 ActiveSupport。
50
+ 需要自定义诊断格式时设置 logger.formatter;需要自定义接收记录时实现 writer.write (String) 并返回已接受字节数。
51
+
52
+ 内部 Session 协作协议通过 YARD `@api private` 标注,不因为 Ruby 可调用就成为发布契约。不要再访问门面实例变量或依赖旧配置类。
53
+ 会话可以被应用扩展,但多个对象即使定义相同的 `==` / `eql?` / `hash`,仍是各自独立的读取来源与生命周期。
54
+
55
+ ## 0.5.x → 0.6.0(历史)
56
+
57
+ 从 0.5.x 直接升级到 0.7.0 时,先理解以下结果值变更,再应用上面的接口表。
58
+
59
+ - 最低运行环境改为 Ruby 3.4;支持 Ruby 3.4 和 4.0 的 Linux/macOS 组合。
60
+ - Result 从可变 Struct 改为不可变 Data;`result.match = value` 改为 `result.with(match: value)`。
61
+ - `result.to_a` 改为位置模式或优先使用键模式,如 `result => { number:, captures: }`。
62
+ - 结果文本和捕获值不可原地修改;调整编码用 `result.match.dup.force_encoding("UTF-8")`。
63
+ - `expect` 统一返回 Result,移除 expect_result;序号比较用 `.number`,成功用 `.matched?`,EOF/超时用 `.eof?` / `.timeout?`。
64
+ - 会话 before、after、match、match_number、captures、error 与 last_result 保留。
65
+ - 模式进入匹配器后冻结,运行中不能追加或替换本轮规则。
66
+ - Gem 仅携带源码、RBS 与使用文档;测试、示例、基准和发布脚本从仓库取得。
67
+ - 0.6.0 曾保留外层 Expect 门面和布尔配置,这两部分已由 0.7.0 的 Session 与操作关键字替代。
68
+ - 单字符串命令保留 Ruby 自动 shell 语义;多参数命令按 argv 传入。
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
3
+ module Expect
4
4
  # 清理作用域只记录本次异常,不受调用者 rescue 中的旧 $! 影响。
5
5
  # @api private
6
6
  module Cleanup
7
- # 包括非局部返回在内的所有退出均清理;常规清理错误只在没有原异常时传播。
7
+ # 包括非局部返回在内的所有退出均清理;StandardError 只在没有原异常时传播。
8
+ # 清理期间新发生的 Interrupt、SystemExit 等致命异常仍原样传播。
8
9
  def self.always(on_exit)
9
10
  yield
10
11
  rescue Exception # rubocop:disable Lint/RescueException -- 中断也必须清理,然后原样传播。
@@ -13,7 +14,7 @@ class Expect
13
14
  ensure
14
15
  begin
15
16
  on_exit.call
16
- rescue IOError, SystemCallError
17
+ rescue StandardError
17
18
  raise unless failed
18
19
  end
19
20
  end
@@ -3,15 +3,20 @@
3
3
  require_relative "relay_writer"
4
4
  require_relative "relay"
5
5
 
6
- # 为会话补充人工接管和多路 IO 转接;核心会话定义位于 lib/expect.rb。
7
- class Expect
8
- # @api private
6
+ # 为会话补充人工接管和多路 IO 转接;核心会话定义位于 session.rb。
7
+ module Expect
8
+ # 为公开 Session 混入转义注册与人工接管接口;协作方法各自标记为内部协议。
9
+ # 宿主提供真实读写 IO、缓冲及 outputs;本模块保存转义、显示历史和待写游标。
10
+ # Relay 暂借输入缓冲并调度游标,Matcher 可在转义回调中临时接管,退出时归还未消费尾部。
9
11
  module Interaction
10
12
  # 正则没有“潜在部分匹配”接口,只保留有限历史;已转发的历史字节不能撤回。
11
13
  REGEXP_ESCAPE_HISTORY_LIMIT = 65_536
12
- private_constant :REGEXP_ESCAPE_HISTORY_LIMIT
14
+ # 没有显式回调的转义仍用同一 callable 协议,返回 false 让 Relay 停止。
15
+ STOP = -> { false }.freeze
16
+ private_constant :REGEXP_ESCAPE_HISTORY_LIMIT, :STOP
13
17
 
14
18
  # 在 raw 本地终端显示远端文本,补齐 LF 所需的 CR,同时保留已有 CRLF。
19
+ # @api private
15
20
  class InteractOutput
16
21
  # 包装器借用目标,不复制或关闭其描述符;换行状态属于这个目标的连续显示流。
17
22
  def initialize(target)
@@ -47,32 +52,28 @@ class Expect
47
52
  end
48
53
  raise ArgumentError, "escape sequence must not be empty" if key == ""
49
54
 
50
- @sequences[key] = block
51
- connection
55
+ @sequences[key] = block || STOP
56
+ self
52
57
  end
53
58
 
54
- # 临时将输入、会话和输出相连,实现人工接管;结束时恢复双方监听器、日志开关和转义设置。
55
- def interact(input: $stdin, escape: nil, output: nil, timeout: nil)
59
+ # 临时将输入、会话和输出相连,实现人工接管;结束时恢复双方输出目标、转义和终端模式。
60
+ def interact(input: $stdin, escape: nil, output: nil, timeout: nil, raw: true)
56
61
  source = interact_source(input)
57
- output ||= input.equal?($stdin) ? $stdout : input
58
- saved_self = [listeners, log_stdout?, log_listeners?]
59
- saved_source = [source.listeners, source.log_stdout?, source.log_listeners?, source.sequences.dup]
60
- terminal_state = prepare_interact_terminal(source)
62
+ output = input.equal?($stdin) ? $stdout : input if output.nil?
63
+ saved_self = outputs
64
+ saved_source = [source.outputs, source.sequences.dup]
65
+ terminal_state = prepare_interact_terminal(source) if raw
61
66
  display = interact_display(source, output, terminal_state)
62
67
  # 临时建立“用户输入 -> 子进程 -> 显示输出”的双向连接,原监听关系在 ensure 中恢复。
63
- self.listeners = [display]
64
- self.log_stdout = false
65
- self.log_listeners = true
66
- source.listeners = [connection]
67
- source.log_stdout = false
68
- source.log_listeners = true
69
- source.on_sequence(escape) if escape
70
- Relay.new([self, source], timeout).run&.connection
68
+ self.outputs = [display]
69
+ source.outputs = [self]
70
+ source.on_sequence(escape) unless escape.nil?
71
+ Relay.new([self, source], timeout).run
71
72
  ensure
72
73
  begin
73
74
  if saved_self
74
- self.listeners, self.log_stdout, self.log_listeners = saved_self
75
- source.listeners, source.log_stdout, source.log_listeners, source.sequences = saved_source
75
+ self.outputs = saved_self
76
+ source.outputs, source.sequences = saved_source
76
77
  end
77
78
  ensure
78
79
  restore_interact_terminal(terminal_state)
@@ -80,58 +81,51 @@ class Expect
80
81
  end
81
82
 
82
83
  # 超时或异常后仍有未交付的数据;重新 interconnect 同一源会话可继续发送。
83
- def pending_output? = @relay_outputs.any? { |output| !output.done? }
84
+ def pending_output? = @pending_writes.any? { |output| !output.done? }
84
85
 
85
- # 转发一个会话的待处理缓冲并剔除转义;返回 false 表示应结束整个转接。
86
+ # 将待处理输入排入唯一的转接发送队列;返回 :ready、:queued 或 :stopped。
86
87
  # 字面序列暂存潜在前缀,正则序列结合历史匹配;final 为真时不再等待后续字节。
87
- def self.relay_buffer(session, buffers, final: false, &)
88
- buffer = buffers.fetch(session)
88
+ # @api private
89
+ def self.queue_input(session, buffer, final: false)
89
90
  loop do
90
91
  sequences = session.sequences.except(:eof)
91
92
  history = session.relay_history
92
93
  found, regexp_scanned, utf8_regexp = scan_sequences(buffer, sequences, history, final:)
93
94
  if found
94
- result = handle_escape(session, buffer, history, found, &)
95
- return result unless result == true
95
+ result = handle_escape(session, buffer, history, found)
96
+ return result unless result == :ready
96
97
 
97
98
  next
98
99
  end
99
100
 
100
101
  # 暂存可能构成字面转义的最长后缀,保证 STOP 分两次读取时 ST 不会提前发给子进程。
101
102
  held = final ? 0 : hold_literal_prefix(buffer, sequences)
102
- count = buffer.bytesize - held
103
- count = [count, READ_SIZE].min if block_given?
104
- if count.positive?
105
- data = buffer.byteslice(0, count)
106
- block_given? ? yield(data) : session.propagate(data)
107
- end
103
+ count = [buffer.bytesize - held, READ_SIZE].min
104
+ session.queue_output(buffer.byteslice(0, count)) if count.positive?
108
105
  if regexp_scanned
109
106
  trim_history(history, buffer.byteslice(0, count), limit: session.buffer_limit || REGEXP_ESCAPE_HISTORY_LIMIT,
110
107
  utf8: utf8_regexp)
111
108
  end
112
109
  buffer.slice!(0, count)
113
- return block_given? && count.positive? ? :pending : true
110
+ return count.positive? ? :queued : :ready
114
111
  end
115
112
  end
116
113
 
117
- # 转义前缀必须交付完才运行回调;队列模式保存回调,直接模式立即调用。
114
+ # 转义前缀必须交付完才运行回调;有前缀时保存回调,交给 Relay 交付后再执行。
118
115
  def self.handle_escape(session, buffer, history, found)
119
116
  position, length, callback = found
120
117
  if position.positive?
121
- if block_given?
122
- yield buffer.byteslice(0, position)
123
- buffer.replace(buffer.byteslice((position + length)..))
124
- history.clear
125
- # 已确定的转义不能在等待写入后重新匹配;尾部可能在等待期间继续增长。
126
- session.relay_callback = [callback]
127
- return :pending
128
- end
129
- session.propagate(buffer.byteslice(0, position))
118
+ session.queue_output(buffer.byteslice(0, position))
119
+ buffer.replace(buffer.byteslice((position + length)..))
120
+ history.clear
121
+ # 已确定的转义不能在等待写入后重新匹配;尾部可能在等待期间继续增长。
122
+ session.relay_callback = callback
123
+ return :queued
130
124
  end
131
125
  buffer.replace(buffer.byteslice([position + length, 0].max..))
132
126
  # 转义消费后清除历史,防止继续回调再次匹配同一个转义。
133
127
  history.clear
134
- !!callback&.call
128
+ callback&.call ? :ready : :stopped
135
129
  end
136
130
 
137
131
  # 单轮正则共用一个组合文本;回调返回后下一轮重新取得规则和历史。
@@ -160,6 +154,7 @@ class Expect
160
154
  end
161
155
 
162
156
  # 普通转发和超时排出的字面前缀都属于正则历史,跨次转接时必须接续同一字节流。
157
+ # @api private
163
158
  def self.remember_output(session, data)
164
159
  regexps = session.sequences.keys.grep(Regexp)
165
160
  return if regexps.empty?
@@ -197,13 +192,11 @@ class Expect
197
192
 
198
193
  private_class_method :handle_escape, :scan_sequences, :hold_literal_prefix, :trim_history
199
194
 
200
- # 为当前数据块冻结目标选择并各建一个发送游标;此后修改 listeners 只影响后续数据。
195
+ # 为当前数据块冻结目标选择并各建一个发送游标;此后修改 outputs 只影响后续数据。
201
196
  # 调用方须先排空旧游标;显示转换也只做一次,短写重试时不能重复转换 CRLF。
197
+ # @api private
202
198
  def queue_output(data)
203
- targets = []
204
- targets << $stdout if log_stdout?
205
- targets.concat(@listeners) if log_listeners?
206
- @relay_outputs = targets.map do |target|
199
+ @pending_writes = @outputs.map do |target|
207
200
  if target.is_a?(InteractOutput)
208
201
  RelayWriter.new(target.target, target.render(data))
209
202
  else
@@ -215,19 +208,26 @@ class Expect
215
208
  public
216
209
 
217
210
  # 仅供转接内部保存和恢复注册表,避免公开可变 Hash 绕过 on_sequence 的校验。
211
+ # @api private
218
212
  attr_accessor :sequences
219
213
  # 让同步写入的背压读取遵守当前转接的数据所有权,退出后恢复普通匹配缓冲。
214
+ # @api private
220
215
  attr_accessor :interaction_buffer
221
216
  # 活跃 Relay 的所有权 token;只限制同源递归转接,不限制转义回调中的 Matcher。
217
+ # @api private
222
218
  attr_accessor :relay_owner
223
219
  # 只有裁剪、替换和消费才改变代次;同一代次只会追加,供字面扫描复用已排除的前缀。
220
+ # @api private
224
221
  attr_reader :buffer_generation
225
222
  # 待交付游标随源会话保存,Relay 的超时或异常退出不会丢失各目标已经写出的进度。
226
- attr_reader :relay_outputs
227
- # 数组包装区分“没有待执行回调”与“已识别无处理器的停止转义”,前缀交付后只派发一次。
223
+ # @api private
224
+ attr_reader :pending_writes
225
+ # 等待前缀交付的动作;缺省停止动作也是真正的 callable,不使用数组包装状态。
226
+ # @api private
228
227
  attr_accessor :relay_callback
229
228
 
230
229
  # 历史属于产生它的转义规则;同规则重入继续匹配,换规则不能重放已转发输入。
230
+ # @api private
231
231
  def relay_history
232
232
  sequences = @sequences.except(:eof)
233
233
  @relay_history = "".b unless @relay_history_sequences == sequences
@@ -236,6 +236,7 @@ class Expect
236
236
  end
237
237
 
238
238
  # 转接尚未处理的输入不能被匹配窗口上限裁掉;下次 expect 会重新应用该上限。
239
+ # @api private
239
240
  def restore_relay_buffer(buffer)
240
241
  @buffer = buffer + @buffer
241
242
  @buffer_generation += 1
@@ -245,15 +246,15 @@ class Expect
245
246
 
246
247
  # 同一输入 IO 重用一个借用会话,以保留上次接管预读的尾部并避免不断积累包装器。
247
248
  def interact_source(input)
248
- return Session.for(input) if input.is_a?(Expect)
249
+ return input if input.is_a?(Session)
249
250
 
250
251
  @interact_inputs.delete_if { |io, session| io.closed? || session.closed? }
251
- Session.for(@interact_inputs[input] ||= Expect.open(input))
252
+ @interact_inputs[input] ||= Expect.open(input)
252
253
  end
253
254
 
254
255
  # 只有 interact 知道哪个流是本地键盘;通用 interconnect 不修改终端模式。
255
256
  def prepare_interact_terminal(source)
256
- return unless source.raw_terminal? && source.tty?
257
+ return unless source.tty?
257
258
 
258
259
  state = nil
259
260
  Cleanup.on_failure(-> { restore_interact_terminal(state) }) do