* Ispravak za cp_mail: pokusava se pozvati sendmail wrapper cak i kad ne
[carnet-tools-cn.git] / cp-update
1 #! /usr/bin/perl -w
2
3 ## Copyright (C) 1998 Hrvoje Niksic
4 ## Modification by Zeljko Boros (block entries, removing old entries)
5 ## More options and use strict by Zoran Dzelajlija on 2004-02-24
6 ## Quite a few improvements by Damir Dzeko on 2005-03-18 (see bellow)
7 ##
8 ## This program is free software; you can redistribute it and/or modify
9 ## it under the terms of the GNU General Public License as published by
10 ## the Free Software Foundation; either version 2 of the License, or
11 ## (at your option) any later version.
12 ##
13 ## This program is distributed in the hope that it will be useful,
14 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 ## GNU General Public License for more details.
17 ##
18 ## You should have received a copy of the GNU General Public License
19 ## along with this program; if not, write to the Free Software
20 ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 ## altered by ddzeko, 2005-03-18
23 ## -> reformated code, introduced coding conventions
24 ## -> increased reliability through atomic actualization
25 ## -> everything is done in the memory - more mem, less i/o
26 ## -> removed append mode - in-place editing considered harmful
27 ## -> added in-place mode as option
28
29 use strict;
30 use Fcntl qw(:DEFAULT :flock :seek);
31
32 sub DEBUG () { 0 };
33
34 # coding convention: CamelCase vars are global here
35 #
36 my ($ProgramName, $UsageLong, $UsageShort, $VERSION);
37
38 $VERSION = '2.1';
39
40 # Looks nicer without the slashes and dots
41 ($ProgramName = $0) =~ s!.*/!!; # strip dir
42 $ProgramName =~ s!\.[^.]+$!!;   # strip last ext
43
44 # 34567890  34567890  34567890  34567890  34567890  34567890  34567890 345678
45
46 $UsageLong = "
47  $ProgramName -- versatile line-based file updating tool
48                  usually used by package configuration scripts
49
50  Usage: $ProgramName [options] PACKAGE FILE < stdin-content
51
52
53   - General options:
54
55            -r | --remove            Remove entry PACKAGE from FILE.
56                                     Default is to add lines from stdin.
57
58            -x | --change            Modify existing block, or add it if
59                                     it does not exist but the begin mark
60                                     can be found.
61
62            -m | --allow-multiple    Allow multiple blocks of the same type.
63                                     By default, old blocks are replaced with
64                                     the new one.
65
66            -h | --help              Print this message (usage reference).
67
68                 --version           Print version message.
69
70   - Placement control:
71
72            -t | --insert-on-top     Insert stdin-content block on top.
73                                     The default is to add it at the bottom.
74
75            -i | --insert-after   x  Insert after this/matching line.
76            -f | --insert-before  x  Insert before this/matching line.
77
78   - Manipulating block marks:
79
80            -c | --comment        x  Use alternative comment char/string.
81                                     The default is shell-style \#-sign.
82
83                 --comment-end    x  Use this marker for comment ending.
84                                     The default is none. Ie. '-->', '*/'.
85
86            -b | --begin-mark     x  Block starting mark (ie. 'service ftp')
87            -e | --end-mark       x  Block ending mark (ie. '}')
88
89                   These will delete to the end of the file if no end mark
90                   is found. The deletion is otherwise not greedy and stops
91                   at the first end mark found. You can use \%P to insert
92                   the PACKAGE argument, and \%\% to insert a literal \% sign
93                   into the mark.
94
95    - File handling options:
96
97            -p | --in-place          Try to preserve original inode.
98            -n | --no-close          Do not close and reopen file when
99                                     editing it in place.
100
101    Options marked with 'x' take single argument. Others do not.
102
103 \n";
104
105 $UsageShort = "$ProgramName -- versatile line-based file updating tool\n";
106 $UsageShort .= " Options: [-r] [-m] [-i AFTER|-f BEFORE|-t] [-b START -e STOP] [-c CHAR] PACKAGE FILE\n";
107 $UsageShort .= " or type  $ProgramName --help  to be choked with help.\n";
108
109 my ($MarkBegin, $MarkEnd, $Trailer, $ParamBegin, $ParamEnd, $Placement,
110     $Package, $File, $Block, $Multi, $InsertRemove, $Comment, $CommentEnd,
111     $MatchLine, $RegexpMatch, $StdinContent, $NewContent, $InPlace,
112     $NoClose, $FileHandle, @Lines, $LinesCount);
113
114 # Placement modes
115 use constant APPEND_AT_END => 0;
116 use constant INSERT_BEFORE => 1;
117 use constant INSERT_AFTER  => 2;
118 use constant INSERT_ON_TOP => 3;
119
120 # InsertRemove modes
121 use constant DO_REMOVE => 0;
122 use constant DO_INSERT => 1;
123 use constant DO_CHANGE => 2;
124
125 # Operation defaults
126 $InsertRemove = DO_INSERT;
127 $Placement    = APPEND_AT_END;
128 $RegexpMatch  = 1;
129 $Comment      = '#';
130 $CommentEnd   = '';
131 $MatchLine    = '';
132 $InPlace      = 0;
133 $NoClose      = 0;
134
135 while (@ARGV) {
136   $_ = shift;
137   if (/^-c$/ || /^--comment$/) {
138     defined ($Comment = shift)
139       || die "$ProgramName: `-c|--comment' must be followed by an argument\n";
140   }
141   elsif (/^--comment-end$/) {
142     defined ($CommentEnd = shift)
143       || die "$ProgramName: `--comment-end' must be followed by an argument\n";
144   }
145   elsif (/^-r$/ || /^--remove$/) {
146     $InsertRemove = DO_REMOVE;
147   }
148   elsif (/^-x$/ || /^--change$/) {
149     $InsertRemove = DO_CHANGE;
150   }
151   elsif (/^-m$/ || /^--allow-multi(?:ple)?$/) {
152     $Multi = 1;
153   }
154   elsif (/^-b$/ || /^--begin(?:-mark)?$/) {
155     defined ($ParamBegin = shift)
156       || die "$ProgramName: '-b|--begin-mark' must be followed by an argument\n";
157     $Block = 1;
158   }
159   elsif (/^-e$/ || /^--end(?:-mark)?$/) {
160     defined ($ParamEnd = shift)
161       || die "$ProgramName: '-e|--end-mark' must be followed by an argument\n";
162     $Block = 1;
163   }
164   elsif (/^-i$/ || /^--insert-after$/) {
165     defined($MatchLine = shift)
166       || die "$ProgramName: '-i|--insert-after' must be followed by an argument\n";
167     $Placement = INSERT_AFTER;
168   }
169   elsif (/^-f$/ || /^--insert-before$/) {
170     defined($MatchLine = shift)
171       || die "$ProgramName: '-f|--insert-before' must be followed by an argument\n";
172     $Placement = INSERT_BEFORE;
173   }
174   elsif (/^-t$/ || /^--insert-on-top$/) {
175     $Placement = INSERT_ON_TOP;
176   }
177   elsif (/^-R$/ || /^--regexp(?:-match|-mode)?$/) {
178     $RegexpMatch = 1; # it's the default
179   }
180   elsif (/^-h$/ || /^--help$/) {
181     die $UsageLong;
182   }
183   elsif (/^-p$/ || /^--in-place$/) {
184     $InPlace = 1;
185   }
186   elsif (/^-n$/ || /^--no-close$/) {
187     $NoClose = 1;
188   }
189   elsif (/^--version$/) {
190     die "$ProgramName (CARNet Packaging file update) $VERSION\n"
191     .   "Copyright (C) 1998-2005 Free Software Foundation, Inc.\n"
192     .   "This is free software; see the source for copying conditions.  There is NO\n"
193     .   "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n";
194   }
195   elsif (/^-/) {
196     die "$ProgramName: Unrecognized option \`$_'\n";
197   }
198   else {
199     unshift(@ARGV, $_);
200     last;
201   }
202   
203   if ($Multi and DO_CHANGE == $InsertRemove) {
204     die "$ProgramName: Cannot use both `--change' and `--allow-multiple'\n";
205   }
206 }
207
208 $Package = shift  or  die $UsageShort;
209 $File    = shift  or  die $UsageShort;
210
211 # prepare block begin and end marks
212 #
213 (! $Block || ($ParamBegin && $ParamEnd))
214   or die ("$ProgramName: must provide both begin and end marks.\n");
215 #
216 if ($Block) {
217   $ParamBegin =~ s,  %P  ,  $Package  ,gx;
218   $ParamBegin =~ s,  %%  ,  %         ,gx;
219   $ParamEnd   =~ s,  %P  ,  $Package  ,gx;
220   $ParamEnd   =~ s,  %%  ,  %         ,gx;
221   $MarkBegin  = $ParamBegin;
222   $MarkEnd    = $ParamEnd;
223   $Trailer    = '';
224 } else {
225   $MarkBegin  = "$Comment Begin update by CARNet package $Package";
226   $MarkEnd    = "$Comment End update by CARNet package $Package";
227   $Trailer    = " -- DO NOT DELETE THIS LINE!".$CommentEnd;
228 }
229
230 DEBUG and print STDERR "MarkBegin='$MarkBegin'\nMarkEnd='$MarkEnd'\nTrailer='$Trailer'\n";
231
232 # compile regexp if provided
233 #
234 if ($RegexpMatch) {
235   # compile user's regexp
236   eval { $MatchLine = qr<$MatchLine> };
237 }
238 else {
239   # we'll do regexp matching anyway :)
240   # just not with user's specials interfering
241   eval { $MatchLine = qr<^\Q$MatchLine\E>o; };
242 }
243 if ($@) {
244   die "$ProgramName: regexp compilation failed: $@ ($!)\n";
245 }
246
247 do_it() and exit(0);
248
249
250 # main procedure
251 # --------------
252
253 sub do_it {
254   $LinesCount = slurp();
255   if (DO_CHANGE == $InsertRemove) {
256     $StdinContent = &stdin_content;
257     change() or add();
258   }
259   elsif (DO_INSERT == $InsertRemove) {
260     $StdinContent = &stdin_content;
261     del() unless $Multi;
262     add();
263   }
264   else {
265     del();
266   }
267   actualize();
268 }
269
270
271 # subroutines
272 # -----------
273
274 sub slurp() {
275   sysopen($FileHandle, $File, O_RDWR) or die "$ProgramName: Cannot open $File: $!\n";
276   @Lines = <$FileHandle>;  # slurp the whole file as lines
277   close($FileHandle) unless ($InPlace && $NoClose);
278
279   # If FILE does not have a trailing newline, be sure to add it
280   # before appending anything else.
281   if (@Lines and $Lines[$#Lines] !~ m/\n$/s) {
282     $Lines[$#Lines] .= "\n";
283   }
284   
285   return scalar @Lines;
286 }
287
288 sub add() {
289   if (APPEND_AT_END == $Placement) {
290     # append stuff at the end of the file
291     push(@Lines, $StdinContent);
292   }
293   elsif (INSERT_ON_TOP == $Placement) {
294     # throw the stuff on top of the file
295     unshift(@Lines, $StdinContent);
296   }
297   else {
298     # insert stuff where needed
299     my ($line, $lineNo, $added);
300     $lineNo = 0;
301     $added  = 0;
302     foreach $line (@Lines) {
303       DEBUG and print STDERR "at: $line";
304       if ($line =~ m/$MatchLine/) {
305         DEBUG and print STDERR "  - MATCH\n";
306         if (INSERT_AFTER == $Placement) {
307           DEBUG and print STDERR "    - INSERT_AFTER\n";
308           splice(@Lines, $lineNo + 1, 0, $StdinContent);
309         }
310         elsif (INSERT_BEFORE == $Placement) {
311           DEBUG and print STDERR "    - INSERT_BEFORE\n";
312           splice(@Lines, $lineNo, 0, $StdinContent);
313         }
314         # once added and we're done
315         $added = 1;
316         last;
317       }
318       ++ $lineNo;
319     }
320     if ($MatchLine and ! $added) {
321       warn "$ProgramName: Inserting lines at the end implicitly! No '$MatchLine'\n";
322       push(@Lines, $StdinContent);
323     }
324   }
325   # add the number of lines in added content to safety counter
326   $LinesCount += ($StdinContent =~ tr/\n//);
327   return $LinesCount; # whatever true
328 }
329
330 sub del() {
331   my ($mytrailer, $mybegin, $myend) =
332     ($Trailer, $MarkBegin, $MarkEnd);
333
334   my ($bm_found, $em_found); # begin/end mark found indicator
335
336   # Make the strings regexp-friendly by quoting non-word chars.
337   $mybegin   =~ s/\W/\\$&/g;
338   $myend     =~ s/\W/\\$&/g;
339   $mytrailer =~ s/\W/\\$&/g;
340
341   my (@filtered);
342   foreach (@Lines) {
343     if (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o) {
344       # additional safety counter for double-check
345       -- $LinesCount;
346     } else {
347       push (@filtered, $_);
348     }
349     # for safety check:
350     $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
351     $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
352   }
353   if ($bm_found and $em_found) {
354     DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n";
355     @Lines = @filtered;
356   }
357   elsif ($bm_found and ! $em_found) {
358     # safety exit    
359     die "$ProgramName: no end-mark after begin-mark!\n";
360   }
361   return $LinesCount; # whatever
362 }
363
364 sub change() {
365   my ($mytrailer, $mybegin, $myend) =
366     ($Trailer, $MarkBegin, $MarkEnd);
367
368   my ($bm_found, $em_found); # begin/end mark found indicator
369
370   # Make the strings regexp-friendly by quoting non-word chars.
371   $mybegin   =~ s/\W/\\$&/g;
372   $myend     =~ s/\W/\\$&/g;
373   $mytrailer =~ s/\W/\\$&/g;
374
375   my (@filtered, $done, $skip);
376   $done = 0; # job done once
377   $skip = 0; # skip original block
378   foreach (@Lines) {
379     if (! $done and $skip > 0) {
380       -- $LinesCount;
381       if (/^$myend(?:$mytrailer)?$/o) {
382         ++ $done; # skipped all that was to skip
383       } else {
384         ++ $skip; # count lines being skipped
385       }
386     }
387     elsif (0 == $skip and $_ =~ /^$mybegin(?:$mytrailer)?$/o) {
388       push (@filtered, $StdinContent);
389       $LinesCount += ($StdinContent =~ tr/\n//);
390       $skip = 1;
391     }
392     else {
393       push (@filtered, $_)
394     }
395     # for safety check:
396     $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
397     $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
398   }
399   if ($bm_found and $em_found) {
400     -- $skip; # correct the counter
401     DEBUG and print STDERR "Replaced block of $skip lines\n";
402     @Lines = @filtered;
403   }
404   elsif ($bm_found and ! $em_found) {
405     # safety exit    
406     die "$ProgramName: no end-mark after begin-mark!\n";
407   }
408   return $done;
409 }
410
411 # written by ddzeko@srce.hr, 2005-03-18
412 # to improve reliabilitah :)
413 #
414 sub actualize() {
415   # put it all thogether
416   my $newContent = join('', @Lines);
417
418   unless (length($newContent) or 0 == $LinesCount) {
419     # safety exit in last second :)
420     die "$ProgramName: New content empty -- aborting file alteration!\n";
421   }
422   
423   if ($InPlace) {
424     # Provdided as means of changing the file content
425     #    and keeping it's inode number still.
426     # -------------------------------------------------------
427     # this is dangerous since it can leave file in bad state
428     # do not use this for highly critical files (ie. inittab)
429     # -------------------------------------------------------
430     # (REVISIT: add File::Copy call to back-up the file so
431     #   we can at least try undoing the file content change)
432     # backup_copy();
433     unless (fileno($FileHandle)) {
434       sysopen($FileHandle, $File, O_WRONLY|O_TRUNC)
435         or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
436     }
437     else {
438       sysseek(*$FileHandle, 0, SEEK_SET)
439         or die "$ProgramName: Failed to seek to the begining of file ($!)\n";
440     }
441     if (length($newContent)) {
442       my $wb = syswrite($FileHandle, $newContent);
443       if (! $wb or length($newContent) != $wb) {
444         # FIXME: try restoring backup copy
445         my $ncl = length($newContent);
446         die "$ProgramName: Failed to write the content to '$File' (wb=$wb, len=$ncl, err:$!)\n";
447       }
448     }
449     if ($NoClose) {
450       # this could be handy for files that had stuff appended
451       # at the end of the file
452       truncate($FileHandle, length($newContent))
453         or die "$ProgramName: Failed to truncate the file ($!)\n";
454     }
455     close($FileHandle);
456   }
457   else {
458     my ($file_new, $file_old) = ($File, $File);
459     $file_new .= '.cp-update.new'; # our .new file
460     $file_old .= '.cp-update.old'; # our .old file
461     # write content in new file in single write op
462     sysopen ($FileHandle, $file_new, O_CREAT|O_TRUNC|O_WRONLY, (stat($File))[2])
463       or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
464     if (length($newContent)) {
465       my $wb = syswrite($FileHandle, $newContent);
466       if (! $wb or length($newContent) != $wb) {
467         unlink($file_new);
468         my ($ncl) = length($newContent);
469         die "$ProgramName: Failed to write the content to '$File' (wb=$wb, len=$ncl, err:$!)\n";
470       }
471     }
472     close($FileHandle);
473     # do the moving (should be atomic)
474     
475     eval { require File::Copy; };
476     if ($@) {
477       # do it the classical way: successive renames
478       rename($File, $file_old)
479         or die "$ProgramName: Failed to rename file '$File' to '$file_old' ($!)\n";
480     }
481     else {
482       import File::Copy;
483       # do it with increased safety: overstepping 
484       #      the old file with it's modified copy
485       copy($File, $file_old)
486         or die "$ProgramName: Failed to create backup of '$File' as '$file_old' ($!)\n";
487       # if the filesystem is absolutelly filled up this will fail 
488       # and the original file will remain unchanged
489     }
490     rename($file_new, $File)
491       or die "$ProgramName: Failed to rename file '$file_new' to '$File' ($!)\n";
492     unlink($file_old)
493       or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n";
494   }
495   DEBUG and print STDERR "actualize: LinesCount=$LinesCount\n";
496 }
497
498 # return content from standard input
499 sub stdin_content() {
500   my ($stdin_content) = join('', <STDIN>);
501   unless (length($stdin_content)) {
502     die "$ProgramName: No stdin-content provided!\n";
503   }
504   if ($stdin_content !~ m/\n$/s) {
505     # add trailing newline
506     $stdin_content .= "\n";
507   }
508
509   my ($mytrailer, $mybegin, $myend) =
510     ($Trailer, $MarkBegin, $MarkEnd);
511
512   $mytrailer .= "\n";
513   $mybegin  .= $mytrailer;
514   $myend    .= $mytrailer;
515  
516   $stdin_content = ($mybegin . $stdin_content . $myend);
517   return $stdin_content;
518 }
519
520 # create backup copy of altered file
521 sub backup_copy() {
522   die "$ProgramName: backup_copy() not implemented";
523 }