File Coverage

blib/lib/App/Greple/update.pm
Criterion Covered Total %
statement 77 101 76.2
branch 17 52 32.6
condition 1 3 33.3
subroutine 21 22 95.4
pod 0 8 0.0
total 116 186 62.3


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