textfsm 0.5.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a3f20945f62b07ca8262e22d04c755ab0490ca5fe18ff649321e446a632ab76
4
- data.tar.gz: 1408f5f71df6b78c372aa0cda2f834e9585d7604641d0c716912c5b8bfbb785a
3
+ metadata.gz: e9aef491cc7a64406840450bdb59d12d767c905796686061866a1ec213a4163a
4
+ data.tar.gz: 396eb22fdc6af17a7f717703729ba336a8105da09d45c24fef7bee25ba9e6d1f
5
5
  SHA512:
6
- metadata.gz: 9eea0e867fcb1abedc089599916fd4caa9ed8530977d5d0438f70334ef71834cb1750cde7017826a8c377967458cab1366ff6200dcccda53bedb89a3fb1e9d65
7
- data.tar.gz: 722dbbae3727a2a8c38fe20c97330cfe893e07dfd86f38ddd7c78cc606787959fdc3c090b953378b8a1e94b3cf68b3832763d01d7106d5380d654c0f1ef5d67f
6
+ metadata.gz: 1dfc58294466d8a5bca5aafa38d05d72803a3195eeb9983202b9bb92e4794fef5bfe8b89f5eded6219580020d70c899c29f60e4fb6140c5514c13f21debeac45
7
+ data.tar.gz: 9d30f8b89367e81303280c86cc60efb3e26cbc041cd3748909e2e8ad8ff02c30484d08b08692dc03a25c6a416ec054bd3e648742f79aefd0ac85a82a223685df
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.5.1(2026-10-11)
4
+
5
+ - 拒绝缺少 `Value` 声明的模板,防止库与 CLI 验证静默接受无字段模板;多模板加载失败时保留之前的结果。
6
+ - 自定义选项产生的 `false` 按有效值处理,不再被 `Required` 丢弃或被 `Fillup` 覆盖,保持回填、继承与清除的生命周期契约。
7
+ - 索引属性转换取得逐属性冻结快照,允许不同属性的 `to_s` 复用缓冲区而不污染本次查询。
8
+ - 索引预计算列位置并用数组保存编译模式,减少加载时的临时对象与逐行扫描的列名查找;增加可复现基准。
9
+
3
10
  ## 0.5.0(2026-10-11)
4
11
 
5
12
  - 修复索引转换回调复用缓冲区时覆盖此前单元格的问题,逐单元格取得独立冻结快照。
data/README.md CHANGED
@@ -31,7 +31,7 @@ gem "textfsm", path: "/path/to/textfsm"
31
31
  ```sh
32
32
  bundle install
33
33
  bundle exec rake build
34
- gem install ./pkg/textfsm-0.5.0.gem
34
+ gem install ./pkg/textfsm-0.5.1.gem
35
35
  ```
36
36
 
37
37
  源码仓库:[gatework/textfsm](https://github.com/gatework/textfsm)。
@@ -73,7 +73,7 @@ p fsm.parse_hashes(text)
73
73
 
74
74
  ## 模板语法
75
75
 
76
- 模板先声明连续的 `Value`,用空行分隔状态。初始状态必须叫 `Start`。状态之间用空行分隔;规则以一个空格、两个空格或一个制表符缩进,随后必须是 `^`。以 `#` 开始的注释行会被忽略。
76
+ 模板至少声明一个 `Value`,连续的声明与状态之间用空行分隔。初始状态必须叫 `Start`。状态之间用空行分隔;规则以一个空格、两个空格或一个制表符缩进,随后必须是 `^`。以 `#` 开始的注释行会被忽略。
77
77
 
78
78
  ```text
79
79
  Value [选项,选项] NAME (正则表达式)
@@ -185,7 +185,7 @@ cisco_version_template, Cisco, sh[[ow]] ve[[rsion]]
185
185
 
186
186
  按文件顺序选择第一条匹配记录;匹配从属性字符串开头开始,空单元格是通配条件,index 未定义的属性会被忽略。`Command` 中 `sh[[ow]]` 支持 `sh`、`sho`、`show`。index 遵循上游简单逗号分隔格式,不处理带引号的 CSV 字段。
187
187
 
188
- 每次查询按需将参与匹配的属性值转换为字符串,并在本次扫描中复用;未知属性和仅遇到通配条件的值不会触发转换。Symbol 与同名 String 属性分别参与匹配,传入的属性集合不会被修改。
188
+ 每次查询按需将参与匹配的属性值转换为字符串,并在本次扫描中复用冻结快照;不同属性的 `to_s` 可以复用可变缓冲区,已转换的值不会被后续转换覆盖。未知属性和仅遇到通配条件的值不会触发转换。Symbol 与同名 String 属性分别参与匹配,传入的属性集合不会被修改。
189
189
 
190
190
  加载索引时,相同模式在当前索引内只编译一次;`IndexTable` 的 `transform:` 和 `compile:` 回调仍按文件顺序逐单元格执行,回调返回值不会被原地修改。
191
191
 
@@ -205,6 +205,8 @@ table.parse(output, templates: "first.textfsm:second.textfsm")
205
205
 
206
206
  记录格式以字符串内容、数值和集合/Struct 成员为准;不要在字符串或 Struct 的额外实例变量中附加可变业务状态,这些附加状态不属于深拷贝契约。
207
207
 
208
+ 自定义选项产生的 `false` 是有效记录值,能满足 `Required`,也能参与 `Fillup`/`Filldown`;后续 `Fillup` 遇到已记录的 `false` 时停止。只有 `nil` 或响应 `empty?` 且返回真值的对象被视为空值,例如空字符串、Array 和 Hash。
209
+
208
210
  `CliTable#keys` 返回模板声明的键,使用 `table.keys = ["ID"]` 显式设置,或 `table.keys += ["NAME"]` 增补。`key_for(row)` 返回键值数组,没有键时返回 `[]`。排序遵循 Ruby 自身的比较规则:
209
211
 
210
212
  ```ruby
@@ -260,6 +262,7 @@ parser = TextFSM::Parser.new(template, options: { "Uppercase" => Uppercase })
260
262
  - 内部动作使用 Symbol,省略目标状态使用 `nil`;模板文本继续使用标准的 `Next`、`Record`、`Clearall` 等语法。
261
263
  - 正则编译使用 `StringScanner`,编译器的游标和分组栈不保留在最终 `Pattern` 对象中。模式始终从输入开头匹配。
262
264
  - 多对一键合并按右行缓存新增列的冻结快照,缓存仅保留在本次合并中。可用 `ruby script/benchmark_merge.rb` 测量这一场景的耗时和对象分配;`ROWS`、`ITEMS` 分别控制左行数和嵌套成员数。此基准不代表所有模板或合并场景。
265
+ - 索引将列名预计算为位置,内部编译模式按表头顺序保存在数组中,查询保持属性顺序与短路规则。可用 `ruby script/benchmark_index.rb` 测量较晚命中的加载、扫描耗时和对象分配;`ROWS`、`QUERIES` 控制索引行数与查询次数,此基准不代表所有查询分布。
263
266
  - `bundle exec rake` 执行 Minitest 和 RuboCop;`rake test`、`rake lint` 可分别运行。代码规则见 `.rubocop.yml`。
264
267
  - `bundle exec rake verify` 进一步构建 gem,从 `bundle install` 准备的本地缓存离线安装运行时依赖和候选包;安装环境仅使用临时目录的 gem 与 Ruby 自带的默认 gem。逐文件核对源码内容,并验证独立加载与 CLI 输出、标准输入和退出码,避免开发依赖掩盖缺失的运行时依赖。生成的包位于 `pkg/`,不会发布到 RubyGems。
265
268
 
@@ -269,7 +272,7 @@ parser = TextFSM::Parser.new(template, options: { "Uppercase" => Uppercase })
269
272
  工作流验证并上传同次 CI 的 gem,再核对 RubyGems 下载和 GitHub Release 的 SHA256。
270
273
  流程、账号绑定及失败恢复见 [发布说明](docs/RELEASING.md)。
271
274
 
272
- 本地预检可执行 `bundle exec ruby script/release.rb --dry-run --artifact pkg/textfsm-0.5.0.gem`;
275
+ 本地预检可执行 `bundle exec ruby script/release.rb --dry-run --artifact pkg/textfsm-0.5.1.gem`;
273
276
  已有产物路径不会重新构建。更新版本及 CHANGELOG、提交并确认远端 CI 后推送对应版本标签。
274
277
 
275
278
  ## 兼容范围与验证
data/lib/textfsm/data.rb CHANGED
@@ -4,6 +4,10 @@ module TextFSM
4
4
  # 记录可包含嵌套列表与命名捕获;所有权边界复制这些值,
5
5
  # 需要只读结果时返回深度冻结的快照。
6
6
  module Data
7
+ def self.empty?(value)
8
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
9
+ end
10
+
7
11
  def self.copy(value, immutable: false)
8
12
  # 只有整个值树都不可变时才共享,冻结外层容器并不代表成员已冻结。
9
13
  return value if immutable && immutable?(value)
data/lib/textfsm/field.rb CHANGED
@@ -5,6 +5,7 @@
5
5
  require_relative "errors"
6
6
  require_relative "options"
7
7
  require_relative "pattern"
8
+ require_relative "data"
8
9
 
9
10
  module TextFSM
10
11
  class Field
@@ -31,7 +32,7 @@ module TextFSM
31
32
  end
32
33
 
33
34
  def empty?
34
- !@value || (@value.respond_to?(:empty?) && @value.empty?)
35
+ Data.empty?(@value)
35
36
  end
36
37
 
37
38
  def assign(value)
@@ -11,9 +11,10 @@ module TextFSM
11
11
  include Enumerable
12
12
 
13
13
  # 转换结果仅属于当前查询;通配条件或未使用的属性不会触发转换。
14
- Attribute = Struct.new(:name, :value) do
14
+ Attribute = Struct.new(:column, :value) do
15
15
  def text
16
- @text ||= value.to_s
16
+ # 不同属性的 to_s 可复用同一缓冲区,缓存必须持有本次转换的快照。
17
+ @text ||= Data.copy(value.to_s, immutable: true)
17
18
  end
18
19
  end
19
20
  private_constant :Attribute
@@ -22,6 +23,7 @@ module TextFSM
22
23
 
23
24
  def initialize(path, transform: nil, compile: nil)
24
25
  @header, @rows = Data.copy(read_table(path, transform), immutable: true)
26
+ @columns = @header.each_with_index.to_h.freeze
25
27
  @patterns = compile_patterns(compile)
26
28
  freeze
27
29
  rescue TemplateError => e
@@ -50,12 +52,12 @@ module TextFSM
50
52
  # 空单元格为通配条件;索引中不存在的属性忽略。
51
53
  def match(attributes)
52
54
  attributes = attributes.filter_map do |key, value|
53
- name = key.to_s
54
- Attribute.new(name, value) if @header.include?(name)
55
+ column = @columns[key.to_s]
56
+ Attribute.new(column, value) if column
55
57
  end
56
58
  index = @patterns.index do |patterns|
57
59
  attributes.all? do |attribute|
58
- pattern = patterns[attribute.name]
60
+ pattern = patterns[attribute.column]
59
61
  pattern.nil? || pattern.match?(attribute.text)
60
62
  end
61
63
  end
@@ -68,13 +70,13 @@ module TextFSM
68
70
  # 仅在本次构建中共享不可变模式;回调仍逐单元格执行,缓存不跨索引保留。
69
71
  compiled = {}
70
72
  @rows.map do |row|
71
- row.to_h do |key, value|
73
+ row.map do |key, value|
72
74
  value = compile.call(key, value) if compile
73
75
  unless value.nil? || value.is_a?(String)
74
76
  raise TemplateError, "Index pattern for column #{key.inspect} must be a String or nil, got #{value.class}"
75
77
  end
76
78
 
77
- [key, value.nil? || value.empty? ? nil : (compiled[value] ||= Pattern.new(value))]
79
+ value.nil? || value.empty? ? nil : (compiled[value] ||= Pattern.new(value))
78
80
  end.freeze
79
81
  end.freeze
80
82
  end
@@ -90,13 +90,13 @@ module TextFSM
90
90
  snapshot = nil
91
91
  (@rows.length - 1).downto(0) do |index|
92
92
  row = @rows[index]
93
- value = row[column]
94
- break if value && !(value.respond_to?(:empty?) && value.empty?)
93
+ break unless Data.empty?(row[column])
95
94
 
96
95
  # 记录不可变,既有快照可安全共享;回填通过替换整行完成。
97
96
  replacement = row.dup
98
97
  # Fillup 接收非空值,同一次回填共享冻结快照,导出时仍各自复制。
99
- replacement[column] = snapshot ||= Data.copy(field.value, immutable: true)
98
+ snapshot = Data.copy(field.value, immutable: true) if snapshot.nil?
99
+ replacement[column] = snapshot
100
100
  @rows[index] = replacement.freeze
101
101
  end
102
102
  end
@@ -197,6 +197,7 @@ module TextFSM
197
197
  end
198
198
 
199
199
  def validate_states
200
+ raise TemplateError, "No Value definitions found" if @fields.empty?
200
201
  raise TemplateError, "Missing state 'Start'." unless @states.key?("Start")
201
202
 
202
203
  %w[End EOF].each do |state|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TextFSM
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textfsm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - TextFSM Ruby contributors