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 +7 -0
- data/LICENSE +202 -0
- data/NOTICE +15 -0
- data/README.md +289 -0
- data/examples/cisco_bgp_summary_example +15 -0
- data/examples/cisco_bgp_summary_template +16 -0
- data/examples/cisco_ipv6_interface_example +53 -0
- data/examples/cisco_ipv6_interface_template +27 -0
- data/examples/cisco_version_example +40 -0
- data/examples/cisco_version_template +17 -0
- data/examples/f10_ip_bgp_summary_example +16 -0
- data/examples/f10_ip_bgp_summary_template +15 -0
- data/examples/f10_version_example +25 -0
- data/examples/f10_version_template +10 -0
- data/examples/index +14 -0
- data/examples/juniper_bgp_summary_example +12 -0
- data/examples/juniper_bgp_summary_template +22 -0
- data/examples/juniper_version_example +10 -0
- data/examples/juniper_version_template +21 -0
- data/examples/unix_ifcfg_example +16 -0
- data/examples/unix_ifcfg_template +18 -0
- data/exe/textfsm +6 -0
- data/lib/textfsm/cli.rb +79 -0
- data/lib/textfsm/cli_table.rb +92 -0
- data/lib/textfsm/data.rb +23 -0
- data/lib/textfsm/errors.rb +8 -0
- data/lib/textfsm/field.rb +83 -0
- data/lib/textfsm/index_table.rb +88 -0
- data/lib/textfsm/options.rb +93 -0
- data/lib/textfsm/parser.rb +233 -0
- data/lib/textfsm/pattern.rb +322 -0
- data/lib/textfsm/rule.rb +72 -0
- data/lib/textfsm/table.rb +96 -0
- data/lib/textfsm/version.rb +5 -0
- data/lib/textfsm.rb +5 -0
- metadata +105 -0
data/lib/textfsm/rule.rb
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby port of Google TextFSM rules. Copyright 2010 Google Inc.
|
|
4
|
+
# Modified in 2026. Licensed under Apache-2.0; see LICENSE.
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "pattern"
|
|
7
|
+
|
|
8
|
+
module TextFSM
|
|
9
|
+
class Rule
|
|
10
|
+
LINE_ACTIONS = { "Continue" => :continue, "Next" => :next, "Error" => :error }.freeze
|
|
11
|
+
RECORD_ACTIONS = { "Clear" => :clear, "Clearall" => :clear_all, "Record" => :record, "NoRecord" => :no_record }.freeze
|
|
12
|
+
OPERATIONS = (LINE_ACTIONS.keys + RECORD_ACTIONS.keys).freeze
|
|
13
|
+
TARGET = '(?:[\p{L}\p{N}_]+|".*")'
|
|
14
|
+
ACTION = /\A\s+(?<line>Continue|Next|Error)(?:\.(?<record>Clear|Clearall|Record|NoRecord))?(?:\s+(?<target>#{TARGET}))?\z/
|
|
15
|
+
RECORD_ACTION = /\A\s+(?<record>Clear|Clearall|Record|NoRecord)(?:\s+(?<target>#{TARGET}))?\z/
|
|
16
|
+
STATE_ACTION = /\A(?:\s+(?<target>#{TARGET}))?\z/
|
|
17
|
+
private_constant :LINE_ACTIONS, :RECORD_ACTIONS, :TARGET, :ACTION, :RECORD_ACTION, :STATE_ACTION
|
|
18
|
+
|
|
19
|
+
attr_reader :source, :pattern, :line_number, :line_action, :record_action, :next_state, :error_message
|
|
20
|
+
|
|
21
|
+
def initialize(line, line_number:, fields:)
|
|
22
|
+
@line_number = line_number
|
|
23
|
+
@line_action = :next
|
|
24
|
+
@record_action = :no_record
|
|
25
|
+
@next_state = @error_message = nil
|
|
26
|
+
@action_source = ""
|
|
27
|
+
line = line.strip
|
|
28
|
+
action = /\A(.*)\s->(.*)\z/.match(line)
|
|
29
|
+
@source = (action ? action[1] : line).freeze
|
|
30
|
+
@pattern = Pattern.new(substitute(@source, fields))
|
|
31
|
+
parse_action(action[2]) if action
|
|
32
|
+
freeze
|
|
33
|
+
rescue TemplateError => e
|
|
34
|
+
raise TemplateError, "#{e.message}. Line: #{@line_number}."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
" #{@source}#{" -> #{@action_source}" unless @action_source.empty?}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def substitute(text, fields)
|
|
44
|
+
text.gsub(/\$(?:\$|\{[a-zA-Z_][a-zA-Z_0-9]*\}|[a-zA-Z_][a-zA-Z_0-9]*|)/) do |token|
|
|
45
|
+
next "$" if token == "$$"
|
|
46
|
+
|
|
47
|
+
name = token[1] == "{" ? token[2...-1] : token[1..]
|
|
48
|
+
fields.fetch(name) { raise TemplateError, "Invalid variable substitution #{token.inspect}" }.capture_source
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_action(action)
|
|
53
|
+
match = ACTION.match(action) || RECORD_ACTION.match(action) || STATE_ACTION.match(action)
|
|
54
|
+
raise TemplateError, "Badly formatted rule action #{action.inspect}" unless match
|
|
55
|
+
|
|
56
|
+
parts = match.named_captures
|
|
57
|
+
@line_action = LINE_ACTIONS.fetch(parts["line"], :next)
|
|
58
|
+
@record_action = RECORD_ACTIONS.fetch(parts["record"], :no_record)
|
|
59
|
+
target = parts["target"]&.freeze
|
|
60
|
+
raise TemplateError, "Continue cannot specify a new state" if @line_action == :continue && target
|
|
61
|
+
raise TemplateError, "Only Error can specify a quoted message" if @line_action != :error && target&.start_with?('"')
|
|
62
|
+
|
|
63
|
+
if @line_action == :error
|
|
64
|
+
@error_message = target
|
|
65
|
+
else
|
|
66
|
+
@next_state = target
|
|
67
|
+
end
|
|
68
|
+
operation = [parts["line"], parts["record"]].compact.join(".")
|
|
69
|
+
@action_source = [operation, target].compact.reject(&:empty?).join(" ").freeze
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "data"
|
|
4
|
+
|
|
5
|
+
module TextFSM
|
|
6
|
+
class Table
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :header, :rows
|
|
10
|
+
|
|
11
|
+
def initialize(header = [], rows = [])
|
|
12
|
+
raise ArgumentError, "Header and rows must be Arrays" unless header.is_a?(Array) && rows.is_a?(Array) && rows.all?(Array)
|
|
13
|
+
raise ArgumentError, "Duplicate table columns" unless header.uniq == header
|
|
14
|
+
raise ArgumentError, "Rows must have one value per column" unless rows.all? { |row| row.size == header.size }
|
|
15
|
+
|
|
16
|
+
@header = Data.copy(header, immutable: true)
|
|
17
|
+
@rows = Data.copy(rows, immutable: true)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def each(&)
|
|
21
|
+
return enum_for(__method__) { size } unless block_given?
|
|
22
|
+
|
|
23
|
+
@rows.each(&)
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def size
|
|
28
|
+
@rows.size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def empty?
|
|
32
|
+
@rows.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def [](index)
|
|
36
|
+
@rows[index]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_a
|
|
40
|
+
Data.copy(@rows)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_hashes
|
|
44
|
+
to_a.map { |row| @header.zip(row).to_h }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
([@header] + @rows).map { |row| "#{row.join(', ')}\n" }.join
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Keep left rows, add new columns, and use the first matching right row.
|
|
52
|
+
# Without keys, align by position. Missing matches receive empty values.
|
|
53
|
+
def merge(other, keys: [])
|
|
54
|
+
dup.merge!(other, keys: keys)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def merge!(other, keys: [])
|
|
58
|
+
missing = (keys - @header) | (keys - other.header)
|
|
59
|
+
raise KeyError, "Unknown key columns: #{missing.join(', ')}" unless missing.empty?
|
|
60
|
+
|
|
61
|
+
columns = other.header - @header
|
|
62
|
+
return self if columns.empty?
|
|
63
|
+
|
|
64
|
+
right_columns = columns.map { |column| other.header.index(column) }
|
|
65
|
+
left_keys = keys.map { |key| @header.index(key) }
|
|
66
|
+
right_keys = keys.map { |key| other.header.index(key) }
|
|
67
|
+
lookup = {}
|
|
68
|
+
unless keys.empty?
|
|
69
|
+
other.each do |row|
|
|
70
|
+
lookup[row.values_at(*right_keys)] ||= row
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
rows = @rows.each_with_index.map do |row, index|
|
|
74
|
+
right = keys.empty? ? other[index] : lookup[row.values_at(*left_keys)]
|
|
75
|
+
row + (right ? right.values_at(*right_columns) : Array.new(columns.size, ""))
|
|
76
|
+
end
|
|
77
|
+
header = Data.copy(@header + columns, immutable: true)
|
|
78
|
+
rows = Data.copy(rows, immutable: true)
|
|
79
|
+
@header = header
|
|
80
|
+
@rows = rows
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sort!(&)
|
|
85
|
+
@rows = @rows.sort(&).freeze
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def sort_by!(&)
|
|
90
|
+
return enum_for(__method__) { size } unless block_given?
|
|
91
|
+
|
|
92
|
+
@rows = @rows.sort_by(&).freeze
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/textfsm.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: textfsm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TextFSM Ruby contributors
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-09-12 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: strscan
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
description: Parses semi-structured text using Google TextFSM template syntax, with
|
|
41
|
+
field options, state transitions, and CLI template indexes.
|
|
42
|
+
executables:
|
|
43
|
+
- textfsm
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- LICENSE
|
|
48
|
+
- NOTICE
|
|
49
|
+
- README.md
|
|
50
|
+
- examples/cisco_bgp_summary_example
|
|
51
|
+
- examples/cisco_bgp_summary_template
|
|
52
|
+
- examples/cisco_ipv6_interface_example
|
|
53
|
+
- examples/cisco_ipv6_interface_template
|
|
54
|
+
- examples/cisco_version_example
|
|
55
|
+
- examples/cisco_version_template
|
|
56
|
+
- examples/f10_ip_bgp_summary_example
|
|
57
|
+
- examples/f10_ip_bgp_summary_template
|
|
58
|
+
- examples/f10_version_example
|
|
59
|
+
- examples/f10_version_template
|
|
60
|
+
- examples/index
|
|
61
|
+
- examples/juniper_bgp_summary_example
|
|
62
|
+
- examples/juniper_bgp_summary_template
|
|
63
|
+
- examples/juniper_version_example
|
|
64
|
+
- examples/juniper_version_template
|
|
65
|
+
- examples/unix_ifcfg_example
|
|
66
|
+
- examples/unix_ifcfg_template
|
|
67
|
+
- exe/textfsm
|
|
68
|
+
- lib/textfsm.rb
|
|
69
|
+
- lib/textfsm/cli.rb
|
|
70
|
+
- lib/textfsm/cli_table.rb
|
|
71
|
+
- lib/textfsm/data.rb
|
|
72
|
+
- lib/textfsm/errors.rb
|
|
73
|
+
- lib/textfsm/field.rb
|
|
74
|
+
- lib/textfsm/index_table.rb
|
|
75
|
+
- lib/textfsm/options.rb
|
|
76
|
+
- lib/textfsm/parser.rb
|
|
77
|
+
- lib/textfsm/pattern.rb
|
|
78
|
+
- lib/textfsm/rule.rb
|
|
79
|
+
- lib/textfsm/table.rb
|
|
80
|
+
- lib/textfsm/version.rb
|
|
81
|
+
homepage: https://github.com/gatework/textfsm
|
|
82
|
+
licenses:
|
|
83
|
+
- Apache-2.0
|
|
84
|
+
metadata:
|
|
85
|
+
source_code_uri: https://github.com/gatework/textfsm
|
|
86
|
+
bug_tracker_uri: https://github.com/gatework/textfsm/issues
|
|
87
|
+
allowed_push_host: https://rubygems.org
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.1'
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 4.0.12
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: A Ruby state machine for parsing text with TextFSM templates
|
|
105
|
+
test_files: []
|