The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/dim/perl/5.8/bin/perl

# $Id: dbshell.pl,v 1.22 2004/11/09 13:44:34 joern Exp $

use strict;
use Getopt::Std;
use DBI;
use Carp;
use File::Basename;

my $STATIC;

my $newspirit_home;
BEGIN {
	$STATIC = 0;
	if ( not $STATIC ) {
		$newspirit_home = $ENV{NEWSPIRIT_HOME} ||
			  "/usr/projects/newspirit2";
		unshift @INC, "$newspirit_home/lib";
		eval {
			require "NewSpirit/SqlShell/Text.pm";
		};
		if ( $@ =~ /Can't locate/i ) {
			print "\n";
			print "ERROR: Unable to load module NewSpirit::SqlShell::Text\n";
			print "       Please install this module or set the environment\n";
			print "       variable NEWSPIRIT_HOME to the installation path\n";
			print "       of new.spirit\n\n";
			exit 1;
		} elsif ( $@ ) {
			die $@;
		}
	}
}

my $VERSION = "0.12";
my $USAGE;

if ( $STATIC ) {
	$USAGE =<<__EOF;

dbshell.pl Version $VERSION - Copyright 2001-2004 (c) dimedis GmbH, Cologne, Germany

usage: dbshell.pl [-s] [-e] [-x] [-u username] [-p password] DBI-Data-Source
       dbshell.pl [-s] [-e] [-x] new.spirit-db-config-file
       dbshell.pl [-s] [-e] [-x] new.spirit-sql-prod-file

       -e      echo sql statements
       -s      print error summary on exit
       -x      abort on error

       In a new.spirit production environment (where no sources
       are available) you can pass a new.spirit-sql-prod-file
       (located in the prod/sql folder) or a new.spirit-db-config-file
       (located in the prod/config folder). dbshell.pl will then
       connect to the database using the according configuration
       information in prod/config. A new.spirit-sql-prod-file will
       be executed.
       
       The exit code is 0 on success and 1 if a statement fails.
__EOF
} else {
	$USAGE =<<__EOF;

dbshell.pl Version $VERSION - Copyright 2001-2002 (c) dimedis GmbH, Cologne, Germany

usage: dbshell.pl [-s] [-u username] [-p password] [-x] DBI-Data-Source
       dbshell.pl [-s] [-e] [-x] new.spirit-Database-Object
       dbshell.pl [-s] [-e] [-x] new.spirit-SQL-Object
       dbshell.pl [-s] [-e] [-x] new.spirit-db-config-file
       dbshell.pl [-s] [-e] [-x] new.spirit-sql-prod-file

       -e      echo sql statements
       -s      print error summary on exit
       -x      abort on error

       If you pass a new.spirit-Database-Object, its information
       is used for connecting the database. If you pass a
       new.spirit-SQL-Object, the program will connect to the
       according database and the SQL code of the object is
       executed.
       
       In a new.spirit production environment (where no sources
       are available) you can pass a new.spirit-sql-prod-file
       (located in the prod/sql folder) or a new.spirit-db-config-file
       (located in the prod/config folder). dbshell.pl will then
       connect to the database using the according configuration
       information in prod/config. A new.spirit-sql-prod-file will
       be executed.
       
       The exit code is 0 on success and 1 if a statement fails.
__EOF
}

$| = 1;

main: {
	my %opts;
	my $ok = getopts ('xesu:p:', \%opts);
	
	my $db_info = shift @ARGV;
	
	if ( not $ok or not $db_info or @ARGV ) {
		print $USAGE;
		exit 1;
	}

	eval {
		db_shell_main (
			opts_href => \%opts,
			db_info   => $db_info
		);
	};

	exception (message => $@) if $@;
}

sub db_shell_main {
	my %par = @_;
	
	my $db_info = $par{db_info};
	my $opts    = $par{opts_href};
	
	print STDERR "dbshell.pl Version $VERSION, Copyright 2000 dimedis GmbH, All Rights Reserved\n\n";
	
	if ( $db_info =~ /^dbi/ ) {
		db_shell (
			username   => $opts->{u},
			password   => $opts->{p},
			source     => $db_info,
			print_error_summary => $opts->{s},
			opts_href  => $opts,
		);
	} elsif ( $db_info =~ /\.(sql|db-conf)$/  ) {
		db_shell (
			get_newspirit_prod_db_conf (
				db_object => $db_info
			),
			print_error_summary => $opts->{s},
			opts_href  => $opts,
		);
	} elsif ( not $STATIC) {
		db_shell (
			get_newspirit_src_db_conf (
				db_object => $db_info
			),
			print_error_summary => $opts->{s},
			opts_href  => $opts,
		);
	} else {
		print STDERR "Unknown file parameter.\n\n";
		exit 1;
	}
}

sub exception {
	my %par = @_;
	
	my $message = $par{message};
	
	$message =~ s/ at .*?line\s*\d+\.//;
	
	print "Exception: $message\n\n";
	exit 1;
}

sub info {
	print join ("\n",@_),"\n";
}

sub get_newspirit_src_db_conf {
	my %par = @_;
	
	my $db_object = $par{db_object};
	my $project = $db_object;
	$project =~ s/\..*//;
	$db_object =~ s/.*?\.//;
	$db_object =~ s!\.!/!g;

	eval {
		require "NewSpirit/Object.pm";
		require "$newspirit_home/etc/newspirit.conf";
		require "$newspirit_home/etc/objecttypes.conf";
	};
	if ( $@ ) {
		print "ERROR: Unable to load new.spirit modules and/or configuration!\n";
		print "       You must have new.spirit installed for connecting to a\n";
		print "       database via a new.spirit database configuration object!\n";
		print "\n";
		print "The error message was:\n$@\n";
		exit 1;
	}
	
	my $cgi = bless {
		ticket   => 'dummy',
		username => 'dummy',
		project  => $project
	}, "MyCGI";

	my $db_obj;
	
	# try to treat the object as a database configuration
	eval {
		$db_obj = new NewSpirit::Object (
			q => $cgi,
			object => $db_object.".cipp-db"
		);
	};

	# otherwise try treating it as a SQL object
	$@ && eval {
		$db_obj = new NewSpirit::Object (
			q => $cgi,
			object => $db_object.".cipp-sql"
		);
	};
	
	if ( $@ ) {
		print STDERR "Can't find a new.spirit DB or SQL object with this name!\n\n";
		exit 1;
	}
	
	if ( $db_obj->{object_type} eq 'cipp-sql' ) {
		# Ok, we have a SQL Object. Lets determine its
		# database configuration object
		my $sql_obj = $db_obj;

		my $meta = $db_obj->get_meta_data;
		
		my $db_obj_name = $meta->{sql_db};
		
		if ( $db_obj_name eq '__default' ) {
			$db_obj_name = $db_obj->get_default_database;
		}
		
		$db_obj = new NewSpirit::Object (
			q => $cgi,
			object => $db_obj_name
		);
		
		# now connect the SQL object file to STDIN
		close STDIN;
		open (STDIN, $sql_obj->{object_file})
			or croak "can't read SQL object file '$sql_obj->{object_file}'";
	}
	
	my $db_data = $db_obj->get_data;

	# set database environment

	my %OLD_ENV = %ENV;

	my @env = split (/\r?\n/, $db_data->{db_env});
	foreach my $env (@env) {
		my ($k,$v) = split (/\s+/, $env, 2);
		$ENV{$k} = $v;
	}

	# decode the password
	my $pass;
	{
		# strange workaround. without this block the
		# regex of NewSpirit::SqlShell::next_command
		# will result in this $1 if no match is found
		( $pass = $db_data->{db_pass} )=~
			s/%(..)/chr(ord(pack('C', hex($1)))^85)/eg;
	}
		
	return (
		username => $db_data->{db_user},
		password => $pass,
		source   => $db_data->{db_source}
	);
}

sub get_newspirit_prod_db_conf {
	my %par = @_;
	
	my $db_object = $par{db_object};
	
	my $db_config_file;
	if ( $db_object =~ /sql$/ ) {
		my $db_info_file = $db_object;
		$db_info_file =~ s/\.sql$//;
		$db_info_file .= ".db";
	
		open (DBINFO, $db_info_file) or croak "can't read $db_info_file";
		$db_config_file = <DBINFO>;
		chomp $db_config_file;
		close DBINFO;
	
		$db_config_file = dirname($db_object)."/".$db_config_file;
		
		# connect sql file to STDIN
		close STDIN;
		open (STDIN, $db_object) or croak "can't read $db_object";
	} else {
		$db_config_file = $db_object;
	}
	
	open (DBCONFIG, $db_config_file) or croak "can't read '$db_config_file'";
	my $config = join ('', <DBCONFIG>);
	close DBCONFIG;
	
	$config =~ s/::cipp.*?::/::/g;
	my $cipp3_config = eval $config;
	croak "can't eval config: $@" if $@;

	if ( ref $cipp3_config ) {
		return (
			username => $cipp3_config->{user},
			password => $cipp3_config->{password},
			source   => $cipp3_config->{data_source},
		);
	}

	return (
		username => $CIPP_Exec::user,
		password => $CIPP_Exec::password,
		source   => $CIPP_Exec::data_source,
	);
}

sub db_shell {
	my %par = @_;
	
	my $username            = $par{username};
	my $password            = $par{password};
	my $source              = $par{source};
	my $print_error_summary = $par{print_error_summary};
	my $opts_href           = $par{opts_href};

	# build prompt label
	$source =~ /^dbi:(\w+)/;
	my $driver = $1;
	$source =~ /^dbi:\w+:(\w*)$/;
	my $db = $1;

	my $selected_db = $db?$db:$driver;
	
	my $lf;
	if ( $username eq '' ) {
		$username = ask (
			label => "Username",
		);
		$lf = 1;
	}
	
	if ( $password eq '' ) {
		$password = ask (
			label => "Password",
			invisible => 1
		);
		$lf = 1;
	}

	print "\n" if $lf;

	my $get_line_cb;
	
	my $shell;
	my $initialize_history;
	my $term;
	
	my $rl_package;
	if ( -t STDIN ) {
		# we're connected to a TTY, so install
		# a Term::Readline input handler
		require "Term/ReadLine.pm";
		$term = new Term::ReadLine 'dbshell';
		
		$rl_package = $term->ReadLine;
		if ( $rl_package eq 'Term::ReadLine::Perl' ) {
			$readline::rl_completion_function = 'main::perl_rl_completion';
		} elsif ( $rl_package eq 'Term::ReadLine::Gnu' ) {
			my $attribs = $term->Attribs;
			$attribs->{'attempted_completion_function'} = undef;
			$attribs->{'completion_entry_function'} = undef;
			$attribs->{'list_completion_function'} = undef;
		}

		$get_line_cb = sub {
			print "\n" if $shell->{command_completed};
			$shell->{command_completed} = 0;
			my $line = $term->readline (
				$shell->{username}.'@'.
				$shell->{selected_db}."> "
			);
			print "> $line" if $opts_href->{e};
			add_line_to_history ($line) if $line;
			print "\n" if not defined $line;
			return $line;
		};
		
		$initialize_history = 1;
		
	} else {
		# no TTY, so simply read from STDIN without
		# giving a prompt
		$get_line_cb = sub {
			my $line = <STDIN>;
			print "> $line" if $opts_href->{e};
			return $line;
		}
	}
	
	$shell = new NewSpirit::SqlShell::Text (
		source      => $source,
		username    => $username,
		password    => $password,
		selected_db => $selected_db,
		autocommit  => 1,
		echo        => 0,
		get_line_cb => $get_line_cb,
		preference_file => home_dir()."/.dbshell_prefs"
	);

	$shell->{abort_mode} = $opts_href->{x};

	if ( $initialize_history ) {
		$shell->info ("Initializing command history");
		initialize_history (
			$term, $shell->get_preference('history_size')
		) 
	}

	if ( $rl_package eq 'Term::ReadLine::Stub' ) {
		$shell->info ("WARNING: Only stub ReadLine support available.",
	        	      "Install Term::ReadLine::Gnu or Term::ReadLine::Perl",
			      "to get enhanced ReadLine support!")
	} elsif ( $rl_package ) {
		$shell->info ("ReadLine support activated using module '$rl_package'");
	}

	$shell->loop;

	print "\n" if $opts_href->{e};

	$shell->error_summary if not $shell->{abort_mode} and
	                         $print_error_summary;

	exit $shell->has_errors;
}

sub perl_rl_completion {
	my ($input) = @_;
	return $input." ";
}

sub ask {
	my %par = @_;
	
	my $label     = $par{label};
	my $invisible = $par{invisible};
	
	if ( $invisible ) {
		eval "use Term::ReadKey";
		ReadMode(2);
	}
	
	print "$label: ";
	my $input = <STDIN>;
	chomp $input;
	
	if ( $invisible ) {
		ReadMode(0);
		print "\n";
	}
	
	return $input;
}

{
	my $file_not_writable_already_warned;
	sub add_line_to_history {
		my ($line) = @_;
		my $home_dir = home_dir();
		return if not $home_dir;
		
		my $history_file = "$home_dir/.dbshell_history";
		if ( not open (FH, ">> $history_file") ) {
			print STDERR "Warning: Can't write '$history_file'\n"
				if not $file_not_writable_already_warned;
			$file_not_writable_already_warned = 1;
			return;
		}
		
		print FH $line, "\n";
		close FH;
	}

	sub initialize_history {
		my ($term, $size) = @_;

		$size ||= 100;

		my $home_dir = home_dir();
		return if not $home_dir;

		my $history_file = "$home_dir/.dbshell_history";
		open (FH, $history_file) or return;
		my @hist = <FH>;
		close FH;

		if ( @hist > $size) {
			@hist = splice(@hist, @hist-$size, $size);
			if ( open (FH, "> $history_file") ) {
				print FH @hist;
				close FH;
			} else {
				print STDERR "Warning: Can't write '$history_file'\n"
					if not $file_not_writable_already_warned;
				$file_not_writable_already_warned = 1;
			}
		}

		foreach ( @hist ) {
			chomp;
			$term->addhistory ($_);
		}
	}
}

sub home_dir {
	return $ENV{HOME}||$ENV{USERPROFILE};
}

package MyCGI;

sub param {
	my $self = shift;
	return $self->{$_[0]};
}