File Coverage

blib/lib/App/Greple/update.pm
Criterion Covered Total %
statement 38 100 38.0
branch 0 48 0.0
condition 0 3 0.0
subroutine 13 22 59.0
pod 0 8 0.0
total 51 181 28.1


line stmt bran cond sub pod time code
1             =encoding utf8
2              
3             =head1 NAME
4              
5             update - Greple module to update file content
6              
7             =head1 SYNOPSIS
8              
9             greple -Mupdate
10              
11             Options:
12              
13             --update replace file content
14             --with-backup make backup files
15              
16             --diff produce diff output
17             --U# specify unified diff context length
18              
19             =head1 VERSION
20              
21             Version 0.03
22              
23             =head1 DESCRIPTION
24              
25             This B module substitute the target file content by command
26             output. For example, next command replace all words in the file to
27             uppercase.
28              
29             greple -Mupdate '\w+' --cm 'sub{uc}' --update file
30              
31             Above is a very simple example but you can implement arbitrarily
32             complex function in conjunction with other various B options.
33              
34             You can check how the file will be edited by B<--diff> option.
35              
36             greple -Mupdate '\w+' --cm 'sub{uc}' --diff file
37              
38             Command B or B would be useful to see the difference
39             visually.
40              
41             greple -Mupdate '\w+' --cm 'sub{uc}' --diff file | cdif
42              
43             This module has been spun off from L module.
44             Consult it for more practical use case.
45              
46             =head1 OPTIONS
47              
48             =over 7
49              
50             =item B<--update>
51              
52             =item B<--update::update>
53              
54             Update the target file by command output. Entire file content is
55             produced and any color effects are canceled. Without this option,
56             B behaves as normal operation, that means only matched lines
57             are printed.
58              
59             File is not touched as far as its content does not change.
60              
61             =item B<--with-backup>[=I]
62              
63             Backup original file with C<.bak> suffix. If optional parameter is
64             given, it is used as a suffix string. If the file exists, C<.bak_1>,
65             C<.bak_2> ... are used.
66              
67             =begin comment
68              
69             =item B<--create>
70              
71             =item B<--update::create>
72              
73             Create new file and write the result. Suffix ".new" is appended to
74             the original filename.
75              
76             =end comment
77              
78             =item B<--diff>
79              
80             =item B<--update::diff>
81              
82             Option B<-diff> produce diff output of original and converted text.
83             Option B<-U#> can be used to specify context length.
84              
85             =begin comment
86              
87             =item B<--diffcmd>=I
88              
89             Specify diff command name used by B<--diff> option. Default is "diff
90             -u".
91              
92             =end comment
93              
94             =back
95              
96             =head1 INSTALL
97              
98             =head2 CPANMINUS
99              
100             $ cpanm App::Greple::update
101              
102             =head2 GITHUB
103              
104             $ cpanm https://github.com/kaz-utashiro/greple-update.git
105              
106             =head1 SEE ALSO
107              
108             L, L
109              
110             L, L
111              
112             L, L
113              
114             L, L
115              
116             =head1 AUTHOR
117              
118             Kazumasa Utashiro
119              
120             =head1 LICENSE
121              
122             Copyright 2022 Kazumasa Utashiro.
123              
124             This library is free software; you can redistribute it and/or modify
125             it under the same terms as Perl itself.
126              
127             =cut
128              
129             package App::Greple::update;
130 1     1   668 use v5.14;
  1         3  
131 1     1   4 use warnings;
  1         2  
  1         35  
132              
133             our $VERSION = '0.03';
134              
135 1     1   522 use utf8;
  1         12  
  1         4  
136 1     1   1039 use open IO => ':utf8';
  1         1075  
  1         5  
137              
138 1     1   86 use Exporter 'import';
  1         3  
  1         83  
139             our @EXPORT = qw(
140             &update_initialize
141             &update_begin
142             &update_diff
143             &update_divert
144             &update_file
145             );
146             our %EXPORT_TAGS = ( );
147             our @EXPORT_OK = qw();
148              
149 1     1   6 use Carp;
  1         2  
  1         54  
150 1     1   476 use Encode;
  1         8920  
  1         65  
151 1     1   474 use Data::Dumper;
  1         5226  
  1         63  
152 1     1   488 use App::Greple::Common;
  1         290  
  1         102  
153 1     1   403 use Text::ParseWords qw(shellwords);
  1         1191  
  1         451  
154              
155             our $debug = 0;
156             our $remember_data = 1;
157             our $opt_update_diffcmd = "diff -u";
158             our $opt_suffix = '';
159             our $opt_backup;
160             our $opt_U = '';
161              
162             my $current_file;
163             my $contents;
164             my @update_diffcmd;
165              
166             sub debug {
167 0     0 0   $debug = 1;
168             }
169              
170             sub update_initialize {
171 0     0 0   @update_diffcmd = shellwords $opt_update_diffcmd;
172 0 0         if ($opt_U ne '') {
173 0           @update_diffcmd = ('diff', "-U$opt_U");
174             }
175 0 0         if (defined $opt_backup) {
176 0 0         $opt_suffix = $opt_backup ne '' ? $opt_backup : '.bak';
177             }
178             }
179              
180             sub update_begin {
181 0     0 0   my %arg = @_;
182 0 0         $current_file = delete $arg{&FILELABEL} or die;
183 0 0         $contents = $_ if $remember_data;
184             }
185              
186             #
187             # define &divert_stdout and &recover_stdout
188             #
189             {
190             my $diverted = 0;
191             my $buffer;
192              
193             sub divert_stdout {
194 0 0   0 0   $buffer = @_ ? shift : '/dev/null';
195 0 0         $diverted = $diverted == 0 ? 1 : return;
196 0 0         open UPDATE_STDOUT, '>&', \*STDOUT or die "open: $!";
197 0           close STDOUT;
198 0 0         open STDOUT, '>', $buffer or die "open: $!";
199             }
200              
201             sub recover_stdout {
202 0 0   0 0   $diverted = $diverted == 1 ? 0 : return;
203 0           close STDOUT;
204 0 0         open STDOUT, '>&', \*UPDATE_STDOUT or die "open: $!";
205             }
206             }
207              
208 1     1   7 use List::Util qw(first);
  1         2  
  1         180  
209              
210             sub update_diff {
211 0     0 0   my $orig = $current_file;
212 0           my $fh;
213 0           state $fdpath = do {
214 0           my $fd = DATA->fileno;
215 0     0     first { -r "$_/$fd" } qw( /dev/fd /proc/self/fd );
  0            
216             };
217              
218 0 0 0       if ($fdpath and $remember_data) {
219 1     1   434 use IO::File;
  1         7292  
  1         109  
220 1     1   7 use Fcntl;
  1         1  
  1         687  
221 0 0         $fh = new_tmpfile IO::File or die "new_tmpfile: $!\n";
222 0           $fh->binmode(':encoding(utf8)');
223 0 0         my $fd = $fh->fcntl(F_GETFD, 0) or die "fcntl F_GETFD: $!\n";
224 0 0         $fh->fcntl(F_SETFD, $fd & ~FD_CLOEXEC) or die "fcntl F_SETFD: $!\n";
225 0           $fh->printflush($contents);
226 0           $fh->seek(0, 0);
227 0           $orig = sprintf "%s/%d", $fdpath, $fh->fileno;
228             }
229              
230 0 0         @update_diffcmd or confess "Empty diff command";
231 0           exec @update_diffcmd, $orig, "-";
232 0           die "exec: $!\n";
233             }
234              
235             my $divert_buffer;
236              
237             sub update_divert {
238 0     0 0   my %arg = @_;
239 0           my $filename = delete $arg{&FILELABEL};
240              
241 0           $divert_buffer = '';
242 0           divert_stdout(\$divert_buffer);
243             }
244              
245             sub update_file {
246 0     0 0   my %arg = @_;
247 0           my $filename = delete $arg{&FILELABEL};
248 0           my $newname = '';
249              
250 0 0         recover_stdout() or die;
251 0           $divert_buffer = decode 'utf8', $divert_buffer;
252              
253 0 0         if ($_ eq $divert_buffer) {
254 0           return;
255             }
256              
257 0 0         if (my $suffix = $opt_suffix) {
258 0           $newname = $filename . $suffix;
259 0           for (my $i = 1; -f $newname; $i++) {
260 0           $newname = $filename . $suffix . "_$i";
261             }
262             }
263              
264 0           my $create = do {
265 0 0         if ($arg{replace}) {
266 0 0         if ($newname ne '') {
267 0           warn "rename $filename -> $newname\n";
268 0 0         rename $filename, $newname or die "rename: $!\n";
269 0 0         die if -f $filename;
270             } else {
271 0           warn "overwrite $filename\n";
272             }
273 0           $filename;
274             } else {
275 0           warn "create $newname\n";
276 0           $newname;
277             }
278             };
279              
280 0 0         open my $fh, ">", $create or die "open: $create $!\n";
281 0           $fh->print($divert_buffer);
282 0           $fh->close;
283             }
284              
285             1;
286              
287             __DATA__