expect-pty 0.10.0 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/docs/API.md +4 -1
- data/docs/MIGRATION.md +4 -0
- data/lib/expect/interaction.rb +2 -2
- data/lib/expect/logging.rb +7 -2
- data/lib/expect/pattern.rb +2 -1
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/responder.rb +4 -2
- data/lib/expect/result.rb +8 -2
- data/lib/expect/session.rb +10 -4
- data/lib/expect/version.rb +1 -1
- data/sig/expect.rbs +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63830f1908113e813431d18f8e42dec3fb82db31bba040518daa206a86511ca2
|
|
4
|
+
data.tar.gz: e34b92d97f8253c87293c440a7f38bb0897c7c44e93c56c0c9225fa88fefb7d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88a1cd1a481521d93f47cb4144d8d72c3b37b6df7fc590024fbd20f6d3cb84c6f74b92944b0d1a4c61cf6db82354d1ea4257d7370adf2e4971e4140c2040f0ec
|
|
7
|
+
data.tar.gz: 8b2aafa1ec75aa2b9255b82cfa81a3fcc2931fcacdda7f273fa1f95beff57e9cf390f088f8ceec408d7d78c6dc6448df55043e0dd42c5fa1fc896142d506fb12
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.10.1 - 2026-10-30
|
|
6
|
+
|
|
7
|
+
- Normalize result, buffer and rule snapshots to plain String/Array values without freezing caller input or retaining attached behavior. Preserve raw bytes, encodings and borrowed object identity.
|
|
8
|
+
- Make Result pretty_print use the safe summary, and execute the same validated argument snapshot exposed by Session#command.
|
|
9
|
+
- Preserve to_s conversion and confirmed write progress while normalizing protocol and transcript bytes before callbacks.
|
|
10
|
+
|
|
5
11
|
## 0.10.0 - 2026-10-30
|
|
6
12
|
|
|
7
13
|
- 最低 Ruby 升级至 4.0.6;Result 明确区分 matched、timeout、eof、error,编号统一为 match_number,error 只保存原始异常。构造和 with 拒绝矛盾状态,旧 number 字段移除。
|
data/docs/API.md
CHANGED
|
@@ -67,7 +67,7 @@ nil 保留全缓冲扫描;指定时,字符串和正则只搜索最新的尾
|
|
|
67
67
|
固定 UTF-8 正则跳过边界切开的合法首字符,等待合法未完成尾字符;窗口内非法字节及 EOF 未完成字符仍报错。
|
|
68
68
|
二进制正则按原始字节匹配。
|
|
69
69
|
|
|
70
|
-
Result 的 `inspect` 与 `
|
|
70
|
+
Result 的 `inspect`、`to_s` 与 `pretty_print`(包括 `PP.pp`)仅显示匹配序号和错误状态,不打印原始文本、异常消息或来源对象。
|
|
71
71
|
需要原始结果时显式读取字段;`redact` 不改变这些字段。
|
|
72
72
|
Result 字段按顺序为 `status`、`match_number`、`error`、`match`、`before`、`after`、`session`、`captures`。
|
|
73
73
|
状态为 `:matched`、`:timeout`、`:eof` 或 `:error`;`error` 只保存原始异常或 nil,等待超时和 EOF 均不是异常。
|
|
@@ -75,6 +75,9 @@ Result 字段按顺序为 `status`、`match_number`、`error`、`match`、`befor
|
|
|
75
75
|
其余状态没有编号、match、after 或捕获,只有 `:error` 状态携带异常。参数、编码与回调异常仍按原规则直接抛出。
|
|
76
76
|
|
|
77
77
|
对象、文本及捕获数组均不可变,未参与的捕获为 nil;来源会话与异常不复制、不冻结。
|
|
78
|
+
文本与捕获容器重建为普通 String/Array,不继承调用方子类、附加状态或单例行为,也不冻结调用方输入。
|
|
79
|
+
手工构造结果保留原始字节与编码;协议缓冲、字面规则、回复、转义及脱敏配置使用普通二进制字符串副本。
|
|
80
|
+
写入仍先执行一次 `to_s`,随后固定普通字节副本;启动参数在修改终端前校验,实际执行与 `command` 使用同一快照。
|
|
78
81
|
使用 `to_h`、位置/键模式解构或 `with` 构造新结果;构造必须指定 status,new、[]、with 都拒绝矛盾状态与非字符串文本。
|
|
79
82
|
没有字段 writer、旧 number 或 to_a。会话的 `last_result`、`before`、`after`、`match`、`match_number`、`captures`、`error`
|
|
80
83
|
读取最近结果;最近等待状态使用 `last_result.status`,与输入 EOF、IO 关闭、进程状态分别查询。
|
data/docs/MIGRATION.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# 迁移说明
|
|
2
2
|
|
|
3
|
+
## 未发布:普通值快照
|
|
4
|
+
|
|
5
|
+
结果文本、捕获数组和字符串规则不再保留输入子类、实例变量或单例方法。依赖这些附加属性的调用方应在外部保存元数据。原始字节、结果编码、nil 捕获、字段与 Data 构造接口保持不变。`PP.pp(result)` 与 `inspect` 使用同一摘要;显式字段和 `to_h` 仍可读取原始值。
|
|
6
|
+
|
|
3
7
|
## 0.9.x → 0.10.0:Ruby 4 与明确的等待结果
|
|
4
8
|
|
|
5
9
|
- 最低运行环境为 Ruby 4.0.6,保留 Linux/macOS 支持;本地开发使用 RVM。
|
data/lib/expect/interaction.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Expect
|
|
|
30
30
|
|
|
31
31
|
# 只改变显示字节,不改写匹配输入;记住上一块的 CR,避免分块的 CRLF 被扩成 CRCRLF。
|
|
32
32
|
def render(data)
|
|
33
|
-
bytes = data.to_s.
|
|
33
|
+
bytes = String.new(data.to_s).force_encoding(Encoding::BINARY)
|
|
34
34
|
rendered = bytes.gsub(/(?<!\r)\n/n, "\r\n")
|
|
35
35
|
rendered = rendered.byteslice(1..) if @previous_carriage_return && bytes.start_with?("\n")
|
|
36
36
|
@previous_carriage_return = bytes.end_with?("\r")
|
|
@@ -49,7 +49,7 @@ module Expect
|
|
|
49
49
|
def on_sequence(sequence, &block)
|
|
50
50
|
key = case sequence
|
|
51
51
|
when :eof, Regexp then sequence
|
|
52
|
-
when String then sequence.
|
|
52
|
+
when String then String.new(sequence).force_encoding(Encoding::BINARY).freeze
|
|
53
53
|
else raise ArgumentError, "sequence must be a String, Regexp or :eof"
|
|
54
54
|
end
|
|
55
55
|
raise ArgumentError, "escape sequence must not be empty" if key == ""
|
data/lib/expect/logging.rb
CHANGED
|
@@ -28,11 +28,12 @@ module Expect
|
|
|
28
28
|
# 秘密仅作用于本会话的接收记录和诊断,不改写匹配或 outputs 的协议字节。
|
|
29
29
|
# 先校验全部值再发布;注册是追加操作,应在首次通信前完成,不能追溯已交付的日志。
|
|
30
30
|
def redact(*secrets)
|
|
31
|
+
secrets.map! { |secret| secret.is_a?(String) ? String.new(secret).force_encoding(Encoding::BINARY).freeze : secret }
|
|
31
32
|
unless secrets.any? && secrets.all? { |secret| secret.is_a?(String) && !secret.empty? }
|
|
32
33
|
raise ArgumentError, "secrets must be nonempty Strings"
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
@secrets = ((@secrets || []) + secrets
|
|
36
|
+
@secrets = ((@secrets || []) + secrets).uniq.freeze
|
|
36
37
|
@transcript_redactor.patterns = @secrets if @transcript_redactor
|
|
37
38
|
@diagnostic_redactors.each_value { |redactor| redactor.patterns = @secrets }
|
|
38
39
|
self
|
|
@@ -53,7 +54,11 @@ module Expect
|
|
|
53
54
|
return unless transcript
|
|
54
55
|
|
|
55
56
|
# 单对象沿用 Session#write 的转换路径,省去数组与 join;转换后仍重新取得目标。
|
|
56
|
-
data = objects.size == 1
|
|
57
|
+
data = if objects.size == 1
|
|
58
|
+
String.new(objects.first.to_s).force_encoding(Encoding::BINARY)
|
|
59
|
+
else
|
|
60
|
+
objects.map { |object| String.new(object.to_s).force_encoding(Encoding::BINARY) }.join("") # rubocop:disable Style/RedundantArgument -- 显式空分隔符隔离全局 $,。
|
|
61
|
+
end
|
|
57
62
|
# to_s 可切换或禁用记录目标;整条记录与其过滤尾部必须属于转换完成后的同一目标。
|
|
58
63
|
target = transcript
|
|
59
64
|
return unless target
|
data/lib/expect/pattern.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Expect
|
|
|
6
6
|
Pattern = Data.define(:match_number, :value, :callback) do
|
|
7
7
|
# 复制字符串模式;Regexp 和 callable 只引用,不取得外部对象的生命周期。
|
|
8
8
|
def initialize(value:, match_number: nil, callback: nil)
|
|
9
|
-
|
|
9
|
+
value = String.new(value).force_encoding(Encoding::BINARY).freeze if value.is_a?(String)
|
|
10
|
+
super
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
# 将匹配会话或超时会话数组交给回调;额外上下文由调用方闭包保存。
|
data/lib/expect/redactor.rb
CHANGED
|
@@ -15,11 +15,12 @@ module Expect
|
|
|
15
15
|
|
|
16
16
|
# 复制冻结替换文本与规则;pending 保存尚不能安全输出的原字节,hidden 用 0/1 标记遮盖。
|
|
17
17
|
def initialize(patterns, replacement: "[FILTERED]")
|
|
18
|
+
replacement = String.new(replacement).force_encoding(Encoding::BINARY) if replacement.is_a?(String)
|
|
18
19
|
unless replacement.is_a?(String) && !replacement.empty?
|
|
19
20
|
raise ArgumentError, "replacement must be a nonempty String"
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
@replacement = replacement.
|
|
23
|
+
@replacement = replacement.freeze
|
|
23
24
|
self.patterns = patterns
|
|
24
25
|
@pending = "".b
|
|
25
26
|
@hidden = "".b
|
|
@@ -28,11 +29,16 @@ module Expect
|
|
|
28
29
|
|
|
29
30
|
# 更新后续匹配规则并保留已有尾部与掩码;不能追溯修改已经交付给日志目标的内容。
|
|
30
31
|
def patterns=(patterns)
|
|
32
|
+
if patterns.is_a?(Array)
|
|
33
|
+
patterns = Array.new(patterns).map! do |pattern|
|
|
34
|
+
pattern.is_a?(String) ? String.new(pattern).force_encoding(Encoding::BINARY).freeze : pattern
|
|
35
|
+
end
|
|
36
|
+
end
|
|
31
37
|
unless patterns.is_a?(Array) && patterns.all? { |pattern| pattern.is_a?(String) && !pattern.empty? }
|
|
32
38
|
raise ArgumentError, "patterns must be an Array of nonempty Strings"
|
|
33
39
|
end
|
|
34
40
|
|
|
35
|
-
@patterns = patterns.
|
|
41
|
+
@patterns = patterns.uniq.freeze
|
|
36
42
|
@lookbehind = [(@patterns.map(&:bytesize).max || 0) - 1, 0].max
|
|
37
43
|
@scanned_bytes = 0
|
|
38
44
|
end
|
|
@@ -74,11 +80,12 @@ module Expect
|
|
|
74
80
|
def append_chunk(data, visible:)
|
|
75
81
|
raise ArgumentError, "data must be a String" unless data.is_a?(String)
|
|
76
82
|
|
|
83
|
+
data = String.new(data).force_encoding(Encoding::BINARY)
|
|
77
84
|
# 没有规则且旧尾部已排空时,原样交付独立字节副本,不构造等长掩码。
|
|
78
85
|
# 非空明文会分隔隐藏区间;空分片不能改变以后重新启用规则时的边界。
|
|
79
86
|
if @patterns.empty? && @pending.empty?
|
|
80
87
|
@masking = false unless data.empty?
|
|
81
|
-
return visible ? data
|
|
88
|
+
return visible ? data : "".b
|
|
82
89
|
end
|
|
83
90
|
|
|
84
91
|
unless visible || data.empty?
|
|
@@ -86,7 +93,7 @@ module Expect
|
|
|
86
93
|
@suppressed_until = @pending.bytesize + data.bytesize
|
|
87
94
|
end
|
|
88
95
|
@suppressed << ((visible ? "\0" : "\1") * data.bytesize) if @suppressed
|
|
89
|
-
@pending << data
|
|
96
|
+
@pending << data
|
|
90
97
|
@hidden << ("\0" * data.bytesize)
|
|
91
98
|
mark_secrets
|
|
92
99
|
release([@pending.bytesize - @lookbehind, 0].max)
|
data/lib/expect/responder.rb
CHANGED
|
@@ -17,14 +17,16 @@ module Expect
|
|
|
17
17
|
# limit 是本规则在一次 expect 中允许的应答次数,nil 不限;规则文本和静态回复复制冻结。
|
|
18
18
|
def on(pattern, reply: nil, limit: nil, &block)
|
|
19
19
|
raise FrozenError, "responder is frozen" if frozen?
|
|
20
|
+
|
|
21
|
+
pattern = String.new(pattern).force_encoding(Encoding::BINARY).freeze if pattern.is_a?(String)
|
|
22
|
+
reply = String.new(reply).force_encoding(Encoding::BINARY).freeze if reply.is_a?(String)
|
|
20
23
|
unless (pattern.is_a?(String) && !pattern.empty?) || pattern.is_a?(Regexp)
|
|
21
24
|
raise ArgumentError, "response pattern must be a nonempty String or Regexp"
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
validate_action!(reply, limit, block)
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
@rules << [pattern, reply&.b&.freeze, limit, block].freeze
|
|
29
|
+
@rules << [pattern, reply, limit, block].freeze
|
|
28
30
|
self
|
|
29
31
|
end
|
|
30
32
|
|
data/lib/expect/result.rb
CHANGED
|
@@ -28,9 +28,10 @@ module Expect
|
|
|
28
28
|
session: nil, captures: [])
|
|
29
29
|
raise ArgumentError, "captures must be an Array" unless captures.is_a?(Array)
|
|
30
30
|
|
|
31
|
+
captures = Array.new(captures)
|
|
31
32
|
validate_outcome!(status, match_number, error, match, after, captures)
|
|
32
33
|
super(status:, match_number:, error:, session:, match: copy_text(match), before: copy_text(before),
|
|
33
|
-
after: copy_text(after), captures: captures.map { |value| copy_text(value) }.freeze)
|
|
34
|
+
after: copy_text(after), captures: captures.map! { |value| copy_text(value) }.freeze)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# 诊断只显示匹配序号和错误状态,不检查原始文本、异常或来源对象。
|
|
@@ -41,6 +42,11 @@ module Expect
|
|
|
41
42
|
# @return [String] 与 inspect 相同的结果摘要。
|
|
42
43
|
def to_s = inspect
|
|
43
44
|
|
|
45
|
+
# PP 使用同一摘要,不展开正文、异常或借用的来源对象。
|
|
46
|
+
# @param printer [PP] Ruby 标准 pretty printer。
|
|
47
|
+
# @return [void]
|
|
48
|
+
def pretty_print(printer) = printer.text(inspect)
|
|
49
|
+
|
|
44
50
|
# 是否命中文本;EOF 和超时没有模式序号。
|
|
45
51
|
def matched? = status == :matched
|
|
46
52
|
|
|
@@ -61,7 +67,7 @@ module Expect
|
|
|
61
67
|
|
|
62
68
|
raise ArgumentError, "result text must be a String or nil" unless value.is_a?(String)
|
|
63
69
|
|
|
64
|
-
value.
|
|
70
|
+
String.new(value).freeze
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
# 状态是唯一判据;new、[] 和 with 都经过同一边界,不能产生相互矛盾的结果。
|
data/lib/expect/session.rb
CHANGED
|
@@ -56,12 +56,14 @@ module Expect
|
|
|
56
56
|
|
|
57
57
|
# 在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。
|
|
58
58
|
def spawn(*command, env: {}, chdir: nil, raw: false, shell: false)
|
|
59
|
+
# 参数快照先于终端修改;公开记录与子进程使用同一份普通字符串。
|
|
60
|
+
command = command.map { |part| part.is_a?(String) ? String.new(part).freeze : part }.freeze
|
|
59
61
|
validate_spawn!(command)
|
|
60
62
|
raise ArgumentError, "shell must be true or false" unless [true, false].include?(shell)
|
|
61
63
|
raise ArgumentError, "shell requires one command string" if shell && command.size != 1
|
|
62
64
|
|
|
63
65
|
@slave.raw! if raw
|
|
64
|
-
@command = command
|
|
66
|
+
@command = command
|
|
65
67
|
Cleanup.on_failure(-> { hard_close }) do
|
|
66
68
|
child = @child_process.spawn(command, env:, chdir:, shell:) do |from_child, to_parent|
|
|
67
69
|
exec_child(command, env:, chdir:, from_child:, to_parent:, shell:)
|
|
@@ -147,7 +149,7 @@ module Expect
|
|
|
147
149
|
def buffer=(value)
|
|
148
150
|
raise ArgumentError, "buffer must be a String" unless value.is_a?(String)
|
|
149
151
|
|
|
150
|
-
@buffer = value.
|
|
152
|
+
@buffer = String.new(value).force_encoding(Encoding::BINARY)
|
|
151
153
|
@buffer_generation += 1
|
|
152
154
|
@relay_history.clear
|
|
153
155
|
trim_buffer
|
|
@@ -174,7 +176,11 @@ module Expect
|
|
|
174
176
|
|
|
175
177
|
begin
|
|
176
178
|
# 单对象不需要中间数组与 join 副本;to_s、诊断和部分写入仍经过同一条路径。
|
|
177
|
-
data = objects.size == 1
|
|
179
|
+
data = if objects.size == 1
|
|
180
|
+
String.new(objects.first.to_s).force_encoding(Encoding::BINARY)
|
|
181
|
+
else
|
|
182
|
+
objects.map { |object| String.new(object.to_s).force_encoding(Encoding::BINARY) }.join("") # rubocop:disable Style/RedundantArgument -- 显式空分隔符隔离全局 $,。
|
|
183
|
+
end
|
|
178
184
|
check_writable!
|
|
179
185
|
trace_data(:sending, data)
|
|
180
186
|
rescue WriteTimeout
|
|
@@ -375,7 +381,7 @@ module Expect
|
|
|
375
381
|
|
|
376
382
|
return mark_eof if data.nil?
|
|
377
383
|
|
|
378
|
-
data = data.
|
|
384
|
+
data = String.new(data).force_encoding(Encoding::BINARY)
|
|
379
385
|
buffer << data
|
|
380
386
|
@received_bytes += data.bytesize
|
|
381
387
|
trim_buffer if trim
|
data/lib/expect/version.rb
CHANGED
data/sig/expect.rbs
CHANGED
|
@@ -253,6 +253,8 @@ module Expect
|
|
|
253
253
|
|
|
254
254
|
def deconstruct: () -> [result_status, Integer?, result_error, String?, String?, String?, Session?, Array[String?]]
|
|
255
255
|
|
|
256
|
+
def pretty_print: (untyped printer) -> void
|
|
257
|
+
|
|
256
258
|
def with: (
|
|
257
259
|
?status: result_status,
|
|
258
260
|
?match_number: Integer?,
|