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,40 @@
1
+ Cisco IOS Software, Catalyst 4500 L3 Switch Software (cat4500-ENTSERVICESK9-M), Version 12.2(31)SGA1, RELEASE SOFTWARE (fc3)
2
+ Technical Support: http://www.cisco.com/techsupport
3
+ Copyright (c) 1986-2007 by Cisco Systems, Inc.
4
+ Compiled Fri 26-Jan-07 14:28 by kellythw
5
+ Image text-base: 0x10000000, data-base: 0x118AD800
6
+
7
+ ROM: 12.2(31r)SGA
8
+ Pod Revision 0, Force Revision 34, Gill Revision 20
9
+
10
+ router.abc uptime is 3 days, 13 hours, 53 minutes
11
+ System returned to ROM by reload
12
+ System restarted at 05:09:09 PDT Wed Apr 2 2008
13
+ System image file is "bootflash:cat4500-entservicesk9-mz.122-31.SGA1.bin"
14
+
15
+
16
+ This product contains cryptographic features and is subject to United
17
+ States and local country laws governing import, export, transfer and
18
+ use. Delivery of Cisco cryptographic products does not imply
19
+ third-party authority to import, export, distribute or use encryption.
20
+ Importers, exporters, distributors and users are responsible for
21
+ compliance with U.S. and local country laws. By using this product you
22
+ agree to comply with applicable laws and regulations. If you are unable
23
+ to comply with U.S. and local laws, return this product immediately.
24
+
25
+ A summary of U.S. laws governing Cisco cryptographic products may be found at:
26
+ http://www.cisco.com/wwl/export/crypto/tool/stqrg.html
27
+
28
+ If you require further assistance please contact us by sending email to export@cisco.com.
29
+
30
+ cisco WS-C4948-10GE (MPC8540) processor (revision 5) with 262144K bytes of memory.
31
+ Processor board ID FOX111700ZP
32
+ MPC8540 CPU at 667Mhz, Fixed Module
33
+ Last reset from Reload
34
+ 2 Virtual Ethernet interfaces
35
+ 48 Gigabit Ethernet interfaces
36
+ 2 Ten Gigabit Ethernet interfaces
37
+ 511K bytes of non-volatile configuration memory.
38
+
39
+ Configuration register is 0x2102
40
+
@@ -0,0 +1,17 @@
1
+ Value Model (\S+)
2
+ Value Memory (\S+)
3
+ Value ConfigRegister (0x\S+)
4
+ Value Uptime (.*)
5
+ Value Version (.*?)
6
+ Value ReloadReason (.*)
7
+ Value ReloadTime (.*)
8
+ Value ImageFile ([^"]+)
9
+
10
+ Start
11
+ ^Cisco IOS Software.*Version ${Version},
12
+ ^.*uptime is ${Uptime}
13
+ ^System returned to ROM by ${ReloadReason}
14
+ ^System restarted at ${ReloadTime}
15
+ ^System image file is "${ImageFile}"
16
+ ^cisco ${Model} .* with ${Memory} bytes of memory
17
+ ^Configuration register is ${ConfigRegister}
@@ -0,0 +1,16 @@
1
+ BGP router identifier 192.0.2.1, local AS number 65551
2
+ BGP table version is 173711, main routing table version 173711
3
+ 255 network entrie(s) using 43260 bytes of memory
4
+ 1114 paths using 75752 bytes of memory
5
+ BGP-RIB over all using 76866 bytes of memory
6
+ 23 BGP path attribute entrie(s) using 1472 bytes of memory
7
+ 3 BGP AS-PATH entrie(s) using 137 bytes of memory
8
+ 10 BGP community entrie(s) using 498 bytes of memory
9
+ 2 BGP route-reflector cluster entrie(s) using 62 bytes of memory
10
+ 6 neighbor(s) using 28128 bytes of memory
11
+
12
+ Neighbor AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/Pfx
13
+
14
+ 10.10.10.10 65551 647 397 73711 0 (0) 10:37:12 5
15
+ 10.10.100.1 65552 664 416 73711 0 (0) 10:38:27 0
16
+ 10.100.10.9 65553 709 526 73711 0 (0) 07:55:38 1
@@ -0,0 +1,15 @@
1
+ Value Filldown RouterID (\d+(\.\d+){3})
2
+ Value Filldown LocalAS (\d+)
3
+ Value RemoteAS (\d+)
4
+ Value Required RemoteIP (\d+(\.\d+){3})
5
+ Value Uptime (\S+)
6
+ Value Received_V4 (\d+)
7
+ Value Received_V6 ()
8
+ Value Status (\D.*)
9
+
10
+ Start
11
+ ^BGP router identifier ${RouterID}, local AS number ${LocalAS}
12
+ ^${RemoteIP}\s+${RemoteAS}(\s+\S+){5}\s+${Uptime}\s+${Received_V4} -> Next.Record
13
+ ^${RemoteIP}\s+${RemoteAS}(\s+\S+){5}\s+${Uptime}\s+${Status} -> Next.Record
14
+
15
+ EOF
@@ -0,0 +1,25 @@
1
+ Force10 Networks Real Time Operating System Software
2
+ Force10 Operating System Version: 1.0
3
+ Force10 Application Software Version: 7.7.1.1
4
+ Copyright (c) 1999-2008 by Force10 Networks, Inc.
5
+ Build Time: Fri Sep 12 14:08:26 PDT 2008
6
+ Build Path: /sites/sjc/work/sw/build/special_build/Release/E7-7-1/SW/SRC
7
+ router.abc uptime is 3 day(s), 2 hour(s), 3 minute(s)
8
+
9
+ System image file is "flash://FTOS-EF-7.7.1.1.bin"
10
+
11
+ Chassis Type: E1200
12
+ Control Processor: IBM PowerPC 750FX (Rev D2.2) with 536870912 bytes of memory.
13
+ Route Processor 1: IBM PowerPC 750FX (Rev D2.2) with 1073741824 bytes of memory.
14
+ Route Processor 2: IBM PowerPC 750FX (Rev D2.2) with 1073741824 bytes of memory.
15
+
16
+ 128K bytes of non-volatile configuration memory.
17
+
18
+ 1 Route Processor Module
19
+ 9 Switch Fabric Module
20
+ 1 48-port GE line card with SFP optics (EF)
21
+ 7 4-port 10GE LAN/WAN PHY line card with XFP optics (EF)
22
+ 1 FastEthernet/IEEE 802.3 interface(s)
23
+ 48 GigabitEthernet/IEEE 802.3 interface(s)
24
+ 28 Ten GigabitEthernet/IEEE 802.3 interface(s)
25
+
@@ -0,0 +1,10 @@
1
+ Value Chassis (\S+)
2
+ Value Model (.*)
3
+ Value Software (.*)
4
+ Value Image ([^"]*)
5
+
6
+ Start
7
+ ^Force10 Application Software Version: ${Software}
8
+ ^Chassis Type: ${Chassis} -> Continue
9
+ ^Chassis Type: ${Model}
10
+ ^System image file is "${Image}"
data/examples/index ADDED
@@ -0,0 +1,14 @@
1
+
2
+ # First line is the header fields for columns and is mandatory.
3
+ # Regular expressions are supported in all fields except the first.
4
+ # Last field supports variable length command completion.
5
+ # abc[[xyz]] is expanded to abc(x(y(z)?)?)?, regexp inside [[]] is not supported
6
+ #
7
+ Template, Hostname, Vendor, Command
8
+ cisco_bgp_summary_template, .*, Cisco, sh[[ow]] ip bg[[p]] su[[mmary]]
9
+ cisco_version_template, .*, Cisco, sh[[ow]] ve[[rsion]]
10
+ f10_ip_bgp_summary_template, .*, Force10, sh[[ow]] ip bg[[p]] sum[[mary]]
11
+ f10_version_template, .*, Force10, sh[[ow]] ve[[rsion]]
12
+ juniper_bgp_summary_template, .*, Juniper, sh[[ow]] bg[[p]] su[[mmary]]
13
+ juniper_version_template, .*, Juniper, sh[[ow]] ve[[rsion]]
14
+ unix_ifcfg_template, hostname[abc].*, .*, ifconfig
@@ -0,0 +1,12 @@
1
+ Groups: 3 Peers: 3 Down peers: 0
2
+ Table Tot Paths Act Paths Suppressed History Damp State Pending
3
+ inet.0 947 310 0 0 0 0
4
+ inet6.0 849 807 0 0 0 0
5
+ Peer AS InPkt OutPkt OutQ Flaps Last Up/Dwn State|#Active/Received/Damped...
6
+ 10.247.68.182 65550 131725 28179233 0 11 6w3d17h Establ
7
+ inet.0: 4/5/1
8
+ inet6.0: 0/0/0
9
+ 10.254.166.246 65550 136159 29104942 0 0 6w5d6h Establ
10
+ inet.0: 0/0/0
11
+ inet6.0: 7/8/1
12
+ 192.0.2.100 65551 1269381 1363320 0 1 9w5d6h 2/3/0 0/0/0
@@ -0,0 +1,22 @@
1
+ Value RemoteAS (\d+)
2
+ Value RemoteIP (\S+)
3
+ Value Uptime (.*[0-9h])
4
+ Value Active_V4 (\d+)
5
+ Value Received_V4 (\d+)
6
+ Value Accepted_V4 (\d+)
7
+ Value Damped_V4 (\d+)
8
+ Value Active_V6 (\d+)
9
+ Value Received_V6 (\d+)
10
+ Value Accepted_V6 (\d+)
11
+ Value Damped_V6 (\d+)
12
+ Value Status ([a-zA-Z]+)
13
+
14
+ Start
15
+ # New format IPv4 & IPv6 split across newlines.
16
+ ^\s+inet.0: ${Active_V4}/${Received_V4}/${Damped_V4}
17
+ ^\s+inet6.0: ${Active_V6}/${Received_V6}/${Damped_V6}
18
+ ^ -> Continue.Record
19
+ ^${RemoteIP}\s+${RemoteAS}(\s+\d+){4}\s+${Uptime}\s+${Status}
20
+ ^${RemoteIP}\s+${RemoteAS}(\s+\d+){4}\s+${Uptime}\s+${Active_V4}/${Received_V4}/${Damped_V4}\s+${Active_V6}/${Received_V6}/${Damped_V6} -> Next.Record
21
+ ^${RemoteIP}\s+${RemoteAS}(\s+\d+){4}\s+${Uptime}\s+${Active_V4}/${Received_V4}/${Accepted_V4}/${Damped_V4}\s+${Active_V6}/${Received_V6}/${Accepted_V6}/${Damped_V6} -> Next.Record
22
+ ^${RemoteIP}\s+${RemoteAS}(\s+\d+){4}\s+${Uptime}\s+${Status} -> Next.Record
@@ -0,0 +1,10 @@
1
+ Hostname: router.abc
2
+ Model: mx960
3
+ JUNOS Base OS boot [9.1S3.5]
4
+ JUNOS Base OS Software Suite [9.1S3.5]
5
+ JUNOS Kernel Software Suite [9.1S3.5]
6
+ JUNOS Crypto Software Suite [9.1S3.5]
7
+ JUNOS Packet Forwarding Engine Support (M/T Common) [9.1S3.5]
8
+ JUNOS Packet Forwarding Engine Support (MX Common) [9.1S3.5]
9
+ JUNOS Online Documentation [9.1S3.5]
10
+ JUNOS Routing Software Suite [9.1S3.5]
@@ -0,0 +1,21 @@
1
+ Value Chassis (\S+)
2
+ Value Required Model (\S+)
3
+ Value Boot (.*)
4
+ Value Base (.*)
5
+ Value Kernel (.*)
6
+ Value Crypto (.*)
7
+ Value Documentation (.*)
8
+ Value Routing (.*)
9
+
10
+ Start
11
+ # Support multiple chassis systems.
12
+ ^\S+:$$ -> Continue.Record
13
+ ^${Chassis}:$$
14
+ ^Model: ${Model}
15
+ ^JUNOS Base OS boot \[${Boot}\]
16
+ ^JUNOS Software Release \[${Base}\]
17
+ ^JUNOS Base OS Software Suite \[${Base}\]
18
+ ^JUNOS Kernel Software Suite \[${Kernel}\]
19
+ ^JUNOS Crypto Software Suite \[${Crypto}\]
20
+ ^JUNOS Online Documentation \[${Documentation}\]
21
+ ^JUNOS Routing Software Suite \[${Routing}\]
@@ -0,0 +1,16 @@
1
+ lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
2
+ inet6 ::1 prefixlen 128
3
+ inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
4
+ inet 127.0.0.1 netmask 0xff000000
5
+ en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
6
+ ether 34:15:9e:27:45:e3
7
+ inet6 fe80::3615:9eff:fe27:45e3%en0 prefixlen 64 scopeid 0x4
8
+ inet6 2001:db8::3615:9eff:fe27:45e3 prefixlen 64 autoconf
9
+ inet 192.0.2.215 netmask 0xfffffe00 broadcast 192.0.2.255
10
+ media: autoselect (1000baseT <full-duplex,flow-control>)
11
+ status: active
12
+ en1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
13
+ ether 90:84:0d:f6:d1:55
14
+ media: <unknown subtype> (<unknown type>)
15
+ status: inactive
16
+
@@ -0,0 +1,18 @@
1
+ Value Required Interface ([^:]+)
2
+ Value MTU (\d+)
3
+ Value State ((in)?active)
4
+ Value MAC ([\d\w:]+)
5
+ Value List Inet ([\d\.]+)
6
+ Value List Netmask (\S+)
7
+ # Don't match interface local (fe80::/10) - achieved with excluding '%'.
8
+ Value List Inet6 ([^%]+)
9
+ Value List Prefix (\d+)
10
+
11
+ Start
12
+ # Record interface record (if we have one).
13
+ ^\S+:.* -> Continue.Record
14
+ # Collect data for new interface.
15
+ ^${Interface}:.* mtu ${MTU}
16
+ ^\s+ether ${MAC}
17
+ ^\s+inet6 ${Inet6} prefixlen ${Prefix}
18
+ ^\s+inet ${Inet} netmask ${Netmask}
data/exe/textfsm ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/textfsm/cli"
5
+
6
+ exit TextFSM::CLI.run
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require_relative "version"
6
+ require_relative "parser"
7
+
8
+ module TextFSM
9
+ class CLI
10
+ def self.run(argv = ARGV, **streams)
11
+ new(**streams).run(argv)
12
+ end
13
+
14
+ def initialize(input: $stdin, output: $stdout, error: $stderr)
15
+ @input = input
16
+ @output = output
17
+ @error = error
18
+ end
19
+
20
+ def run(argv)
21
+ settings = { format: "json", rows: false, validate: false, help: false, version: false }
22
+ options = option_parser(settings)
23
+ args = options.parse(argv.dup)
24
+ if settings[:help] || settings[:version]
25
+ @output.puts(settings[:help] ? options : VERSION)
26
+ return 0
27
+ end
28
+ raise OptionParser::InvalidArgument, "expected TEMPLATE and optional INPUT" unless (1..2).cover?(args.length)
29
+
30
+ parser = Parser.from_file(args.first)
31
+ if settings[:validate]
32
+ @output.puts("Template OK: #{parser.header.join(', ')}")
33
+ return 0
34
+ end
35
+ text = args[1] && args[1] != "-" ? File.read(args[1], encoding: "UTF-8") : @input.read
36
+ rows = parser.parse(text)
37
+ write_result(parser.header, rows, settings)
38
+ 0
39
+ rescue Error, OptionParser::ParseError, SystemCallError, IOError, ArgumentError => e
40
+ @error.puts("textfsm: #{e.message}")
41
+ 2
42
+ end
43
+
44
+ private
45
+
46
+ def option_parser(settings)
47
+ OptionParser.new do |parser|
48
+ parser.banner = "Usage: textfsm [options] TEMPLATE [INPUT|-]"
49
+ parser.on("--format FORMAT", %w[json table], "Output format: json (default), table") do |format|
50
+ settings[:format] = format
51
+ end
52
+ parser.on("--rows", "JSON output as {header, rows}") do
53
+ settings[:rows] = true
54
+ end
55
+ parser.on("--validate", "Validate template without parsing input") do
56
+ settings[:validate] = true
57
+ end
58
+ parser.on("-v", "--version", "Print version") do
59
+ settings[:version] = true
60
+ end
61
+ parser.on("-h", "--help", "Print help") do
62
+ settings[:help] = true
63
+ end
64
+ end
65
+ end
66
+
67
+ def write_result(header, rows, settings)
68
+ if settings[:format] == "json"
69
+ data = settings[:rows] ? { header: header, rows: rows } : rows.map { |row| header.zip(row).to_h }
70
+ @output.puts(JSON.pretty_generate(data))
71
+ else
72
+ @output.puts(header.join("\t"))
73
+ rows.each do |row|
74
+ @output.puts(row.map { |value| value.is_a?(String) ? value : JSON.generate(value) }.join("\t"))
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby port of Google TextFSM CLI tables. Copyright 2022 Google Inc.
4
+ # Modified in 2026. Licensed under Apache-2.0; see LICENSE.
5
+ require_relative "parser"
6
+ require_relative "index_table"
7
+ require_relative "table"
8
+
9
+ module TextFSM
10
+ class CliTable < Table
11
+ attr_reader :index, :template_dir, :input, :keys
12
+
13
+ def initialize(index: nil, template_dir: ".")
14
+ super()
15
+ @template_dir = File.path(template_dir).dup.freeze
16
+ @keys = [].freeze
17
+ load_index(index) if index
18
+ end
19
+
20
+ def load_index(path)
21
+ candidate = IndexTable.new(
22
+ File.join(@template_dir, path),
23
+ transform: ->(key, value) { key == "Command" ? expand_command(value) : value },
24
+ compile: ->(key, value) { key == "Template" ? nil : value }
25
+ )
26
+ raise IndexError, "Index must contain a Template column" unless candidate.header.include?("Template")
27
+
28
+ @index = candidate
29
+ self
30
+ end
31
+
32
+ def parse(text, attributes: {}, templates: nil)
33
+ names = template_names(templates || find_templates(attributes))
34
+ text = text.read if text.respond_to?(:read)
35
+ raise ArgumentError, "text must be a String or readable IO" unless text.is_a?(String)
36
+
37
+ keys = []
38
+ table = names.reduce(nil) do |result, name|
39
+ parser = Parser.from_file(File.join(@template_dir, name))
40
+ keys = parser.fields_with_option("Key") if keys.empty?
41
+ parsed = Table.new(parser.header, parser.parse(text))
42
+ result ? result.merge!(parsed, keys: keys) : parsed
43
+ end
44
+ # Replace the result only after every template and merge succeeds.
45
+ input = text.dup.freeze
46
+ keys = Data.copy(keys, immutable: true)
47
+ @input = input
48
+ @header = table.header
49
+ @rows = table.rows
50
+ @keys = keys
51
+ self
52
+ end
53
+
54
+ def keys=(columns)
55
+ missing = columns - @header
56
+ raise KeyError, "Unknown key columns: #{missing.join(', ')}" unless missing.empty?
57
+
58
+ @keys = Data.copy(columns.uniq, immutable: true)
59
+ end
60
+
61
+ def key_for(row)
62
+ row.values_at(*@keys.map { |key| @header.index(key) })
63
+ end
64
+
65
+ private
66
+
67
+ def find_templates(attributes)
68
+ raise IndexError, "Provide templates or load an index" unless @index
69
+
70
+ row = @index.match(attributes)
71
+ raise IndexError, "No template found for attributes: #{attributes.inspect}" unless row
72
+
73
+ row.fetch("Template")
74
+ end
75
+
76
+ def template_names(templates)
77
+ raise ArgumentError, "templates must be a String or an Array of names" unless templates.is_a?(String) || templates.is_a?(Array)
78
+
79
+ names = templates.is_a?(Array) ? templates : templates.split(":", -1)
80
+ raise ArgumentError, "Template list cannot be empty" if names.empty? || names.any? { |name| !name.is_a?(String) || name.strip.empty? }
81
+
82
+ names.map(&:strip)
83
+ end
84
+
85
+ def expand_command(command)
86
+ command.gsub(/\[\[(.+?)\]\]/) do
87
+ word = Regexp.last_match(1)
88
+ "(#{word.each_char.to_a.join('(')}#{')?' * word.length}"
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TextFSM
4
+ # Records contain nested lists and named captures. Every ownership boundary
5
+ # copies these values; callers can request an immutable result snapshot.
6
+ module Data
7
+ def self.copy(value, immutable: false)
8
+ result = case value
9
+ when Array
10
+ value.map { |item| copy(item, immutable: immutable) }
11
+ when Hash
12
+ value.to_h do |key, item|
13
+ [copy(key, immutable: immutable), copy(item, immutable: immutable)]
14
+ end
15
+ else
16
+ value.dup
17
+ end
18
+ immutable ? result.freeze : result
19
+ end
20
+ end
21
+
22
+ private_constant :Data
23
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TextFSM
4
+ class Error < StandardError; end
5
+ class TemplateError < Error; end
6
+ class ParseError < Error; end
7
+ class IndexError < Error; end
8
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby port of Google TextFSM values. Copyright 2010 Google Inc.
4
+ # Modified in 2026. Licensed under Apache-2.0; see LICENSE.
5
+ require_relative "errors"
6
+ require_relative "options"
7
+ require_relative "pattern"
8
+
9
+ module TextFSM
10
+ class Field
11
+ attr_reader :parser, :name, :source, :capture_source, :option_names, :pattern
12
+ attr_accessor :value
13
+
14
+ def initialize(line, parser:, options: Options::BUILTINS)
15
+ @parser = parser
16
+ parse_declaration(line)
17
+ @pattern = Pattern.new(@source)
18
+ @capture_source = "(?P<#{@name}>#{@source[1..]}".freeze
19
+ @options = @option_names.map do |name|
20
+ options.fetch(name) { raise TemplateError, "Unknown option #{name.inspect}" }.new(self)
21
+ end.freeze
22
+ @options.each(&:after_initialize)
23
+ end
24
+
25
+ def option?(name)
26
+ @option_names.include?(name)
27
+ end
28
+
29
+ def visible?
30
+ @options.all?(&:visible?)
31
+ end
32
+
33
+ def empty?
34
+ !@value || (@value.respond_to?(:empty?) && @value.empty?)
35
+ end
36
+
37
+ def assign(value)
38
+ @value = value
39
+ @options.each(&:after_assign)
40
+ end
41
+
42
+ def clear
43
+ @value = nil
44
+ @options.each(&:after_clear)
45
+ end
46
+
47
+ def reset
48
+ @value = nil
49
+ @options.each(&:after_reset)
50
+ end
51
+
52
+ def prepare_record
53
+ @options.each(&:before_record)
54
+ end
55
+
56
+ def to_s
57
+ ["Value", (@option_names.join(",") unless @option_names.empty?), @name, @source].compact.join(" ")
58
+ end
59
+
60
+ private
61
+
62
+ def parse_declaration(line)
63
+ # The template grammar splits on literal spaces, including empty fields.
64
+ tokens = line.split(/ /, -1)
65
+ raise TemplateError, "Expect at least 3 tokens in Value declaration" if tokens.length < 3
66
+
67
+ if tokens[2].start_with?("(")
68
+ @name = tokens[1].freeze
69
+ @source = tokens[2..].join(" ").freeze
70
+ @option_names = [].freeze
71
+ else
72
+ @name = tokens[2].freeze
73
+ @source = tokens[3..].join(" ").freeze
74
+ @option_names = tokens[1].split(",", -1).map(&:freeze).freeze
75
+ end
76
+ raise TemplateError, "Invalid Value name #{@name.inspect}" unless @name.match?(/\A[a-zA-Z_][a-zA-Z_0-9]*\z/) && @name.length <= 48
77
+ raise TemplateError, "Duplicate option" if @option_names.uniq != @option_names
78
+ return if @source.start_with?("(") && @source.end_with?(")")
79
+
80
+ raise TemplateError, "Value #{@source.inspect} must be contained within a '()' pair"
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby port of Google TextFSM index matching. Copyright 2022 Google Inc.
4
+ # Modified in 2026. Licensed under Apache-2.0; see LICENSE.
5
+ require_relative "errors"
6
+ require_relative "pattern"
7
+ require_relative "data"
8
+
9
+ module TextFSM
10
+ class IndexTable
11
+ include Enumerable
12
+
13
+ attr_reader :header
14
+
15
+ def initialize(path, transform: nil, compile: nil)
16
+ @header, @rows = Data.copy(read_table(path, transform), immutable: true)
17
+ @patterns = @rows.map do |row|
18
+ row.to_h do |key, value|
19
+ value = compile.call(key, value) if compile
20
+ [key, value.nil? || value.empty? ? nil : Pattern.new(value)]
21
+ end.freeze
22
+ end.freeze
23
+ freeze
24
+ rescue TemplateError => e
25
+ raise IndexError, "#{path}: #{e.message}"
26
+ end
27
+
28
+ def each(&)
29
+ return enum_for(__method__) { size } unless block_given?
30
+
31
+ @rows.each(&)
32
+ self
33
+ end
34
+
35
+ def size
36
+ @rows.size
37
+ end
38
+
39
+ def empty?
40
+ @rows.empty?
41
+ end
42
+
43
+ def [](index)
44
+ @rows[index]
45
+ end
46
+
47
+ # Empty cells are wildcards. Attributes absent from the index are ignored.
48
+ def match(attributes)
49
+ index = @patterns.index do |patterns|
50
+ attributes.all? do |key, value|
51
+ pattern = patterns[key.to_s]
52
+ pattern.nil? || pattern.match?(value.to_s)
53
+ end
54
+ end
55
+ @rows[index] if index
56
+ end
57
+
58
+ private
59
+
60
+ def read_table(path, transform)
61
+ lines = File.foreach(path, encoding: "UTF-8")
62
+ header = nil
63
+ rows = []
64
+ lines.each do |raw|
65
+ line = raw.strip
66
+ next if line.empty? || line.start_with?("#")
67
+
68
+ line = line.split("#", 2).first.rstrip unless header
69
+
70
+ fields = line.split(",", -1).map(&:strip)
71
+ unless header
72
+ header = fields
73
+ raise IndexError, "Duplicate index header" unless header.uniq == header
74
+
75
+ next
76
+ end
77
+ next unless fields.size == header.size
78
+
79
+ row = header.zip(fields).to_h
80
+ row = row.to_h { |key, value| [key, transform.call(key, value)] } if transform
81
+ rows << row
82
+ end
83
+ raise IndexError, "Missing index header" unless header
84
+
85
+ [header, rows]
86
+ end
87
+ end
88
+ end