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