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