r90: Dodan --change u cp-update
[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.0';
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);
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   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
282   if (@Lines and $Lines[$#Lines] !~ m/\n$/s) {
283     $Lines[$#Lines] .= "\n";
284   }
285 }
286
287 sub add() {
288   if (APPEND_AT_END == $Placement) {
289     # append stuff at the end of the file
290     push(@Lines, $StdinContent);
291   }
292   elsif (INSERT_ON_TOP == $Placement) {
293     # throw the stuff on top of the file
294     unshift(@Lines, $StdinContent);
295   }
296   else {
297     # insert stuff where needed
298     my ($line, $lineNo, $added);
299     $lineNo = 0;
300     $added  = 0;
301     foreach $line (@Lines) {
302       DEBUG and print STDERR "at: $line";
303       if ($line =~ m/$MatchLine/) {
304         DEBUG and print STDERR "  - MATCH\n";
305         if (INSERT_AFTER == $Placement) {
306           DEBUG and print STDERR "    - INSERT_AFTER\n";
307           splice(@Lines, $lineNo + 1, 0, $StdinContent);
308         }
309         elsif (INSERT_BEFORE == $Placement) {
310           DEBUG and print STDERR "    - INSERT_BEFORE\n";
311           splice(@Lines, $lineNo, 0, $StdinContent);
312         }
313         # once added and we're done
314         $added = 1;
315         last;
316       }
317       ++ $lineNo;
318     }
319     if ($MatchLine and ! $added) {
320       warn "$ProgramName: Inserting lines at the end implicitly! No '$MatchLine'\n";
321       push(@Lines, $StdinContent);
322     }
323   }
324   return scalar(@Lines); # whatever
325 }
326
327 sub del() {
328   my ($mytrailer, $mybegin, $myend) =
329     ($Trailer, $MarkBegin, $MarkEnd);
330
331   my ($bm_found, $em_found); # begin/end mark found indicator
332
333   # Make the strings regexp-friendly by quoting non-word chars.
334   $mybegin   =~ s/\W/\\$&/g;
335   $myend     =~ s/\W/\\$&/g;
336   $mytrailer =~ s/\W/\\$&/g;
337
338   my (@filtered);
339   foreach (@Lines) {
340     push (@filtered, $_)
341       unless (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o);
342       
343     # for safety check:
344     $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
345     $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
346   }
347   if ($bm_found and $em_found) {
348     DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n";
349     @Lines = @filtered;
350   }
351   elsif ($bm_found and ! $em_found) {
352     # safety exit    
353     die "$ProgramName: no end-mark after begin-mark!\n";
354   }
355   return scalar(@Lines); # whatever
356 }
357
358 sub change() {
359   my ($mytrailer, $mybegin, $myend) =
360     ($Trailer, $MarkBegin, $MarkEnd);
361
362   my ($bm_found, $em_found); # begin/end mark found indicator
363
364   # Make the strings regexp-friendly by quoting non-word chars.
365   $mybegin   =~ s/\W/\\$&/g;
366   $myend     =~ s/\W/\\$&/g;
367   $mytrailer =~ s/\W/\\$&/g;
368
369   my (@filtered, $done, $skip);
370   $done = 0; # job done once
371   $skip = 0; # skip original block
372   foreach (@Lines) {
373     if (! $done and $skip > 0) {
374       if (/^$myend(?:$mytrailer)?$/o) {
375         ++ $done; # skipped all that was to skip
376       } else {
377         ++ $skip; # count lines being skipped
378       }
379     }
380     elsif (0 == $skip and $_ =~ /^$mybegin(?:$mytrailer)?$/o) {
381       push (@filtered, $StdinContent);
382       $skip = 1;
383     }
384     else {
385       push (@filtered, $_)
386     }
387     # for safety check:
388     $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
389     $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
390   }
391   if ($bm_found and $em_found) {
392     -- $skip; # correct the counter
393     DEBUG and print STDERR "Replaced block of $skip lines\n";
394     @Lines = @filtered;
395   }
396   elsif ($bm_found and ! $em_found) {
397     # safety exit    
398     die "$ProgramName: no end-mark after begin-mark!\n";
399   }
400   return $done;
401 }
402
403 # written by ddzeko@srce.hr, 2005-03-18
404 # to improve reliabilitah :)
405 #
406 sub actualize() {
407   # put it all thogether
408   my $newContent = join('', @Lines);
409
410   unless (length($newContent)) {
411     # safety exit in last second :)
412     die "$ProgramName: New content empty -- aborting file alteration!\n";
413   }
414   
415   if ($InPlace) {
416     # Provdided as means of changing the file content
417     #    and keeping it's inode number still.
418     # -------------------------------------------------------
419     # this is dangerous since it can leave file in bad state
420     # do not use this for highly critical files (ie. inittab)
421     # -------------------------------------------------------
422     # (REVISIT: add File::Copy call to back-up the file so
423     #   we can at least try undoing the file content change)
424     # backup_copy();
425     unless (fileno($FileHandle)) {
426       sysopen($FileHandle, $File, O_WRONLY|O_TRUNC)
427         or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
428     }
429     else {
430       sysseek(*$FileHandle, 0, SEEK_SET)
431         or die "$ProgramName: Failed to seek to the begining of file ($!)\n";
432     }
433     my $wb = syswrite($FileHandle, $newContent);
434     if (! $wb or length($newContent) != $wb) {
435       # FIXME: try restoring backup copy
436       die "$ProgramName: Failed to write the content to '$File' ($!)\n";
437     }
438     if ($NoClose) {
439       # this could be handy for files that had stuff appended
440       # at the end of the file
441       truncate($FileHandle, length($newContent))
442         or die "$ProgramName: Failed to truncate the file ($!)\n";
443     }
444     close($FileHandle);
445   }
446   else {
447     my ($file_new, $file_old) = ($File, $File);
448     $file_new .= '.cp-update.new'; # our .new file
449     $file_old .= '.cp-update.old'; # our .old file
450     # write content in new file in single write op
451     sysopen ($FileHandle, $file_new, O_CREAT|O_TRUNC|O_WRONLY)
452       or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
453     my $wb = syswrite($FileHandle, $newContent);
454     if (! $wb or length($newContent) != $wb) {
455       unlink($file_new);
456       die "$ProgramName: Failed to write the content to '$File' ($!)\n";
457     }
458     close($FileHandle);
459     # do the moving (should be atomic)
460     
461     eval { require File::Copy; };
462     if ($@) {
463       # do it the classical way: successive renames
464       rename($File, $file_old)
465         or die "$ProgramName: Failed to rename file '$File' to '$file_old' ($!)\n";
466     }
467     else {
468       import File::Copy;
469       # do it with increased safety: overstepping 
470       #      the old file with it's modified copy
471       copy($File, $file_old)
472         or die "$ProgramName: Failed to create backup of '$File' as '$file_old' ($!)\n";
473       # if the filesystem is absolutelly filled up this will fail 
474       # and the original file will remain unchanged
475     }
476     rename($file_new, $File)
477       or die "$ProgramName: Failed to rename file '$file_new' to '$File' ($!)\n";
478     unlink($file_old)
479       or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n";
480   }
481 }
482
483 # return content from standard input
484 sub stdin_content() {
485   my ($stdin_content) = join('', <STDIN>);
486   unless (length($stdin_content)) {
487     die "$ProgramName: No stdin-content provided!\n";
488   }
489   if ($stdin_content !~ m/\n$/s) {
490     # add trailing newline
491     $stdin_content .= "\n";
492   }
493
494   my ($mytrailer, $mybegin, $myend) =
495     ($Trailer, $MarkBegin, $MarkEnd);
496
497   $mytrailer .= "\n";
498   $mybegin  .= $mytrailer;
499   $myend    .= $mytrailer;
500  
501   $stdin_content = ($mybegin . $stdin_content . $myend);
502   return $stdin_content;
503 }
504
505 # create backup copy of altered file
506 sub backup_copy() {
507   die "$ProgramName: backup_copy() not implemented";
508 }