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);
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";
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.
282 if (@Lines and $Lines[$#Lines] !~ m/\n$/s) {
283 $Lines[$#Lines] .= "\n";
288 if (APPEND_AT_END == $Placement) {
289 # append stuff at the end of the file
290 push(@Lines, $StdinContent);
292 elsif (INSERT_ON_TOP == $Placement) {
293 # throw the stuff on top of the file
294 unshift(@Lines, $StdinContent);
297 # insert stuff where needed
298 my ($line, $lineNo, $added);
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);
309 elsif (INSERT_BEFORE == $Placement) {
310 DEBUG and print STDERR " - INSERT_BEFORE\n";
311 splice(@Lines, $lineNo, 0, $StdinContent);
313 # once added and we're done
319 if ($MatchLine and ! $added) {
320 warn "$ProgramName: Inserting lines at the end implicitly! No '$MatchLine'\n";
321 push(@Lines, $StdinContent);
324 return scalar(@Lines); # whatever
328 my ($mytrailer, $mybegin, $myend) =
329 ($Trailer, $MarkBegin, $MarkEnd);
331 my ($bm_found, $em_found); # begin/end mark found indicator
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;
341 unless (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o);
344 $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
345 $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
347 if ($bm_found and $em_found) {
348 DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n";
351 elsif ($bm_found and ! $em_found) {
353 die "$ProgramName: no end-mark after begin-mark!\n";
355 return scalar(@Lines); # whatever
359 my ($mytrailer, $mybegin, $myend) =
360 ($Trailer, $MarkBegin, $MarkEnd);
362 my ($bm_found, $em_found); # begin/end mark found indicator
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;
369 my (@filtered, $done, $skip);
370 $done = 0; # job done once
371 $skip = 0; # skip original block
373 if (! $done and $skip > 0) {
374 if (/^$myend(?:$mytrailer)?$/o) {
375 ++ $done; # skipped all that was to skip
377 ++ $skip; # count lines being skipped
380 elsif (0 == $skip and $_ =~ /^$mybegin(?:$mytrailer)?$/o) {
381 push (@filtered, $StdinContent);
388 $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
389 $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
391 if ($bm_found and $em_found) {
392 -- $skip; # correct the counter
393 DEBUG and print STDERR "Replaced block of $skip lines\n";
396 elsif ($bm_found and ! $em_found) {
398 die "$ProgramName: no end-mark after begin-mark!\n";
403 # written by ddzeko@srce.hr, 2005-03-18
404 # to improve reliabilitah :)
407 # put it all thogether
408 my $newContent = join('', @Lines);
410 unless (length($newContent)) {
411 # safety exit in last second :)
412 die "$ProgramName: New content empty -- aborting file alteration!\n";
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)
425 unless (fileno($FileHandle)) {
426 sysopen($FileHandle, $File, O_WRONLY|O_TRUNC)
427 or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
430 sysseek(*$FileHandle, 0, SEEK_SET)
431 or die "$ProgramName: Failed to seek to the begining of file ($!)\n";
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";
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";
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) {
456 die "$ProgramName: Failed to write the content to '$File' ($!)\n";
459 # do the moving (should be atomic)
461 eval { require File::Copy; };
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";
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
476 rename($file_new, $File)
477 or die "$ProgramName: Failed to rename file '$file_new' to '$File' ($!)\n";
479 or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n";
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";
489 if ($stdin_content !~ m/\n$/s) {
490 # add trailing newline
491 $stdin_content .= "\n";
494 my ($mytrailer, $mybegin, $myend) =
495 ($Trailer, $MarkBegin, $MarkEnd);
498 $mybegin .= $mytrailer;
499 $myend .= $mytrailer;
501 $stdin_content = ($mybegin . $stdin_content . $myend);
502 return $stdin_content;
505 # create backup copy of altered file
507 die "$ProgramName: backup_copy() not implemented";