File Coverage

blib/lib/App/Greple/update.pm
Criterion Covered Total %
statement 38 101 37.6
branch 0 50 0.0
condition 0 3 0.0
subroutine 13 22 59.0
pod 0 8 0.0
total 51 184 27.7


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