From: Damir Dzeko Date: Tue, 29 Mar 2005 15:08:34 +0000 (+0000) Subject: r26: set -e bug corrected in cp_check_and_sed X-Git-Tag: debian/3.0.0~80 X-Git-Url: http://ftp.carnet.hr/pub/carnet-debian/scm?a=commitdiff_plain;h=b6374f775ed04f3c9767d19a364ad0994a94ac4f;p=carnet-tools-cn.git r26: set -e bug corrected in cp_check_and_sed minor modifications in cp-update (--version) --- diff --git a/cp-update b/cp-update index fdbfaf9..6491c78 100755 --- a/cp-update +++ b/cp-update @@ -18,214 +18,417 @@ ## along with this program; if not, write to the Free Software ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. -## $Id: cp-update,v 2.2 2004/02/24 jelly Exp $ +## altered by ddzeko, 2005-03-18 +## -> reformated code, introduced coding conventions +## -> increased reliability through atomic actualization +## -> everything is done in the memory - more mem, less i/o +## -> removed append mode - in-place editing considered harmfull +## -> added in-place mode as option use strict; +use Fcntl qw(:DEFAULT :flock :seek); -my ($name, $usage); - -($name = $0) =~ s%.*/%%; # Looks nicer without the slashes -$usage = "Usage:\n"; -$usage .= "$name [-r] [-m] [-i AFTER|-t] [-b START -e STOP] [-c CHAR] PACKAGE FILE\n"; -$usage .= " -r Remove entry PACKAGE from FILE\n"; -$usage .= " Default is to add lines from stdin.\n"; -$usage .= " -m Allow multiple blocks of the same type\n"; -$usage .= " By default, old blocks are replaced with the new one.\n"; -$usage .= "\n"; -$usage .= " Special placement of config blocks:\n"; -$usage .= " -i Insert after this line\n"; -$usage .= " -t Insert on top\n"; -$usage .= " The default is to append at the bottom.\n"; -$usage .= "\n"; -$usage .= " Manipulating block marks:\n"; -$usage .= " -c Use alternative comment character (default: #)\n"; -$usage .= " Alternative block marks:\n"; -$usage .= " -b Start mark (ie 'service ftp')\n"; -$usage .= " -e Stop mark (ie. '}')\n"; -$usage .= " These will delete to the end of the file if no end mark\n"; -$usage .= " is found. The deletion is otherwise not greedy and\n"; -$usage .= " stops at the first end mark found. You can use\n"; -$usage .= " %P to insert the PACKAGE argument, and\n"; -$usage .= " %% to insert a literal % sign into the mark.\n"; - - - - -my ($begin, $end, $trailer); -my ($parambegin, $paramend, $insert); -my ($pkg, $file); -my ($block, $multi, $addafter, $addontop); - -my $add = 1; -my $comment = "#"; -my $SEEK_END = 2; - -while (@ARGV) -{ - $_ = shift; - if (/^-c$/ || /^--comment$/) - { - defined ($comment = shift) - || die "$name: `-c' must be followed by an argument\n"; - } - elsif (/^-r$/ || /^--remove$/) - { - $add = 0; - } - elsif (/^-m$/ || /^--allow-multiple$/) - { - $multi = 1; - } - elsif (/^-b$/ || /^--begin$/) - { - defined ($parambegin = shift) - || die "$name: '-b' must be followed by an argument\n"; - $block = 1; - } - elsif (/^-e$/ || /^--end$/) - { - defined ($paramend = shift) - || die "$name: '-e' must be followed by an argument\n"; - $block = 1; - } - elsif (/^-i$/ || /^--insert-after$/) - { - defined($insert = shift) - || die "$name: '-i' must be followed by an anrgument\n"; - #$multi = 1; # uncomment for -i backward compatibility - $addafter = 1; - } - elsif (/^-t$/ || /^--insert-on-top$/) - { - $insert = ""; - $addafter = 1; - } - elsif (/^-/) - { - die "$name: Unrecognized option \`$_'\n"; - } - else - { - unshift(@ARGV, $_); - last; - } +sub DEBUG () { 0 }; + +# coding convention: CamelCase vars are global here +# +my ($ProgramName, $UsageLong, $UsageShort, $VERSION); + +$VERSION = '2.0'; + +# Looks nicer without the slashes and dots +($ProgramName = $0) =~ s!.*/!!; # strip dir +$ProgramName =~ s!\.[^.]+$!!; # strip last ext + +# 34567890 34567890 34567890 34567890 34567890 34567890 34567890 345678 + +$UsageLong = " + $ProgramName -- versatile line-based file updating tool + usually used by package configuration scripts + + Usage: $ProgramName [options] PACKAGE FILE < stdin-content + + + - General options: + + -r | --remove Remove entry PACKAGE from FILE. + Default is to add lines from stdin. + + -m | --allow-multiple Allow multiple blocks of the same type. + By default, old blocks are replaced with + the new one. + + -h | --help Print this message (usage reference). + + --version Print version message. + + - Placement control: + + -t | --insert-on-top Insert stdin-content block on top. + The default is to add it at the bottom. + + -i | --insert-after x Insert after this/matching line. + -f | --insert-before x Insert before this/matching line. + -R | --regexp-match Use regexp mode for line matching. + + - Manipulating block marks: + + -c | --comment x Use alternative comment char/string. + The default is shell-style \#-sign. + + --comment-end x Use this marker for comment ending. + The default is none. Ie. '-->', '*/'. + + -b | --begin-mark x Block starting mark (ie. 'service ftp') + -e | --end-mark x Block ending mark (ie. '}') + + These will delete to the end of the file if no end mark + is found. The deletion is otherwise not greedy and stops + at the first end mark found. You can use \%P to insert + the PACKAGE argument, and \%\% to insert a literal \% sign + into the mark. + + - File handling options: + + -p | --in-place Try to preserve original inode. + -n | --no-close Do not close and reopen file when + editing it in place. + + Options marked with 'x' take single argument. Others do not. + +\n"; + +$UsageShort = "$ProgramName -- versatile line-based file updating tool\n"; +$UsageShort .= " Options: [-r] [-m] [-i AFTER|-f BEFORE|-t] [-b START -e STOP] [-c CHAR] PACKAGE FILE\n"; +$UsageShort .= " or type $ProgramName --help to be choked with help.\n"; + +my ($MarkBegin, $MarkEnd, $Trailer, $ParamBegin, $ParamEnd, $Placement, + $Package, $File, $Block, $Multi, $InsertRemove, $Comment, $CommentEnd, + $MatchLine, $RegexpMatch, $StdinContent, $NewContent, $InPlace, + $NoClose, $FileHandle, @Lines); + +# Placement modes +use constant APPEND_AT_END => 0; +use constant INSERT_BEFORE => 1; +use constant INSERT_AFTER => 2; +use constant INSERT_ON_TOP => 3; + +# InsertRemove modes +use constant DO_REMOVE => 0; +use constant DO_INSERT => 1; + +# Operation defaults +$InsertRemove = DO_INSERT; +$Placement = APPEND_AT_END; +$Comment = '#'; +$CommentEnd = ''; +$MatchLine = ''; +$InPlace = 0; +$NoClose = 0; + +while (@ARGV) { + $_ = shift; + if (/^-c$/ || /^--comment$/) { + defined ($Comment = shift) + || die "$ProgramName: `-c|--comment' must be followed by an argument\n"; + } + elsif (/^--comment-end$/) { + defined ($CommentEnd = shift) + || die "$ProgramName: `--comment-end' must be followed by an argument\n"; + } + elsif (/^-r$/ || /^--remove$/) { + $InsertRemove = DO_REMOVE; + } + elsif (/^-m$/ || /^--allow-multi(?:ple)?$/) { + $Multi = 1; + } + elsif (/^-b$/ || /^--begin(?:-mark)?$/) { + defined ($ParamBegin = shift) + || die "$ProgramName: '-b|--begin-mark' must be followed by an argument\n"; + $Block = 1; + } + elsif (/^-e$/ || /^--end(?:-mark)?$/) { + defined ($ParamEnd = shift) + || die "$ProgramName: '-e|--end-mark' must be followed by an argument\n"; + $Block = 1; + } + elsif (/^-i$/ || /^--insert-after$/) { + defined($MatchLine = shift) + || die "$ProgramName: '-i|--insert-after' must be followed by an argument\n"; + $Placement = INSERT_AFTER; + } + elsif (/^-f$/ || /^--insert-before$/) { + defined($MatchLine = shift) + || die "$ProgramName: '-f|--insert-before' must be followed by an argument\n"; + $Placement = INSERT_BEFORE; + } + elsif (/^-t$/ || /^--insert-on-top$/) { + $Placement = INSERT_ON_TOP; + } + elsif (/^-R$/ || /^--regexp(?:-match|-mode)?$/) { + $RegexpMatch = 1; + } + elsif (/^-h$/ || /^--help$/) { + die $UsageLong; + } + elsif (/^-p$/ || /^--in-place$/) { + $InPlace = 1; + } + elsif (/^-n$/ || /^--no-close$/) { + $NoClose = 1; + } + elsif (/^--version$/) { + die "$ProgramName (CARNet Packaging file update) $VERSION\n" + . "Copyright (C) 1998-2005 Free Software Foundation, Inc.\n" + . "This is free software; see the source for copying conditions. There is NO\n" + . "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"; + } + elsif (/^-/) { + die "$ProgramName: Unrecognized option \`$_'\n"; + } + else { + unshift(@ARGV, $_); + last; + } } -($pkg = shift) || die $usage; -($file = shift) || die $usage; -(! $block || ($parambegin && $paramend)) - || die ("$name: must provide both begin and end marks.\n"); - -if ($block) -{ - $parambegin =~ s,%P,$pkg,g; - $parambegin =~ s,%%,%,g; - $paramend =~ s,%P,$pkg,g; - $paramend =~ s,%%,%,g; - $begin = $parambegin; - $end = $paramend; - $trailer = ""; +$Package = shift or die $UsageShort; +$File = shift or die $UsageShort; + +# prepare block begin and end marks +# +(! $Block || ($ParamBegin && $ParamEnd)) + or die ("$ProgramName: must provide both begin and end marks.\n"); +# +if ($Block) { + $ParamBegin =~ s, %P , $Package ,gx; + $ParamBegin =~ s, %% , % ,gx; + $ParamEnd =~ s, %P , $Package ,gx; + $ParamEnd =~ s, %% , % ,gx; + $MarkBegin = $ParamBegin; + $MarkEnd = $ParamEnd; + $Trailer = ''; +} else { + $MarkBegin = "$Comment Begin update by CARNet package $Package"; + $MarkEnd = "$Comment End update by CARNet package $Package"; + $Trailer = " -- DO NOT DELETE THIS LINE!".$CommentEnd; +} + +DEBUG and print STDERR "MarkBegin='$MarkBegin'\nMarkEnd='$MarkEnd'\nTrailer='$Trailer'\n"; + +# compile regexp if provided +# +if ($RegexpMatch) { + # compile user's regexp + eval { $MatchLine = qr<$MatchLine> }; } -else -{ - $begin = "$comment Begin update by CARNet package $pkg"; - $end = "$comment End update by CARNet package $pkg"; - $trailer = " -- DO NOT DELETE THIS LINE!"; +else { + # we'll do regexp matching anyway :) + # just not with user's specials interfering + eval { $MatchLine = qr<^\Q$MatchLine\E>o; }; +} +if ($@) { + die "$ProgramName: regexp compilation failed: $@ ($!)\n"; } -if ($add) -{ - if (! $multi) - { - &del; - } - &add; +do_it() and exit(0); + + +# main procedure +# -------------- + +sub do_it { + slurp(); + if (DO_INSERT == $InsertRemove) { + $StdinContent = &stdin_content; + del() unless $Multi; + add(); + } + else { + del(); + } + actualize(); } -else -{ - &del; + + +# subroutines +# ----------- + +sub slurp() { + sysopen($FileHandle, $File, O_RDWR) or die "$ProgramName: Cannot open $File: $!\n"; + @Lines = <$FileHandle>; # slurp the whole file as lines + close($FileHandle) unless ($InPlace && $NoClose); + + # If FILE does not have a trailing newline, be sure to add it + # before appending anything else. + + if (@Lines and $Lines[$#Lines] !~ m/\n$/s) { + $Lines[$#Lines] .= "\n"; + } } sub add() { - $trailer .= "\n"; - $begin .= $trailer; - $end .= $trailer; - my $byte = ""; # @#$@#$@#$$@#$@#$#@$! -w - my $prefix = ""; - - open (HANDLE, "<$file") || die "$file: $!\n"; - - # Need to load it all into memory if we want to write it - # out to the same file. - my @lines = if ($addafter); - - # If FILE does not have a trailing newline, be suire to add it - # before appending anything else. - - # Unless it is size 0! - if ((-s $file)) - { - seek HANDLE, -1, $SEEK_END; - defined (read HANDLE, $byte, 1) && ($byte ne "\n") && ($prefix = "\n"); - close HANDLE; - } - - if(!$addafter) - { - # The actual work is in the following three lines: - open (HANDLE, ">>$file") || die "$file: $!\n"; - print HANDLE $prefix . $begin . join('', ) . $end; - close HANDLE; - } - else - { - # Overwrite it - open (HANDLE, ">$file") || die "$file: $!\n"; - my $added = 0; - foreach (@lines) - { - if (! $added && /^$insert/o) - { - print HANDLE if ($insert ne ""); - print HANDLE $begin . join('', ) . $end; - print HANDLE if ($insert eq ""); - $added = 1; - } - else - { - print HANDLE; - } + if (APPEND_AT_END == $Placement) { + # append stuff at the end of the file + push(@Lines, $StdinContent); + } + elsif (INSERT_ON_TOP == $Placement) { + # throw the stuff on top of the file + unshift(@Lines, $StdinContent); + } + else { + # insert stuff where needed + my ($line, $lineNo, $added); + $lineNo = 0; + $added = 0; + foreach $line (@Lines) { + DEBUG and print STDERR "at: $line"; + if ($line =~ m/$MatchLine/) { + DEBUG and print STDERR " - MATCH\n"; + if (INSERT_AFTER == $Placement) { + DEBUG and print STDERR " - INSERT_AFTER\n"; + splice(@Lines, $lineNo + 1, 0, $StdinContent); } - if(! $added) - { - print HANDLE $prefix . $begin . join('', ) . $end; + elsif (INSERT_BEFORE == $Placement) { + DEBUG and print STDERR " - INSERT_BEFORE\n"; + splice(@Lines, $lineNo, 0, $StdinContent); } - close HANDLE; + # once added and we're done + $added = 1; + last; + } + ++ $lineNo; } + if (! $added) { + warn "$ProgramName: Inserting lines at the end implicitly!\n"; + push(@Lines, $StdinContent); + } + } + return scalar(@Lines); # whatever } sub del() { - # saving vars for &add function... - my ($mybegin, $myend, $mytrailer); - - $mybegin = $begin; - $myend = $end; - $mytrailer = $trailer; - - # Make the strings regexp-friendly by quoting non-word chars. - $mybegin =~ s/\W/\\$&/g; - $myend =~ s/\W/\\$&/g; - $mytrailer =~ s/\W/\\$&/g; - open (HANDLE, "<$file") || die "$file: $!\n"; - # Need to load it all into memory, because we'll want to write it - # out to the same file. - my @lines = ; - close HANDLE; - open (HANDLE, ">$file") || die "$file: $!\n"; - foreach (@lines) - { - print HANDLE unless (/^$mybegin(?:$mytrailer)?$/o - .. /^$myend(?:$mytrailer)?$/o); + my ($mytrailer, $mybegin, $myend) = + ($Trailer, $MarkBegin, $MarkEnd); + + # Make the strings regexp-friendly by quoting non-word chars. + $mybegin =~ s/\W/\\$&/g; + $myend =~ s/\W/\\$&/g; + $mytrailer =~ s/\W/\\$&/g; + + my (@filtered); + foreach (@Lines) { + push (@filtered, $_) + unless (/^$mybegin(?:$mytrailer)?$/o .. /^$myend(?:$mytrailer)?$/o); + } + DEBUG and print STDERR "Deleted ". (@Lines - @filtered) ." out of ".scalar(@Lines)." lines\n"; + @Lines = @filtered; + return scalar(@Lines); # whatever +} + +# written by ddzeko@srce.hr, 2005-03-18 +# to improve reliabilitah :) +# +sub actualize() { + # put it all thogether + my $newContent = join('', @Lines); + + unless (length($newContent)) { + # safety exit in last second :) + die "$ProgramName: New content empty -- aborting file alteration!\n"; + } + + if ($InPlace) { + # Provdided as means of changing the file content + # and keeping it's inode number still. + # ------------------------------------------------------- + # this is dangerous since it can leave file in bad state + # do not use this for highly critical files (ie. inittab) + # ------------------------------------------------------- + # (REVISIT: add File::Copy call to back-up the file so + # we can at least try undoing the file content change) + # backup_copy(); + unless (fileno($FileHandle)) { + sysopen($FileHandle, $File, O_WRONLY|O_TRUNC) + or die "$ProgramName: Failed to open file '$File' for writing ($!)\n"; + } + else { + sysseek(*$FileHandle, 0, SEEK_SET) + or die "$ProgramName: Failed to seek to the begining of file ($!)\n"; + } + my $wb = syswrite($FileHandle, $newContent); + if (! $wb or length($newContent) != $wb) { + # FIXME: try restoring backup copy + die "$ProgramName: Failed to write the content to '$File' ($!)\n"; + } + if ($NoClose) { + # this could be handy for files that had stuff appended + # at the end of the file + truncate($FileHandle, length($newContent)) + or die "$ProgramName: Failed to truncate the file ($!)\n"; } - close HANDLE; + close($FileHandle); + } + else { + my ($file_new, $file_old) = ($File, $File); + $file_new .= '.cp-update.new'; # our .new file + $file_old .= '.cp-update.old'; # our .old file + # write content in new file in single write op + sysopen ($FileHandle, $file_new, O_CREAT|O_TRUNC|O_WRONLY) + or die "$ProgramName: Failed to open file '$File' for writing ($!)\n"; + my $wb = syswrite($FileHandle, $newContent); + if (! $wb or length($newContent) != $wb) { + unlink($file_new); + die "$ProgramName: Failed to write the content to '$File' ($!)\n"; + } + close($FileHandle); + # do the moving (should be atomic) + + eval { require File::Copy; }; + if ($@) { + # do it the classical way: successive renames + rename($File, $file_old) + or die "$ProgramName: Failed to rename file '$File' to '$file_old' ($!)\n"; + } + else { + import File::Copy; + # do it with increased safety: overstepping + # the old file with it's modified copy + copy($File, $file_old) + or die "$ProgramName: Failed to create backup of '$File' as '$file_old' ($!)\n"; + # if the filesystem is absolutelly filled up this will fail + # and the original file will remain unchanged + } + rename($file_new, $File) + or die "$ProgramName: Failed to rename file '$file_new' to '$File' ($!)\n"; + unlink($file_old) + or warn "$ProgramName: Failed to remove file '$file_old' ($!)\n"; + } +} + +# return content from standard input +sub stdin_content() { + my ($stdin_content) = join('', ); + unless (length($stdin_content)) { + die "$ProgramName: No stdin-content provided!\n"; + } + if ($stdin_content !~ m/\n$/s) { + # add trailing newline + $stdin_content .= "\n"; + } + + my ($mytrailer, $mybegin, $myend) = + ($Trailer, $MarkBegin, $MarkEnd); + + $mytrailer .= "\n"; + $mybegin .= $mytrailer; + $myend .= $mytrailer; + + $stdin_content = ($mybegin . $stdin_content . $myend); + return $stdin_content; +} + +# create backup copy of altered file +sub backup_copy() { + die "$ProgramName: backup_copy() not implemented"; } diff --git a/functions-more.sh b/functions-more.sh deleted file mode 100644 index 361d8b4..0000000 --- a/functions-more.sh +++ /dev/null @@ -1,33 +0,0 @@ -# by ddzeko, Mon, 21 Mar 2005 13:35:42 +0100 -cp_yes_no () { - [ "$CP_SCRIPT_DEBUG" ] && set -vx - local prompt answer - prompt="$1" - [ "$prompt" ] || prompt="Are you sure?" - RET="" - - # check to prevent user from using this when debconf is active - # (it would break communication with its backend - uses stdin/out) - if [ "$DEBCONF_REDIR" ]; then - echo "cn_yes_no: debconf redirection detected" >&2 - return 1 - fi - - echo -n "$prompt (y)es/(n)o: " - read answer - case "$answer" in - y*) - echo - RET=y - ;; - n*) - echo - RET=n - ;; - *) - echo - echo "Please read the message and choose y or n" - cn_yes_no - ;; - esac -} diff --git a/functions.sh b/functions.sh index b0c8be6..f2fc9a1 100644 --- a/functions.sh +++ b/functions.sh @@ -179,7 +179,9 @@ cp_check_and_sed() { for i in "$@" do [ -e "$i" ] || continue - egrep -q "$s" "$i" || continue + if egrep -q "$s" "$i"; then + continue + fi [ -h "$i" ] && i=$(readlink -f "$i") sed "$sedcmd" "$i" > "$i.dpkg-tmp" if ! cmp -s "$i" "$i.dpkg-tmp" 2>&1 >/dev/null; then diff --git a/test/test-file b/test/test-file index be1b37b..1e6e4b7 100644 --- a/test/test-file +++ b/test/test-file @@ -26,4 +26,4 @@ string after the package name # Begin update by CARNet package broken2 -It's the end of the file and guess what? This line does not end with \n \ No newline at end of file +It's the end of the file and guess what? This line does not end with \n