ctx_parser 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +22 -0
- data/README-zh.md +102 -0
- data/README.md +102 -0
- data/docs/architecture-zh.md +273 -0
- data/docs/release.md +13 -0
- data/examples/offline.rb +30 -0
- data/lib/ctx_parser/banner.rb +23 -0
- data/lib/ctx_parser/block.rb +99 -0
- data/lib/ctx_parser/capture.rb +61 -0
- data/lib/ctx_parser/command_catalog.rb +54 -0
- data/lib/ctx_parser/configuration_parser.rb +73 -0
- data/lib/ctx_parser/cursor.rb +177 -0
- data/lib/ctx_parser/document.rb +41 -0
- data/lib/ctx_parser/immutable.rb +18 -0
- data/lib/ctx_parser/input.rb +31 -0
- data/lib/ctx_parser/line.rb +28 -0
- data/lib/ctx_parser/parser.rb +46 -0
- data/lib/ctx_parser/profile.rb +134 -0
- data/lib/ctx_parser/records.rb +36 -0
- data/lib/ctx_parser/registry.rb +39 -0
- data/lib/ctx_parser/result.rb +22 -0
- data/lib/ctx_parser/result_set.rb +36 -0
- data/lib/ctx_parser/values.rb +59 -0
- data/lib/ctx_parser/version.rb +6 -0
- data/lib/ctx_parser.rb +17 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1fa31bb20dcff8d61e0855e926e92be68473aa7b3bc33fea86e95bfd08caaac8
|
|
4
|
+
data.tar.gz: 02d5738230c47a01921c8e2f530bfe4acc740f607f46fe3677c355092cd3e068
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: af7037925fd0ecdaccb72464cdcab399daeb35e3939e51e72b7a53fedd3f55f5962f390bac4a1558c0f704e2995c7fe526e4d1fd7c3411bed068fd34671a9b4b
|
|
7
|
+
data.tar.gz: 1a7fc7f989267dcde2c9f9d4b0f6e7abe59348ffbcca529ae73c0425043ec36fcbff70ebd14d8b92931bcb823d4a4970454d61c36d5f198539b61e5a551842cb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
|
+
|
|
5
|
+
## 0.1.1 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
- Publish verified artifacts using either an environment API key or RubyGems Trusted Publishing.
|
|
8
|
+
- The v0.1.0 tag did not reach RubyGems because credential setup failed.
|
|
9
|
+
|
|
10
|
+
## 0.1.0 (2026-10-16, unpublished)
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Shared parser contracts, configuration provenance, immutable values, and platform registries.
|
|
15
|
+
- Standalone RubyGems packaging under `ctx_parser` with the `ctx_parser` require path and `CtxParser` namespace.
|
|
16
|
+
- English and Chinese documentation, an offline example, and standalone RSpec/RuboCop development tasks.
|
|
17
|
+
- Linux/macOS CI, isolated package verification, and automatic RubyGems and GitHub Releases with identical artifacts.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Cross-module dependencies resolve through RubyGems/Bundler instead of relative sibling paths.
|
|
22
|
+
- Motor persistence and application-specific adapters remain in the host application.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Song Huang
|
|
4
|
+
Copyright (c) 2026 Gatework contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/README-zh.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ctx_parser
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [源码](https://github.com/gatework/ctx_parser) | [变更记录](CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
面向网络 CLI 解析的纯 Ruby 公共组件,提供不可变结果、来源游标、配置树、命令目录及平台策略。厂商语法和设备会话由对应领域模块负责。
|
|
6
|
+
|
|
7
|
+
## 能力
|
|
8
|
+
|
|
9
|
+
- `Parser`、`ConfigurationParser` 定义文本输入及完整性边界。
|
|
10
|
+
- `Cursor`、`Document`、`Capture` 保留来源位置和嵌套配置上下文。
|
|
11
|
+
- `Profile`、`CommandCatalog`、`Registry` 声明平台/型号行为,同一回显可共享解析。
|
|
12
|
+
- `Values`、`Records`、`Immutable` 统一值转换、身份检查与防御性复制。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
要求 **Ruby 4.0.6 或更新版本**。在应用的 `Gemfile` 中添加:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "ctx_parser"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
然后执行 `bundle install`;也可直接执行 `gem install ctx_parser`。Ruby 加载入口为 `ctx_parser`,公开命名空间为 `CtxParser`;gem 包名与 Ruby 命名空间分别维护。
|
|
23
|
+
|
|
24
|
+
本模块不依赖其他 Gatework 领域 gem。 gemspec 的运行依赖不添加版本约束;应用应提交自己的 `Gemfile.lock`,保持部署可复现。使用本库不需要启动 Rails 或配置兄弟模块的本地路径。
|
|
25
|
+
|
|
26
|
+
发布产物和校验值见 [GitHub Releases](https://github.com/gatework/ctx_parser/releases),已发布版本以 [RubyGems](https://rubygems.org/gems/ctx_parser) 为准。
|
|
27
|
+
|
|
28
|
+
## 离线示例
|
|
29
|
+
|
|
30
|
+
在开发检出目录安装依赖后执行:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bundle exec ruby examples/offline.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
示例使用合成数据,不访问网络。完整代码见 [examples/offline.rb](examples/offline.rb):
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "ctx_parser"
|
|
40
|
+
|
|
41
|
+
# 示例仅实现小型语法;厂商语法应放入对应领域 gem。
|
|
42
|
+
class ExampleStatusParser < CtxParser::Parser
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# 保留未知行,并遵循调用方声明的传输完整性。
|
|
46
|
+
def parse_output(text, complete:)
|
|
47
|
+
records = []
|
|
48
|
+
issues = []
|
|
49
|
+
text.each_line.with_index(1) do |line, number|
|
|
50
|
+
next if line.strip.empty?
|
|
51
|
+
|
|
52
|
+
match = /\A([A-Za-z0-9\/]+) (up|down)\z/.match(line.strip)
|
|
53
|
+
if match
|
|
54
|
+
records << { name: match[1], state: match[2].to_sym }
|
|
55
|
+
else
|
|
56
|
+
issues << { code: :unrecognized_row, line: number }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
status = complete && issues.empty? && !records.empty? ? :complete : :partial
|
|
60
|
+
CtxParser::Result.new(status: status, records: records, issues: issues)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
result = ExampleStatusParser.parse("Eth1/1 up\nEth1/2 down\n", complete: true)
|
|
65
|
+
puts "Parse status: #{result.status}"
|
|
66
|
+
result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 公开契约与限制
|
|
70
|
+
|
|
71
|
+
`Parser.parse(text, complete: false)` 返回包含 `status`、`records`、`issues` 的 `Result`;多视图解析器可返回 `ResultSet`。状态为 `complete`、`partial`、`failed`、`unsupported`。子类实现 `parse_output`,配置子类通过 `analyze` 和 `capture` 保留来源。
|
|
72
|
+
|
|
73
|
+
`complete: true` 只表示调用方确认输入完整接收,不证明所有语法都已理解。未知行、身份冲突及空表证据不足必须明确表达。结果不可变;来源游标有意保留输入原文,含秘密时不可写入日志。
|
|
74
|
+
|
|
75
|
+
本 gem 不提供厂商命令目录、传输、持久化或健康策略。参数和注册表配置错误保持异常,由调用方在对应边界处理。
|
|
76
|
+
|
|
77
|
+
详细厂商语义、历史验证记录和 Motor 专用集成说明保留在[架构参考](docs/architecture-zh.md)。其中宿主集成代码需要 Motor,不属于独立 gem API。适配层位于 [Motor 仓库](https://github.com/gatework/motor/tree/main/backend/lib) 的 `backend/lib/baseline`、`backend/lib/vxlan` 和 `backend/lib/wireless`。
|
|
78
|
+
|
|
79
|
+
## 开发与测试
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
bundle install
|
|
83
|
+
bundle exec rake
|
|
84
|
+
bundle exec ruby examples/offline.rb
|
|
85
|
+
bundle exec rake build
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`bundle exec rake` 执行 RSpec、RuboCop 及离线示例。测试放在 `spec/`,沿用项目 RSpec 约定;样例采用合成或脱敏证据。新增解析格式时补充成功、不完整、不支持及非法输入回归。离线测试不代表所有设备版本均通过实机验证。
|
|
89
|
+
|
|
90
|
+
使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
|
|
91
|
+
|
|
92
|
+
## 发布
|
|
93
|
+
|
|
94
|
+
本目录可独立维护为 [ctx_parser](https://github.com/gatework/ctx_parser) 仓库。更新 `VERSION` 和 `CHANGELOG.md`,运行上述验证与构建命令,检查包内文件,再创建 `vVERSION` 标签。
|
|
95
|
+
|
|
96
|
+
CI 在 Linux、macOS 上执行测试、lint、示例、构建和隔离安装。`main` 的 CI 通过后推送 `vVERSION` 标签,工作流将已验证的 Linux 产物发布到 RubyGems 和 GitHub Release,附带 `SHA256SUMS` 并回读下载字节。GitHub `release` environment 可配置 `RUBYGEMS_API_KEY` secret;未配置时使用 RubyGems Trusted Publishing,绑定本仓库、`release.yml` 和 `release` environment。失败时重跑失败的发布 job,以复用原 CI 产物;同版本已存在时必须校验字节一致。详见[发布说明](docs/release.md)。
|
|
97
|
+
|
|
98
|
+
先发布依赖模块。标签、构建成功或工作流文件都不等于已经发布,需单独确认 RubyGems 的实际结果。版本变化见 [CHANGELOG.md](CHANGELOG.md)。
|
|
99
|
+
|
|
100
|
+
## 许可证
|
|
101
|
+
|
|
102
|
+
MIT,见 [LICENSE](LICENSE)。
|
data/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ctx_parser
|
|
2
|
+
|
|
3
|
+
[简体中文](README-zh.md) | [Source](https://github.com/gatework/ctx_parser) | [Changelog](CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
Shared, pure Ruby building blocks for network CLI parsing. It provides immutable results, source cursors, configuration trees, command catalogs, and platform profiles without owning vendor syntax or device sessions.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- `Parser` and `ConfigurationParser` define explicit text and completeness boundaries.
|
|
10
|
+
- `Cursor`, `Document`, and `Capture` preserve source positions and nested configuration context.
|
|
11
|
+
- `Profile`, `CommandCatalog`, and `Registry` select declared platform/model behavior and share parsing across views.
|
|
12
|
+
- `Values`, `Records`, and `Immutable` centralize conversion, identity checks, and defensive copies.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Requires **Ruby 4.0.6 or newer**. Add this line to your application's `Gemfile`:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "ctx_parser"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run `bundle install`, or install directly with `gem install ctx_parser`. The require path is `ctx_parser` and the public namespace is `CtxParser`; package names and Ruby namespaces are intentionally separate.
|
|
23
|
+
|
|
24
|
+
This package has no dependency on the other Gatework domain gems. The gemspec declares runtime dependencies without version constraints. Applications should commit their own `Gemfile.lock` to make deployments reproducible. No Rails boot or sibling checkout paths are required.
|
|
25
|
+
|
|
26
|
+
Release artifacts and checksums are available from [GitHub Releases](https://github.com/gatework/ctx_parser/releases). Check [RubyGems](https://rubygems.org/gems/ctx_parser) for published versions.
|
|
27
|
+
|
|
28
|
+
## Offline example
|
|
29
|
+
|
|
30
|
+
From a development checkout after installing dependencies:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bundle exec ruby examples/offline.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The example uses synthetic data and performs no network access:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "ctx_parser"
|
|
40
|
+
|
|
41
|
+
# 示例仅实现小型语法;厂商语法应放入对应领域 gem。
|
|
42
|
+
class ExampleStatusParser < CtxParser::Parser
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# 保留未知行,并遵循调用方声明的传输完整性。
|
|
46
|
+
def parse_output(text, complete:)
|
|
47
|
+
records = []
|
|
48
|
+
issues = []
|
|
49
|
+
text.each_line.with_index(1) do |line, number|
|
|
50
|
+
next if line.strip.empty?
|
|
51
|
+
|
|
52
|
+
match = /\A([A-Za-z0-9\/]+) (up|down)\z/.match(line.strip)
|
|
53
|
+
if match
|
|
54
|
+
records << { name: match[1], state: match[2].to_sym }
|
|
55
|
+
else
|
|
56
|
+
issues << { code: :unrecognized_row, line: number }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
status = complete && issues.empty? && !records.empty? ? :complete : :partial
|
|
60
|
+
CtxParser::Result.new(status: status, records: records, issues: issues)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
result = ExampleStatusParser.parse("Eth1/1 up\nEth1/2 down\n", complete: true)
|
|
65
|
+
puts "Parse status: #{result.status}"
|
|
66
|
+
result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Public contract and limits
|
|
70
|
+
|
|
71
|
+
`Parser.parse(text, complete: false)` returns a `Result` with `status`, `records`, and `issues`; a multi-view parser can return `ResultSet`. Result statuses are `complete`, `partial`, `failed`, and `unsupported`. Subclasses implement `parse_output`. Configuration subclasses can use `analyze` and `capture` to preserve provenance.
|
|
72
|
+
|
|
73
|
+
`complete: true` confirms that the caller received the entire input; it does not certify that all syntax was understood. Unknown rows, identity conflicts, and insufficient empty-table evidence must remain explicit. Results are immutable. Source cursors intentionally retain original input and must not be logged when it contains secrets.
|
|
74
|
+
|
|
75
|
+
This gem supplies no vendor command catalog, transport, persistence, or health policy. Parser/registry argument errors remain exceptions for callers to handle at their own boundary.
|
|
76
|
+
|
|
77
|
+
Detailed vendor behavior, historical validation notes, and Motor-only integration are preserved in [the architecture reference](docs/architecture-zh.md). Host integration examples in that reference require Motor; they are not standalone gem APIs. Motor adapters live under `backend/lib/baseline`, `backend/lib/vxlan`, and `backend/lib/wireless` in the [Motor repository](https://github.com/gatework/motor/tree/main/backend/lib).
|
|
78
|
+
|
|
79
|
+
## Development and tests
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
bundle install
|
|
83
|
+
bundle exec rake
|
|
84
|
+
bundle exec ruby examples/offline.rb
|
|
85
|
+
bundle exec rake build
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`bundle exec rake` runs RSpec, RuboCop, and the offline examples. Tests live in `spec/`, following the existing project's RSpec convention; fixtures use synthetic or sanitized evidence. Add regression cases for successful, incomplete, unsupported, and malformed inputs when extending a parser. Offline tests do not certify all device versions or replace live integration checks.
|
|
89
|
+
|
|
90
|
+
Use the declared minimum Ruby version for compatibility checks. Document source methods and non-obvious business logic in concise Chinese, preserve public return/error contracts, and never add raw device captures or credentials to examples, fixtures, logs, or release archives.
|
|
91
|
+
|
|
92
|
+
## Release
|
|
93
|
+
|
|
94
|
+
This directory can be maintained as the independent [ctx_parser](https://github.com/gatework/ctx_parser) repository. Update `VERSION` and `CHANGELOG.md`, run the validation and build commands above, and inspect the packaged file list before tagging `vVERSION`.
|
|
95
|
+
|
|
96
|
+
CI runs tests, lint, examples, build, and an isolated installation on Linux and macOS. After CI passes on `main`, push `vVERSION` to run the release workflow. It publishes the tested Linux artifact to RubyGems and GitHub Releases, attaches `SHA256SUMS`, and verifies downloaded bytes. The GitHub `release` environment accepts a `RUBYGEMS_API_KEY` secret; without it, configure RubyGems Trusted Publishing for this repository, `release.yml`, and the `release` environment. Retry a failed publish job to reuse its CI artifact; an already published version must have identical bytes. See [release details](docs/release.md).
|
|
97
|
+
|
|
98
|
+
Publish dependency gems first. A Git tag, a successful build, or a workflow file does not establish that the gem has reached RubyGems; confirm the registry result separately. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
> 本文保留模块从 Motor 提取前的领域设计、覆盖范围、限制与验证记录,供兼容迁移和故障分析使用。
|
|
2
|
+
> 当前独立 gem 的安装、依赖、开发与发布入口以 [README-zh.md](../README-zh.md) 为准。
|
|
3
|
+
> 涉及 `Motor::*`、`Sync`、`Baseline::Check`、数据库、Rails、Rake 运维任务及私有实机证据的内容均属于 Motor 宿主集成,不随 gem 提供;历史验证记录不代表当前用户设备已经验证。
|
|
4
|
+
|
|
5
|
+
# 多厂商 CLI 解析基础
|
|
6
|
+
|
|
7
|
+
`CtxParser` 是独立于 Rails 的纯 Ruby 抽象。输入是 CLI 回显,输出是结构化事实和诊断。具体语法归所属业务模块维护:当前接入 Cisco N9K 的 `vxlan/cisco_nxos`、H3C 无线控制器的 `wireless/h3c`,以及多厂商配置基线 `baseline`。
|
|
8
|
+
|
|
9
|
+
## 对象职责
|
|
10
|
+
|
|
11
|
+
| 对象 | 职责 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `Parser` | 统一 `.parse(text, complete: false)` 接口和参数校验;具体策略实现 `parse_output` |
|
|
14
|
+
| `ConfigurationParser` | 共用配置节点状态标记、来源捕获和嵌套路径管理;配置语法由具体策略处理 |
|
|
15
|
+
| `Cursor` | 保存原文,顺序读取、窥视、回退,独立记录访问状态和逐行解析状态 |
|
|
16
|
+
| `Line` | 不可变的源行,保留原始文本;构造时计算并缓存缩进和匹配文本 |
|
|
17
|
+
| `Block` | 配置树节点,保存父子层次、源行范围和完整配置块原文 |
|
|
18
|
+
| `Capture` | 已提取数据的不可变快照,将结果路径关联到来源配置块 |
|
|
19
|
+
| `Document` | 把通用块树与可覆写的逐行分类策略组合;设备平台声明自身边界 |
|
|
20
|
+
| `Input` | 按调用方选择编码、换行、NUL 和最大字节数规则 |
|
|
21
|
+
| `Values` | 严格转换十进制整数、MAC、主机地址和 IPv4 前缀;范围由解析器传入 |
|
|
22
|
+
| `Records` | 按调用方的身份键分析重复项,提供首条保留与无歧义投影 |
|
|
23
|
+
| `Immutable` | 复制并冻结纯数据,与调用方的可变对象隔离 |
|
|
24
|
+
| `Result` | 单个清单的 `status / records / issues` |
|
|
25
|
+
| `ResultSet` | 同一回显产生的多个清单,各自保留 `Result` 和完整性 |
|
|
26
|
+
| `CommandCatalog` | 一次声明命令、解析器与结果维度,校验并生成不可变索引 |
|
|
27
|
+
| `Profile` | 组合命令与解析器,按型号覆盖策略,共享命令只解析一次 |
|
|
28
|
+
| `Registry` | 由业务模块声明支持的平台和别名,选择对应 `Profile` |
|
|
29
|
+
|
|
30
|
+
公共库不导入具体厂商,也不维护任何命令正则、设备会话或数据库投影。业务模块按职责复用公共抽象;VXLAN、Wireless 使用命令注册表,Baseline 直接从配置文本进入 `ConfigurationParser`。
|
|
31
|
+
|
|
32
|
+
`Profile#queries(names)` 生成冻结的命令到维度计划,保留选择顺序并合并共享命令,支持同一维度的多条脚本。
|
|
33
|
+
Result 的记录和诊断是防御性复制后的不可变数据;通过 `with(...)` 替换,不能原地修改。
|
|
34
|
+
具体兼容边界见 [模块架构](https://github.com/gatework/motor/blob/main/backend/vendor/ARCHITECTURE.md)。
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
vendor/
|
|
38
|
+
ctx_parser/lib/ctx_parser/
|
|
39
|
+
parser.rb
|
|
40
|
+
cursor.rb
|
|
41
|
+
line.rb
|
|
42
|
+
block.rb
|
|
43
|
+
capture.rb
|
|
44
|
+
document.rb
|
|
45
|
+
configuration_parser.rb
|
|
46
|
+
input.rb
|
|
47
|
+
values.rb
|
|
48
|
+
records.rb
|
|
49
|
+
immutable.rb
|
|
50
|
+
result.rb
|
|
51
|
+
result_set.rb
|
|
52
|
+
command_catalog.rb
|
|
53
|
+
profile.rb
|
|
54
|
+
registry.rb
|
|
55
|
+
vxlan/lib/vxlan/
|
|
56
|
+
parser_registry.rb
|
|
57
|
+
cisco_nxos/
|
|
58
|
+
profile.rb
|
|
59
|
+
parser.rb
|
|
60
|
+
configuration.rb # 特性、模板与网段事实归约
|
|
61
|
+
evpn_l2route.rb
|
|
62
|
+
parsers/show_*.rb # 12 条 NX-OS 命令
|
|
63
|
+
wireless/lib/wireless/
|
|
64
|
+
parser_registry.rb
|
|
65
|
+
fields.rb # 无线领域共用的值转换
|
|
66
|
+
h3c/
|
|
67
|
+
profile.rb
|
|
68
|
+
parser.rb
|
|
69
|
+
command_output.rb # 配置与运行命令共用输入边界
|
|
70
|
+
configuration.rb # 实体身份、归属与认证类型
|
|
71
|
+
verbose_parser.rb
|
|
72
|
+
session_parser.rb
|
|
73
|
+
authentication_parser.rb
|
|
74
|
+
parsers/display_*.rb # 8 条 H3C 命令
|
|
75
|
+
baseline/lib/baseline/
|
|
76
|
+
document.rb # Baseline 自身的 banner 和提示行分类
|
|
77
|
+
facts.rb # 跨命令的事实归约,不依赖解析游标
|
|
78
|
+
parsers/*.rb # 各平台的管理基线规则
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```mermaid
|
|
82
|
+
classDiagram
|
|
83
|
+
CtxParser <|-- NxosParser
|
|
84
|
+
CtxParser <|-- H3cParser
|
|
85
|
+
NxosParser <|-- ShowIpOspfNeighbors
|
|
86
|
+
H3cParser <|-- DisplayCurrentConfiguration
|
|
87
|
+
H3cParser <|-- VerboseParser
|
|
88
|
+
VerboseParser <|-- DisplayWlanApAllVerbose
|
|
89
|
+
VerboseParser <|-- SessionParser
|
|
90
|
+
CtxParserProfile <|-- NxosProfile
|
|
91
|
+
CtxParserProfile <|-- H3cProfile
|
|
92
|
+
Registry --> CtxParserProfile : selects
|
|
93
|
+
CtxParserProfile --> CtxParser : composes
|
|
94
|
+
CtxParser --> Result : returns
|
|
95
|
+
CtxParser <|-- ConfigurationParser
|
|
96
|
+
CtxParser --> Cursor : owns per parse
|
|
97
|
+
Cursor --> Line : reads and tracks
|
|
98
|
+
Cursor --> Capture : records
|
|
99
|
+
Capture --> Block : source
|
|
100
|
+
Block --> Block : children
|
|
101
|
+
Document --> Cursor : owns
|
|
102
|
+
Document --> Block : builds
|
|
103
|
+
BaselineDocument --|> Document : line classification
|
|
104
|
+
BaselineParser --|> ConfigurationParser : analyze and capture
|
|
105
|
+
BaselineParser --> BaselineFacts : normalized observations
|
|
106
|
+
ShowRunningConfig --|> ConfigurationParser
|
|
107
|
+
DisplayCurrentConfiguration --|> ConfigurationParser
|
|
108
|
+
ConfigurationParser --> Capture : records scopes
|
|
109
|
+
DisplayWlanApAllVerbose --> ResultSet : returns AP and radio results
|
|
110
|
+
ResultSet --> Result : contains
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 调用
|
|
114
|
+
|
|
115
|
+
安装对应 gem 后,Ruby 脚本可以直接 require 对应命令文件;模块间依赖由 RubyGems/Bundler 解析,模块内部继续使用 `require_relative`。
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
require "vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors"
|
|
119
|
+
|
|
120
|
+
result = Vxlan::CiscoNxos::Parsers::ShowIpOspfNeighbors.parse(cli_output, complete: true)
|
|
121
|
+
result.status
|
|
122
|
+
result.records
|
|
123
|
+
result.issues
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
按实际设备平台和型号选择策略:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
require "vxlan/parser_registry"
|
|
130
|
+
require "wireless/parser_registry"
|
|
131
|
+
|
|
132
|
+
nxos = Vxlan::ParserRegistry.fetch(:cisco_nxos, model: "N9K-C93180YC-FX")
|
|
133
|
+
result = nxos.parse(:ospf_neighbors, ospf_output, complete: true)
|
|
134
|
+
|
|
135
|
+
h3c = Wireless::ParserRegistry.fetch(:h3c_wireless, model: controller_model)
|
|
136
|
+
results = h3c.parse_many(%i[access_points radios], ap_output, complete: true)
|
|
137
|
+
results.fetch(:access_points).records
|
|
138
|
+
results.fetch(:radios).records
|
|
139
|
+
|
|
140
|
+
# 直接调用共享命令时,返回 ResultSet。
|
|
141
|
+
parsed = Wireless::H3c::Parsers::DisplayWlanApAllVerbose.parse(ap_output, complete: true)
|
|
142
|
+
parsed.fetch(:access_points).complete?
|
|
143
|
+
parsed.fetch(:radios).issues
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`h3c_wireless` 是 `h3c` 的显式别名。未知平台或维度抛出 `ArgumentError`;一份回显不能通过 `parse_many` 解释为多条不同命令。
|
|
147
|
+
|
|
148
|
+
共享回显的视图逐个投影:缺失视图返回 `failed / missing_parser_view`,投影异常返回
|
|
149
|
+
`failed / parser_projection_failed`,非 `Result` 返回值标记 `invalid_parser_result`;其他有效视图保留。
|
|
150
|
+
命令选择、输入校验和解析器执行本身的异常仍沿原调用契约抛出,由领域采集器在该命令边界隔离。
|
|
151
|
+
公共解析层不执行网络操作;三个领域共用的命令执行边界见 [`ctx_driver`](https://github.com/gatework/ctx_driver)。
|
|
152
|
+
|
|
153
|
+
`complete` 默认 `false`,仅在调用方确认完整接收时传 `true`。它不保证解析结果完整:未知行、计数矛盾、身份冲突或空表证据不足仍会降低状态。NX-OS 和 H3C 保留各自的编码、空表、去重和失败策略,例如 NX-OS 剔除所有冲突身份,H3C 保留首条并标记不完整。具体策略不会把未知回显当作完整空清单。
|
|
154
|
+
|
|
155
|
+
业务适配器将结果转换成原有 `Vxlan::Observation`、`Wireless::Observation`,补充维度和采集证据。LLDP 解析器只提取回显中的端点,AP 名称由无线适配器补入。配置中的秘密只保留必要的存在标记。
|
|
156
|
+
|
|
157
|
+
## 有状态的配置分析
|
|
158
|
+
|
|
159
|
+
配置策略使用同一个游标和配置块模型。`Cursor` 不包含厂商命令;`Block.read` 使用游标逐行建立缩进树,厂商策略声明注释和显式边界,再解释每个节点。块树构建与领域外子树的忽略使用显式栈,避免深层缩进耗尽 Ruby 调用栈。Cisco N9K 使用缩进及 `end`,H3C 使用缩进及 `#`、`return`。完整块包含未知子语句、空行和注释,保留 LF、CRLF、CR 和末行没有换行的原貌;显式结束行独立保留在游标中,不包含在前一个块内。
|
|
160
|
+
|
|
161
|
+
`Line#text` 返回缓存的冻结字符串,`Line#indent` 返回缓存的缩进宽度;`raw` 和 `Data` 成员仍为 `index / raw`。需要编辑匹配文本时先 `dup`,不要修改源行。缓存只属于当前文档,不跨解析调用共享。
|
|
162
|
+
|
|
163
|
+
需要追踪来源时,保留解析器实例:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
profile = Vxlan::ParserRegistry.fetch(:cisco_nxos, model: "N9K-C93180YC-FX")
|
|
167
|
+
parser = profile.parser_for(:configuration).new
|
|
168
|
+
result = parser.parse(cli_output, complete: true)
|
|
169
|
+
|
|
170
|
+
cursor = parser.cursor
|
|
171
|
+
cursor.position # 下一行的零基索引;构建块树后位于 EOF
|
|
172
|
+
cursor.state(0) # :parsed / :ignored / :unsupported / :invalid / :pending
|
|
173
|
+
cursor.visited?(0) # 读过不代表解析成功
|
|
174
|
+
cursor.unparsed_content # 未处理、未知或无效行的原文
|
|
175
|
+
parser.blocks # 根配置块;children 可查看嵌套上下文
|
|
176
|
+
|
|
177
|
+
capture = cursor.captures_for([:interfaces, "Vlan110"]).first
|
|
178
|
+
capture.data # 此块处理完成时的结构化数据快照
|
|
179
|
+
capture.line_range # 面向用户的一基行号范围
|
|
180
|
+
capture.content # 此接口的完整原始配置,包括尚未识别的子命令
|
|
181
|
+
capture.complete? # 块内每行均已解析或明确忽略;不表示传输完整
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
捕获路径相对于 `result.records.first`,可用 `result.records.first.dig(*capture.path)` 对照最终结果。H3C 的组、AP、模板、型号和 Radio 块也保留来源,例如 `[:service_templates, 0]`。同一结果可有多个来源;捕获按源文档顺序返回,各自保存处理该块时的数据,后面的配置覆盖值不会改写前面的快照。重复或冲突的配置保留原文及无效状态,捕获信息用于追踪解析过程,最终可用数据仍以 `Result` 为准。
|
|
185
|
+
|
|
186
|
+
同一次事实归约关联多条来源时,使用 `cursor.capture_many(blocks, data:, path:)`。
|
|
187
|
+
该调用先校验所有来源属于当前游标,再复制并深度冻结一份值与路径供各 Capture 引用;返回冻结的 Capture 数组。
|
|
188
|
+
调用方数据不会被修改或冻结,下一次捕获仍产生独立快照。`captures_for(path)` 使用不可变路径索引并缓存排序结果,
|
|
189
|
+
新增捕获或 `reset!` 会使对应缓存失效;`complete?` 仍实时读取行状态,不因来源查询缓存而过时。
|
|
190
|
+
|
|
191
|
+
游标也可独立用于新策略:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
cursor = CtxParser::Cursor.new(cli_output)
|
|
195
|
+
line = cursor.peek # 不前进、不标记访问
|
|
196
|
+
line = cursor.read # 前进并标记访问,语义状态仍为 :pending
|
|
197
|
+
cursor.mark(line.index, state: :parsed) # 仅在成功解释后标记
|
|
198
|
+
cursor.step_back # 只回退位置
|
|
199
|
+
cursor.unread # 回退并把对应行恢复为 :pending
|
|
200
|
+
cursor.defer # 当前上一行恢复待解析,位置不变
|
|
201
|
+
cursor.rewind # 回到开头,保留访问、解析状态和捕获
|
|
202
|
+
cursor.reset! # 清空全部分析状态和捕获,重新开始
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`read_pending` 从当前位置查找下一条 `:pending` 行,也不会自动标成成功。空行和注释由块构建器标为 `:ignored`;领域外配置由具体策略明确忽略,未知相关语句标为 `:unsupported`,无效值标为 `:invalid`。读取原文、解释语法和保存结构化结果因此各有明确职责。
|
|
206
|
+
|
|
207
|
+
每次 `parse` 都重新建立游标和块树,包括失败调用;前一次保留的游标不会被下一次调用改写。单个解析器实例供一次顺序分析使用,并发调用应各用一个实例,`Profile` 已按此方式创建策略。
|
|
208
|
+
|
|
209
|
+
原始配置可能含密钥。原文只通过 `cursor.content`、`line.raw`、`block.content`、`capture.content` 显式读取;这些对象及解析器的 `inspect`、`as_json` 仅输出元数据。`Capture#data` 是脱离可变结果的冻结快照。`Result#to_h` 仍只有 `status / records / issues`,现有采集与库存序列化不携带游标或原文。
|
|
210
|
+
|
|
211
|
+
## 多业务共用的纯能力
|
|
212
|
+
|
|
213
|
+
VXLAN、Wireless 和 Baseline 使用同一套输入与结构能力;各模块继续维护自己的 Profile、设备语法和业务结果规则。
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
CtxParser::Input.text(raw, encoding: :utf8, newlines: :lf)
|
|
217
|
+
CtxParser::Values.integer("4094", min: 1, max: 4094)
|
|
218
|
+
CtxParser::Values.mac("aabb.ccdd.eeff") # => "aa:bb:cc:dd:ee:ff"
|
|
219
|
+
|
|
220
|
+
rows = CtxParser::Records.new(records, identity: [ :name ])
|
|
221
|
+
rows.first_occurrences
|
|
222
|
+
rows.unambiguous
|
|
223
|
+
rows.duplicate_identity_count
|
|
224
|
+
|
|
225
|
+
snapshot = CtxParser::Immutable.copy({ "facts" => [ "ssh" ] })
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`Wireless::Fields` 继续提供无线领域的兼容入口和单位转换;通用输入边界、数字、MAC、IP 与去重委托给 `ctx_parser`。`Records` 分析身份,但不决定如何处置重复行:无线保留首条并诊断,NX-OS 剔除同身份的所有记录。Baseline 共用 `Document` 块树与不可变数据复制,banner、无效回显标记和事实规则仍由 Baseline 定义。
|
|
229
|
+
|
|
230
|
+
## 新厂商、型号和格式
|
|
231
|
+
|
|
232
|
+
新增厂商时,在所属业务模块下建立平台目录、具体命令类和 `Profile`,加入该模块的 `ParserRegistry`。无需修改公共库或其他厂商的解析代码。
|
|
233
|
+
|
|
234
|
+
每个平台通过普通 Ruby 数据声明一份 `CATALOG`,命令字符串只出现一次。相同回显的多个视图放在同一条声明中:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
CATALOG = CtxParser::CommandCatalog.new([
|
|
238
|
+
{ dimensions: %i[access_points radios], command: "display wlan ap all verbose",
|
|
239
|
+
parser: Parsers::DisplayWlanApAllVerbose },
|
|
240
|
+
{ dimensions: [ :clients ], command: "display wlan client verbose",
|
|
241
|
+
parser: Parsers::DisplayWlanClientVerbose }
|
|
242
|
+
])
|
|
243
|
+
COMMANDS = CATALOG.commands
|
|
244
|
+
PARSERS = CATALOG.parsers
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`CommandCatalog` 在构造时拒绝空声明、重复命令或维度、含控制字符的命令,以及非 `Parser` 子类。目录复制并冻结声明数据,不冻结解析器类或调用方的原始数组。由此派生的 `COMMANDS`(维度到命令)和 `PARSERS`(命令到策略)保持原有查询接口;已有自定义 Profile 仍可使用这两个映射。
|
|
248
|
+
|
|
249
|
+
具有共同语法的命令可复用平台基类;特有字段、表头、身份与总数规则留在对应命令中。会话、命令执行和证据持久化由采集器负责;`CommandCatalog` 不执行 I/O,也不引入动态类名查找或自动注册。
|
|
250
|
+
|
|
251
|
+
型号差异用平台 `Profile::MODEL_PARSERS` 声明,形状为 `{ "规范化型号" => { "命令文本" => 专用解析器类 } }`。型号键使用去除首尾空格的小写完整型号,按精确名称匹配;专用解析器可继承已有命令类,仅覆盖有差异的方法。Profile 初始化时校验并复制所选型号的覆盖表:只允许覆盖已声明命令,策略必须继承 `Parser`;修改原表不会改变已创建的 Profile。未覆盖命令和无专用策略的型号使用该平台的公共格式,未知语法仍会产生不完整结果。采集器传入现有 `Motor::Device#model`,AP LLDP 使用 AP 自身型号。
|
|
252
|
+
|
|
253
|
+
当前 N9K 和 H3C 无线控制器使用仓库已有回显样本的格式。新增型号或固件差异规则,需要脱敏样本及完整、截断、未知字段回归。型号覆盖机制已用测试策略验证,生产映射中没有虚构的型号或固件规则。
|
|
254
|
+
|
|
255
|
+
## 验证
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
RAILS_ENV=test bundle exec rspec spec/vendor/ctx_parser spec/vendor/vxlan spec/vendor/wireless spec/vendor/baseline
|
|
259
|
+
RAILS_ENV=test bundle exec rails zeitwerk:check
|
|
260
|
+
bundle exec rubocop vendor/ctx_parser vendor/vxlan vendor/wireless vendor/baseline
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
测试包含无 Rails 的独立加载、型号选择、共享回显解析、输入完整性、旧接口兼容、替身会话和测试数据库投影。真实设备联调另行验证。
|
|
264
|
+
|
|
265
|
+
纯 Ruby 性能样本覆盖 10,000 个合成接口块(20,001 行)、1,000 条同事实来源及1,000个路径查询。
|
|
266
|
+
分别报告耗时和分配对象数,每次验证事实输出、来源顺序及值快照。构造输入不计入耗时;
|
|
267
|
+
比较不同实现时使用相同 Ruby、脚本与空闲环境,通过 `-I` 选择对应版本的 `baseline/lib`:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
bundle exec ruby -I vendor/baseline/lib vendor/ctx_parser/benchmark/configuration.rb
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
该样本用于观察公共配置树与行匹配的成本,不代表设备采集、数据库同步或真实网络吞吐。
|
data/docs/release.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Release
|
|
2
|
+
|
|
3
|
+
1. Update `lib/ctx_parser/version.rb` and `CHANGELOG.md`; run `bundle exec rake` and `bundle exec rake build`.
|
|
4
|
+
2. Run `ruby script/verify_package.rb pkg/ctx_parser-VERSION.gem`. This installs and loads the built gem outside Bundler and the checkout.
|
|
5
|
+
3. Commit and push `main`; wait for both Linux and macOS CI jobs.
|
|
6
|
+
4. Push the matching `vVERSION` tag. The Release workflow repeats CI, then publishes its exact Linux artifact to RubyGems and GitHub with `SHA256SUMS`.
|
|
7
|
+
5. Verify the workflow, registry version, tag commit, and downloaded checksums. A local build is not publication evidence.
|
|
8
|
+
|
|
9
|
+
Configure `RUBYGEMS_API_KEY` as an encrypted secret in the GitHub `release` environment, with noninteractive gem-push permission. Alternatively configure [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) for `gatework/ctx_parser`, `release.yml`, environment `release`, and omit that secret. Never commit credentials.
|
|
10
|
+
|
|
11
|
+
A failed publish is not permission to overwrite an existing version. Rerun the failed publish job to retain the original CI artifact. The script verifies an existing RubyGems version before proceeding and rejects different bytes or yanked versions. `workflow_dispatch` is restricted to version tags by the publish validation.
|
|
12
|
+
|
|
13
|
+
Raw device `*.log`, local secrets, and build outputs are ignored. Package files use an explicit gemspec allowlist; review both the Git index and gem contents before publishing. Tests and examples must use synthetic or sanitized evidence.
|
data/examples/offline.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser"
|
|
4
|
+
|
|
5
|
+
# 示例仅实现小型语法;厂商语法应放入对应领域 gem。
|
|
6
|
+
class ExampleStatusParser < CtxParser::Parser
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# 保留未知行,并遵循调用方声明的传输完整性。
|
|
10
|
+
def parse_output(text, complete:)
|
|
11
|
+
records = []
|
|
12
|
+
issues = []
|
|
13
|
+
text.each_line.with_index(1) do |line, number|
|
|
14
|
+
next if line.strip.empty?
|
|
15
|
+
|
|
16
|
+
match = /\A([A-Za-z0-9\/]+) (up|down)\z/.match(line.strip)
|
|
17
|
+
if match
|
|
18
|
+
records << { name: match[1], state: match[2].to_sym }
|
|
19
|
+
else
|
|
20
|
+
issues << { code: :unrecognized_row, line: number }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
status = complete && issues.empty? && !records.empty? ? :complete : :partial
|
|
24
|
+
CtxParser::Result.new(status: status, records: records, issues: issues)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result = ExampleStatusParser.parse("Eth1/1 up\nEth1/2 down\n", complete: true)
|
|
29
|
+
puts "Parse status: #{result.status}"
|
|
30
|
+
result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CtxParser
|
|
4
|
+
# 消费带分隔符的 CLI 横幅,不将其正文解释为配置。
|
|
5
|
+
class Banner
|
|
6
|
+
# 跟踪横幅分隔符,避免内嵌文本被当作配置。
|
|
7
|
+
def consume(text)
|
|
8
|
+
if @delimiter
|
|
9
|
+
@delimiter = nil if text.include?(@delimiter)
|
|
10
|
+
return true
|
|
11
|
+
end
|
|
12
|
+
match = /\A(?:banner\s+\S+|header\s+\S+(?:\s+information)?)\s+(.+)\z/.match(text)
|
|
13
|
+
return false unless match
|
|
14
|
+
|
|
15
|
+
delimiter = match[1].start_with?("^") ? match[1][0, 2] : match[1][0]
|
|
16
|
+
@delimiter = delimiter unless match[1].delete_prefix(delimiter).include?(delimiter)
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 判断横幅是否仍缺少闭合分隔符。
|
|
21
|
+
def open? = !@delimiter.nil?
|
|
22
|
+
end
|
|
23
|
+
end
|