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);
37 # Looks nicer without the slashes and dots
38 ($ProgramName = $0) =~ s!.*/!!; # strip dir
39 $ProgramName =~ s!\.[^.]+$!!; # strip last ext
41 # 34567890 34567890 34567890 34567890 34567890 34567890 34567890 345678
44 $ProgramName -- versatile line-based file updating tool
45 usually used by package configuration scripts
47 Usage: $ProgramName [options] PACKAGE FILE < stdin-content
52 -r | --remove Remove entry PACKAGE from FILE.
53 Default is to add lines from stdin.
55 -m | --allow-multiple Allow multiple blocks of the same type.
56 By default, old blocks are replaced with
59 -h | --help Print this message.
63 -t | --insert-on-top Insert stdin-content block on top.
64 The default is to add it at the bottom.
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.
70 - Manipulating block marks:
72 -c | --comment x Use alternative comment char/string.
73 The default is shell-style \#-sign.
75 --comment-end x Use this marker for comment ending.
76 The default is none. Ie. '-->', '*/'.
78 -b | --begin-mark x Block starting mark (ie. 'service ftp')
79 -e | --end-mark x Block ending mark (ie. '}')
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
87 - File handling options:
89 -p | --in-place Try to preserve original inode.
90 -n | --no-close Do not close and reopen file when
93 Options marked with 'x' take single argument. Others do not.
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";
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);
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;
113 use constant DO_REMOVE => 0;
114 use constant DO_INSERT => 1;
117 $InsertRemove = DO_INSERT;
118 $Placement = APPEND_AT_END;
127 if (/^-c$/ || /^--comment$/) {
128 defined ($Comment = shift)
129 || die "$ProgramName: `-c|--comment' must be followed by an argument\n";
131 elsif (/^--comment-end$/) {
132 defined ($CommentEnd = shift)
133 || die "$ProgramName: `--comment-end' must be followed by an argument\n";
135 elsif (/^-r$/ || /^--remove$/) {
136 $InsertRemove = DO_REMOVE;
138 elsif (/^-m$/ || /^--allow-multi(?:ple)?$/) {
141 elsif (/^-b$/ || /^--begin(?:-mark)?$/) {
142 defined ($ParamBegin = shift)
143 || die "$ProgramName: '-b|--begin-mark' must be followed by an argument\n";
146 elsif (/^-e$/ || /^--end(?:-mark)?$/) {
147 defined ($ParamEnd = shift)
148 || die "$ProgramName: '-e|--end-mark' must be followed by an argument\n";
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;
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;
161 elsif (/^-t$/ || /^--insert-on-top$/) {
162 $Placement = INSERT_ON_TOP;
164 elsif (/^-R$/ || /^--regexp(?:-match|-mode)?$/) {
167 elsif (/^-h$/ || /^--help$/) {
170 elsif (/^-p$/ || /^--in-place$/) {
173 elsif (/^-n$/ || /^--no-close$/) {
177 die "$ProgramName: Unrecognized option \`$_'\n";
185 $Package = shift or die $UsageShort;
186 $File = shift or die $UsageShort;
188 # prepare block begin and end marks
190 (! $Block || ($ParamBegin && $ParamEnd))
191 or die ("$ProgramName: must provide both begin and end marks.\n");
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;
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;
207 DEBUG and print STDERR "MarkBegin='$MarkBegin'\nMarkEnd='$MarkEnd'\nTrailer='$Trailer'\n";
209 # compile regexp if provided
212 # compile user's regexp
213 eval { $MatchLine = qr<$MatchLine> };
216 # we'll do regexp matching anyway :)
217 # just not with user's specials interfering
218 eval { $MatchLine = qr<^\Q$MatchLine\E>o; };
221 die "$ProgramName: regexp compilation failed: $@ ($!)\n";
232 if (DO_INSERT == $InsertRemove) {
233 $StdinContent = &stdin_content;
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);
252 # If FILE does not have a trailing newline, be sure to add it
253 # before appending anything else.
255 if (@Lines and $Lines[$#Lines] !~ m/\n$/s) {
256 $Lines[$#Lines] .= "\n";
261 if (APPEND_AT_END == $Placement) {
262 # append stuff at the end of the file
263 push(@Lines, $StdinContent);
265 elsif (INSERT_ON_TOP == $Placement) {
266 # throw the stuff on top of the file
267 unshift(@Lines, $StdinContent);
270 # insert stuff where needed
271 my ($line, $lineNo, $added);
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);
282 elsif (INSERT_BEFORE == $Placement) {
283 DEBUG and print STDERR " - INSERT_BEFORE\n";
284 splice(@Lines, $lineNo, 0, $StdinContent);
286 # once added and we're done
293 warn "$ProgramName: Inserting lines at the end implicitly!\n";
294 push(@Lines, $StdinContent);
297 return scalar(@Lines); # whatever
301 my ($mytrailer, $mybegin, $myend) =
302 ($Trailer, $MarkBegin, $MarkEnd);
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;
312 unless (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o);
314 DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n";
316 return scalar(@Lines); # whatever
319 # written by ddzeko@srce.hr, 2005-03-18
320 # to improve reliabilitah :)
323 # put it all thogether
324 my $newContent = join('', @Lines);
326 unless (length($newContent)) {
327 # safety exit in last second :)
328 die "$ProgramName: New content empty -- aborting file alteration!\n";
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)
341 unless (fileno($FileHandle)) {
342 sysopen($FileHandle, $File, O_WRONLY|O_TRUNC)
343 or die "$ProgramName: Failed to open file '$File' for writing ($!)\n";
346 sysseek(*$FileHandle, 0, SEEK_SET)
347 or die "$ProgramName: Failed to seek to the begining of file ($!)\n";
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";
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";
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) {
372 die "$ProgramName: Failed to write the content to '$File' ($!)\n";
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";
381 or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n";
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";
391 if ($stdin_content !~ m/\n$/s) {
392 # add trailing newline
393 $stdin_content .= "\n";
396 my ($mytrailer, $mybegin, $myend) =
397 ($Trailer, $MarkBegin, $MarkEnd);
400 $mybegin .= $mytrailer;
401 $myend .= $mytrailer;
403 $stdin_content = ($mybegin . $stdin_content . $myend);
404 return $stdin_content;
407 # create backup copy of altered file
409 die "$ProgramName: backup_copy() not implemented";