File Coverage

blib/lib/Puppet/Tidy.pm
Criterion Covered Total %
statement 101 137 73.7
branch 33 62 53.2
condition 4 18 22.2
subroutine 16 19 84.2
pod 9 13 69.2
total 163 249 65.4


line stmt bran cond sub pod time code
1             # Copyright (c) 2012 Jasper Lievisse Adriaanse
2             # Copyright (c) 2012-2013 M:tier Ltd.
3             #
4             # Permission to use, copy, modify, and distribute this software for any
5             # purpose with or without fee is hereby granted, provided that the above
6             # copyright notice and this permission notice appear in all copies.
7             #
8             # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9             # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10             # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11             # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12             # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13             # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14             # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15              
16             package Puppet::Tidy;
17              
18 9     9   9348 use 5.008;
  9         32  
  9         420  
19 9     9   57 use strict;
  9         14  
  9         286  
20 9     9   57 use Exporter;
  9         15  
  9         967  
21 9     9   10673 use File::Copy;
  9         85596  
  9         1032  
22 9     9   11878 use Text::Tabs;
  9         9483  
  9         1339  
23              
24 9     9   61 use vars qw(@ISA @EXPORT $VERSION);
  9         17  
  9         49231  
25              
26             @ISA = qw( Exporter );
27             @EXPORT = qw( &puppettidy );
28              
29             $VERSION = '0.3';
30              
31             my %config = (
32             output_type => 'file',
33             output_ext => 'tdy',
34             output_stream => undef,
35             input_files => undef,
36             input_stream => undef,
37             validate => 0,
38             );
39              
40             sub puppettidy(%){
41 31     31 0 20408 my %defaults = (
42             argv => undef,
43             source => undef,
44             destination => undef,
45             );
46              
47 31         109 my %args_hash = @_;
48 31         189 %args_hash = (%defaults, %args_hash);
49              
50             # Don't bother with commandline args, if we're using the args_hash
51             # to pass parameters.
52 31 50 33     170 if ($args_hash{'source'} or $args_hash{'destination'}) {
53 31         73 $config{'output_type'} = 'stream';
54 31         80 $config{'input_stream'} = $args_hash{'source'};
55 31         57 $config{'output_stream'} = $args_hash{'destination'};
56 31         44 push(@{$config{'input_files'}}, "-");
  31         137  
57             } else {
58 0         0 parse_options(@ARGV);
59             }
60              
61 31 50       230 if ($config{'input_files'} eq "-") {
62 0 0 0     0 die unless ($config{'input_stream'} && $config{'output_stream'})
63             }
64              
65 31         44 foreach my $file (@{$config{input_files}}) {
  31         76  
66 84         99 my @input;
67              
68 84 50       166 if ($config{'output_type'} eq "file") {
69             # Just open it once for slurping, and open it once for writing later.
70 0 0       0 open(IF, "<$file") or die("Cannot open $file for reading: $!");
71 0         0 @input = ;
72 0         0 close(IF);
73             } else {
74 84         168 @input = $config{'input_stream'};
75             }
76              
77 84         191 expand_tabs(\@input);
78 84         1684 commenting(\@input);
79 84         809 trailing_whitespace(\@input);
80 84         852 variable_string(\@input);
81 84         487 quotes_resource_ref_type(\@input);
82 84         199 quotes_title(\@input);
83 84         189 quotes_attribute(\@input);
84 84         215 handle_modes(\@input);
85 84         187 quoted_booleans(\@input);
86              
87 84 50       220 if ($config{'output_type'} eq "file") {
88 0 0       0 open(OF, ">$file.tdy") or die("Cannot open $file.tdy for writing: $!");
89 0         0 foreach my $line (@input)
90             {
91 0         0 print OF $line;
92             }
93 0         0 close(OF);
94 0         0 pp_validate("$file.tdy");
95             } else {
96 84         98 @{$config{'output_stream'}} = @input;
  84         414  
97             }
98             }
99             }
100              
101             sub usage()
102             {
103 0     0 0 0 print STDERR << "EOF";
104             Puppet::Tidy $VERSION
105             usage: $0 [-ch] [file ...]
106             -c : Check/validate the output with "puppet parser validate".
107             -h : Show this help message.
108             EOF
109 0         0 exit 1;
110             }
111              
112             sub parse_options(@)
113             {
114 0     0 0 0 require Getopt::Std;
115              
116 0         0 my %opt;
117 0         0 Getopt::Std::getopts('ch', \%opt);
118              
119 0 0 0     0 usage() if defined($opt{h}) or (@ARGV < 1);
120              
121 0 0       0 if (defined($opt{c})) {
122             # Make sure puppet is installed
123 0 0       0 unless (grep { -x "$_/puppet"}split /:/,$ENV{PATH}) {
  0         0  
124 0         0 print STDERR "Puppet is not installed or cannot be run. Make sure it's in your \$PATH.\n";
125 0         0 exit 127;
126             }
127              
128 0         0 $config{'validate'} = 1;
129             }
130              
131             # Check if input files are readable at all up front.
132 0         0 foreach my $f (@ARGV) {
133 0         0 my $mode = (stat($f))[2];
134 0 0 0     0 if (defined($mode) && ($mode & 4)) {
135 0         0 push(@{$config{'input_files'}}, $f);
  0         0  
136             } else {
137 0         0 print "ERROR: $f is not readable or does not exist.\n";
138 0         0 exit 127;
139             }
140             }
141             }
142              
143             # Check the output of Puppet::Tidy with "puppet parser validate".
144             sub pp_validate($)
145             {
146 0     0 0 0 my $file = shift;
147              
148 0 0       0 open(PP, "puppet parser validate $file |") or
149             die("Failed to validate manifest: $!\n");
150 0         0 close(PP);
151             }
152              
153             # Expand literal tabs to two spaces
154             sub expand_tabs(@)
155             {
156 84     84 1 519 my $input = shift;
157 84         115 $tabstop = 2;
158              
159 84         249 @$input = Text::Tabs::expand(@$input);
160             }
161              
162             # Remove trailing whitespace.
163             sub trailing_whitespace(@)
164             {
165 84     84 1 104 my $input = shift;
166 84         728 foreach my $line (@$input)
167             {
168 84         1943 $line =~ s/[^\S\n]+$//g;
169             }
170             }
171              
172             # Wuoted strings containing only a variable shouldn't be quoted, also
173             # single quoted strings containing a variable must be double quoted.
174             sub variable_string(@)
175             {
176 84     84 1 107 my $input = shift;
177              
178 84         1289 foreach my $line (@$input)
179             {
180             # Skip commented lines.
181 84 50 33     461 next if (($line eq "\n") or ($line =~ m/^#/));
182              
183             # Remove double quotes around a standalone variable
184 84         137 $line =~ s/"\$\{(.*?)\}"/\$\{$1\}/g;
185 84         131 $line =~ s/"\$(.*?)"/\$$1/g;
186              
187             # Remove single quotes around a standalone variable
188 84         266 $line =~ s/\x27\$(.*?)\x27/\$$1/g;
189             }
190             }
191              
192             # Gix double quotes when used when references resources (File, Group, etc).
193             # Bariables were already removed in the previous step so we can't have
194             # a Package["$pkg"] here anymore.
195             sub quotes_resource_ref_type(@)
196             {
197 84     84 1 102 my $input = shift;
198 84         183 foreach my $line (@$input) {
199 84 100       300 if ($line =~ m/([^a-z][a-zA-Z]+)\[.*\]/) {
200 10         21 my $type = $1;
201 10 100       92 next unless $line =~ m/$type\[\"/;
202              
203 5         86 $line =~ s/$type\["(.*?)"\]/$type\[\x27$1\x27\]/g;
204             }
205             }
206             }
207              
208             # Titles, like '/etc/fstab': shouldn't contain doubles quotes, unless
209             # it contains or is a variable. Otherwise all titles should be single quoted.
210             sub quotes_title(@)
211             {
212 84     84 1 104 my $input = shift;
213              
214 84         126 foreach my $line (@$input) {
215 84 100       234 next if $line =~ m/\s*path/; # XXX: Tighten regexps below and remove me
216 79 100       205 next if $line =~ m/\s*command/; # XXX: Tighten regexps below and remove me
217 73 100       168 next if ($line =~ m/\:\:/); # XXX: Skip lines with qualified variables
218              
219             # Strings with a variable should be double quoted, but care
220             # must be taken if it's alse single quoted which is wrong anyway.
221 67 100       170 if ($line =~ m/\$.*?:(\s*|$)/) {
222 7 50       77 next if $line =~ s/(\x27*)(\$\w+)(\x27*)/"$2"/g;
223 0 0       0 next if $line =~ s/(\$\w+)/"$1"/g;
224             }
225 60         86 $line =~ s/"(.*?)":/\x27$1\x27:/g; # Double to single quoted
226 60         483 $line =~ s/(?!['"])(\w+):(?!.+['"]+)/\x27$1\x27:/g; # Bare word to single quoted
227             }
228             }
229              
230             # Certain attributes should be single quoted, unless it is, or contains
231             # a variable.
232             sub quotes_attribute(@)
233             {
234 84     84 1 106 my $input = shift;
235 84         270 my @attributes = qw(mode path); # XXX: non-exhaustive list
236              
237             # "Bare" to single quoted with no variables.
238 84         133 foreach my $line (@$input) {
239 84 100       272 next if $line =~ m/\$/;
240 65         86 foreach my $attr (@attributes)
241             {
242 130         2347 $line =~ s/($attr)(\s+)=>(\s+)(\w+)/$1$2=>$3\x27$4\x27/g;
243             }
244             }
245              
246             # Double quoted to single quoted with no variables.
247 84         161 foreach my $line (@$input) {
248 84 100       275 next unless $line =~ m/=\> "/;
249 8 50       23 next if $line =~ m/\$/;
250 8         15 foreach my $attr (@attributes)
251             {
252 16         270 $line =~ s/($attr)(\s+)=>(\s+)"(\w+)"/$1$2=>$3\x27$4\x27/g;
253             }
254             }
255              
256             # Variables should be double quoted for string interpolation,
257             # which won't be done for at least mode since it's fully nummeric.
258 84         138 foreach my $line (@$input) {
259 84 50 66     767 next unless (($line =~ m/=\> '/) and ($line =~ m/\$/));
260 0         0 foreach my $attr (@attributes)
261             {
262 0 0       0 next if ($attr eq "mode");
263 0         0 $line =~ s/($attr)(\s+)=>(\s+)'(\w+)'/$1$2$3\x22$4\x22/g;
264             }
265             }
266             }
267              
268             # File modes need to be specified with 4 digits.
269             sub handle_modes(@)
270             {
271 84     84 1 109 my $input = shift;
272              
273 84         117 foreach my $line (@$input)
274             {
275 84 100       253 next if $line =~ m/\$/;
276             # Rewrite to four digits if only three are defined.
277 65         376 $line =~ s/mode(\s+)=>(\s+)\x27(\d{3})\x27/mode$1=>$2\x270$3\x27/g;
278             }
279             }
280              
281             # C (/**/) or C++ (//) style comments are not recommended.
282             sub commenting(@)
283             {
284 84     84 1 107 my $input = shift;
285              
286 84         147 foreach my $line (@$input)
287             {
288 84         162 $line =~ s,(?!['"].+)//(?!.+['"]),#,; # C++ style
289 84         1269 $line =~ s,/\*(.*?)(\s+)\*/,#$1,; # C style
290             }
291             }
292              
293             # Insert a warning regarding quoted booleans. The lines aren't actually
294             # changed since this will change the meaning of the statement, so instead
295             # we just insert an XXX.
296             sub quoted_booleans(@)
297             {
298 84     84 1 100 my $input = shift;
299              
300 84         119 foreach my $line (@$input)
301             {
302 84 100       518 next unless ($line =~ /(\x27|\x22)(false|true)(\x27|\x22)/);
303              
304 10 100       25 if ($line =~ /false/) {
305 3 50       14 ($] < 5.010000) ? $line =~ s/(?>\x0D\x0A|\v)//g : $line =~ s/\R//g;
306 3         5 $line = $line . " # XXX: Quoted boolean encountered.\n";
307             }
308              
309 10 100       26 if ($line =~ /true/) {
310 7         43 $line =~ s/(\x22|\x27)true(\x22|\x27)/true/g;
311             }
312             }
313             }
314              
315             1;
316              
317             __END__