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
|
@@ -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
|
+
|
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
data/lib/textfsm/cli.rb
ADDED
|
@@ -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
|
data/lib/textfsm/data.rb
ADDED
|
@@ -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,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
|