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)
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.
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.
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.
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
30 use Fcntl qw(:DEFAULT :flock :seek);
34 # coding convention: CamelCase vars are global here
36 my ($ProgramName, $UsageLong, $UsageShort, $VERSION);
40 # Looks nicer without the slashes and dots
41 ($ProgramName = $0) =~ s!.*/!!; # strip dir
42 $ProgramName =~ s!\.[^.]+$!!; # strip last ext
44 # 34567890 34567890 34567890 34567890 34567890 34567890 34567890 345678
47 $ProgramName -- versatile line-based file updating tool
48 usually used by package configuration scripts
50 Usage: $ProgramName [options] PACKAGE FILE < stdin-content
55 -r | --remove Remove entry PACKAGE from FILE.
56 Default is to add lines from stdin.
58 -x | --change Modify existing block, or add it if
59 it does not exist but the begin mark
62 -m | --allow-multiple Allow multiple blocks of the same type.
63 By default, old blocks are replaced with
66 -h | --help Print this message (usage reference).
68 --version Print version message.
72 -t | --insert-on-top Insert stdin-content block on top.
73 The default is to add it at the bottom.
75 -i | --insert-after x Insert after this/matching line.
76 -f | --insert-before x Insert before this/matching line.
78 - Manipulating block marks:
80 -c | --comment x Use alternative comment char/string.
81 The default is shell-style \#-sign.
83 --comment-end x Use this marker for comment ending.
84 The default is none. Ie. '-->', '*/'.
86 -b | --begin-mark x Block starting mark (ie. 'service ftp')
87 -e | --end-mark x Block ending mark (ie. '}')
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
95 - File handling options:
97 -p | --in-place Try to preserve original inode.
98 -n | --no-close Do not close and reopen file when
101 Options marked with 'x' take single argument. Others do not.
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";
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);
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;
121 use constant DO_REMOVE => 0;
122 use constant DO_INSERT => 1;
123 use constant DO_CHANGE => 2;
126 $InsertRemove = DO_INSERT;
127 $Placement = APPEND_AT_END;
137 if (/^-c$/ || /^--comment$/) {
138 defined ($Comment = shift)
139 || die "$ProgramName: `-c|--comment' must be followed by an argument\n";
141 elsif (/^--comment-end$/) {
142 defined ($CommentEnd = shift)
143 || die "$ProgramName: `--comment-end' must be followed by an argument\n";
145 elsif (/^-r$/ || /^--remove$/) {
146 $InsertRemove = DO_REMOVE;
148 elsif (/^-x$/ || /^--change$/) {
149 $InsertRemove = DO_CHANGE;
151 elsif (/^-m$/ || /^--allow-multi(?:ple)?$/) {
154 elsif (/^-b$/ || /^--begin(?:-mark)?$/) {
155 defined ($ParamBegin = shift)
156 || die "$ProgramName: '-b|--begin-mark' must be followed by an argument\n";
159 elsif (/^-e$/ || /^--end(?:-mark)?$/) {
160 defined ($ParamEnd = shift)
161 || die "$ProgramName: '-e|--end-mark' must be followed by an argument\n";
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;
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;
174 elsif (/^-t$/ || /^--insert-on-top$/) {
175 $Placement = INSERT_ON_TOP;
177 elsif (/^-R$/ || /^--regexp(?:-match|-mode)?$/) {
178 $RegexpMatch = 1; # it's the default
180 elsif (/^-h$/ || /^--help$/) {
183 elsif (/^-p$/ || /^--in-place$/) {
186 elsif (/^-n$/ || /^--no-close$/) {
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";
196 die "$ProgramName: Unrecognized option \`$_'\n";
203 if ($Multi and DO_CHANGE == $InsertRemove) {
204 die "$ProgramName: Cannot use both `--change' and `--allow-multiple'\n";
208 $Package = shift or die $UsageShort;
209 $File = shift or die $UsageShort;
211 # prepare block begin and end marks
213 (! $Block || ($ParamBegin && $ParamEnd))
214 or die ("$ProgramName: must provide both begin and end marks.\n");
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;
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;
230 DEBUG and print STDERR "MarkBegin='$MarkBegin'\nMarkEnd='$MarkEnd'\nTrailer='$Trailer'\n";
232 # compile regexp if provided
235 # compile user's regexp
236 eval { $MatchLine = qr<$MatchLine> };
239 # we'll do regexp matching anyway :)
240 # just not with user's specials interfering
241 eval { $MatchLine = qr<^\Q$MatchLine\E>o; };
244 die "$ProgramName: regexp compilation failed: $@ ($!)\n";
254 $LinesCount = slurp();
255 if (DO_CHANGE == $InsertRemove) {
256 $StdinContent = &stdin_content;
259 elsif (DO_INSERT == $InsertRemove) {
260 $StdinContent = &stdin_content;
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);
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";
285 return scalar @Lines;
289 if (APPEND_AT_END == $Placement) {
290 # append stuff at the end of the file
291 push(@Lines, $StdinContent);
293 elsif (INSERT_ON_TOP == $Placement) {
294 # throw the stuff on top of the file
295 unshift(@Lines, $StdinContent);
298 # insert stuff where needed
299 my ($line, $lineNo, $added);
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);
310 elsif (INSERT_BEFORE == $Placement) {
311 DEBUG and print STDERR " - INSERT_BEFORE\n";
312 splice(@Lines, $lineNo, 0, $StdinContent);
314 # once added and we're done
320 if ($MatchLine and ! $added) {
321 warn "$ProgramName: Inserting lines at the end implicitly! No '$MatchLine'\n";
322 push(@Lines, $StdinContent);
325 # add the number of lines in added content to safety counter
326 $LinesCount += ($StdinContent =~ tr/\n//);
327 return $LinesCount; # whatever true
331 my ($mytrailer, $mybegin, $myend) =
332 ($Trailer, $MarkBegin, $MarkEnd);
334 my ($bm_found, $em_found); # begin/end mark found indicator
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;
343 if (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o) {
344 # additional safety counter for double-check
347 push (@filtered, $_);
350 $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
351 $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
353 if ($bm_found and $em_found) {
354 DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n";
357 elsif ($bm_found and ! $em_found) {
359 die "$ProgramName: no end-mark after begin-mark!\n";
361 return $LinesCount; # whatever
365 my ($mytrailer, $mybegin, $myend) =
366 ($Trailer, $MarkBegin, $MarkEnd);
368 my ($bm_found, $em_found); # begin/end mark found indicator
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;
375 my (@filtered, $done, $skip);
376 $done = 0; # job done once
377 $skip = 0; # skip original block
379 if (! $done and $skip > 0) {
381 if (/^$myend(?:$mytrailer)?$/o) {
382 ++ $done; # skipped all that was to skip
384 ++ $skip; # count lines being skipped
387 elsif (0 == $skip and $_ =~ /^$mybegin(?:$mytrailer)?$/o) {
388 push (@filtered, $StdinContent);
389 $LinesCount += ($StdinContent =~ tr/\n//);
396 $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
397 $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
399 if ($bm_found and $em_found) {
400 -- $skip; # correct the counter
401 DEBUG and print STDERR "Replaced block of $skip lines\n";
404 elsif ($bm_found and ! $em_found) {
406 die "$ProgramName: no end-mark after begin-mark!\n";
411 # written by ddzeko@srce.hr, 2005-03-18
412 # to improve reliabilitah :)
415 # put it all thogether
416 my $newContent = join('', @Lines);
418 unless (length($newContent) or 0 == $LinesCount) {
419 # safety exit in last second :)
420 die "$ProgramName: New content empty -- aborting file alteration!\n";
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)
433 unless (fileno($FileHandle)) {
434 sysopen($FileHandle, $File, O_WRONLY|O_TRUNC)
435 or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
438 sysseek(*$FileHandle, 0, SEEK_SET)
439 or die "$ProgramName: Failed to seek to the begining of file ($!)\n";
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";
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";
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)
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) {
468 my ($ncl) = length($newContent);
469 die "$ProgramName: Failed to write the content to '$File' (wb=$wb, len=$ncl, err:$!)\n";
473 # do the moving (should be atomic)
475 eval { require File::Copy; };
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";
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
490 rename($file_new, $File)
491 or die "$ProgramName: Failed to rename file '$file_new' to '$File' ($!)\n";
493 or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n";
495 DEBUG and print STDERR "actualize: LinesCount=$LinesCount\n";
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";
504 if ($stdin_content !~ m/\n$/s) {
505 # add trailing newline
506 $stdin_content .= "\n";
509 my ($mytrailer, $mybegin, $myend) =
510 ($Trailer, $MarkBegin, $MarkEnd);
513 $mybegin .= $mytrailer;
514 $myend .= $mytrailer;
516 $stdin_content = ($mybegin . $stdin_content . $myend);
517 return $stdin_content;
520 # create backup copy of altered file
522 die "$ProgramName: backup_copy() not implemented";