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
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.
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.
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.
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
29 use Fcntl qw(:DEFAULT :flock :seek);
33 # coding convention: CamelCase vars are global here
35 my ($ProgramName, $UsageLong, $UsageShort, $VERSION);
39 # Looks nicer without the slashes and dots
40 ($ProgramName = $0) =~ s!.*/!!; # strip dir
41 $ProgramName =~ s!\.[^.]+$!!; # strip last ext
43 # 34567890 34567890 34567890 34567890 34567890 34567890 34567890 345678
46 $ProgramName -- versatile line-based file updating tool
47 usually used by package configuration scripts
49 Usage: $ProgramName [options] PACKAGE FILE < stdin-content
54 -r | --remove Remove entry PACKAGE from FILE.
55 Default is to add lines from stdin.
57 -m | --allow-multiple Allow multiple blocks of the same type.
58 By default, old blocks are replaced with
61 -h | --help Print this message (usage reference).
63 --version Print version message.
67 -t | --insert-on-top Insert stdin-content block on top.
68 The default is to add it at the bottom.
70 -i | --insert-after x Insert after this/matching line.
71 -f | --insert-before x Insert before this/matching line.
73 - Manipulating block marks:
75 -c | --comment x Use alternative comment char/string.
76 The default is shell-style \#-sign.
78 --comment-end x Use this marker for comment ending.
79 The default is none. Ie. '-->', '*/'.
81 -b | --begin-mark x Block starting mark (ie. 'service ftp')
82 -e | --end-mark x Block ending mark (ie. '}')
84 These will delete to the end of the file if no end mark
85 is found. The deletion is otherwise not greedy and stops
86 at the first end mark found. You can use \%P to insert
87 the PACKAGE argument, and \%\% to insert a literal \% sign
90 - File handling options:
92 -p | --in-place Try to preserve original inode.
93 -n | --no-close Do not close and reopen file when
96 Options marked with 'x' take single argument. Others do not.
100 $UsageShort = "$ProgramName -- versatile line-based file updating tool\n";
101 $UsageShort .= " Options: [-r] [-m] [-i AFTER|-f BEFORE|-t] [-b START -e STOP] [-c CHAR] PACKAGE FILE\n";
102 $UsageShort .= " or type $ProgramName --help to be choked with help.\n";
104 my ($MarkBegin, $MarkEnd, $Trailer, $ParamBegin, $ParamEnd, $Placement,
105 $Package, $File, $Block, $Multi, $InsertRemove, $Comment, $CommentEnd,
106 $MatchLine, $RegexpMatch, $StdinContent, $NewContent, $InPlace,
107 $NoClose, $FileHandle, @Lines);
110 use constant APPEND_AT_END => 0;
111 use constant INSERT_BEFORE => 1;
112 use constant INSERT_AFTER => 2;
113 use constant INSERT_ON_TOP => 3;
116 use constant DO_REMOVE => 0;
117 use constant DO_INSERT => 1;
120 $InsertRemove = DO_INSERT;
121 $Placement = APPEND_AT_END;
131 if (/^-c$/ || /^--comment$/) {
132 defined ($Comment = shift)
133 || die "$ProgramName: `-c|--comment' must be followed by an argument\n";
135 elsif (/^--comment-end$/) {
136 defined ($CommentEnd = shift)
137 || die "$ProgramName: `--comment-end' must be followed by an argument\n";
139 elsif (/^-r$/ || /^--remove$/) {
140 $InsertRemove = DO_REMOVE;
142 elsif (/^-m$/ || /^--allow-multi(?:ple)?$/) {
145 elsif (/^-b$/ || /^--begin(?:-mark)?$/) {
146 defined ($ParamBegin = shift)
147 || die "$ProgramName: '-b|--begin-mark' must be followed by an argument\n";
150 elsif (/^-e$/ || /^--end(?:-mark)?$/) {
151 defined ($ParamEnd = shift)
152 || die "$ProgramName: '-e|--end-mark' must be followed by an argument\n";
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;
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;
165 elsif (/^-t$/ || /^--insert-on-top$/) {
166 $Placement = INSERT_ON_TOP;
168 elsif (/^-R$/ || /^--regexp(?:-match|-mode)?$/) {
169 $RegexpMatch = 1; # it's the default
171 elsif (/^-h$/ || /^--help$/) {
174 elsif (/^-p$/ || /^--in-place$/) {
177 elsif (/^-n$/ || /^--no-close$/) {
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";
187 die "$ProgramName: Unrecognized option \`$_'\n";
195 $Package = shift or die $UsageShort;
196 $File = shift or die $UsageShort;
198 # prepare block begin and end marks
200 (! $Block || ($ParamBegin && $ParamEnd))
201 or die ("$ProgramName: must provide both begin and end marks.\n");
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;
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;
217 DEBUG and print STDERR "MarkBegin='$MarkBegin'\nMarkEnd='$MarkEnd'\nTrailer='$Trailer'\n";
219 # compile regexp if provided
222 # compile user's regexp
223 eval { $MatchLine = qr<$MatchLine> };
226 # we'll do regexp matching anyway :)
227 # just not with user's specials interfering
228 eval { $MatchLine = qr<^\Q$MatchLine\E>o; };
231 die "$ProgramName: regexp compilation failed: $@ ($!)\n";
242 if (DO_INSERT == $InsertRemove) {
243 $StdinContent = &stdin_content;
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);
262 # If FILE does not have a trailing newline, be sure to add it
263 # before appending anything else.
265 if (@Lines and $Lines[$#Lines] !~ m/\n$/s) {
266 $Lines[$#Lines] .= "\n";
271 if (APPEND_AT_END == $Placement) {
272 # append stuff at the end of the file
273 push(@Lines, $StdinContent);
275 elsif (INSERT_ON_TOP == $Placement) {
276 # throw the stuff on top of the file
277 unshift(@Lines, $StdinContent);
280 # insert stuff where needed
281 my ($line, $lineNo, $added);
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);
292 elsif (INSERT_BEFORE == $Placement) {
293 DEBUG and print STDERR " - INSERT_BEFORE\n";
294 splice(@Lines, $lineNo, 0, $StdinContent);
296 # once added and we're done
302 if ($MatchLine and ! $added) {
303 warn "$ProgramName: Inserting lines at the end implicitly! No '$MatchLine'\n";
304 push(@Lines, $StdinContent);
307 return scalar(@Lines); # whatever
311 my ($mytrailer, $mybegin, $myend) =
312 ($Trailer, $MarkBegin, $MarkEnd);
314 my ($bm_found, $em_found); # begin/end mark found indicator
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;
324 unless (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o);
327 $bm_found = 1 if (/^$mybegin(?:$mytrailer)?$/o);
328 $em_found = 1 if (/^$myend(?:$mytrailer)?$/o);
330 if ($bm_found and $em_found) {
331 DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n";
334 elsif ($bm_found and ! $em_found) {
336 die "$ProgramName: no end-mark after begin-mark!\n";
338 return scalar(@Lines); # whatever
341 # written by ddzeko@srce.hr, 2005-03-18
342 # to improve reliabilitah :)
345 # put it all thogether
346 my $newContent = join('', @Lines);
348 unless (length($newContent)) {
349 # safety exit in last second :)
350 die "$ProgramName: New content empty -- aborting file alteration!\n";
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)
363 unless (fileno($FileHandle)) {
364 sysopen($FileHandle, $File, O_WRONLY|O_TRUNC)
365 or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
368 sysseek(*$FileHandle, 0, SEEK_SET)
369 or die "$ProgramName: Failed to seek to the begining of file ($!)\n";
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";
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";
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) {
394 die "$ProgramName: Failed to write the content to '$File' ($!)\n";
397 # do the moving (should be atomic)
399 eval { require File::Copy; };
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";
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
414 rename($file_new, $File)
415 or die "$ProgramName: Failed to rename file '$file_new' to '$File' ($!)\n";
417 or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n";
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";
427 if ($stdin_content !~ m/\n$/s) {
428 # add trailing newline
429 $stdin_content .= "\n";
432 my ($mytrailer, $mybegin, $myend) =
433 ($Trailer, $MarkBegin, $MarkEnd);
436 $mybegin .= $mytrailer;
437 $myend .= $mytrailer;
439 $stdin_content = ($mybegin . $stdin_content . $myend);
440 return $stdin_content;
443 # create backup copy of altered file
445 die "$ProgramName: backup_copy() not implemented";