File Coverage

blib/lib/Mnet/Stanza.pm
Criterion Covered Total %
statement 54 54 100.0
branch 25 26 96.1
condition 22 27 81.4
subroutine 6 6 100.0
pod 3 3 100.0
total 110 116 94.8


line stmt bran cond sub pod time code
1             package Mnet::Stanza;
2              
3             =head1 NAME
4              
5             Mnet::Stanza - Manipulate stanza outline text
6              
7             =head1 SYNOPSIS
8              
9             # use this module
10             use Mnet::Stanza;
11              
12             # read current config from standard input, trim extra spaces
13             my $sh_run = undef;
14             $sh_run .= "$_\n" while ;
15             $sh_run = Mnet::Stanza::trim($sh_run);
16              
17             # parse existing version of secure acl from current config
18             my $acl_old = Mnet::Stanza::parse($sh_run, qr/^ip access-list DMZ/);
19              
20             # note latest version of secure acl, trim extra spaces
21             my $acl_new = Mnet::Stanza::trim("
22             ip access-list DMZ
23             permit 192.168.0.0 0.0.255.255
24             ");
25              
26             # print config to update acl if current acl is different than latest
27             if (Mnet::Stanza::diff($acl_old, $acl_new)) {
28             print "no ip access-list DMZ\n" if $acl_old;
29             print "$acl_new\n";
30             }
31              
32             # print config applying acl to shutdown interfaces, if needed
33             my @ints = Mnet::Stanza::parse($sh_run, qr/^interface/);
34             foreach my $int (@ints) {
35             next if $int !~ /^\s*shutdown/m;
36             next if $int =~ /^\s*ip access-group DMZ in/m;
37             die "error, $int" if $int !~ /^interface (\S+)/;
38             print "interface $1\n";
39             print " ip access-group DMZ in\n";
40             }
41              
42             =head1 DESCRIPTION
43              
44             Mnet::Stanza can be used on text arranged in stanzas of indented lines or text
45             in outline format, such as the following:
46              
47             line
48             stanza 1
49             indented line
50             stanza 2
51             sub-stanza 1
52             indented 1
53             indented 2
54             sub-sub-stanza 1
55             indented 1
56             indented 2
57             end
58              
59             In the above example the following would be true:
60              
61             stanza 1 contains a single indented line
62             stanza 2 contains sub-stanza 1 and everything under sub-stanza 1
63             sub-stanza 1 contains two indented lines and a sub-sub-stanza 1
64             sub-sub-stanza 1 contains two indented lines
65              
66             This can be used to parse cisco ios configs, amongst other things.
67              
68             =head1 FUNCTIONS
69              
70             Mnet::Stanza implements the functions listed below.
71              
72             =cut
73              
74             # required modules
75 1     1   514 use warnings;
  1         8  
  1         33  
76 1     1   5 use strict;
  1         2  
  1         22  
77 1     1   4 use Carp;
  1         1  
  1         859  
78              
79              
80              
81             sub trim {
82              
83             =head2 trim
84              
85             $output = Mnet::Stanza::trim($input)
86              
87             The Mnet::Stanza::trim function can be used to normalize stanza spacing and may
88             be useful before calling the diff function or outputting a stanza to the user.
89              
90             This function does the following:
91              
92             - replaces multiple spaces inside text with single spaces
93             - removes spaces at the end of any line of input
94             - removes blank lines and any linefeeds at end of input
95             - removes extra leading spaces while preserving indentation
96              
97             A null value will be output if the input is undefined.
98              
99             Note that in some cases extra spaces in the input may be significant and it
100             may not be appropriate to use this trim function. This must be determined
101             by the developer. Also note that this function does not touch tabs.
102              
103             =cut
104              
105             # read input stanza text
106 5     5 1 1187 my $input = shift;
107              
108             # init trimmed output text from input, null if undefined
109 5   50     16 my $output = $input // "";
110              
111             # trim double spaces inside a line, trailing spaces, and blank lines
112 5         36 $output =~ s/(\S) +/$1 /g;
113 5         34 $output =~ s/\s+$//m;
114 5         11 $output =~ s/\n\n+/\n/g;
115 5         57 $output =~ s/(^\n+|\n+$)//g;
116              
117             # determine smallest indent common to all lines
118 5         10 my $indent_init = 999999999999;
119 5         9 my $indent = $indent_init;
120 5         17 foreach my $line (split(/\n/, $output)) {
121 20 100 66     95 if ($line =~ /^(\s*)\S/ and length($1) < $indent) {
122 5         11 $indent = length($1);
123             }
124             }
125              
126             # trim extra indent spaces from left of every line in output
127 5 100 66     57 $output =~ s/^ {$indent}//mg if $indent and $indent < $indent_init;
128              
129             # finished trim function, return trimmed output text
130 5         37 return $output;
131             }
132              
133              
134              
135             sub parse {
136              
137             =head2 parse
138              
139             @output = Mnet::Stanza::parse($input, qr/$match_re/)
140             $output = Mnet::Stanza::parse($input, qr/$match_re/)
141              
142             The Mnet::Stanza::parse function can be used to output one or more matching
143             stanza sections from the input text, either as a list of matching stanzas or
144             a single string.
145              
146             Here's some sample input text:
147              
148             hostname test
149             interface Ethernet1
150             no ip address
151             shutdown
152             interface Ethernet2
153             ip address 1.2.3.4 255.255.255.0
154              
155             Using an input match_re of qr/^interface/ the following two stanzas are output:
156              
157             interface Ethernet1
158             no ip address
159             shutdown
160             interface Ethernet2
161             ip address 1.2.3.4 255.255.255.0
162              
163             Note that blank lines don't terminate stanzas.
164              
165             Refer also to the Mnet::Stanza::trim function in this module.
166              
167             =cut
168              
169             # read input stanza text and match regular expression
170 4     4 1 9 my $input = shift;
171 4   33     11 my $match_re = shift // croak("missing match_re arg");
172              
173             # init list of matched output stanzas
174             # each output stanza will include lines indented under matched line
175 4         9 my @output = ();
176              
177             # loop through lines, set matching output stanzas
178             # use indent var to track indent level of current matched stanza line
179             # if line matches current indent or is blank then append to output stanza
180             # elsif line matches input mathc_re then push to a new output stanza
181             # else reset current indet to undef, to wait for a new match_re line
182 4         6 my $indent = undef;
183 4         12 foreach my $line (split(/\n/, $input)) {
184 19 100 100     160 if (defined $indent and $line =~ /^($indent|\s*$)/) {
    100          
185 5         14 $output[-1] .= "$line\n";
186             } elsif ($line =~ $match_re) {
187 7         20 push @output, "$line\n";
188 7 50       30 $indent = "$1 " if $line =~ /^(\s*)/;
189             } else {
190 7         16 $indent = undef;
191             }
192             }
193              
194             # remove last end of line from all output stanzas
195 4         9 chomp(@output);
196              
197             # finished parse function, return output stanzas as list or string
198 4 100       33 return wantarray ? @output : join("\n", @output);
199             }
200              
201              
202              
203             sub diff {
204              
205             =head2 diff
206              
207             $diff = Mnet::Stanza::diff($old, $new)
208              
209             The Mnet::Stanza::diff function checks to see if the input old and new stanza
210             strings are the same.
211              
212             The returned diff value will be set as follows:
213              
214             indicates old and new inputs match
215             indicates both inputs are undefined
216             undef indicates either new or old is undefined
217             line indicates mismatch line number and line text
218             other indicates mismatch such as extra eol chars at end
219              
220             Note that blank lines and all other spaces are significant. To remove extra
221             spaces use the Mnet::Stanza::trim function before calling this function.
222              
223             =cut
224              
225             # read input old and new stanzas
226 9     9 1 23 my ($old, $new) = (shift, shift);
227 9   100     39 my ($length_old, $length_new) = (length($old // ""), length($new // ""));
      100        
228              
229             # init output diff value
230 9         14 my $diff = undef;
231              
232             # set diff undef if old and new are both undefined
233 9 100 100     43 if (not defined $old and not defined $new) {
    100          
    100          
    100          
234 1         2 $diff = undef;
235              
236             # set diff if old stanza is undefined
237             } elsif (not defined $old) {
238 1         3 $diff = "undef: old";
239              
240             # set diff if new stanza is undefined
241             } elsif (not defined $new) {
242 1         2 $diff = "undef: new";
243              
244             # set diff to null if old and new stanzas match
245             } elsif ($old eq $new) {
246 1         3 $diff = "";
247              
248             # set diff to first old or new line that doesn't match
249             # loop through old lines, looking for equivalant new lines
250             # look for additional new lines that are not present in old
251             # set diff to other if we don't know why old is not equal to new
252             } else {
253 5         17 my @new = split(/\n/, $new);
254 5         7 my $count = 0;
255 5         14 foreach my $line (split(/\n/, $old)) {
256 7         9 $count++;
257 7 100 100     26 if (defined $new[0] and $new[0] eq $line) {
258 4         9 shift @new;
259             } else {
260 3         8 $diff = "line $count: $line";
261 3         6 last;
262             }
263             }
264 5         7 $count++;
265 5 100 100     18 $diff = "line $count: $new[0]" if defined $new[0] and not defined $diff;
266 5 100       11 $diff = "other" if not defined $diff;
267              
268             # finished setting output diff
269             }
270              
271             # finished diff function, return diff text
272 9         34 return $diff;
273             }
274              
275              
276              
277             =head1 SEE ALSO
278              
279             L
280              
281             L
282              
283             =cut
284              
285             # normal end of package
286             1;
287