textfsm 0.2.0 → 0.2.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: f381555ee4b666d74fb0fb9f8ed82d3881089872c9ce69d69bdb20e6febd1335
4
- data.tar.gz: 9edc61cf09c9e24a494a0fb15aba98cc57162afba32bafa6dd8ccee5c00b414c
3
+ metadata.gz: 83bccc705a542dc12d22b1d207d02ab788a6593b27600b387af652b765f384a3
4
+ data.tar.gz: 8d01a3b8b44ff7e9852b274b827738c76e01e1627110b4f2b1909ef8153cb1fd
5
5
  SHA512:
6
- metadata.gz: 7bb5d2d0907d9f1dee50c6db10cc3ad62681cbddb6d188a9053164502c37cbb3cb908d3538d3923f796ece79e311974fce95b63499595a69c5ab2a19901dea23
7
- data.tar.gz: ba2cc24808ac78daad55cbd6bf7bb9c228b9f3965d865ad0801bca3c19c5d406873fc189a6627ea2187e1366c3367b69750c2957a2d728d4d2e62bcafd390c89
6
+ metadata.gz: ff5180f0c3c3a307399af1eab10c3489c9ac717a8907cf8e79d8161da1717356fedab25c0370e8f9b9d26f8425d3c541ef9891e639eb89ce0ece7b9feed66258
7
+ data.tar.gz: e57c4ad056cf000b4495741b3dc12c3b16999259c85accf960823b66f53e49975ace6a4adef8f04e72802306e33609c86e1de8b7680614d7646807dd6bbd2dc5
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # 变更记录
2
+
3
+ ## 0.2.1(2026-10-01)
4
+
5
+ - 解析器复用深度冻结的记录快照,`Fillup` 通过替换记录回填,减少分段解析和读取结果的重复复制。
6
+ - 表合并只复制新增列,索引匹配每次查询只转换一次属性键,保持嵌套数据隔离与首条匹配语义。
7
+ - 显式传入 `templates: false` 时报告参数错误并保留上次结果,不再意外使用索引选取模板。
8
+ - CLI 遇到不兼容的输入编码时按错误契约返回退出码 `2`。
9
+ - 安装验证隔离全局与开发依赖,从本地缓存安装实际运行时依赖,防止漏声明的依赖被环境掩盖。
10
+ - 补齐 CLI 的 `optparse` 运行时依赖,保持 Ruby 3.1 及以上支持。
11
+ - 运行时依赖仅声明最低版本,不限制主版本;开发验证仍使用 `Gemfile.lock`。
12
+ - 收紧 gem 的可执行文件与示例文件清单,补充文档、变更记录链接和 RubyGems MFA 元数据。
13
+
14
+ ## 0.2.0(2026-09-12)
15
+
16
+ - 提供 `TextFSM::Parser`、`Field`、`Rule`、`Table` 和 `CliTable` Ruby API。
17
+ - 实现五种字段选项、状态动作、命令索引、多模板合并及 JSON/表格 CLI 输出。
18
+ - 结果使用独立快照;提供可修改的深拷贝、选项扩展钩子与 Python TextFSM 对照测试。
19
+ - 提供构建、隔离安装验证与 RubyGems 发布检查脚本。
data/README.md CHANGED
@@ -2,13 +2,25 @@
2
2
 
3
3
  使用 Ruby 实现的模板驱动文本解析状态机。读取 TextFSM 模板和半结构化文本,输出二维数组或 Hash 数组,适合解析网络设备 CLI 输出、系统命令输出等。
4
4
 
5
- 解析语义参考 [Google TextFSM 2.1.0](https://github.com/google/textfsm/tree/f80bbb459c55ff5f21651e48d2529722d667af97),运行时使用 Ruby 自身的正则引擎,不需要 Python 或外部进程;运行时依赖 Ruby 标准库 gem `json` 和 `strscan`。要求 Ruby 3.1 或以上。
5
+ 解析语义参考 [Google TextFSM 2.1.0](https://github.com/google/textfsm/tree/f80bbb459c55ff5f21651e48d2529722d667af97),运行时使用 Ruby 自身的正则引擎,不需要 Python 或外部进程;运行时依赖 Ruby 标准库 gem `json`、`optparse` 和 `strscan`。要求 Ruby 3.1 或以上。
6
6
 
7
7
  0.2 使用 `TextFSM` 模块组织 `Parser`、`Field`、`Rule`、`Table` 和 `CliTable`;公开方法与选项扩展接口按 Ruby 风格重新设计,不保留 0.1 的接口别名。
8
8
 
9
9
  ## 安装
10
10
 
11
- 本项目尚未发布到 RubyGems。在其他项目的 `Gemfile` 中使用本地路径:
11
+ 在项目的 `Gemfile` 中添加:
12
+
13
+ ```ruby
14
+ gem "textfsm"
15
+ ```
16
+
17
+ 然后运行 `bundle install`。也可以直接安装命令行工具:
18
+
19
+ ```sh
20
+ gem install textfsm
21
+ ```
22
+
23
+ 开发时可使用本地路径:
12
24
 
13
25
  ```ruby
14
26
  gem "textfsm", path: "/path/to/textfsm"
@@ -19,7 +31,7 @@ gem "textfsm", path: "/path/to/textfsm"
19
31
  ```sh
20
32
  bundle install
21
33
  bundle exec rake build
22
- gem install ./pkg/textfsm-0.2.0.gem
34
+ gem install ./pkg/textfsm-0.2.1.gem
23
35
  ```
24
36
 
25
37
  源码仓库:[gatework/textfsm](https://github.com/gatework/textfsm)。
@@ -214,7 +226,7 @@ parser = TextFSM::Parser.new(template, options: { "Uppercase" => Uppercase })
214
226
  ## 工程约定
215
227
 
216
228
  - `lib/textfsm.rb` 是统一入口,各组件也可单独 `require "textfsm/parser"`、`require "textfsm/pattern"` 等。CLI 位于 `exe/textfsm`。
217
- - `exe/` 遵循 Bundler 的 gem 可执行文件约定,存放安装给使用者的命令;`script/` 存放项目维护脚本。`Gemfile.lock` 纳入 Git,固定开发和发布验证使用的依赖,不会打进 gem;使用者的依赖范围由 gemspec 声明。
229
+ - `exe/` 遵循 Bundler 的 gem 可执行文件约定,存放安装给使用者的命令;`script/` 存放项目维护脚本。`Gemfile.lock` 纳入 Git,固定开发和发布验证使用的依赖,不会打进 gem;gemspec 中的运行时依赖仅声明最低版本,不限制主版本。
218
230
  - 类和模块使用 `CamelCase`,方法、参数和实例变量使用 `snake_case`;查询使用 `?`,属性赋值使用 `=`。类方法写成 `def self.method_name`,解析状态属于实例,固定映射使用冻结常量。
219
231
  - 集合通过 `Enumerable`、`each`、`[]`、`size` 和 `empty?` 提供 Ruby 接口;需要代码块的迭代方法在未传块时返回 `Enumerator`。`merge`/`merge!` 区分创建新表和修改当前表,排序复用 Ruby 比较器。
220
232
  - 数据转换使用 `map`、`filter_map`、`transform_values`、`reduce` 等集合方法;简单转换使用单行 `{ ... }`,多行逻辑和有副作用的迭代使用 `do ... end`。简短取值使用条件表达式,复杂状态分支使用 `if`/`case`。
@@ -222,7 +234,7 @@ parser = TextFSM::Parser.new(template, options: { "Uppercase" => Uppercase })
222
234
  - 内部动作使用 Symbol,省略目标状态使用 `nil`;模板文本继续使用标准的 `Next`、`Record`、`Clearall` 等语法。
223
235
  - 正则编译使用 `StringScanner`,编译器的游标和分组栈不保留在最终 `Pattern` 对象中。模式始终从输入开头匹配。
224
236
  - `bundle exec rake` 执行 Minitest 和 RuboCop;`rake test`、`rake lint` 可分别运行。代码规则见 `.rubocop.yml`。
225
- - `bundle exec rake verify` 进一步构建 gem,在临时目录安装后逐文件核对源码内容,并验证独立加载与 CLI 输出、标准输入和退出码。生成的包位于 `pkg/`,不会发布到 RubyGems。
237
+ - `bundle exec rake verify` 进一步构建 gem,从 `bundle install` 准备的本地缓存离线安装运行时依赖和候选包;安装环境仅使用临时目录的 gem 与 Ruby 自带的默认 gem。逐文件核对源码内容,并验证独立加载与 CLI 输出、标准输入和退出码,避免开发依赖掩盖缺失的运行时依赖。生成的包位于 `pkg/`,不会发布到 RubyGems。
226
238
 
227
239
  ## 发布到 RubyGems
228
240
 
@@ -236,7 +248,7 @@ bundle exec rake release:check
236
248
 
237
249
  检查模式可以在尚未提交的工作区运行,不访问 RubyGems,也不会创建提交或标签。正式发布需要工作区干净,所有入包文件已提交到 Git:
238
250
 
239
- 1. 修改 `lib/textfsm/version.rb` 中的版本号。新版本使用 RubyGems 版本格式,例如 `0.2.1` 或 `0.3.0.pre.1`。
251
+ 1. 修改 `lib/textfsm/version.rb` 中的版本号。新版本使用 RubyGems 版本格式,例如 `0.2.2` 或 `0.3.0.pre.1`。
240
252
  2. 运行 `bundle install` 更新 `Gemfile.lock`,再运行 `bundle exec rake release:check`。
241
253
  3. 提交并推送本次版本的源码与锁文件。
242
254
  4. 使用自己的 RubyGems 账号发布:
@@ -247,15 +259,15 @@ bundle exec rake release
247
259
  # 等价于 bundle exec ruby script/release.rb --push
248
260
  ```
249
261
 
250
- 凭据由 RubyGems 管理;自动化环境可通过密钥管理服务注入 `GEM_HOST_API_KEY`,密钥需有 `push_rubygem` 权限。需要 MFA 时由 `gem push` 处理交互。不要把密钥放进仓库或发布脚本。详见 [RubyGems 发布指南](https://guides.rubygems.org/publishing/)和 [API key scopes](https://guides.rubygems.org/api-key-scopes/)。
262
+ 凭据由 RubyGems 管理;自动化环境可通过密钥管理服务注入 `GEM_HOST_API_KEY`,密钥需有 `push_rubygem` 权限。gemspec 声明 `rubygems_mfa_required: true`,发布者需在 RubyGems 账号启用 MFA;需要验证时由 `gem push` 处理交互。不要把密钥放进仓库或发布脚本。详见 [RubyGems 发布指南](https://guides.rubygems.org/publishing/)和 [API key scopes](https://guides.rubygems.org/api-key-scopes/)。
251
263
 
252
264
  正式发布复用 `rake verify` 的完整检查:执行测试、RuboCop 和构建,再将 gem 复制到独立临时目录,由 `script/verify_package.rb` 验证安装后的库、CLI 和文件内容。候选包在验证前后必须保持同一 SHA256,HEAD 和工作区也必须保持不变,随后上传该候选包到 `https://rubygems.org`,读取版本元数据核对 SHA256。输出包含源码提交号和包校验值;相同版本、相同校验值已发布时直接成功,相同版本内容不同时拒绝覆盖。构建使用提交时间作为 `SOURCE_DATE_EPOCH`,便于同一提交重复构建。
253
265
 
254
266
  上传后只重试查询,不自动重复上传。若提示结果未确认,先检查 RubyGems 上该版本及其校验值。脚本不创建或推送 Git 提交和标签;RubyGems 核对成功后,可按已发布版本标记源码,例如:
255
267
 
256
268
  ```sh
257
- git tag -a v0.2.0 -m "Release v0.2.0"
258
- git push origin v0.2.0
269
+ git tag -a v0.2.1 -m "Release v0.2.1"
270
+ git push origin v0.2.1
259
271
  ```
260
272
 
261
273
  ## 兼容范围与验证
data/lib/textfsm/cli.rb CHANGED
@@ -36,7 +36,7 @@ module TextFSM
36
36
  rows = parser.parse(text)
37
37
  write_result(parser.header, rows, settings)
38
38
  0
39
- rescue Error, OptionParser::ParseError, SystemCallError, IOError, ArgumentError => e
39
+ rescue Error, OptionParser::ParseError, SystemCallError, IOError, ArgumentError, EncodingError => e
40
40
  @error.puts("textfsm: #{e.message}")
41
41
  2
42
42
  end
@@ -30,7 +30,7 @@ module TextFSM
30
30
  end
31
31
 
32
32
  def parse(text, attributes: {}, templates: nil)
33
- names = template_names(templates || find_templates(attributes))
33
+ names = template_names(templates.nil? ? find_templates(attributes) : templates)
34
34
  text = text.read if text.respond_to?(:read)
35
35
  raise ArgumentError, "text must be a String or readable IO" unless text.is_a?(String)
36
36
 
@@ -46,9 +46,10 @@ module TextFSM
46
46
 
47
47
  # Empty cells are wildcards. Attributes absent from the index are ignored.
48
48
  def match(attributes)
49
+ attributes = attributes.map { |key, value| [key.to_s, value] }
49
50
  index = @patterns.index do |patterns|
50
51
  attributes.all? do |key, value|
51
- pattern = patterns[key.to_s]
52
+ pattern = patterns[key]
52
53
  pattern.nil? || pattern.match?(value.to_s)
53
54
  end
54
55
  end
@@ -38,7 +38,7 @@ module TextFSM
38
38
  end
39
39
 
40
40
  def rows
41
- Data.copy(@rows, immutable: true)
41
+ @rows.dup.freeze
42
42
  end
43
43
 
44
44
  def to_a
@@ -80,11 +80,15 @@ module TextFSM
80
80
  column = @output_fields.index(field)
81
81
  return unless column
82
82
 
83
- @rows.reverse_each do |row|
83
+ (@rows.length - 1).downto(0) do |index|
84
+ row = @rows[index]
84
85
  value = row[column]
85
86
  break if value && !(value.respond_to?(:empty?) && value.empty?)
86
87
 
87
- row[column] = Data.copy(field.value)
88
+ # Records are immutable so earlier snapshots can safely share them.
89
+ replacement = row.dup
90
+ replacement[column] = Data.copy(field.value, immutable: true)
91
+ @rows[index] = replacement.freeze
88
92
  end
89
93
  end
90
94
 
@@ -224,7 +228,7 @@ module TextFSM
224
228
  @fields.each_value(&:prepare_record)
225
229
  record = @output_fields.map(&:value)
226
230
  unless record.all? { |value| value.nil? || value == [] }
227
- @rows << Data.copy(record.map { |value| value.nil? ? "" : value })
231
+ @rows << record.map! { |value| Data.copy(value.nil? ? "" : value, immutable: true) }.freeze
228
232
  end
229
233
  end
230
234
  @fields.each_value(&:clear)
data/lib/textfsm/table.rb CHANGED
@@ -72,12 +72,13 @@ module TextFSM
72
72
  end
73
73
  rows = @rows.each_with_index.map do |row, index|
74
74
  right = keys.empty? ? other[index] : lookup[row.values_at(*left_keys)]
75
- row + (right ? right.values_at(*right_columns) : Array.new(columns.size, ""))
75
+ extra = right ? Data.copy(right.values_at(*right_columns), immutable: true) : Array.new(columns.size, "")
76
+ # Existing cells are immutable; only incoming values need an owned copy.
77
+ (row + extra).freeze
76
78
  end
77
- header = Data.copy(@header + columns, immutable: true)
78
- rows = Data.copy(rows, immutable: true)
79
+ header = (@header + Data.copy(columns, immutable: true)).freeze
79
80
  @header = header
80
- @rows = rows
81
+ @rows = rows.freeze
81
82
  self
82
83
  end
83
84
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TextFSM
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,40 +1,54 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textfsm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - TextFSM Ruby contributors
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-12 00:00:00.000000000 Z
10
+ date: 2026-10-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: json
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '2.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: optparse
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: strscan
28
42
  requirement: !ruby/object:Gem::Requirement
29
43
  requirements:
30
- - - "~>"
44
+ - - ">="
31
45
  - !ruby/object:Gem::Version
32
46
  version: '3.0'
33
47
  type: :runtime
34
48
  prerelease: false
35
49
  version_requirements: !ruby/object:Gem::Requirement
36
50
  requirements:
37
- - - "~>"
51
+ - - ">="
38
52
  - !ruby/object:Gem::Version
39
53
  version: '3.0'
40
54
  description: Parses semi-structured text using Google TextFSM template syntax, with
@@ -44,6 +58,7 @@ executables:
44
58
  extensions: []
45
59
  extra_rdoc_files: []
46
60
  files:
61
+ - CHANGELOG.md
47
62
  - LICENSE
48
63
  - NOTICE
49
64
  - README.md
@@ -84,6 +99,9 @@ licenses:
84
99
  metadata:
85
100
  source_code_uri: https://github.com/gatework/textfsm
86
101
  bug_tracker_uri: https://github.com/gatework/textfsm/issues
102
+ documentation_uri: https://github.com/gatework/textfsm/blob/main/README.md
103
+ changelog_uri: https://github.com/gatework/textfsm/blob/main/CHANGELOG.md
104
+ rubygems_mfa_required: 'true'
87
105
  allowed_push_host: https://rubygems.org
88
106
  rdoc_options: []
89
107
  require_paths: