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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TextFSM
4
+ VERSION = "0.2.0"
5
+ end
data/lib/textfsm.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "textfsm/version"
4
+ require_relative "textfsm/parser"
5
+ require_relative "textfsm/cli_table"
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: []