r28: upute za cp_yes_no i cp_db_get_wrap
[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 (! $added) {
303       warn "$ProgramName: Inserting lines at the end implicitly!\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   # Make the strings regexp-friendly by quoting non-word chars.
315   $mybegin   =~ s/\W/\\$&/g;
316   $myend     =~ s/\W/\\$&/g;
317   $mytrailer =~ s/\W/\\$&/g;
318
319   my (@filtered);
320   foreach (@Lines) {
321     push (@filtered, $_)
322       unless (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o);
323   }
324   DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n";
325   @Lines = @filtered;
326   return scalar(@Lines); # whatever
327 }
328
329 # written by ddzeko@srce.hr, 2005-03-18
330 # to improve reliabilitah :)
331 #
332 sub actualize() {
333   # put it all thogether
334   my $newContent = join('', @Lines);
335
336   unless (length($newContent)) {
337     # safety exit in last second :)
338     die "$ProgramName: New content empty -- aborting file alteration!\n";
339   }
340   
341   if ($InPlace) {
342     # Provdided as means of changing the file content
343     #    and keeping it's inode number still.
344     # -------------------------------------------------------
345     # this is dangerous since it can leave file in bad state
346     # do not use this for highly critical files (ie. inittab)
347     # -------------------------------------------------------
348     # (REVISIT: add File::Copy call to back-up the file so
349     #   we can at least try undoing the file content change)
350     # backup_copy();
351     unless (fileno($FileHandle)) {
352       sysopen($FileHandle, $File, O_WRONLY|O_TRUNC)
353         or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
354     }
355     else {
356       sysseek(*$FileHandle, 0, SEEK_SET)
357         or die "$ProgramName: Failed to seek to the begining of file ($!)\n";
358     }
359     my $wb = syswrite($FileHandle, $newContent);
360     if (! $wb or length($newContent) != $wb) {
361       # FIXME: try restoring backup copy
362       die "$ProgramName: Failed to write the content to '$File' ($!)\n";
363     }
364     if ($NoClose) {
365       # this could be handy for files that had stuff appended
366       # at the end of the file
367       truncate($FileHandle, length($newContent))
368         or die "$ProgramName: Failed to truncate the file ($!)\n";
369     }
370     close($FileHandle);
371   }
372   else {
373     my ($file_new, $file_old) = ($File, $File);
374     $file_new .= '.cp-update.new'; # our .new file
375     $file_old .= '.cp-update.old'; # our .old file
376     # write content in new file in single write op
377     sysopen ($FileHandle, $file_new, O_CREAT|O_TRUNC|O_WRONLY)
378       or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
379     my $wb = syswrite($FileHandle, $newContent);
380     if (! $wb or length($newContent) != $wb) {
381       unlink($file_new);
382       die "$ProgramName: Failed to write the content to '$File' ($!)\n";
383     }
384     close($FileHandle);
385     # do the moving (should be atomic)
386     
387     eval { require File::Copy; };
388     if ($@) {
389       # do it the classical way: successive renames
390       rename($File, $file_old)
391         or die "$ProgramName: Failed to rename file '$File' to '$file_old' ($!)\n";
392     }
393     else {
394       import File::Copy;
395       # do it with increased safety: overstepping 
396       #      the old file with it's modified copy
397       copy($File, $file_old)
398         or die "$ProgramName: Failed to create backup of '$File' as '$file_old' ($!)\n";
399       # if the filesystem is absolutelly filled up this will fail 
400       # and the original file will remain unchanged
401     }
402     rename($file_new, $File)
403       or die "$ProgramName: Failed to rename file '$file_new' to '$File' ($!)\n";
404     unlink($file_old)
405       or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n";
406   }
407 }
408
409 # return content from standard input
410 sub stdin_content() {
411   my ($stdin_content) = join('', <STDIN>);
412   unless (length($stdin_content)) {
413     die "$ProgramName: No stdin-content provided!\n";
414   }
415   if ($stdin_content !~ m/\n$/s) {
416     # add trailing newline
417     $stdin_content .= "\n";
418   }
419
420   my ($mytrailer, $mybegin, $myend) =
421     ($Trailer, $MarkBegin, $MarkEnd);
422
423   $mytrailer .= "\n";
424   $mybegin  .= $mytrailer;
425   $myend    .= $mytrailer;
426  
427   $stdin_content = ($mybegin . $stdin_content . $myend);
428   return $stdin_content;
429 }
430
431 # create backup copy of altered file
432 sub backup_copy() {
433   die "$ProgramName: backup_copy() not implemented";
434 }