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, /^ip access-list SECURE/);
19              
20             # note latest version of secure acl, trim extra spaces
21             my $acl_new = Mnet::Stanza::trim("
22             ip access-list SECURE
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 SECURE\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 SECURE in/m;
37             die "error, $int" if $int !~ /^interface (\S+)/;
38             print "interface $1\n";
39             print " ip access-group SECURE 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   515 use warnings;
  1         7  
  1         32  
76 1     1   5 use strict;
  1         2  
  1         19  
77 1     1   5 use Carp;
  1         1  
  1         144  
78 1     1   457 use Mnet::Log::Conditional qw( DEBUG );
  1         3  
  1         911  
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 763 my $input = shift;
108 3   50     20 DEBUG("trim starting, input ".length($input // "")." chars");
109              
110             # init trimmed output text from input, null if undefined
111 3   50     7 my $output = $input // "";
112              
113             # trim double spaces inside a line, trailing spaces, and blank lines
114 3         25 $output =~ s/(\S) +/$1 /g;
115 3         24 $output =~ s/\s+$//m;
116 3         6 $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         6 my $indent_init = 999999999999;
121 3         4 my $indent = $indent_init;
122 3         11 foreach my $line (split(/\n/, $output)) {
123 15 100 66     70 if ($line =~ /^(\s+)\S/ and length($1) < $indent) {
124 3         6 $indent = length($1);
125             }
126             }
127              
128             # trim extra indent spaces from left of every line in output
129 3 50 33     49 $output =~ s/^ {$indent}//mg if $indent and $indent < $indent_init;
130              
131             # finished trim function, return trimmed output text
132 3         14 DEBUG("trim finished, output ".length($output)." chars");
133 3         21 return $output;
134             }
135              
136              
137              
138             sub parse {
139              
140             =head2 parse
141              
142             @output = Mnet::Stanza::parse($input, /$match_re/)
143             $output = Mnet::Stanza::parse($input, /$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             Refer also to the trim function in this module.
166              
167             =cut
168              
169             # read input stanza text and match regular expression
170 2     2 1 4 my $input = shift;
171 2   33     5 my $match_re = shift // croak("missing match_re arg");
172 2   50     10 DEBUG("parse starting, input ".length($input // "")." chars");
173              
174             # init list of matched output stanzas
175             # each output stanza will include lines indented under matched line
176 2         6 my @output = ();
177              
178             # loop through lines, set matching output stanzas
179             # use indent var to track indent level of current matched stanza line
180             # if line matches current indent then append to current output stanza
181             # elsif line matches input mathc_re then push to a new output stanza
182             # else reset current indet to undef, to wait for a new match_re line
183 2         4 my $indent = undef;
184 2         7 foreach my $line (split(/\n/, $input)) {
185 12 100 100     89 if (defined $indent and $line =~ /^$indent/) {
    100          
186 2         6 $output[-1] .= "$line\n";
187             } elsif ($line =~ $match_re) {
188 4         9 push @output, "$line\n";
189 4 50       17 $indent = "$1 " if $line =~ /^(\s*)/;
190             } else {
191 6         13 $indent = undef;
192             }
193             }
194              
195             # remove last end of line from all output stanzas
196 2         8 chomp(@output);
197              
198             # finished parse function, return output stanzas as list or string
199 2         12 DEBUG("parse finished, output ".length("@output")." chars");
200 2 100       11 return wantarray ? @output : join("\n", @output);
201             }
202              
203              
204              
205             sub diff {
206              
207             =head2 diff
208              
209             $diff = Mnet::Stanza::diff($old, $new)
210              
211             The diff function checks to see if the input old and new stanza strings are
212             the same.
213              
214             The returned diff value will be set as follows:
215              
216             indicates old and new inputs match
217             indicates both inputs are undefined
218             undef indicates either new or old is undefined
219             line indicates mismatch line number and line text
220             other indicates mismatch such as extra eol chars at end
221              
222             Note that blank lines and all other spaces are significant. The trim function
223             in this module can be used to remove extra spaces before calling this function.
224              
225             =cut
226              
227             # read input old and new stanzas
228 9     9 1 24 my ($old, $new) = (shift, shift);
229 9   100     40 my ($length_old, $length_new) = (length($old // ""), length($new // ""));
      100        
230 9         39 DEBUG("diff starting, input old $length_old chars, new $length_new chars");
231              
232             # init output diff value
233 9         16 my $diff = undef;
234              
235             # set diff undef if old and new are both undefined
236 9 100 100     43 if (not defined $old and not defined $new) {
    100          
    100          
    100          
237 1         3 $diff = undef;
238              
239             # set diff if old stanza is undefined
240             } elsif (not defined $old) {
241 1         3 $diff = "undef: old";
242              
243             # set diff if new stanza is undefined
244             } elsif (not defined $new) {
245 1         2 $diff = "undef: new";
246              
247             # set diff to null if old and new stanzas match
248             } elsif ($old eq $new) {
249 1         3 $diff = "";
250              
251             # set diff to first old or new line that doesn't match
252             # loop through old lines, looking for equivalant new lines
253             # look for additional new lines that are not present in old
254             # set diff to other if we don't know why old is not equal to new
255             } else {
256 5         17 my @new = split(/\n/, $new);
257 5         8 my $count = 0;
258 5         12 foreach my $line (split(/\n/, $old)) {
259 7         9 $count++;
260 7 100 100     28 if (defined $new[0] and $new[0] eq $line) {
261 4         9 shift @new;
262             } else {
263 3         8 $diff = "line $count: $line";
264 3         5 last;
265             }
266             }
267 5         7 $count++;
268 5 100 100     19 $diff = "line $count: $new[0]" if defined $new[0] and not defined $diff;
269 5 100       13 $diff = "other" if not defined $diff;
270              
271             # finished setting output diff
272             }
273              
274             # finished diff function, return diff text
275 9   100     37 DEBUG("diff finished, output ".length($diff // "")." chars");
276 9         36 return $diff;
277             }
278              
279              
280              
281             =head1 SEE ALSO
282              
283             L
284              
285             L
286              
287             =cut
288              
289             # normal end of package
290             1;
291