textfsm 0.2.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f381555ee4b666d74fb0fb9f8ed82d3881089872c9ce69d69bdb20e6febd1335
4
+ data.tar.gz: 9edc61cf09c9e24a494a0fb15aba98cc57162afba32bafa6dd8ccee5c00b414c
5
+ SHA512:
6
+ metadata.gz: 7bb5d2d0907d9f1dee50c6db10cc3ad62681cbddb6d188a9053164502c37cbb3cb908d3538d3923f796ece79e311974fce95b63499595a69c5ab2a19901dea23
7
+ data.tar.gz: ba2cc24808ac78daad55cbd6bf7bb9c228b9f3965d865ad0801bca3c19c5d406873fc189a6627ea2187e1366c3367b69750c2957a2d728d4d2e62bcafd390c89
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
data/NOTICE ADDED
@@ -0,0 +1,15 @@
1
+ TextFSM Ruby
2
+
3
+ This project is a Ruby port of the parsing engine and CLI template selection
4
+ from Google TextFSM (https://github.com/google/textfsm).
5
+
6
+ Original TextFSM: Copyright 2010 Google Inc. All Rights Reserved.
7
+ Original CLI tables: Copyright 2022 Google Inc. All Rights Reserved.
8
+ Licensed under the Apache License, Version 2.0 (see LICENSE).
9
+
10
+ Reference: TextFSM 2.1.0, commit f80bbb459c55ff5f21651e48d2529722d667af97.
11
+ The Ruby source is a reimplementation, modified in 2026.
12
+ Files in examples/ and test/fixtures/upstream/ are copied from that reference.
13
+ test/fixtures/python_conformance.json contains results generated from the
14
+ upstream parser tests and examples. The generator is in script/.
15
+
data/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # TextFSM Ruby
2
+
3
+ 使用 Ruby 实现的模板驱动文本解析状态机。读取 TextFSM 模板和半结构化文本,输出二维数组或 Hash 数组,适合解析网络设备 CLI 输出、系统命令输出等。
4
+
5
+ 解析语义参考 [Google TextFSM 2.1.0](https://github.com/google/textfsm/tree/f80bbb459c55ff5f21651e48d2529722d667af97),运行时使用 Ruby 自身的正则引擎,不需要 Python 或外部进程;运行时依赖 Ruby 标准库 gem `json` 和 `strscan`。要求 Ruby 3.1 或以上。
6
+
7
+ 0.2 使用 `TextFSM` 模块组织 `Parser`、`Field`、`Rule`、`Table` 和 `CliTable`;公开方法与选项扩展接口按 Ruby 风格重新设计,不保留 0.1 的接口别名。
8
+
9
+ ## 安装
10
+
11
+ 本项目尚未发布到 RubyGems。在其他项目的 `Gemfile` 中使用本地路径:
12
+
13
+ ```ruby
14
+ gem "textfsm", path: "/path/to/textfsm"
15
+ ```
16
+
17
+ 也可以构建并安装:
18
+
19
+ ```sh
20
+ bundle install
21
+ bundle exec rake build
22
+ gem install ./pkg/textfsm-0.2.0.gem
23
+ ```
24
+
25
+ 源码仓库:[gatework/textfsm](https://github.com/gatework/textfsm)。
26
+
27
+ ## 快速使用
28
+
29
+ ```ruby
30
+ require "textfsm"
31
+
32
+ template = <<~'FSM'
33
+ Value Required INTERFACE (\S+)
34
+ Value ADDRESS (\S+)
35
+ Value STATUS (up|down|administratively down)
36
+ Value PROTOCOL (up|down)
37
+
38
+ Start
39
+ ^${INTERFACE}\s+${ADDRESS}\s+${STATUS}\s+${PROTOCOL}$$ -> Record
40
+ FSM
41
+
42
+ text = <<~TEXT
43
+ GigabitEthernet0/0 192.0.2.1 up up
44
+ GigabitEthernet0/1 unassigned administratively down down
45
+ TEXT
46
+
47
+ fsm = TextFSM::Parser.new(template)
48
+ p fsm.header
49
+ # => ["INTERFACE", "ADDRESS", "STATUS", "PROTOCOL"]
50
+ p fsm.parse(text)
51
+ # => [["GigabitEthernet0/0", "192.0.2.1", "up", "up"],
52
+ # ["GigabitEthernet0/1", "unassigned", "administratively down", "down"]]
53
+
54
+ fsm.reset
55
+ p fsm.parse_hashes(text)
56
+ # => [{"INTERFACE"=>"GigabitEthernet0/0", "ADDRESS"=>"192.0.2.1",
57
+ # "STATUS"=>"up", "PROTOCOL"=>"up"}, ...]
58
+ ```
59
+
60
+ 模板使用单引号 heredoc(`<<~'FSM'`),避免 Ruby 提前处理正则中的反斜杠。`TextFSM::Parser.new` 的字符串参数是**模板内容**;从文件读取请用 `TextFSM::Parser.from_file("template.textfsm")`,也可以传入已打开的文件或 `StringIO`。IO 从当前位置读取并保持在读完的位置,不会自动 `rewind`。
61
+
62
+ ## 模板语法
63
+
64
+ 模板先声明连续的 `Value`,用空行分隔状态。初始状态必须叫 `Start`。状态之间用空行分隔;规则以一个空格、两个空格或一个制表符缩进,随后必须是 `^`。以 `#` 开始的注释行会被忽略。
65
+
66
+ ```text
67
+ Value [选项,选项] NAME (正则表达式)
68
+
69
+ Start
70
+ ^匹配规则 -> 行操作.记录操作 下一个状态
71
+ ```
72
+
73
+ 规则中的 `$NAME` 或 `${NAME}` 会替换为对应字段的捕获组。规则需要正则行尾锚点 `$` 时写 `$$`;需要匹配字面量美元符号时写 `\$$`。字段名和状态名最长 48 个字符。
74
+
75
+ 字段选项:
76
+
77
+ | 选项 | 行为 |
78
+ | --- | --- |
79
+ | `Required` | 字段为空时丢弃当前记录 |
80
+ | `Filldown` | 保存字段值,后续记录沿用,直到重新赋值或 `Clearall` |
81
+ | `Fillup` | 新值向上填充此前连续的空字段,遇到非空值停止 |
82
+ | `List` | 将多次匹配累积为数组 |
83
+ | `Key` | 标记多模板表合并所使用的键 |
84
+
85
+ 选项按照声明顺序执行,保留 Python 的顺序语义,例如 `List,Required` 与 `Required,List` 在可选匹配或空值时可能有不同结果。
86
+
87
+ 行操作和记录操作可以省略;默认是 `Next.NoRecord`:
88
+
89
+ | 操作 | 行为 |
90
+ | --- | --- |
91
+ | `Next` | 消耗当前行,从当前或目标状态的第一条规则处理下一行 |
92
+ | `Continue` | 保留当前行,继续当前状态的下一条规则;不能同时指定目标状态 |
93
+ | `Error ["消息"]` | 抛出 `TextFSM::ParseError`,包含模板规则行号和输入行 |
94
+ | `NoRecord` | 保留当前字段,不输出记录 |
95
+ | `Record` | 输出符合条件的记录,清除普通字段,保留 `Filldown` |
96
+ | `Clear` | 清除普通字段,保留 `Filldown` |
97
+ | `Clearall` | 清除全部字段,包括 `Filldown` 和列表缓存 |
98
+
99
+ 记录操作先于行操作执行。未匹配的行会被跳过,可添加 `^.* -> Error "Unexpected input"` 拒绝未识别的输入。缺失的普通字段输出 `""`,未匹配的列表输出 `[]`;纯未赋值的记录不会输出,实际匹配到的空字符串可以输出。
100
+
101
+ 输入结束时默认执行一次 `Record`。定义空的 `EOF` 状态可以关闭这一行为。跳转到 `End` 会停止解析并跳过最终记录;需要保留当前记录时使用 `Record End`。`EOF` 和 `End` 的显式状态定义必须为空。
102
+
103
+ `Filldown` 本身也会让最终记录非空,因此可能产生尾部记录。通常给每条业务记录的标识字段设置 `Required`,或根据模板含义定义空 `EOF`。
104
+
105
+ ### 列表中的结构化数据
106
+
107
+ `List` 字段包含 Python 风格的命名子捕获组时,每次匹配会产生一个 Hash:
108
+
109
+ ```text
110
+ Value List PERSON ((?P<name>\w+)\s+(?P<age>\d+))
111
+
112
+ Start
113
+ ^${PERSON}
114
+ ```
115
+
116
+ 解析 `Bob 32\nAlice 27\n` 后得到:
117
+
118
+ ```ruby
119
+ [[[{ "name" => "Bob", "age" => "32" }, { "name" => "Alice", "age" => "27" }]]]
120
+ ```
121
+
122
+ 所有捕获值保持字符串;可选命名子组未匹配时为 `nil`。同一条规则里不能出现重复的命名捕获组,包括重复引用同一个 Value。
123
+
124
+ ## 分段解析与复用
125
+
126
+ ```ruby
127
+ fsm = TextFSM::Parser.from_file("template.textfsm")
128
+ fsm.parse(first_chunk, eof: false)
129
+ rows = fsm.parse(last_chunk)
130
+
131
+ fsm.reset
132
+ rows = fsm.parse(another_command_output)
133
+ ```
134
+
135
+ `parse` 和 `rows` 返回累计结果的深度冻结快照;它们不会自动重置解析器。需要可修改的数据时使用 `to_a`、`to_hashes` 或 `parse_hashes`,这些接口返回独立的深拷贝。分段必须按完整行切分,`eof: false` 仅抑制最终 `Record`,不缓存半行。`parse` 也接受可读取的 IO,但会一次读取内容。不同设备输出应先 `reset` 或使用新实例;有状态的实例不应由多个线程并发共享。`Fillup` 会更新解析器内部的历史行;此前返回的快照不变,后续 `parse`/`rows` 会包含回填后的结果。
136
+
137
+ 可用接口:`header`、`rows`、`current_state`、`states`、`state_names`、`fields_with_option("Key")`、`to_a`、`to_hashes`、`to_s`(重建去除注释的模板)。表头、状态规则及编译后的模式只读;字段的运行时状态保留在解析器内部。`fields_with_option` 也接受 `:Key` 等 Symbol。
138
+
139
+ ## 根据设备与命令选择模板
140
+
141
+ `CliTable` 读取与 Python TextFSM 相同的 index 文件格式:
142
+
143
+ ```ruby
144
+ require "textfsm/cli_table"
145
+
146
+ table = TextFSM::CliTable.new(index: "index", template_dir: "/path/to/templates")
147
+ table.parse(output, attributes: { "Vendor" => "Cisco", "Command" => "sh ver" })
148
+ p table.header
149
+ p table.to_a
150
+ p table.to_hashes
151
+ ```
152
+
153
+ index 示例:
154
+
155
+ ```text
156
+ Template, Vendor, Command
157
+ cisco_version_template, Cisco, sh[[ow]] ve[[rsion]]
158
+ ```
159
+
160
+ 按文件顺序选择第一条匹配记录;匹配从属性字符串开头开始,空单元格是通配条件,index 未定义的属性会被忽略。`Command` 中 `sh[[ow]]` 支持 `sh`、`sho`、`show`。index 遵循上游简单逗号分隔格式,不处理带引号的 CSV 字段。
161
+
162
+ 可绕过 index 显式传入模板:
163
+
164
+ ```ruby
165
+ table = TextFSM::CliTable.new(template_dir: "/path/to/templates")
166
+ table.parse(output, templates: "first.textfsm:second.textfsm")
167
+ # templates 也接受文件名数组。
168
+ ```
169
+
170
+ 多个模板重复解析同一份输入,只合入新增列。有 `Key` 时按键匹配首条记录,否则按行位置合并。保留第一张表的行数,缺少匹配的新增列填 `""`。表格支持 Enumerable,行号从零开始。`IndexTable#match(attributes)` 返回首条匹配的只读 Hash,未命中返回 `nil`,无需换算行号。
171
+
172
+ `Table#merge(other, keys: ["ID"])` 返回合入新列的新表,保留原表;`merge!` 更新当前表并返回自身。新表保留接收者的类型,因此 `CliTable#merge` 也保留索引、输入和键。表头和 `rows`/`table[index]` 只读;`to_a` 和 `to_hashes` 返回独立副本,嵌套的列表与 Hash 也不会共享可变数据。构造表格时拒绝重复列名和宽度不一致的行。
173
+
174
+ `CliTable#keys` 返回模板声明的键,使用 `table.keys = ["ID"]` 显式设置,或 `table.keys += ["NAME"]` 增补。`key_for(row)` 返回键值数组,没有键时返回 `[]`。排序遵循 Ruby 自身的比较规则:
175
+
176
+ ```ruby
177
+ table.sort! # 按行比较
178
+ table.sort_by! { |row| table.key_for(row) } # 按声明的键排序
179
+ table.sort_by! { |row| row[0].to_i } # 明确按数值排序
180
+ table.sort! { |left, right| right <=> left } # 倒序
181
+ ```
182
+
183
+ `load_index("another_index")` 可替换索引。多模板解析、合并或索引加载失败时保留上次成功的表头、数据、键及输入。
184
+
185
+ ## 命令行
186
+
187
+ ```sh
188
+ # 本地源码运行;安装 gem 后可直接使用 textfsm 命令
189
+ ruby exe/textfsm examples/cisco_version_template examples/cisco_version_example
190
+ cat examples/cisco_version_example | ruby exe/textfsm examples/cisco_version_template
191
+ ruby exe/textfsm --rows examples/cisco_version_template examples/cisco_version_example
192
+ ruby exe/textfsm --format table examples/cisco_version_template examples/cisco_version_example
193
+ ruby exe/textfsm --validate examples/cisco_version_template
194
+ ```
195
+
196
+ 默认输出 JSON 对象数组;`--rows` 输出包含 `header` 和 `rows` 的 JSON;`--format table` 输出制表符分隔的可读表格。成功退出码为 `0`,参数、文件、模板或解析错误为 `2`。`--validate` 只验证模板,不读取标准输入。
197
+
198
+ ## 扩展字段选项
199
+
200
+ ```ruby
201
+ class Uppercase < TextFSM::Options::Base
202
+ def after_assign
203
+ field.value = field.value&.upcase
204
+ end
205
+ end
206
+
207
+ parser = TextFSM::Parser.new(template, options: { "Uppercase" => Uppercase })
208
+ ```
209
+
210
+ 模板中可以声明 `Value Uppercase NAME (...)`。显式传入的选项与五种内置选项合并,只对该解析器生效;内置注册表不可修改。选项类必须继承 `TextFSM::Options::Base`。
211
+
212
+ 可实现 `after_initialize`、`after_assign`、`after_clear`、`after_reset`、`before_record` 钩子,通过 `field.value` 读写当前值。在 `before_record` 中执行 `throw :skip_record` 可以丢弃当前记录;字段仍会按正常规则清除。选项的 `visible?` 返回 `false` 可以隐藏字段,输出列在模板加载时确定,`Fillup` 使用同一份列映射。隐藏字段仍执行选项钩子,可用于控制记录是否输出。
213
+
214
+ ## 工程约定
215
+
216
+ - `lib/textfsm.rb` 是统一入口,各组件也可单独 `require "textfsm/parser"`、`require "textfsm/pattern"` 等。CLI 位于 `exe/textfsm`。
217
+ - `exe/` 遵循 Bundler 的 gem 可执行文件约定,存放安装给使用者的命令;`script/` 存放项目维护脚本。`Gemfile.lock` 纳入 Git,固定开发和发布验证使用的依赖,不会打进 gem;使用者的依赖范围由 gemspec 声明。
218
+ - 类和模块使用 `CamelCase`,方法、参数和实例变量使用 `snake_case`;查询使用 `?`,属性赋值使用 `=`。类方法写成 `def self.method_name`,解析状态属于实例,固定映射使用冻结常量。
219
+ - 集合通过 `Enumerable`、`each`、`[]`、`size` 和 `empty?` 提供 Ruby 接口;需要代码块的迭代方法在未传块时返回 `Enumerator`。`merge`/`merge!` 区分创建新表和修改当前表,排序复用 Ruby 比较器。
220
+ - 数据转换使用 `map`、`filter_map`、`transform_values`、`reduce` 等集合方法;简单转换使用单行 `{ ... }`,多行逻辑和有副作用的迭代使用 `do ... end`。简短取值使用条件表达式,复杂状态分支使用 `if`/`case`。
221
+ - 构造方法组织对象初始化,声明解析等细节放入私有方法。字段选项通过明确的生命周期钩子扩展;错误使用 `TextFSM::Error` 的子类,预期的跳过记录使用 `catch`/`throw`。
222
+ - 内部动作使用 Symbol,省略目标状态使用 `nil`;模板文本继续使用标准的 `Next`、`Record`、`Clearall` 等语法。
223
+ - 正则编译使用 `StringScanner`,编译器的游标和分组栈不保留在最终 `Pattern` 对象中。模式始终从输入开头匹配。
224
+ - `bundle exec rake` 执行 Minitest 和 RuboCop;`rake test`、`rake lint` 可分别运行。代码规则见 `.rubocop.yml`。
225
+ - `bundle exec rake verify` 进一步构建 gem,在临时目录安装后逐文件核对源码内容,并验证独立加载与 CLI 输出、标准输入和退出码。生成的包位于 `pkg/`,不会发布到 RubyGems。
226
+
227
+ ## 发布到 RubyGems
228
+
229
+ 发布入口是 `script/release.rb`,也可通过 Rake 运行。直接执行脚本默认只检查和构建:
230
+
231
+ ```sh
232
+ bundle install
233
+ bundle exec rake release:check
234
+ # 等价于 bundle exec ruby script/release.rb --dry-run
235
+ ```
236
+
237
+ 检查模式可以在尚未提交的工作区运行,不访问 RubyGems,也不会创建提交或标签。正式发布需要工作区干净,所有入包文件已提交到 Git:
238
+
239
+ 1. 修改 `lib/textfsm/version.rb` 中的版本号。新版本使用 RubyGems 版本格式,例如 `0.2.1` 或 `0.3.0.pre.1`。
240
+ 2. 运行 `bundle install` 更新 `Gemfile.lock`,再运行 `bundle exec rake release:check`。
241
+ 3. 提交并推送本次版本的源码与锁文件。
242
+ 4. 使用自己的 RubyGems 账号发布:
243
+
244
+ ```sh
245
+ gem signin --host https://rubygems.org
246
+ bundle exec rake release
247
+ # 等价于 bundle exec ruby script/release.rb --push
248
+ ```
249
+
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/)。
251
+
252
+ 正式发布复用 `rake verify` 的完整检查:执行测试、RuboCop 和构建,再将 gem 复制到独立临时目录,由 `script/verify_package.rb` 验证安装后的库、CLI 和文件内容。候选包在验证前后必须保持同一 SHA256,HEAD 和工作区也必须保持不变,随后上传该候选包到 `https://rubygems.org`,读取版本元数据核对 SHA256。输出包含源码提交号和包校验值;相同版本、相同校验值已发布时直接成功,相同版本内容不同时拒绝覆盖。构建使用提交时间作为 `SOURCE_DATE_EPOCH`,便于同一提交重复构建。
253
+
254
+ 上传后只重试查询,不自动重复上传。若提示结果未确认,先检查 RubyGems 上该版本及其校验值。脚本不创建或推送 Git 提交和标签;RubyGems 核对成功后,可按已发布版本标记源码,例如:
255
+
256
+ ```sh
257
+ git tag -a v0.2.0 -m "Release v0.2.0"
258
+ git push origin v0.2.0
259
+ ```
260
+
261
+ ## 兼容范围与验证
262
+
263
+ 本项目实现 TextFSM 模板解析、状态机、全部五种内置字段选项、列表命名子组、字典输出,以及命令索引与多模板表合并。官方示例保存在 `examples/`。
264
+
265
+ Ruby 使用 Onigmo 正则引擎。本项目转换 Python 命名组 `(?P<name>...)`、命名与数字反向引用、贪婪/懒惰/占有量词、常用内联标志 `i/m/s/x/a/u`、Unicode 字符类及锚点语义。**这不是完整 Python `re` 引擎的替代实现**:Unicode 名称转义 `\N{...}`、条件组等不支持的扩展会报 `TemplateError`;两种引擎的大小写折叠、复杂后行断言等细节仍可能不同。使用额外模板库时,应以实际模板与输入执行对照测试。
266
+
267
+ Ruby 的 `End` / `EOF` 跳转在后续调用中继续保持终止状态,重新解析需 `reset`;不会复现 Python 在后续调用中再次处理一行的边界行为。CLI JSON 格式和错误文案采用 Ruby 接口;未移植 Python `terminal` 的终端控制功能或 `texttable` 的完整展示/编辑 API。模板 IO 不自动回卷;结果采用快照语义;输入必须是字符串或可读取的 IO。
268
+
269
+ 运行测试:
270
+
271
+ ```sh
272
+ bundle install
273
+ bundle exec rake
274
+ ```
275
+
276
+ 测试包含单元测试及离线 Python 对照数据,正常运行无需 Python 或网络。`test/fixtures/python_conformance.json` 来自固定上游提交的 33 项原测试、8 个官方示例,以及 140 组确定性的选项/状态组合场景,共 203 组实例轨迹。对照校验表头、模板重建、选项元数据、累计结果、重置和目标状态。CI 覆盖 Ruby 3.1、3.2、3.3、3.4 和 4.0,每个版本执行 `rake verify`。额外回归覆盖结果隔离、自定义选项、隐藏列回填、索引事务、CLI 错误和正则语法边界。
277
+
278
+ 需要重新生成对照数据时:
279
+
280
+ ```sh
281
+ git clone https://github.com/google/textfsm.git /tmp/google-textfsm
282
+ git -C /tmp/google-textfsm checkout f80bbb459c55ff5f21651e48d2529722d667af97
283
+ python3 script/generate_conformance.py /tmp/google-textfsm
284
+ bundle exec rake
285
+ ```
286
+
287
+ ## 许可证
288
+
289
+ Apache-2.0。原项目归属、参考版本及复制文件说明见 [NOTICE](NOTICE),许可证全文见 [LICENSE](LICENSE)。
@@ -0,0 +1,15 @@
1
+ BGP router identifier 192.0.2.70, local AS number 65550
2
+ BGP table version is 9, main routing table version 9
3
+ 4 network entries using 468 bytes of memory
4
+ 4 path entries using 208 bytes of memory
5
+ 3/2 BGP path/bestpath attribute entries using 420 bytes of memory
6
+ 1 BGP AS-PATH entries using 24 bytes of memory
7
+ 1 BGP community entries using 24 bytes of memory
8
+ 0 BGP route-map cache entries using 0 bytes of memory
9
+ 0 BGP filter-list cache entries using 0 bytes of memory
10
+ BGP using 1144 total bytes of memory
11
+ BGP activity 12/4 prefixes, 12/4 paths, scan interval 5 secs
12
+
13
+ Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd
14
+ 192.0.2.77 4 65551 6965 1766 9 0 0 5w4d 1
15
+ 192.0.2.78 4 65552 6965 1766 9 0 0 5w4d 10
@@ -0,0 +1,16 @@
1
+ # Carry down the local end information so that it is present on each row item.
2
+ Value Filldown RouterID (\S+)
3
+ Value Filldown LocalAS (\d+)
4
+ Value RemoteAS (\d+)
5
+ Value Required RemoteIP (\d+(\.\d+){3})
6
+ Value Uptime (\d+\S+)
7
+ Value Received_V4 (\d+)
8
+ Value Status (\D.*)
9
+
10
+ Start
11
+ ^BGP router identifier ${RouterID}, local AS number ${LocalAS}
12
+ ^${RemoteIP}\s+\d+\s+${RemoteAS}(\s+\S+){5}\s+${Uptime}\s+${Received_V4} -> Record
13
+ ^${RemoteIP}\s+\d+\s+${RemoteAS}(\s+\S+){5}\s+${Uptime}\s+${Status} -> Record
14
+
15
+ # Last record is already recorded then skip doing so here.
16
+ EOF
@@ -0,0 +1,53 @@
1
+ Dialer0 is up, line protocol is up
2
+ IPv6 is enabled, link-local address is FE80::21B:2BFF:FECE:4EE3
3
+ No Virtual link-local address(es):
4
+ Description: PPP Dialer
5
+ Stateless address autoconfig enabled
6
+ General-prefix in use for addressing
7
+ Global unicast address(es):
8
+ 2001:4567:1212:B2:21B:2BFF:FECE:4EE3, subnet is 2001:4567:1212:B2::/64 [EUI/CAL/PRE]
9
+ valid lifetime 5041 preferred lifetime 5041
10
+ 2001:4567:1111:56FF::1, subnet is 2001:4567:1111:56FF::1/128 [CAL/PRE]
11
+ valid lifetime 5945 preferred lifetime 2344
12
+ Joined group address(es):
13
+ FF02::1
14
+ FF02::2
15
+ FF02::1:FF00:1
16
+ FF02::1:FFCE:4EE3
17
+ MTU is 1500 bytes
18
+ ICMP error messages limited to one every 100 milliseconds
19
+ ICMP redirects are enabled
20
+ ICMP unreachables are sent
21
+ Input features: Access List
22
+ Inbound access list IPV6-IN
23
+ ND DAD is enabled, number of DAD attempts: 1
24
+ ND reachable time is 30000 milliseconds (using 21397)
25
+ Hosts use stateless autoconfig for addresses.
26
+ Vlan1 is up, line protocol is up
27
+ IPv6 is enabled, link-local address is FE80::21B:2BFF:FECE:4EE3
28
+ No Virtual link-local address(es):
29
+ Description: Local VLAN
30
+ General-prefix in use for addressing
31
+ Global unicast address(es):
32
+ 2001:4567:1212:5600::1, subnet is 2001:4567:1212:5600::/64 [CAL/PRE]
33
+ valid lifetime 5943 preferred lifetime 2342
34
+ Joined group address(es):
35
+ FF02::1
36
+ FF02::2
37
+ FF02::1:2
38
+ FF02::1:FF00:1
39
+ FF02::1:FFCE:4EE3
40
+ FF05::1:3
41
+ MTU is 1500 bytes
42
+ ICMP error messages limited to one every 100 milliseconds
43
+ ICMP redirects are enabled
44
+ ICMP unreachables are sent
45
+ ND DAD is enabled, number of DAD attempts: 1
46
+ ND reachable time is 30000 milliseconds (using 26371)
47
+ ND advertised reachable time is 0 (unspecified)
48
+ ND advertised retransmit interval is 0 (unspecified)
49
+ ND router advertisements are sent every 200 seconds
50
+ ND router advertisements live for 1800 seconds
51
+ ND advertised default router preference is Medium
52
+ Hosts use stateless autoconfig for addresses.
53
+ Hosts use DHCP to obtain other configuration.
@@ -0,0 +1,27 @@
1
+ Value Interface (\S+)
2
+ Value Admin (\S+)
3
+ Value Oper (\S+)
4
+ Value Description (.*)
5
+ Value LinkLocal (\S+)
6
+ Value List Addresses (\S+)
7
+ Value List Subnets (\S+)
8
+ Value List GroupAddresses (\S+)
9
+ Value Mtu (\d+)
10
+
11
+ Start
12
+ ^${Interface} is ${Admin}, line protocol is ${Oper}
13
+ ^.*link-local address is ${LinkLocal}
14
+ ^ Description: ${Description}
15
+ ^ Global unicast address -> Unicast
16
+ ^ Joined group address -> Multicast
17
+ ^ MTU is ${Mtu} bytes -> Record
18
+
19
+ Unicast
20
+ ^ ${Addresses}, subnet is ${Subnets}
21
+ ^ Joined group address -> Multicast
22
+ ^ \S -> Start
23
+
24
+ Multicast
25
+ ^ ${GroupAddresses}
26
+ ^ MTU is ${Mtu} bytes -> Record
27
+ ^ \S -> Start