expect-pty 0.5.3 → 0.6.1

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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -19
  4. data/docs/API.md +88 -0
  5. data/docs/MIGRATION.md +15 -0
  6. data/lib/expect/cleanup.rb +33 -0
  7. data/lib/expect/configuration.rb +87 -66
  8. data/lib/expect/interaction.rb +226 -209
  9. data/lib/expect/logging.rb +157 -156
  10. data/lib/expect/matcher.rb +30 -29
  11. data/lib/expect/pattern.rb +8 -2
  12. data/lib/expect/pattern_list.rb +35 -5
  13. data/lib/expect/redactor.rb +11 -4
  14. data/lib/expect/relay.rb +97 -73
  15. data/lib/expect/relay_writer.rb +2 -1
  16. data/lib/expect/result.rb +32 -8
  17. data/lib/expect/session.rb +508 -0
  18. data/lib/expect/session_resources.rb +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  24. data/.rubocop.yml +0 -66
  25. data/CONTRIBUTING.md +0 -29
  26. data/Gemfile +0 -28
  27. data/Rakefile +0 -38
  28. data/benchmark/matching.rb +0 -98
  29. data/benchmark/redactor.rb +0 -64
  30. data/benchmark/relay.rb +0 -48
  31. data/benchmark/scaling.rb +0 -70
  32. data/benchmark/send_slow.rb +0 -41
  33. data/benchmark/support.rb +0 -114
  34. data/docs/COMPATIBILITY.md +0 -98
  35. data/docs/INTERNAL_CONTRACTS.md +0 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  39. data/examples/dialogue.rb +0 -30
  40. data/examples/kibitz/README.md +0 -81
  41. data/examples/kibitz/kibitz.rb +0 -142
  42. data/examples/kibitz/test_kibitz.rb +0 -37
  43. data/examples/ssh_auto.rb +0 -94
  44. data/examples/ssh_interact.rb +0 -159
  45. data/examples/ssh_login.rb +0 -64
  46. data/expect-pty.gemspec +0 -33
  47. data/script/ci +0 -121
  48. data/script/release.rb +0 -319
  49. data/test/buffer_accounting_test.rb +0 -83
  50. data/test/cleanup_test.rb +0 -251
  51. data/test/compare_upstream.rb +0 -157
  52. data/test/configuration_test.rb +0 -133
  53. data/test/deadline_test.rb +0 -237
  54. data/test/diagnostics_test.rb +0 -353
  55. data/test/edge_case_test.rb +0 -262
  56. data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
  57. data/test/fixtures/ssh_scripts/02_output.sh +0 -5
  58. data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
  59. data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
  60. data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
  61. data/test/initialization_failure_test.rb +0 -92
  62. data/test/integration/README.md +0 -109
  63. data/test/integration/ssh_scripts.rb +0 -94
  64. data/test/interact_test.rb +0 -253
  65. data/test/interconnect_test.rb +0 -425
  66. data/test/io_test.rb +0 -321
  67. data/test/kibitz_test.rb +0 -45
  68. data/test/lifecycle_contract_test.rb +0 -71
  69. data/test/literal_scan_test.rb +0 -97
  70. data/test/matching_test.rb +0 -211
  71. data/test/multi_session_test.rb +0 -66
  72. data/test/ownership_sequence_test.rb +0 -208
  73. data/test/pattern_offset_test.rb +0 -43
  74. data/test/process_interruption_test.rb +0 -296
  75. data/test/process_test.rb +0 -291
  76. data/test/redactor_test.rb +0 -183
  77. data/test/relay_recovery_test.rb +0 -451
  78. data/test/relay_reentrancy_test.rb +0 -159
  79. data/test/release_test.rb +0 -375
  80. data/test/ruby_api_test.rb +0 -515
  81. data/test/scan_reuse_test.rb +0 -109
  82. data/test/script_logging_test.rb +0 -124
  83. data/test/support/interact_probe.rb +0 -125
  84. data/test/support/kibitz_probe.rb +0 -177
  85. data/test/support/script_probe.rb +0 -158
  86. data/test/terminal_cleanup_test.rb +0 -349
  87. data/test/test_helper.rb +0 -58
  88. data/test/timeout_test.rb +0 -289
  89. data/test/write_contract_test.rb +0 -105
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85c1290ea232f7bf915d4c0d95a50f1c8fd2e4ac89ce423ee46cdc66f3632ff5
4
- data.tar.gz: f2db309b7f7543a82736dd39b216de1b003d75c92c044582f95999b010077677
3
+ metadata.gz: f26cf6f3329f5fce5c415a68148b495d144c3339b4c4e9bf48509785c70c5628
4
+ data.tar.gz: f563051727dad3f619aac32a69535bb6a197b0313bfd5bbfd38bde4763625873
5
5
  SHA512:
6
- metadata.gz: f26e3067e300d27354ffb3251474c7644d87df6b8efa72615ff4e6695437de65a395fc7654fbb06e21dfb52ca744131110e2b14590250b930d7492ef71b29ca3
7
- data.tar.gz: 1b86664a4d28868d44a4bd5c513a1c26e07483b0aae23562cd41c62bfc9bfc0a9ecb8cadc9fb44011445174b34524ff3905a34670bd24f7a9675b5d5e9dbb8fc
6
+ metadata.gz: cc6300944319d2e472719cd21e8b921086fbc38e4514a9767407dd684c16f86a069e3776e3c19d19fe3dbd0fa614a4adcb25ca54ad4fa74c1e855a71c1bff464
7
+ data.tar.gz: 66bd73096c3b70adf6034f975ce162f8f6abac511e65005500b2f2a086b256528e626f9523ff5111a8c2882459913da8f921c7eb2b7674288ea6d2aaeb614389
data/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.1 - 2026-09-30
6
+
7
+ - **错误归属**:多来源等待在 EOF 后继续时,后续 IO 选择错误只更新仍活跃的来源,保留已结束来源的 EOF 与尾部结果。
8
+ - **写入进度**:发送前对象转换或诊断中的嵌套写入超时,对当前命令报告零进度,并通过 `cause` 保留嵌套错误的真实进度。
9
+ - **调度性能**:已处理 EOF 使用身份集合,批量转接就绪使用一次性 IO 身份索引,减少多来源场景的重复检索;保留事件、读取和回调顺序。
10
+ - **脱敏性能**:流结束时先排除不可能匹配的秘密前缀长度,减少长尾未命中的扫描和分配;保留部分秘密、重叠及二进制语义。
11
+ - **进程归属**:父进程创建 PTY、fork 后才启动命令时,按实际启动者登记子进程归属,保证等待与关闭能回收该子进程;继承已有 PID 的副本仍不接管它。
12
+ - **启动清理**:错误管道关闭失败仍尝试另一端,清理错误不再覆盖原始 fork 错误或 `SpawnError`。
13
+ - **来源身份**:匹配分组、EOF 派发、就绪查询与转接按会话和 IO 对象身份处理;自定义值相等规则不再合并独立来源或遗漏可写目标。
14
+ - **转义恢复**:超时交付的字面转义前缀也进入正则历史,混合规则跨次转接时不会漏掉完整退出序列。
15
+ - **写入协议**:自定义监听器返回 `:wait_writable` 时按非法写入计数抛出 `IOError`,该哨兵只用于真实 IO 的非阻塞写入。
16
+
17
+ ## 0.6.0 - 2026-09-30
18
+
19
+ - `expect` 统一返回不可变 Result,移除 `expect_result`;保留最近结果的六个便捷读取方法。
20
+ - 布尔配置仅保留 predicate 与 setter;模式声明在匹配前冻结,内部 Pattern 使用私有 Data。
21
+ - 修正 Result 构造及 callable 日志协议的 RBS,API 门禁覆盖模块方法和 Data 构造接口。
22
+ - 明确单字符串命令的 Ruby shell 语义,迁移示例和检查脚本,移除 Perl 差分工具与过时接口迁移表。
23
+ - **Ruby 现代化(破坏性)**:最低 Ruby 3.4;CI 使用 Ruby 3.4/4.0。`Result` 改为不可变 Data,复制冻结文本和捕获值,移除字段写入和 `to_a`,使用 `to_h` 或模式解构。
24
+ - **职责重组**:用户门面委托内部 Session;日志、终端和交互成为职责模块,Matcher/Relay 直接调用内核协议;回调与结果来源仍为用户会话。
25
+ - **清理可靠性**:统一始终/失败清理作用域,日志交接和终端恢复的常规清理错误不再覆盖原始异常;保留所属资源、PID 与转接恢复语义。
26
+ - **工程门禁**:增加公开 API 的 YARD/RBS 与覆盖检查、README 版本校验;精简 Gem 为运行源码、类型签名及使用文档,开发材料保留在仓库。
27
+ - **独立检查脚本**:API 检查按脚本位置定位项目文件,可从任意工作目录执行;发布工具仅在联网发布时加载 `net/http`。
28
+ - **测试稳定性**:模式解构断言使用 Ruby 原语;Ctrl-C 用例以处理器专用退出码验证信号,不依赖终端可能刷掉的输出或固定 sleep。
29
+
30
+ - 移除多余的 io-wait 依赖和加载,使用 Ruby 内置的 IO 等待方法,避免新版空壳 gem 的弃用警告。
31
+ - 保留标准库运行时依赖下限,去掉无已知兼容性依据的上限,避免限制宿主应用依赖解析。
32
+
5
33
  ## 0.5.3 - 2026-09-27
6
34
 
7
35
  - **初始化清理**:资源账本建立前的中断也会尝试关闭全部所属 IO,保留借用 IO 与原始异常身份。
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  IO、同时监听多个会话和转接人工交互。交互能力参考 [Expect.pm](https://github.com/jacoby/expect.pm),接口采用 Ruby
7
7
  的属性、关键字参数和代码块。
8
8
 
9
- 要求 **Ruby 3.2+、POSIX 系统(Linux/macOS)**。运行时仅使用 Ruby 标准库,其中可独立安装的 gem 已在 gemspec 中声明,由
9
+ 要求 **Ruby 3.4+、POSIX 系统(Linux/macOS)**。运行时仅使用 Ruby 标准库,其中可独立安装的 gem 已在 gemspec 中声明,由
10
10
  RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提供独立的 `Expect` 类,不修改标准库的 `IO#expect`。
11
11
 
12
12
  源码中的解释性注释主要使用中文;欢迎用中文或英文提交 issue 和 PR,参与方式见 [贡献指南](CONTRIBUTING.md)。
@@ -16,7 +16,7 @@ RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提
16
16
  项目和仓库名为 `expect-ruby`,Gem 名为 `expect-pty`。在应用的 Gemfile 中添加以下内容,然后运行 `bundle install`:
17
17
 
18
18
  ```ruby
19
- gem "expect-pty", "~> 0.5.2", require: "expect/pty"
19
+ gem "expect-pty", "~> 0.6.1", require: "expect/pty"
20
20
  ```
21
21
 
22
22
  也可直接执行 `gem install expect-pty`。需要跟随开发分支时,可从 GitHub 安装:
@@ -29,8 +29,8 @@ gem "expect-pty", git: "https://github.com/gatework/expect-ruby.git", branch: "m
29
29
 
30
30
  ```sh
31
31
  mkdir -p tmp
32
- gem build expect-pty.gemspec --output tmp/expect-pty-0.5.2.gem
33
- gem install ./tmp/expect-pty-0.5.2.gem
32
+ gem build expect-pty.gemspec --output tmp/expect-pty-0.6.1.gem
33
+ gem install ./tmp/expect-pty-0.6.1.gem
34
34
  ```
35
35
 
36
36
  ```ruby
@@ -38,7 +38,7 @@ require "expect/pty"
38
38
 
39
39
  Expect.spawn("/bin/sh", "-i") do |shell|
40
40
  shell.puts("printf 'hello ruby\\n'")
41
- if shell.expect(/^hello ruby\r?$/, timeout: 3)
41
+ if shell.expect(/^hello ruby\r?$/, timeout: 3).matched?
42
42
  puts shell.match
43
43
  else
44
44
  warn shell.error
@@ -94,7 +94,7 @@ end
94
94
  | `reset_timeout_on_read` | `false` | 每次收到数据时重置匹配期限 |
95
95
  | `graceful_close` | `false` | `close` 先尝试软关闭,再完成强制清理 |
96
96
 
97
- 布尔属性均提供 `name`、`name?` 和 `name=`,按 Ruby 真值规则转换:仅 `nil` / `false` 为假,`0` 为真。超时必须有限且非负,`nil`
97
+ 布尔属性只提供 `name?` 和 `name=`,按 Ruby 真值规则转换:仅 `nil` / `false` 为假,`0` 为真。超时必须有限且非负,`nil`
98
98
  表示无限;无效赋值不改变原值。`debug_level` 仅接受整数 `0..3`。
99
99
 
100
100
  ## 等待和匹配
@@ -109,11 +109,11 @@ session.expect(timeout: 1) # 仅收集输出,直到超时或 EOF
109
109
  ```
110
110
 
111
111
  字符串始终按字面匹配,包括 `"-i"`、`"-re"`、`"timeout"` 和 `"eof"`;正则直接使用 Ruby `Regexp`
112
- 。按声明顺序选择第一个能匹配的模式,不按它们在文本中的位置排序。返回模式的 **1 起始序号**,超时、EOF 或 IO 错误返回 `nil`
113
- 。超时使用单调时钟。
112
+ 。按声明顺序选择第一个能匹配的模式,不按它们在文本中的位置排序。返回不可变 `Expect::Result`,`number` 为模式的 **1 起始序号**;超时、EOF 或 IO 错误时 `number` 为 `nil`。
113
+ 判断成功使用 `matched?`,不能直接判断 Result 对象的真值。超时使用单调时钟。
114
114
 
115
115
  ```ruby
116
- result = session.expect_result(/value=(\d+)/, timeout: 3)
116
+ result = session.expect(/value=(\d+)/, timeout: 3)
117
117
  result.matched?
118
118
  result.timeout?
119
119
  result.eof?
@@ -122,17 +122,18 @@ result.captures
122
122
  result.session
123
123
  result.error # nil、:timeout、:eof 或原始 IOError / SystemCallError 对象
124
124
 
125
- number, error, match, before, after, session, captures = result.to_a
125
+ result => { number:, match:, captures: }
126
126
  ```
127
127
 
128
- `Result` 使用原生 Ruby `Struct`,支持 `to_a`、`to_h`、模式解构;没有隐式 `to_ary`。会话提供 `last_result`,以及 `match`、
128
+ `Result` 使用原生 Ruby `Data`,支持 `to_h`、位置和键模式解构。结果、文本和捕获数组均为不可变快照;
129
+ 来源会话与原始异常保持原对象。不提供字段写入、`to_a` 或隐式 `to_ary`。会话提供 `last_result`,以及 `match`、
129
130
  `before`、`after`、`match_number`、`captures`、`error` 快捷读取方法。
130
131
 
131
132
  成功匹配后删除匹配内容及其之前的内容,尾部留给下次匹配;超时保留缓冲,EOF 将未匹配内容放入 `before` 并清空缓冲。EOF
132
133
  与子进程退出是不同事件,使用 `wait` / `process_status` 判断进程结果。底层 IO 错误保留原始异常,回调中的普通异常直接抛出。
133
134
 
134
135
  接收缓冲、匹配和捕获值为 `ASCII-8BIT` 字节串,保留控制字符、NUL 和无效 UTF-8。固定 UTF-8
135
- 正则会等待读取末尾拆开的字符收齐后再匹配,以免尾部锚点提前命中;显示捕获内容时可 `.force_encoding("UTF-8")`
136
+ 正则会等待读取末尾拆开的字符收齐后再匹配,以免尾部锚点提前命中;显示捕获内容时可 `.dup.force_encoding("UTF-8")`
136
137
  。任意二进制流请用字面字符串或二进制正则 `/.../n`。固定 UTF-8 正则遇到无效数据抛出 `EncodingError`;EOF
137
138
  时仍未收齐的字符也属于无效编码,匹配缓冲保留原字节供诊断或二进制匹配。缓冲上限按字节截断,应为文本设置足够的上限。
138
139
 
@@ -167,14 +168,13 @@ end
167
168
  ```
168
169
 
169
170
  回调通过闭包访问局部变量。无参数声明块在模式构建器中执行;希望保留调用方 `self` 时使用 `do |patterns|`,调用
170
- `patterns.on(...)`。所有模式注册完成后才读取 IO;注册异常或 `break` 不消费输入。块和位置模式不能混用。`expect_result`
171
- 支持同样的声明方式。
171
+ `patterns.on(...)`。所有模式注册完成后才读取 IO;注册异常或 `break` 不消费输入。块和位置模式不能混用。注册完成后规则冻结,回调不能再向本次等待追加模式。
172
172
 
173
173
  `continue` 继续等待并重新计时;`continue(reset_timeout: false)`
174
174
  保留原期限,类和实例均可调用。回调返回后若保留的期限已过,不再扫描新的文本匹配,未消费的输入留给下一次等待。无回调或返回其他值时结束本次匹配。超时回调只有返回重置计时的
175
175
  `continue` 才再次等待。EOF 回调继续时移除该源并等待其余会话;已知的 EOF 仍依次派发,全部 EOF 时直接返回,期限已过时仅对剩余活跃源触发超时。
176
176
 
177
- `expect` / `expect_result` 另接受 `deadline:`,值为 `Expect.monotonic` 时钟上的绝对秒数,`nil` 表示不设总期限。总期限与普通
177
+ `expect` 另接受 `deadline:`,值为 `Expect.monotonic` 时钟上的绝对秒数,`nil` 表示不设总期限。总期限与普通
178
178
  `timeout` 取较早者,接收重置、文本/EOF 继续及超时回调均不能延长它。同一个 deadline 可用于连续多次等待:
179
179
 
180
180
  ```ruby
@@ -343,7 +343,7 @@ IO 期限不会强行中断这些代码。普通 `expect` 的同步日志和监
343
343
  `ArgumentError`。日志包括被转接过滤的转义,显式启用 `redact` 时遮盖注册秘密;在 `expect` / `interconnect` 之间切换不会重复记录。
344
344
 
345
345
  一次转接尚未返回时,递归 `interconnect` 的来源若与活跃来源重叠,会在移动缓冲和修改发送游标前抛出
346
- `Expect::ReentrancyError`。完全独立的来源仍可嵌套转接;`on_sequence` 中的嵌套 `expect/expect_result` 及返回后再次转接仍受支持。
346
+ `Expect::ReentrancyError`。完全独立的来源仍可嵌套转接;`on_sequence` 中的嵌套 `expect` 及返回后再次转接仍受支持。
347
347
  自定义 `write` 若已产生副作用却抛错、未返回计数,库无法推断已接受的字节数,此时不能保证恢复交付恰好一次。
348
348
  这一保护不代表所有会话 API 都可以跨线程并发调用。
349
349
 
@@ -404,7 +404,7 @@ ruby examples/ssh_interact.rb --auto
404
404
  的双终端示例与验证见 [examples/kibitz/](examples/kibitz/README.md)。
405
405
 
406
406
  [GitHub Actions](https://github.com/gatework/expect-ruby/actions/workflows/ci.yml) 在推送 `main`、推送 `v*` 标签、提交到
407
- `main` 的 Pull Request 或手动触发时运行。流水线覆盖 Ubuntu 24.04 / macOS 15 与 Ruby 3.2、3.3、3.4、4.0 的 8 种组合;每个环境执行
407
+ `main` 的 Pull Request 或手动触发时运行。流水线覆盖 Ubuntu 24.04 / macOS 15 与 Ruby 3.4、4.0 的 4 种组合;每个环境执行
408
408
  `script/ci`,包括真实 PTY 测试和构建包的隔离安装验证。Ubuntu / Ruby 4.0 作业保留已验证的 Gem 构建产物 14 天,可从该次工作流的
409
409
  Artifacts 下载。
410
410
 
@@ -420,7 +420,7 @@ Minitest、Rake、RuboCop 及发布工具的依赖。
420
420
  |---------------|---------------|--------------------------|
421
421
  | `forwardable` | `forwardable` | 会话配置委托 |
422
422
  | `io/console` | `io-console` | 终端模式和窗口大小 |
423
- | `io/wait` | `io-wait` | IO 可读等待 |
423
+ | `IO#wait_readable` | Ruby 3.2 内置 | IO 可读等待,无独立 gem |
424
424
  | `shellwords` | `shellwords` | `stty` 参数拆分 |
425
425
  | `stringio` | `stringio` | Ruby `puts` 语义 |
426
426
  | `pty` | Ruby 自带扩展 | POSIX 伪终端,无独立 gem |
@@ -435,6 +435,15 @@ known_hosts 文件。`ssh_auto.rb` 顶部 `COMMANDS` 可直接修改,日志写
435
435
  多脚本验证入口为 `test/integration/ssh_scripts.rb`,人工/自动接管入口为 `examples/ssh_interact.rb`
436
436
  ,详细配置及日志检查见 [SSH 测试说明](test/integration/README.md)。
437
437
 
438
- 当前接口迁移表见 [接口说明](docs/COMPATIBILITY.md),本次与历史验证分列在 [验证记录](docs/VERIFICATION.md)
438
+ 当前行为边界见 [接口说明](docs/COMPATIBILITY.md),本次与历史验证分列在 [验证记录](docs/VERIFICATION.md)
439
439
  。此次重构直接移除了旧入口,不提供兼容别名。
440
440
  防火墙连接器已迁移到相邻的 `algosec` 项目;本库只保留 `Expect` 与 `expect-pty` 通用传输能力。
441
+
442
+ ## 类型、文档与 0.6.0 接口变更
443
+
444
+ 0.6.0 最低支持 Ruby 3.4,包含不可变 `Result` 和内部 Session 重组。完整接口见 [API 文档](docs/API.md),从 0.5.x 升级前请阅读
445
+ [迁移说明](docs/MIGRATION.md)。类型签名随 Gem 发布在 `sig/expect.rbs`。
446
+
447
+ 发布包仅包含运行源码、类型签名和使用文档。测试、基准、示例及维护脚本请从仓库取得;
448
+ 运行 `bundle exec rake api` 验证文档覆盖与 RBS 声明,运行 `bundle exec yard doc --output-dir tmp/yard`
449
+ 生成 HTML 文档。签名验证不代表全库已经通过静态类型检查。
data/docs/API.md ADDED
@@ -0,0 +1,88 @@
1
+ # Ruby API
2
+
3
+ 支持 Ruby 3.4+ 和 Linux/macOS。加载入口为 `require "expect/pty"`,独立流过滤器可加载 `expect/redactor`。
4
+
5
+ ## 创建与关闭
6
+
7
+ `Expect.spawn(*command, env: {}, chdir: nil, **configuration)` 创建并启动 PTY;`Expect.new` 可先创建 PTY,再通过实例 `spawn` 启动。
8
+ 单字符串遵循 Ruby 的自动 shell 语义(含 shell 元字符时可能交给 shell);多个独立参数按 argv 执行。
9
+ 外部输入应作为独立参数传入,避免拼接进单字符串命令。启动失败抛出 `Expect::SpawnError`。
10
+
11
+ `Expect.open(io, writer: io, own: false, **configuration)` 适配真实可 select 的 IO。默认借用;`own: true` 接管关闭责任,
12
+ 包括初始化失败。类级 `spawn`、`open` 无块时返回 Expect,有块时返回块结果并确保关闭。非局部返回也清理。
13
+
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
+ 借用 IO 不随会话关闭。输入 EOF、IO 关闭与子进程退出分别由 `eof?`、`closed?`、`process_status` 表示。
17
+
18
+ ## 匹配与回调
19
+
20
+ `expect(*patterns, timeout:, deadline:)` 统一返回 `Expect::Result`;`number` 为从 1 开始的文本模式序号或 nil。
21
+ 使用 `matched?` 判断匹配成功,Result 对象本身始终为真值。
22
+ 类方法增加 `from:` 指定默认来源。模式为 String、Regexp、`:eof` 或 `:timeout`,位置模式与声明块不能混用。
23
+
24
+ ```ruby
25
+ result = session.expect(timeout: 3) do |patterns|
26
+ patterns.on("name: ") do |connection|
27
+ connection.puts("Ruby")
28
+ connection.continue
29
+ end
30
+ patterns.on(/hello (\w+)/)
31
+ end
32
+ result => { number:, captures: }
33
+ ```
34
+
35
+ 无参数声明块在 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 期限不会强行中断用户回调或单次正则计算。
40
+
41
+ Result 字段为 `number`、`error`、`match`、`before`、`after`、`session`、`captures`。对象、字符串及捕获数组均不可变,未参与捕获的组为 nil。
42
+ `matched?`、`timeout?`、`eof?` 查询结果。错误为 nil、`:timeout`、`:eof` 或原始 IO 异常;原始异常与来源会话不冻结。
43
+ 使用 `to_h`、位置/键模式解构或 `with` 构造新结果;没有字段 writer 或 `to_a`。
44
+ `last_result` 以及会话上的 `before`、`after`、`match`、`match_number`、`captures`、`error` 读取最近结果。
45
+
46
+ 匹配和捕获按原始字节保存;文本显示时先 `dup` 再 `force_encoding`。固定 UTF-8 正则等待尾部字符收齐,非法编码抛出 EncodingError。
47
+
48
+ ## 配置与读写
49
+
50
+ `Expect.configuration` 返回冻结默认配置,`Expect.configure(**options) { |config| ... }` 发布新快照;失败不发布部分设置。
51
+ 每个会话拥有独立配置,可以通过同名 reader/writer 修改。未知配置键抛出 ArgumentError。
52
+
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 先软关闭 |
61
+
62
+ 布尔项只提供 `name?` 和 `name=`,赋值遵循 Ruby 真值规则。`buffer` 返回副本,`buffer=` 复制并应用上限,`clear_buffer` 移交并清空缓冲;
63
+ `buffer_discarded_bytes` 累计窗口裁剪量,匹配消费不计入。
64
+
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=` 使用 `[行数, 列数]`。
69
+
70
+ ## 日志、脱敏与转接
71
+
72
+ `log_to(target, mode: "a")` 接收路径或可写目标,也可只给块。路径以私有权限打开并取得关闭责任;IO/块只借用。
73
+ `log_output=` 更换目标或设 nil 停止;`write_log` 补写日志而不发送到子进程。IO writer 必须返回实际接受的字节数,支持短写;
74
+ 日志块的返回值不控制匹配。日志记录真实读取,不因匹配/转接重复记录。
75
+
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`;流尾部默认隐藏疑似秘密前缀。
79
+
80
+ `listeners=` 校验并复制可写目标数组,`listeners` 返回副本。`Expect.interconnect(*sources, timeout:)` 按监听关系转接,
81
+ 返回停止来源或在总期限到达时返回 nil。`on_sequence(String/Regexp/:eof) { ... }` 注册无参数回调,nil/false 停止,其余值继续。
82
+ `pending_output?` 表示尚未交付数据;再次转接同源会话继续发送,不重放成功前缀。同源递归转接抛出 ReentrancyError,转义回调可以嵌套匹配。
83
+
84
+ `interact(input: $stdin, escape: nil, output: nil, timeout: nil)` 临时建立双向转接,退出后恢复监听关系和本地终端模式。
85
+ 日志、诊断及自定义 writer 回调同步执行,应及时返回。
86
+
87
+ `Expect.monotonic` 读取单调时钟,`Expect.duration` 校验秒数,`Expect.readable_sessions(*sources, timeout: 0)` 返回就绪来源。
88
+ 内部 Session、Matcher、Relay 和资源账本不属于用户 API。RBS 覆盖公开声明与协作协议;`rbs validate` 不验证 Ruby 方法体。
data/docs/MIGRATION.md ADDED
@@ -0,0 +1,15 @@
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。
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Expect
4
+ # 清理作用域只记录本次异常,不受调用者 rescue 中的旧 $! 影响。
5
+ # @api private
6
+ module Cleanup
7
+ # 包括非局部返回在内的所有退出均清理;常规清理错误只在没有原异常时传播。
8
+ def self.always(on_exit)
9
+ yield
10
+ rescue Exception # rubocop:disable Lint/RescueException -- 中断也必须清理,然后原样传播。
11
+ failed = true
12
+ raise
13
+ ensure
14
+ begin
15
+ on_exit.call
16
+ rescue IOError, SystemCallError
17
+ raise unless failed
18
+ end
19
+ end
20
+
21
+ # 完成初始化或交接后释放清理责任;异常与非局部退出仍回滚未发布资源。
22
+ def self.on_failure(on_exit)
23
+ completed = false
24
+ always(-> { on_exit.call unless completed }) do
25
+ result = yield
26
+ completed = true
27
+ result
28
+ end
29
+ end
30
+ end
31
+
32
+ private_constant :Cleanup
33
+ end
@@ -4,36 +4,36 @@ class Expect
4
4
  # 集中校验会话配置。类级默认值以冻结快照发布,每个会话再构造独立副本。
5
5
  # 这里只保存策略值,不持有 IO、缓冲或日志对象;修改默认配置不会追溯影响已创建的会话。
6
6
  class Configuration
7
- # 会话通过 Forwardable 委托这些属性;to_h 使用同一清单生成配置副本。
8
- ATTRIBUTES = %i[
9
- timeout write_timeout buffer_limit debug_level raw_pty preserve_buffer
10
- log_stdout log_listeners raw_terminal reset_timeout_on_read graceful_close
11
- ].freeze
12
- # 布尔属性同时提供普通读方法和问号查询,写入统一采用 Ruby 真值规则。
13
- PREDICATES = %i[
14
- raw_pty? preserve_buffer? log_stdout? log_listeners? raw_terminal?
15
- reset_timeout_on_read? graceful_close?
16
- ].freeze
17
-
18
- attr_reader :timeout, :write_timeout, :buffer_limit, :debug_level, :raw_pty, :preserve_buffer, :log_stdout,
19
- :log_listeners, :raw_terminal, :reset_timeout_on_read, :graceful_close
20
-
21
- # 通过 setter 校验构造参数,确保默认值、构造覆盖和后续赋值遵守同一规则。
22
- def initialize(timeout: nil, write_timeout: nil, buffer_limit: nil, debug_level: 0,
23
- raw_pty: false, preserve_buffer: false, log_stdout: false,
24
- log_listeners: true, raw_terminal: true, reset_timeout_on_read: false,
25
- graceful_close: false)
26
- self.timeout = timeout
27
- self.write_timeout = write_timeout
28
- self.buffer_limit = buffer_limit
29
- self.debug_level = debug_level
30
- self.raw_pty = raw_pty
31
- self.preserve_buffer = preserve_buffer
32
- self.log_stdout = log_stdout
33
- self.log_listeners = log_listeners
34
- self.raw_terminal = raw_terminal
35
- self.reset_timeout_on_read = reset_timeout_on_read
36
- self.graceful_close = graceful_close
7
+ # 各配置项的初始值;会话仅保存经 setter 验证后的副本。
8
+ DEFAULTS = {
9
+ timeout: nil, write_timeout: nil, buffer_limit: nil, debug_level: 0,
10
+ raw_pty: false, preserve_buffer: false, log_stdout: false,
11
+ log_listeners: true, raw_terminal: true, reset_timeout_on_read: false,
12
+ graceful_close: false
13
+ }.freeze
14
+ # 属性委托和快照导出使用同一配置清单。
15
+ ATTRIBUTES = DEFAULTS.keys.freeze
16
+ # 配置键与读取接口分开:布尔值只提供谓词,导出仍使用原配置键。
17
+ READERS = DEFAULTS.to_h do |name, value|
18
+ [name, [true, false].include?(value) ? :"#{name}?" : name]
19
+ end.freeze
20
+
21
+ attr_reader :timeout, :write_timeout, :buffer_limit, :debug_level
22
+
23
+ # 布尔读写遵循 Ruby 真值规则,仅提供问号查询和 setter。
24
+ def self.boolean_attribute(name)
25
+ define_method(:"#{name}?") { instance_variable_get(:"@#{name}") }
26
+
27
+ define_method(:"#{name}=") { |value| instance_variable_set(:"@#{name}", !!value) }
28
+ end
29
+ private_class_method :boolean_attribute
30
+
31
+ # 先拒绝未知键,再经 setter 校验;发布中的冻结快照不会被部分修改。
32
+ def initialize(**options)
33
+ unknown = options.keys - ATTRIBUTES
34
+ raise ArgumentError, "unknown configuration: #{unknown.join(", ")}" unless unknown.empty?
35
+
36
+ DEFAULTS.merge(options).each { |name, value| public_send(:"#{name}=", value) }
37
37
  end
38
38
 
39
39
  # 设置匹配等待的默认秒数;nil 表示无限等待,0 表示只轮询现有数据。
@@ -65,58 +65,79 @@ class Expect
65
65
  end
66
66
 
67
67
  # 控制 spawn 前是否将子进程终端设为 raw,关闭回显和换行转换。
68
- def raw_pty=(value)
69
- @raw_pty = !!value
70
- end
71
-
72
- alias raw_pty? raw_pty
68
+ # @!method raw_pty?
69
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
70
+ # @return [Boolean] 当前布尔配置。
71
+ # @!method raw_pty=(value)
72
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
73
+ # @param value [Object] 除 nil/false 外均转换为 true。
74
+ # @return [Boolean] 归一化的布尔配置。
75
+ boolean_attribute :raw_pty
73
76
 
74
77
  # 控制匹配成功后是否保留完整缓冲;启用时由继续回调自行消费匹配内容。
75
- def preserve_buffer=(value)
76
- @preserve_buffer = !!value
77
- end
78
-
79
- alias preserve_buffer? preserve_buffer
78
+ # @!method preserve_buffer?
79
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
80
+ # @return [Boolean] 当前布尔配置。
81
+ # @!method preserve_buffer=(value)
82
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
83
+ # @param value [Object] 除 nil/false 外均转换为 true。
84
+ # @return [Boolean] 归一化的布尔配置。
85
+ boolean_attribute :preserve_buffer
80
86
 
81
87
  # 控制接收字节是否同步输出到当前 $stdout;默认关闭。
82
- def log_stdout=(value)
83
- @log_stdout = !!value
84
- end
85
-
86
- alias log_stdout? log_stdout
88
+ # @!method log_stdout?
89
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
90
+ # @return [Boolean] 当前布尔配置。
91
+ # @!method log_stdout=(value)
92
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
93
+ # @param value [Object] 除 nil/false 外均转换为 true。
94
+ # @return [Boolean] 归一化的布尔配置。
95
+ boolean_attribute :log_stdout
87
96
 
88
97
  # 控制接收字节是否转发给监听器,与 stdout 和日志目标分别管理。
89
- def log_listeners=(value)
90
- @log_listeners = !!value
91
- end
92
-
93
- alias log_listeners? log_listeners
98
+ # @!method log_listeners?
99
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
100
+ # @return [Boolean] 当前布尔配置。
101
+ # @!method log_listeners=(value)
102
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
103
+ # @param value [Object] 除 nil/false 外均转换为 true。
104
+ # @return [Boolean] 归一化的布尔配置。
105
+ boolean_attribute :log_listeners
94
106
 
95
107
  # 控制 interact 是否临时设置并恢复本地输入终端;通用 interconnect 不修改终端模式。
96
- def raw_terminal=(value)
97
- @raw_terminal = !!value
98
- end
99
-
100
- alias raw_terminal? raw_terminal
108
+ # @!method raw_terminal?
109
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
110
+ # @return [Boolean] 当前布尔配置。
111
+ # @!method raw_terminal=(value)
112
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
113
+ # @param value [Object] 除 nil/false 外均转换为 true。
114
+ # @return [Boolean] 归一化的布尔配置。
115
+ boolean_attribute :raw_terminal
101
116
 
102
117
  # 控制收到任何新数据时是否刷新匹配期限,适用于按静默时长判断超时。
103
118
  # 只刷新相对 timeout;单次等待显式指定的绝对 deadline 仍是不可延长的上限。
104
- def reset_timeout_on_read=(value)
105
- @reset_timeout_on_read = !!value
106
- end
107
-
108
- alias reset_timeout_on_read? reset_timeout_on_read
119
+ # @!method reset_timeout_on_read?
120
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
121
+ # @return [Boolean] 当前布尔配置。
122
+ # @!method reset_timeout_on_read=(value)
123
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
124
+ # @param value [Object] 除 nil/false 外均转换为 true。
125
+ # @return [Boolean] 归一化的布尔配置。
126
+ boolean_attribute :reset_timeout_on_read
109
127
 
110
128
  # 控制通用 close 是否先软关闭;最终资源清理仍由硬关闭兜底。
111
- def graceful_close=(value)
112
- @graceful_close = !!value
113
- end
114
-
115
- alias graceful_close? graceful_close
129
+ # @!method graceful_close?
130
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
131
+ # @return [Boolean] 当前布尔配置。
132
+ # @!method graceful_close=(value)
133
+ # 查询或设置此会话策略,使用 Ruby 真值规则。
134
+ # @param value [Object] 除 nil/false 外均转换为 true。
135
+ # @return [Boolean] 归一化的布尔配置。
136
+ boolean_attribute :graceful_close
116
137
 
117
138
  # 导出新的属性 Hash,用于构造会话副本或发布下一份默认配置。
118
139
  def to_h
119
- ATTRIBUTES.to_h { |name| [name, public_send(name)] }
140
+ READERS.to_h { |name, reader| [name, public_send(reader)] }
120
141
  end
121
142
  end
122
143
  end