File Coverage

blib/lib/Mnet/Stanza.pm
Criterion Covered Total %
statement 63 63 100.0
branch 24 26 92.3
condition 25 33 75.7
subroutine 7 7 100.0
pod 3 3 100.0
total 122 132 92.4


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, /^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   535 use warnings;
  1         8  
  1         33  
76 1     1   7 use strict;
  1         2  
  1         19  
77 1     1   5 use Carp;
  1         1  
  1         99  
78 1     1   484 use Mnet::Log::Conditional qw( DEBUG );
  1         3  
  1         859  
79              
80              
81              
82             sub trim {
83              
84             =head2 trim
85              
86             $output = Mnet::Stanza::trim($input)
87              
88             The trim function can be used to normalize stanza spacing and may be useful
89             before calling the diff function or outputting a stanza to the user.
90              
91             This function does the following:
92              
93             - replaces multiple spaces inside text with single spaces
94             - removes spaces at the end of any line of input
95             - removes blank lines and any linefeeds at end of input
96             - removes extra leading spaces while preserving indentation
97              
98             A null value will be output if the input is undefined.
99              
100             Note that in some cases extra spaces in the input may be significant and it
101             may not be appropriate to use this trim function. This must be determined
102             by the developer. Also note that this function does not touch tabs.
103              
104             =cut
105              
106             # read input stanza text
107 3     3 1 679 my $input = shift;
108 3   50     21 DEBUG("trim starting, input ".length($input // "")." chars");
109              
110             # init trimmed output text from input, null if undefined
111 3   50     8 my $output = $input // "";
112              
113             # trim double spaces inside a line, trailing spaces, and blank lines
114 3         27 $output =~ s/(\S) +/$1 /g;
115 3         23 $output =~ s/\s+$//m;
116 3         7 $output =~ s/\n\n+/\n/g;
117 3         45 $output =~ s/(^\n+|\n+$)//g;
118              
119             # determine smallest indent common to all lines
120 3         5 my $indent_init = 999999999999;
121 3         5 my $indent = $indent_init;
122 3         12 foreach my $line (split(/\n/, $output)) {
123 15 100 66     66 if ($line =~ /^(\s+)\S/ and length($1) < $indent) {
124 3         7 $indent = length($1);
125             }
126             }
127              
128             # trim extra indent spaces from left of every line in output
129 3 50 33     48 $output =~ s/^ {$indent}//mg if $indent and $indent < $indent_init;
130              
131             # finished trim function, return trimmed output text
132 3         16 DEBUG("trim finished, output ".length($output)." chars");
133 3         19 return $output;
134             }
135              
136              
137              
138             sub parse {
139              
140             =head2 parse
141              
142             @output = Mnet::Stanza::parse($input, qr/$match_re/)
143             $output = Mnet::Stanza::parse($input, qr/$match_re/)
144              
145             The parse function can be used to output one or more matching stanza sections
146             from the input text, either as a list of matching stanzas or a single string.
147              
148             Here's some sample input text:
149              
150             hostname test
151             interface Ethernet1
152             no ip address
153             shutdown
154             interface Ethernet2
155             ip address 1.2.3.4 255.255.255.0
156              
157             Using an input match_re of qr/^interface/ the following two stanzas are output:
158              
159             interface Ethernet1
160             no ip address
161             shutdown
162             interface Ethernet2
163             ip address 1.2.3.4 255.255.255.0
164              
165             Note that blank lines don't terminate stanzas.
166              
167             Refer also to the trim function in this module.
168              
169             =cut
170              
171             # read input stanza text and match regular expression
172 3     3 1 5 my $input = shift;
173 3   33     9 my $match_re = shift // croak("missing match_re arg");
174 3   50     14 DEBUG("parse starting, input ".length($input // "")." chars");
175              
176             # init list of matched output stanzas
177             # each output stanza will include lines indented under matched line
178 3         6 my @output = ();
179              
180             # loop through lines, set matching output stanzas
181             # use indent var to track indent level of current matched stanza line
182             # if line matches current indent or is blank then append to output stanza
183             # elsif line matches input mathc_re then push to a new output stanza
184             # else reset current indet to undef, to wait for a new match_re line
185 3         4 my $indent = undef;
186 3         10 foreach my $line (split(/\n/, $input)) {
187 16 100 100     106 if (defined $indent and $line =~ /^($indent|\s*$)/) {
    100          
188 4         12 $output[-1] .= "$line\n";
189             } elsif ($line =~ $match_re) {
190 5         13 push @output, "$line\n";
191 5 50       21 $indent = "$1 " if $line =~ /^(\s*)/;
192             } else {
193 7         14 $indent = undef;
194             }
195             }
196              
197             # remove last end of line from all output stanzas
198 3         8 chomp(@output);
199              
200             # finished parse function, return output stanzas as list or string
201 3         14 DEBUG("parse finished, output ".length("@output")." chars");
202 3 100       17 return wantarray ? @output : join("\n", @output);
203             }
204              
205              
206              
207             sub diff {
208              
209             =head2 diff
210              
211             $diff = Mnet::Stanza::diff($old, $new)
212              
213             The diff function checks to see if the input old and new stanza strings are
214             the same.
215              
216             The returned diff value will be set as follows:
217              
218             indicates old and new inputs match
219             indicates both inputs are undefined
220             undef indicates either new or old is undefined
221             line indicates mismatch line number and line text
222             other indicates mismatch such as extra eol chars at end
223              
224             Note that blank lines and all other spaces are significant. The trim function
225             in this module can be used to remove extra spaces before calling this function.
226              
227             =cut
228              
229             # read input old and new stanzas
230 9     9 1 21 my ($old, $new) = (shift, shift);
231 9   100     40 my ($length_old, $length_new) = (length($old // ""), length($new // ""));
      100        
232 9         39 DEBUG("diff starting, input old $length_old chars, new $length_new chars");
233              
234             # init output diff value
235 9         15 my $diff = undef;
236              
237             # set diff undef if old and new are both undefined
238 9 100 100     44 if (not defined $old and not defined $new) {
    100          
    100          
    100          
239 1         2 $diff = undef;
240              
241             # set diff if old stanza is undefined
242             } elsif (not defined $old) {
243 1         2 $diff = "undef: old";
244              
245             # set diff if new stanza is undefined
246             } elsif (not defined $new) {
247 1         2 $diff = "undef: new";
248              
249             # set diff to null if old and new stanzas match
250             } elsif ($old eq $new) {
251 1         2 $diff = "";
252              
253             # set diff to first old or new line that doesn't match
254             # loop through old lines, looking for equivalant new lines
255             # look for additional new lines that are not present in old
256             # set diff to other if we don't know why old is not equal to new
257             } else {
258 5         14 my @new = split(/\n/, $new);
259 5         10 my $count = 0;
260 5         12 foreach my $line (split(/\n/, $old)) {
261 7         10 $count++;
262 7 100 100     26 if (defined $new[0] and $new[0] eq $line) {
263 4         11 shift @new;
264             } else {
265 3         7 $diff = "line $count: $line";
266 3         4 last;
267             }
268             }
269 5         9 $count++;
270 5 100 100     19 $diff = "line $count: $new[0]" if defined $new[0] and not defined $diff;
271 5 100       11 $diff = "other" if not defined $diff;
272              
273             # finished setting output diff
274             }
275              
276             # finished diff function, return diff text
277 9   100     38 DEBUG("diff finished, output ".length($diff // "")." chars");
278 9         33 return $diff;
279             }
280              
281              
282              
283             =head1 SEE ALSO
284              
285             L
286              
287             L
288              
289             =cut
290              
291             # normal end of package
292             1;
293