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   506 use warnings;
  1         8  
  1         33  
76 1     1   6 use strict;
  1         2  
  1         19  
77 1     1   4 use Carp;
  1         2  
  1         121  
78 1     1   450 use Mnet::Log::Conditional qw( DEBUG );
  1         14  
  1         879  
79              
80              
81              
82             sub trim {
83              
84             =head2 trim
85              
86             $output = Mnet::Stanza::trim($input)
87              
88             The Mnet::Stanza::trim function can be used to normalize stanza spacing and may
89             be useful 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 672 my $input = shift;
108 3   50     33 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         25 $output =~ s/\s+$//m;
116 3         8 $output =~ s/\n\n+/\n/g;
117 3         44 $output =~ s/(^\n+|\n+$)//g;
118              
119             # determine smallest indent common to all lines
120 3         6 my $indent_init = 999999999999;
121 3         3 my $indent = $indent_init;
122 3         13 foreach my $line (split(/\n/, $output)) {
123 15 100 66     87 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         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 Mnet::Stanza::parse function can be used to output one or more matching
146             stanza sections from the input text, either as a list of matching stanzas or
147             a single string.
148              
149             Here's some sample input text:
150              
151             hostname test
152             interface Ethernet1
153             no ip address
154             shutdown
155             interface Ethernet2
156             ip address 1.2.3.4 255.255.255.0
157              
158             Using an input match_re of qr/^interface/ the following two stanzas are output:
159              
160             interface Ethernet1
161             no ip address
162             shutdown
163             interface Ethernet2
164             ip address 1.2.3.4 255.255.255.0
165              
166             Note that blank lines don't terminate stanzas.
167              
168             Refer also to the Mnet::Stanza::trim function in this module.
169              
170             =cut
171              
172             # read input stanza text and match regular expression
173 3     3 1 7 my $input = shift;
174 3   33     8 my $match_re = shift // croak("missing match_re arg");
175 3   50     13 DEBUG("parse starting, input ".length($input // "")." chars");
176              
177             # init list of matched output stanzas
178             # each output stanza will include lines indented under matched line
179 3         6 my @output = ();
180              
181             # loop through lines, set matching output stanzas
182             # use indent var to track indent level of current matched stanza line
183             # if line matches current indent or is blank then append to output stanza
184             # elsif line matches input mathc_re then push to a new output stanza
185             # else reset current indet to undef, to wait for a new match_re line
186 3         6 my $indent = undef;
187 3         9 foreach my $line (split(/\n/, $input)) {
188 16 100 100     116 if (defined $indent and $line =~ /^($indent|\s*$)/) {
    100          
189 4         11 $output[-1] .= "$line\n";
190             } elsif ($line =~ $match_re) {
191 5         12 push @output, "$line\n";
192 5 50       21 $indent = "$1 " if $line =~ /^(\s*)/;
193             } else {
194 7         12 $indent = undef;
195             }
196             }
197              
198             # remove last end of line from all output stanzas
199 3         6 chomp(@output);
200              
201             # finished parse function, return output stanzas as list or string
202 3         16 DEBUG("parse finished, output ".length("@output")." chars");
203 3 100       17 return wantarray ? @output : join("\n", @output);
204             }
205              
206              
207              
208             sub diff {
209              
210             =head2 diff
211              
212             $diff = Mnet::Stanza::diff($old, $new)
213              
214             The Mnet::Stanza::diff function checks to see if the input old and new stanza
215             strings are the same.
216              
217             The returned diff value will be set as follows:
218              
219             indicates old and new inputs match
220             indicates both inputs are undefined
221             undef indicates either new or old is undefined
222             line indicates mismatch line number and line text
223             other indicates mismatch such as extra eol chars at end
224              
225             Note that blank lines and all other spaces are significant. To remove extra
226             spaces use the Mnet::Stanza::trim function before calling this function.
227              
228             =cut
229              
230             # read input old and new stanzas
231 9     9 1 22 my ($old, $new) = (shift, shift);
232 9   100     43 my ($length_old, $length_new) = (length($old // ""), length($new // ""));
      100        
233 9         42 DEBUG("diff starting, input old $length_old chars, new $length_new chars");
234              
235             # init output diff value
236 9         14 my $diff = undef;
237              
238             # set diff undef if old and new are both undefined
239 9 100 100     42 if (not defined $old and not defined $new) {
    100          
    100          
    100          
240 1         2 $diff = undef;
241              
242             # set diff if old stanza is undefined
243             } elsif (not defined $old) {
244 1         2 $diff = "undef: old";
245              
246             # set diff if new stanza is undefined
247             } elsif (not defined $new) {
248 1         3 $diff = "undef: new";
249              
250             # set diff to null if old and new stanzas match
251             } elsif ($old eq $new) {
252 1         3 $diff = "";
253              
254             # set diff to first old or new line that doesn't match
255             # loop through old lines, looking for equivalant new lines
256             # look for additional new lines that are not present in old
257             # set diff to other if we don't know why old is not equal to new
258             } else {
259 5         16 my @new = split(/\n/, $new);
260 5         8 my $count = 0;
261 5         13 foreach my $line (split(/\n/, $old)) {
262 7         9 $count++;
263 7 100 100     26 if (defined $new[0] and $new[0] eq $line) {
264 4         9 shift @new;
265             } else {
266 3         8 $diff = "line $count: $line";
267 3         6 last;
268             }
269             }
270 5         9 $count++;
271 5 100 100     18 $diff = "line $count: $new[0]" if defined $new[0] and not defined $diff;
272 5 100       12 $diff = "other" if not defined $diff;
273              
274             # finished setting output diff
275             }
276              
277             # finished diff function, return diff text
278 9   100     37 DEBUG("diff finished, output ".length($diff // "")." chars");
279 9         34 return $diff;
280             }
281              
282              
283              
284             =head1 SEE ALSO
285              
286             L
287              
288             L
289              
290             =cut
291              
292             # normal end of package
293             1;
294