File Coverage

blib/lib/Module/Install/Substitute.pm
Criterion Covered Total %
statement 75 78 96.1
branch 22 32 68.7
condition 5 11 45.4
subroutine 9 9 100.0
pod 1 1 100.0
total 112 131 85.5


line stmt bran cond sub pod time code
1             package Module::Install::Substitute;
2              
3 2     2   26484 use strict;
  2         4  
  2         79  
4 2     2   9 use warnings;
  2         5  
  2         58  
5 2     2   54 use 5.008; # I don't care much about earlier versions
  2         10  
  2         73  
6              
7 2     2   1237 use Module::Install::Base;
  2         5  
  2         2370  
8             our @ISA = qw(Module::Install::Base);
9              
10             our $VERSION = '0.03';
11              
12             require File::Temp;
13             require File::Spec;
14             require Cwd;
15              
16             =head1 NAME
17              
18             Module::Install::Substitute - substitute values into files before install
19              
20             =head1 SYNOPSIS
21              
22             ... Makefile.PL ...
23             substitute(
24             {
25             LESS => '/usr/bin/less',
26             APXS => '/usr/bin/apxs2',
27             },
28             'bin/my-app'
29             );
30              
31             ... bin/my-app ...
32             ### after: my $less_path = '@LESS@';
33             my $less_path = '/usr/bin/less';
34              
35             =head1 DESCRIPTION
36              
37             This is extension for L system that allow you to substitute
38             values into files before install, for example paths to libs or binary executables.
39              
40             =head1 METHODS
41              
42             =head2 substitute {SUBSTITUTIONS} [{OPTIONS}] @FILES
43              
44             Takes a hash reference with substituations key value pairs, an optional hash
45             reference with options and a list of files to deal with.
46              
47             =head3 Options
48              
49             Several options are available:
50              
51             =over 3
52              
53             =item sufix
54              
55             Sufix for source files, for example you can use sufix C<.in> and results of
56             processing of F would be writen into file F. Note
57             that you don't need to specify sufixes in the list of files.
58              
59             =item from
60              
61             Source base dir. By default it's the current working directory (L). All
62             files in the list are treated as relative to this directory.
63              
64             =item to
65              
66             Destination base dir. By default it's the current working directory (L).
67              
68             =back
69              
70             =head3 File format
71              
72             In the files the following constructs are replaced:
73            
74             ###\s*after:\s?some string with @KEY@
75             some string with @KEY@
76              
77             some string with value
78             ###\s*before:\s?some string with @KEY@
79              
80             ###\s*replace:\s?some string with @KEY@
81              
82             So string should start with three # characters followed by optional spaces,
83             action keyword and some string where @SOME_KEY@ are substituted.
84              
85             This module can replace lines after or before above constructs based on
86             action keyword to allow you to change files in place without moving them
87             around and to make it possible to run substitution multiple times.
88              
89             =cut
90              
91             sub substitute
92             {
93 6     6 1 4589 my $self = shift;
94 6         17 $self->{__subst} = shift;
95 6         17 $self->{__option} = {};
96 6 100       41 if( UNIVERSAL::isa( $_[0], 'HASH' ) ) {
97 4         8 my $opts = shift;
98 4         20 while( my ($k,$v) = each( %$opts ) ) {
99 6   50     41 $self->{__option}->{ lc( $k ) } = $v || '';
100             }
101             }
102 6         20 $self->_parse_options;
103              
104 6         14 my @file = @_;
105 6         10 foreach my $f (@file) {
106 6         14 $self->_rewrite_file( $f );
107             }
108              
109 6         24 return;
110             }
111              
112             sub _parse_options
113             {
114 6     6   9 my $self = shift;
115 6         37 my $cwd = Cwd::getcwd();
116 6         13 foreach my $t ( qw(from to) ) {
117 12 100       43 $self->{__option}->{$t} = $cwd unless $self->{__option}->{$t};
118 12         16 my $d = $self->{__option}->{$t};
119 12 50 33     313 die "Couldn't read directory '$d'" unless -d $d && -r _;
120             }
121             }
122              
123             sub _rewrite_file
124             {
125 6     6   9 my ($self, $file) = @_;
126 6         82 my $source = File::Spec->catfile( $self->{__option}{from}, $file );
127 6 100       24 $source .= $self->{__option}{sufix} if $self->{__option}{sufix};
128 6 50 33     189 unless( -f $source && -r _ ) {
129 0         0 print STDERR "Couldn't find file '$source'\n";
130 0         0 return;
131             }
132 6         81 my $dest = File::Spec->catfile( $self->{__option}{to}, $file );
133 6         409 return $self->__rewrite_file( $source, $dest );
134             }
135              
136             sub __rewrite_file
137             {
138 6     6   11 my ($self, $source, $dest) = @_;
139              
140 6         212 my $mode = (stat($source))[2];
141              
142 6 50       271 open my $sfh, "<$source" or die "Couldn't open '$source' for read";
143 6         1031 print "Open input '$source' file for substitution\n";
144              
145 6         29 my ($tmpfh, $tmpfname) = File::Temp::tempfile('mi-subst-XXXX', UNLINK => 1);
146 6 100       3359 $self->__process_streams( $sfh, $tmpfh, ($source eq $dest)? 1: 0 );
147 6         62 close $sfh;
148              
149 6 50       239 seek $tmpfh, 0, 0 or die "Couldn't seek in tmp file";
150              
151 6 50       540 open my $dfh, ">$dest" or die "Couldn't open '$dest' for write";
152 6         1078 print "Open output '$dest' file for substitution\n";
153              
154 6         59 while( <$tmpfh> ) {
155 24         102 print $dfh $_;
156             }
157 6         186 close $dfh;
158 6 50       300 chmod $mode, $dest or "Couldn't change mode on '$dest'";
159             }
160              
161             sub __process_streams
162             {
163 6     6   15 my ($self, $in, $out, $replace) = @_;
164            
165 6         31 my @queue = ();
166 6         10 my $subst = $self->{'__subst'};
167 6         9 my $re_subst = join('|', map {"\Q$_"} keys %{ $subst } );
  6         22  
  6         17  
168              
169 6         108 while( my $str = <$in> ) {
170 18 100       70 if( $str =~ /^###\s*(before|replace|after)\:\s?(.*)$/s ) {
171 12         27 my ($action, $nstr) = ($1,$2);
172 12         86 $nstr =~ s/\@($re_subst)\@/$subst->{$1}/ge;
  12         31  
173              
174 12 50 66     43 die "Replace action is bad idea for situations when dest is equal to source"
175             if $replace && $action eq 'replace';
176 12 100       37 if( $action eq 'before' ) {
    50          
    50          
177 6 50       19 die "no line before 'before' action" unless @queue;
178             # overwrite prev line;
179 6         10 pop @queue;
180 6         12 push @queue, $nstr;
181 6         13 push @queue, $str;
182             } elsif( $action eq 'replace' ) {
183 0         0 push @queue, $nstr;
184             } elsif( $action eq 'after' ) {
185 6         8 push @queue, $str;
186 6         8 push @queue, $nstr;
187             # skip one line;
188 6         15 <$in>;
189             }
190             } else {
191 6         14 push @queue, $str;
192             }
193 18         64 while( @queue > 3 ) {
194 6         212 print $out shift(@queue);
195             }
196             }
197 6         16 while( scalar @queue ) {
198 18         46 print $out shift(@queue);
199             }
200             }
201              
202             1;
203              
204             =head1 AUTHOR
205              
206             Ruslan Zakirov Eruz@cpan.orgE
207              
208             =head1