#!/usr/bin/perl # Filename: vidb # Author: David Ljung Madison # See License: http://MarginalHacks.com/License # Description: Edits a database like a text file use strict; use BerkeleyDB; my $PROGNAME = $0; $PROGNAME =~ s|.*/||; my $EDITOR = $ENV{EDITOR} || "vi"; my $TMP = "/tmp/$PROGNAME.$$"; ################################################## # Usage ################################################## sub usage { foreach my $msg (@_) { print STDERR "ERROR: $msg\n"; } print STDERR "\n"; print STDERR "Usage:\t$PROGNAME [-l] [-d] \n"; print STDERR "\tEdits a database like a text file\n"; print STDERR "\t-l\tJust list the database contents\n"; print STDERR "\t-n\tCreate db if not existing\n"; print STDERR "\n"; print STDERR "\t-d\tSet debug mode\n"; print STDERR "\n"; exit -1; } sub parse_args { my ($list,@files); while (my $arg=shift(@ARGV)) { if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; } if ($arg =~ /^-l$/) { $list=1; next; } if ($arg =~ /^-n$/) { $MAIN::CREATE=1; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } push(@files,$arg); } usage("No database specified") unless @files; ($list,@files); } sub debug { return unless $MAIN::DEBUG; foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; } } ################################################## # Code ################################################## sub from_db { my ($str) = @_; $str =~ s/([%\t\n])/"%".sprintf("%0.2x",ord($1))/eg; $str; } sub to_db { my ($str) = @_; $str =~ s/%([0-9a-f]{2})/chr(hex($1))/eig; $str; } sub get_db { my ($file) = @_; my %db; return \%db if (!-e $file && $MAIN::CREATE); tie %db, 'BerkeleyDB::Hash', -Filename => $file, -Flags => DB_RDONLY, or die("Can't read db [$file]"); my %copy = %db; untie %db; \%copy; } sub list_db { my ($file) = @_; my $db = get_db($file); foreach my $k ( sort keys %$db ) { print "$k\t-> $db->{$k}\n"; } } sub edit_db { my ($file) = @_; # Get the database # Saves a copy (so we know which keys are deleted) # (any keys changed by another process while we are editing are lost) my $db = get_db($file); # Setup the tmp file open(TMP,">$TMP") || die("Couldn't write [$TMP]\n"); foreach my $k ( sort keys %$db ) { print TMP from_db($k),"\t",from_db($db->{$k}),"\n"; debug("Read: $k\t-> $db->{$k}\n"); } close TMP; my $done=0; my %new; while (!$done) { # Edit the tmp file my $mod = -M $TMP; system("$EDITOR $TMP"); return unlink($TMP) unless -M $TMP != $mod; # Read in the edits $done = 1; open(TMP,"<$TMP") || die("Couldn't read [$TMP]\n"); while() { chomp; next unless /\S/; if (/^([^\t]+)(\t(.+)?)?$/) { my ($k,$v) = (to_db($1),to_db($3)); $new{$k} = $v; debug("Wrote: $k->$v\n"); } else { # I don't actually think we can ever see errors by the REGEXP above... print STDERR "Can't parse database line [$.]:\n $_"; $done = 0; } } close TMP; # Errors? unless ($done) { print STDERR "\nE)dit it again, F)orget it? e"; # Char mode my $ttyname=`/usr/bin/tty`; system "/bin/stty -icanon -echo min 1 < $ttyname " if (! $?); my $ans; read(STDIN,$ans,1); # Line mode `/usr/bin/tty -s`; system "/bin/stty icanon echo < $ttyname " if (! $? ); print "$ans\n"; return unlink($TMP) if ($ans =~ /F/i); } } unlink($TMP); # Write the database my %write; my $flags = $MAIN::CREATE ? DB_CREATE : 0; tie %write, 'BerkeleyDB::Hash', -Filename => $file, -Flags => $flags, or die("Can't write db [$file]"); # All the changes/values foreach my $k ( keys %new ) { $write{$k} = $new{$k}; } # And all the deletions foreach my $k ( keys %$db ) { delete $write{$k} if $db->{$k} && !defined $new{$k}; } } ################################################## # Main code ################################################## sub main { my ($list,@files) = parse_args(); foreach my $file ( @files ) { $list ? list_db($file) : edit_db($file); } } main();