|
rtfm / SQL / dbishell / src/DBIShell.pm
|
|
package DBIShell; # dbishell: A generic database shell based on the Perl DBI layer # Copyright (C) 2000 Vivek Dasmohapatra (vivek@etla.org) # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. use integer; #::use lib '<%LDIR%>'; use 5.004; use strict; use Fcntl qw(:DEFAULT :flock); use DBIShell::UTIL qw(:DEFAULT :pwent :readmode :stat :context unslash); use DBIShell::Help; use DBIShell::Term_CTL (); use Exporter (); use Getopt::Long (); use DBIShell::Readline (); use DBIShell::Fixup; use IO::Seekable; use vars qw($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS @ISA); use vars qw(@const *FH *SFH *PAGER $CUR_EOL); use subs qw|connect readline($;$)|; use constant WINDOWING => (($ENV{TERM} =~ /xterm|rxvt/i) && -t(*STDERR)); use constant TMPDIR => 'draft'; use constant HISTORY => 'history'; use constant RCFILE => 'rc'; use constant HOOK_ANTE => 0; use constant HOOK_POST => 1; use constant DBIOPT_NAME => 0; use constant DBIOPT_PRMT => 1; use constant DBIOPT_MODE => 2; use constant DBI_OPT_SPEC => ( [ driver => "DBI Driver: (eg Oracle): ", READMODE_NORMAL ], [ dsn => "DSN String: (see dbishell -h): ", READMODE_NORMAL ], [ user => "User Name: ", READMODE_NORMAL ], [ pass => "Password: ", READMODE_NOECHO ], ); use constant GETOPT_OPTS => qw(auto_abbrev no_getopt_compat require_order bundling); use constant GETOPT_SPEC => ('driver=s' , # DBIShell driver 'dsn:s', # dsn [The bit after 'dbi:<DRIVER>:' ] 'user|u:s' , # database user name 'pass|passwd|password|p:s', # database password 'noconn|x' , # don't connect to start with [broken?] 'help|h' , # print the help and exit 'shell-driver=s', # DBIShell driver to use if using driver=Proxy # or driver=ODBC 'intp|interpolate|interpolation!', 'escp|escape|escaping!', 'dotdir=s' ); SEQUENCE: { my $seqno = 0; sub seqno () { $seqno++ } } DBIShell::Fixup::patch( 'Getopt::Long' ); Getopt::Long::Configure( GETOPT_OPTS ); @const = qw( DBIOPT_NAME DBIOPT_PRMT DBIOPT_MODE DBI_OPT_SPEC GETOPT_OPTS GETOPT_SPEC ); $VERSION = 0.01_59; @EXPORT = (); @EXPORT_OK = (@const); %EXPORT_TAGS = (const => \ @const); @ISA = qw(Exporter); sub error ($) { $_[0]->{ERROR} } sub errno ($) { $_[0]->{ERRNO} } sub readline ($;$) { my $sh = shift(@_); my $line = $sh->{TERM}->readline($_[0]); foreach my $k (keys(%{$sh->{SPOOL}{IN}})) { my $fh = $sh->{SPOOL}{IN}{$k}; print($fh $line,$/); } #warn("returning \$line\n"); return $line; } sub outputf ($$;@) { my $sh = shift(@_); my $ct = shift(@_); # unused context parameter printf({ $sh->{OUTFH} } @_); foreach my $k (keys(%{$sh->{SPOOL}{OUT}})) { my $fh = $sh->{SPOOL}{OUT}{$k}; printf($fh @_); } } sub errputf ($$;@) { my $sh = shift(@_); my $ct = shift(@_); # unused context parameter printf({ $sh->{ERRFH} } @_); foreach my $k (keys(%{$sh->{SPOOL}{ERR}})) { my $fh = $sh->{SPOOL}{ERR}{$k}; printf($fh @_); } } # sub _strip_eol_from_history ($) # { # my $sh = $_[0]; # my $ceol = $sh->getvar( EOL => '/' ); # my $trlo = $sh->{TERM}; # # my @hist = $trlo->get_history(); # # if( $ceol ne '/' ) # { # foreach ( @hist ) # { # my $ep = -1; # s/\s*$//; # $ep = rindex( $_, $CUR_EOL ); # if( ($ep > -1) && (substr($_, -length($CUR_EOL)) eq $CUR_EOL) ) # { # substr( $_, $ep, length($CUR_EOL), '' ); # } # } # } # else # { # foreach ( @hist ) # { # s|^/\s*$||; # s|([^*\\])/\s*$|$1|; # } # } # # $trlo->set_history( @hist ); # } # sub _setv_eol_hook_ante ($$$) # { # my $sh = $_[0]; # my $name = $_[1]; # my $neol = $_[2]; # # $sh->_strip_eol_from_history(); # } sub _setv_eol_hook_post ($$$) { my $sh = $_[0]; my $name = $_[1]; my $ceol = $_[2]; $CUR_EOL = defined($ceol) ? $ceol : DEOL; } sub _obj_struct (;$) { return { TMPQ => 0, # skip the next output? VAR => { '^parameters' => [], '^param_names' => [], '^param_io' => [] }, # SQL parameter stack ERRNO => 0, # errno ERROR => '', # errstr (cf. strerror) OPT => { intp => 1, escp => 1 }, # getopt storage ACMT => [], # command line accumulator TERM => undef(), # readline object; OUTFH => undef(), ERRFH => undef(), INFH => undef(), SPOOL => { #\ OUT => {}, # | ERR => {}, # |) i/o/e stream map for spool functionality IN => {} # | }, #/ ENGINE => undef(), # the thing that actually knows about the DB PAGER => 0, # is the pager active right now? CMDBUF => [], CMDLIM => 100, IN_SCRIPT => 0, # number of calls to exec_script in progress ADDHISTORY => undef(), # function to call to get lines into history LAST_QUERY => undef(), DFLT_SIGPIPE => $SIG{PIPE}, LAST_TITLE => undef(), ORIG_TITLE => undef(), ORIG_ITITLE => undef(), SETV_HOOKS => {}, QUIT => 0, }; } sub new ($) { my $sh; my $package = ref($_[0]) ? ref(shift(@_)) : $_[0]; $sh = _obj_struct(); $sh->{TERM} = DBIShell::Readline->new('dbishell'); # readline object; $sh->{OUTFH} = $sh->{TERM}->OUT() || *STDOUT; $sh->{ERRFH} = *STDERR; $sh->{INFH} = $sh->{TERM}->IN() || *STDIN; $sh->{SETV_HOOKS}{EOL} ||= []; #$sh->{SETV_HOOKS}{EOL}[ HOOK_ANTE ] = \&_setv_eol_hook_ante; $sh->{SETV_HOOKS}{EOL}[ HOOK_POST ] = \&_setv_eol_hook_post; # unbuffer the output handles: select((select($sh->{OUTFH}),$|=1)[0]); select((select($sh->{ERRFH}),$|=1)[0]); bless($sh, $package); $sh->set_dotdir(); # set up some of the more important variables so that # tab completion can find them. $sh->setvar("CASE_SENSITIVE=0"); $sh->setvar("PRESCAN_ROWS=0"); $sh->setvar("TRUNCATE_COLUMN_NAMES=0"); $sh->setvar("PAGING=1"); $sh->setvar("PAGE_SIZE=0"); $sh->setvar("FIELD_SEPARATOR=|"); # some other initialisation: $sh->init_rl_package(); return $sh; } sub init_rl_package ($) { my $sh = shift(@_); # we can capture multiline commands neatly with GRL, as well as # filtering out duplicate queries - but in order to do so, we should # turn ordinary auto-history off: if ( defined($sh->{TERM}->auto_history(FALSE)) ) { $sh->{ADDHISTORY} = sub { my $q; ( $q = $_[0] ) =~ s([\r\n]+)[\\n]g; if ( $q ne $sh->{LAST_QUERY} ) { $sh->{TERM}->add_history( $sh->{LAST_QUERY} = $q ); } }; } else { $sh->{ADDHISTORY} = undef(); } eval { my $fn; my $fcw; my $rl = $sh->{TERM}; # set up the tab completion function: # WARNING: we have to use a closure here, because readline # uses callbacks to let you customise tab completion, # and we need to pass in extra state: $fn = sub { $sh->complete($_[0],$_[1],$_[2]) }; $rl->set_completion_function( $fn ); $rl->set_signal_handlers(); $fcw = sprintf('FUSSY_COMPLETION_WORKAROUND=%d', $rl->fussy_completion()); $sh->setvar( $fcw ); }; if( $@ ) { $sh->errputf(CONTEXT_NIL, '%s', $@) } } sub add_command ($$) { my $sh = $_[0]; my $cmd = $_[1]; # edit itself shouldn't make it into the command buffer, as it is a meta # command. $cmd =~ /^\s*edit\b/ && return; my $buf = $sh->{CMDBUF}; my $lim = $sh->{CMDLIM}; if(@{$buf} >= $lim) { shift(@{$buf}); push(@{$buf}, $cmd) } else { push(@{$buf}, $cmd) } } sub command ($$) { my $sh = $_[0]; my $n = abs(int($_[1])); my $buf = $sh->{CMDBUF}; my $pos = $#{$buf} - $n; if($pos >= 0){ return $buf->[$pos] } else { return "/* command [$pos] empty */\n" } } sub paging ($$) { my $sh = $_[0]; my $rows = int(($_[1] > 0) ? $_[1] : 0); my $size = $sh->getvar('PAGE_SIZE') || (DBIShell::UTIL::term_size())[1]; # warn("PAGE_SIZE = $size\n"); # warn("DATA_LENGTH = $rows\n"); return ($sh->getvar('PAGING', 0) && (($rows > $size) || !$rows)) ? 1 : 0; } #sub outfh ($) { (@_ > 1) ? $_[0]->{OUTFH} = $_[1] : $_[0]->{OUTFH} } #sub errfh ($) { (@_ > 1) ? $_[0]->{ERRFH} = $_[1] : $_[0]->{ERRFH} } #sub infh ($) { (@_ > 1) ? $_[0]->{INFH} = $_[1] : $_[0]->{INFH} } sub start_pager ($$) { local(*PAGER); my $sh = $_[0]; my $n = $_[1]; $sh->paging($n) || return; #if paging is not required, do nothing $sh->{PAGER} && return; #if paging has been started, do nothing. my $pager = $sh->getvar('PAGER',undef()) || $ENV{PAGER} || 'less -S'; if(open(*PAGER, "|$pager")) { $sh->{DFLT_SIGPIPE} = $SIG{PIPE}; $SIG{PIPE} = 'IGNORE'; select((select(*PAGER),$|=1)[0]); $sh->{PAGER} = 1; $sh->{OUTFH} = *PAGER; $sh->{ERRFH} = *PAGER; } } sub stop_pager ($) { my $sh = $_[0]; if($sh->{PAGER}) { local(*PAGER); *PAGER = $sh->{OUTFH}; $sh->{OUTFH} = $sh->{TERM}->OUT() || *STDOUT; $sh->{PAGER} = 0; $sh->{ERRFH} = *STDERR; close(*PAGER); $SIG{PIPE} = $sh->{DFLT_SIGPIPE}; } } sub set_dotdir ($) { my $sh = shift(@_); my $opt = $sh->{OPT}; my $tmpdir; unless($opt->{dotdir}) { my $uid = $<; $opt->{dotdir} = join('/',(getpwuid($uid))[PWENT_DIR],'.dbishell'); } $tmpdir = join('/',$opt->{dotdir}, TMPDIR); unless(-d($tmpdir)) { unless(-d($opt->{dotdir})) { unless(mkdir($opt->{dotdir}, 0700)) { $sh->{ERRNO} = $!; $sh->{ERROR} = "Failed to create dotdir $opt->{dotdir}"; return undef(); } } unless(mkdir($tmpdir, 0700)) { $sh->{ERRNO} = $!; $sh->{ERROR} = "Failed to create tmpdir $tmpdir"; return undef(); } } } # read in the command line options: sub getopts ($;@) { my $sh = shift(@_); grep { s/^--?no-/--no/ } @ARGV; Getopt::Long::GetOptions($sh->{OPT}, GETOPT_SPEC); } sub disconnect ($) { my $sh = shift(@_); if($sh->{ENGINE}) { $sh->{ENGINE}->disconnect() } $sh->set_dot_history(); # flush history to a dot file $sh->{ENGINE} = undef(); # kill any old objects $sh->set_dbi_nullp_parameters(); # zap the connect parameters $sh->connect() # connect to the NullP driver || $sh->errputf(CONTEXT_NIL, "%s\n",$sh->error); } sub reconnect ($;@) { my $sh = shift(@_); my @new_conn_args = @_; $sh->set_dot_history(); if($sh->{ENGINE}) { $sh->{ENGINE}->disconnect() } $sh->{ENGINE} = undef(); foreach my $thing (DBI_OPT_SPEC) { my $key = $thing->[DBIOPT_NAME]; $sh->{OPT}{$key} = shift(@new_conn_args); } $sh->connect() || $sh->errputf(CONTEXT_NIL, "%s\n",$sh->error); } sub spool ($$) { my @stream; my $sh= shift(@_); my($stream,$target,$on) = split(/\s+/,$_[0]); #warn('($stream,$target,$on) == ',"($stream,$target,$on)\n"); unless(length($on)) { $on = 1 } else { $on = IS_TRUE($on) } #warn('($stream,$target,$on) == ',"($stream,$target,$on)\n"); $stream =~ /\b(?:i|(?:std)?in|input)\b/i && push(@stream, 'IN'); $stream =~ /\b(?:o|(?:std)?out|output)\b/i && push(@stream, 'OUT'); $stream =~ /\b(?:e|(?:std)?err|error)\b/i && push(@stream, 'ERR'); eval { unless (@stream) { die("spool error: no valid streams specified [$stream]\n"); } if ($on) { my $sfh = local(*SFH); sysopen($sfh, $target, O_WRONLY|O_CREAT|O_APPEND, 0640) || die("spool error on sysopen: $!\n"); select((select($sfh),$|=1)[0]); foreach my $S (@stream) { $sh->{SPOOL}{$S}{$target} ||= $sfh } } else { foreach my $S (@stream) { if (lc($target) eq 'off') { %{$sh->{SPOOL}{$S}} = () } elsif (exists($sh->{SPOOL}{$S}{$target})) { delete($sh->{SPOOL}{$S}{$target}); $sh->errputf(CONTEXT_NIL, "Removed %s from %s spool\n", $target, $S ); } } } }; if($@) { $sh->{ERRNO} = -1; chomp($sh->{ERROR} = $@); return 0; } return 1; } sub exec_command ($) { my $i; my $q; my $eol; my $sh = shift(@_); # trim eol character(s) from the accumulator: if( defined( $eol = $sh->getvar('EOL', undef()) ) ) { my $l = length( $eol ); $sh->{ACMT}[ $#{$sh->{ACMT}} ] =~ s/\s*$//; if( substr( $sh->{ACMT}[ $#{$sh->{ACMT}} ], -$l ) eq $eol ) { substr( $sh->{ACMT}[ $#{$sh->{ACMT}} ], -$l, $l, '' ); } } else { $sh->{ACMT}[ $#{$sh->{ACMT}} ] =~ s/\/\s*$//; } #construct+store query $q = join($/, @{ $sh->{ACMT} }); #empty accumulator @{ $sh->{ACMT} } = (); # nothing to see here, move along: $q =~ /\S/ || return 1; # store the command in the edit stack: $sh->add_command($q); # if we are NOT in a script, and we have a history filter, # put the filtered command into the history. # Note that autohistory should be disabled, or at least sidestepped # if the filter is in effect (currently we use MinLine to do this). # (always put the default EOL seq into the history) if ( !$sh->{IN_SCRIPT} && $sh->{ADDHISTORY} ) { $sh->{ADDHISTORY}->( $q . DEOL ); } # special case: if($q =~ /^\s*quit\b/si) { return $sh->{QUIT} = 1 } if ( !defined($i) ) { $i = $sh->pre_interpret($q) } if ( !defined($i) ) { $i = $sh->interpret($q) } if ( !defined($i) ) { $i = $sh->post_interpret($q) } # if we asked for temporary silence, unset the silence flag, # then bail out of this iteration if ($sh->{TMPQ}) { $sh->{TMPQ} = 0; return $i } if ( $i ) { $sh->errputf(CONTEXT_NIL, "\n%s : success\n", $q) } else { $sh->errputf(CONTEXT_NIL, "\n%s : error: <%s>\n", $q, $sh->error()); } return $i; } # remember to increment the in script counter on entry, # and decrement it at each exit point: sub exec_script ($$) { local(*SFH); my $sh = shift(@_); my $file = shift(@_); my $nocl = 0; my $sfh; if(ref(\$file) eq 'GLOB') { eval { stat($file) || die("$!\n"); -r(_) || die("Not Readable\n"); $nocl = 1; $sfh = $file; }; } else { eval { stat($file) || die("$!\n"); -r(_) || die("Not Readable\n"); -p(_) || -T(_) || die("Not a pipe or text file\n"); sysopen(*SFH, $file, O_RDONLY) || die("open failed: $!\n"); $nocl = 0; $sfh = *SFH; }; } if ($@) { chomp($sh->{ERROR} = $@); return 0 } if($sfh) { my $I = 1; my($eol); my($q,$i); $sh->{IN_SCRIPT}++; SCRIPT_LINE: while(defined($q = CORE::readline($sfh))) { chomp($q); # ignore leading blank lines only: (scalar(@{$sh->{ACMT}}) == 0) && ($q !~ /\S/) && next SCRIPT_LINE; # store the line push(@{$sh->{ACMT}}, $q); # did we terminate the command? $sh->is_terminated($q) || next SCRIPT_LINE; $i = $sh->exec_command(); if ( $sh->{QUIT} ) { $sh->{IN_SCRIPT}--; return $sh->{QUIT} } if ( $sh->{TMPQ} ) { $sh->{TMPQ} = 0; next SCRIPT_LINE } $I = $i ? 1 : 0; } # allow the last line to be unterminated: if( @{$sh->{ACMT}} ) { $i = $sh->exec_command(); if ( $sh->{QUIT} ) { $sh->{IN_SCRIPT}--; return $sh->{QUIT} } if ( $sh->{TMPQ} ) { $sh->{TMPQ} = 0; next SCRIPT_LINE } $I = $i ? 1 : 0; } $nocl || close($sfh); $sh->{IN_SCRIPT}--; return $I; } } sub read_rc_script ($) { local(*FH); my $sh = shift(@_); my $dotdir = $sh->{OPT}{dotdir}; my $rcfile = $sh->dotfile(RCFILE); unless(-f($rcfile)) { my $sd_cache = $sh->{OPT}{'shell-driver'}; $sh->{OPT}{'shell-driver'} = 'DEFAULT'; $rcfile = $sh->dotfile(RCFILE); $sh->{OPT}{'shell-driver'} = $sd_cache; } if (-f($rcfile)) { if(sysopen(*FH, $rcfile, O_RDONLY)) { eval { my $uid; my $perm; my @rcstat; @rcstat = stat(*FH); $perm = $rcstat[STAT_MODE] & STAT_PERM_MASK; $uid = $rcstat[STAT_UID]; # uid must be correct. ($uid != $> ) && die("rc file not owned by current UID\n"); # g+w and o+rw perms not allowed ($perm & 0026) && die("rc file has lax permissions\n"); }; if($@) { chomp($sh->{ERROR} = $@); return 0; } return $sh->exec_script(*FH); } else { $sh->{ERROR} = "$!"; $sh->{ERRNO} = int($!); return 0; } } else { return 1; } } sub set_show_var ($$) { my $sh = shift(@_); my $d = shift(@_); my($k, $rv) = $sh->setvar($d); if (!$@ && $k) { $sh->outputf(CONTEXT_NIL, "%s=%s\n", $k, $rv); return $sh->{TMPQ} = 1; } return 0; } sub setvar ($$$) { use constant DBI_ATTR_EEK => <<DbiAttrEek; You can't just waltz in and delete dbi handle attributes: All sorts of horrible things could happen! FNORD! DbiAttrEek use constant GRL_ATTR_EEK => <<GRLEEK; Deletion of GRL Attributes disallowed. GRLEEK my $k; my $rv; my $hook; my $sh = shift(@_); my $d = shift(@_); my $nv = shift(@_); my $v = $sh->{VAR}; # this method gets called during the instantiation of a # new DBIShell::dr::DEFAULT object, so there may not be # a $sh->{ENGINE} here yet: my $h = eval { $sh->{ENGINE}->dbh() }; if ($d =~ /^(?:env:|grl:|dbi:)?\w+\s*=(.*)/ix) { $nv = $sh->interpolate($1, undef()); #warn("interpolate($1,undef)\n"); } if( $hook = $sh->{SETV_HOOKS}{ $k }[ HOOK_ANTE ] ) { $hook->( $sh, $k, $nv ); } eval { local($_) = $d; # environment variables: if (/^!env:(\w+) /ix) { $k = $1; delete($ENV{$1}) } elsif (/^ env:(\w+)\s*=.*/ix) { $k = $1; $rv = $ENV{$1} = $nv } elsif (/^ env:(\w+) /ix) { $k = $1; $rv = $ENV{$1} } # dbi attributes: elsif (/^!dbi:(\w+) /ix) { $k = $1; die(DBI_ATTR_EEK) } elsif (/^ dbi:(\w+)\s*=.*/ix) { $k = $1; $rv = $h->{$1} = $nv } elsif (/^ dbi:(\w+) /ix) { $k = $1; $rv = $h->{$1} } # grl variables: elsif (/^!grl:(\w+) /ix) { $k = $1; die(GRL_ATTR_EEK) } elsif (/^grl: /ix) { my $g = $sh->{TERM}->attr(); if (/^grl:(\w+)(\s*=.*)?/ix) { if ($2) { $k = $1; $rv = $g->{$1} = $nv } else { $k = $1; $rv = $g->{$1} } } } # dbishell variables: elsif (/^!(\w+) /x) { $k = $1; delete($v->{$1}) } elsif (/^ (\w+)\s*=.*/x) { $k = $1; $rv = $v->{$1} = $nv } elsif (/^ (\w+) /x) { $k = $1; $rv = $v->{$1}; } else { $sh->{ERRNO} = -1; $sh->{ERROR} = "Unable to find a valid variable name"; return $sh->{TMPQ} = 0; } }; if ($@) { $sh->{ERROR} = sprintf("Error %s while setting %s", $@, $k) } if( $hook = $sh->{SETV_HOOKS}{ $k }[ HOOK_POST ] ) { $hook->( $sh, $k, $rv ); } return ($k,$rv); } sub subshell ($$) { my $sh = $_[0]; my $cmd = $_[1]; $cmd = ($cmd =~ /\S/) ? $cmd : -x($ENV{SHELL}) ? $ENV{SHELL} : '/bin/sh'; $sh->errputf(CONTEXT_NIL, "system('%s')\n",$cmd); system($cmd); unless($? >> 8) { $sh->{ERRNO} = $?; $sh->{ERROR} = "Subshell command failed"; } return !($? >> 8); } sub getvar ($$$) { my $sh = shift(@_); local($_) = shift(@_); my $nrep = shift(@_); my $v = undef(); if (/^env:(\w+)/i){ $v = defined($ENV{$1}) ? $ENV{$1} : $nrep } elsif(/^dbi:(\w+)/i){ $v = eval { $sh->{ENGINE}->dbh()->{$1} } || $nrep } elsif(/^grl:(\w+)/i) { my $g = $sh->{TERM}->attr(); $v = defined($g->{$1}) ? $g->{$1} : $nrep; } elsif(/^(\w+)$/) { $v = defined($sh->{VAR}{$1})? $sh->{VAR}{$1}: $nrep } else { $sh->{ERRNO} = -1; $sh->{ERROR} = "Unable to find valid variable name"; $sh->errputf(CONTEXT_NIL, "%s: %s\n", $sh->{ERROR}, $_); } if ($@) { $sh->errputf(CONTEXT_NIL, "%s: %s\n", ($sh->{ERROR} = $@), $_); } return $v; } sub getvar_ref ($$$) { my $sh = shift(@_); local($_) = shift(@_); my $nrep = shift(@_); my $vs = $sh->{VAR}; my $v = undef(); if(/^dbi:/i) { warn(<<EEKWarning); Sorry, I can't let you bind DBI attributes as parameters, all sorts of hideous things might happen: Great Cthulhu might rise. The Ghost of Tim Bunce(tm) might mystically appear and assault me with freeze dried socks. Who knows? EEKWarning return (); } if(/^grl:/i) { warn(<<EEKWarning); Sorry - disallowing binding of GRL attributes as parameters. Nih! EEKWarning return (); } # make sure we force them into existence if they don't exist: if (/^env:(\w+)/){ $v= exists($ENV{$1})? \$ENV{$1} :\($ENV{$1}=undef()) } elsif(/^(\w+)$/) { $v= exists($$vs{$1})? \$$vs{$1} :\($$vs{$1}=undef()) } else { $sh->{ERRNO} = -1; $sh->{ERROR} = "Unable to find valid variable name"; $sh->errputf(CONTEXT_NIL, "%s: %s\n", $sh->{ERROR}, $_); } return $v; } sub put_parameter ($$) { my $sh = shift(@_); my $pa = shift(@_); my $io = shift(@_); push(@{ $sh->{VAR}{'^parameters'} }, $sh->getvar_ref($pa)); push(@{ $sh->{VAR}{'^param_names'}}, $pa); push(@{ $sh->{VAR}{'^param_io'} }, $io); } sub clear_parameters ($) { @{ $_[0]->{VAR}{'^parameters'} } = (); @{ $_[0]->{VAR}{'^param_names'} } = (); @{ $_[0]->{VAR}{'^param_io'} } = (); } sub get_parameter ($) { $_[0]->{VAR}{'^parameters'}[$_[1]] } sub get_parameter_name ($) { $_[0]->{VAR}{'^param_names'}[$_[1]] } sub get_parameter_io ($) { $_[0]->{VAR}{'^param_io'}[$_[1]] } sub interpolation ($$) { $_[0]->{OPT}{intp} = $_[1]; $_[0]->{TMPQ} = 1 } sub escaping ($$) { $_[0]->{OPT}{escp} = $_[1]; $_[0]->{TMPQ} = 1 } sub show_license ($) { if($ENV{PAGER} && open(PAGER, "|$ENV{PAGER}")) { syswrite(PAGER, DBIShell::UTIL::GPL_LICENSE, length(DBIShell::UTIL::GPL_LICENSE) ); close(PAGER); } else { $_[0]->outputf(CONTEXT_NIL, '%s',DBIShell::UTIL::GPL_LICENSE); } 1; } sub pre_interpret ($$) { # this method is in charge of catching the stuff we don't want the # DB specific engine to see [ever] # quit, spool etc spring to mind: my $sh = shift(@_); local($_) = shift(@_); #warn("PRE: $_\n"); if ($_) { m/^\s*_dump\((.*)\)/si && return $sh->_dump($1); m/^\s*cd\b\s*(.*)/si && return chdir($1); m/^\s*edit(?:\s+[+-]?(\d+))?/si && return $sh->edit(int($1)); m/^\s*spool\s+(.*)/si && return $sh->spool($1); m/^\s*disconnect/si && return $sh->disconnect(); m/^!(.*)/s && return $sh->subshell($1); m/^\s*escap(?:e|ing)\s+on/si && return $sh->escaping(1); m/^\s*escap(?:e|ing)\s+off/si && return $sh->escaping(0); m/^\s*interpolat(?:e|ion)\s+on/si && return $sh->interpolation(1); m/^\s*interpolat(?:e|ion)\s+off/si && return $sh->interpolation(0); m/^\s*read\s+(.*)/si && return $sh->exec_script($1); m/^\$(.*)/ && return $sh->set_show_var($1); m/^\s*license/ && return $sh->show_license(); m/^\s*connect(?:\s+(.*))?/si && return $sh->reconnect(split(/\s+/,$1)); return undef(); } return 1; } sub _dump ($$) { my $obj; my $txt; my $sh = $_[0]; my @key = map { s/^\s+|\s+$//g; $_ } split(/,/,$_[1]); my $pkg = DBIShell::UTIL::dynaload('Data::Dumper'); eval { $obj = $sh; foreach (@key) { $obj = exists($obj->{$_}) ? $obj->{$_} : die("Member $_ not found\n"); } }; if ($@) { $sh->{ERROR} = $@; return 0 } if($pkg) { my $lines; my $ref = ref($sh); $txt = $pkg->Dump([$obj],[join('->',$ref,@key)]); $lines = ($txt =~ tr/\n/\n/); $sh->start_pager($lines); $sh->outputf(CONTEXT_NIL, "%s\n", $txt); $sh->stop_pager(); } return 1; } sub interpret ($$) { my $sh = shift(@_); my $q = $sh->interpolate( shift(@_) ); my $i = $sh->{ENGINE}->interpret($sh, $q); if(!$i) { $sh->{ERRNO} = $sh->{ENGINE}->errno(); $sh->{ERROR} = $sh->{ENGINE}->error(); } return $i; } sub post_interpret ($$) { # if the DB specifc engine failed to interpret this, have another shot: # mostly a catcher for when the db engine doesn't implement something. my $sh = shift(@_); local($_) = shift(@_); # we don't actually catch any thing, but if we did, # here is where we would catch it return 1; } sub edit ($$) { my $retval; my $sh = $_[0]; my $n = abs(int($_[1])); my $cmd = $sh->command($n); my $editor = $sh->getvar('EDITOR','') || $ENV{EDITOR} || 'ed'; my $file = $sh->tmpfile(sprintf('%s.%s', $sh->{OPT}{'shell-driver'} || $sh->{OPT}{driver}, 'edit' ) ); eval { local(*FH); # race condition? I can't see how not to make it a race cond, though: sysopen(*FH, $file, O_WRONLY|O_CREAT|O_EXCL, 0600) || die("a:$!\n"); sysseek(*FH, 0, SEEK_SET) || die("d:$!\n"); defined(syswrite(*FH, $cmd, length($cmd))) || die("b:$!\n"); close(*FH); system("$editor $file") && die("'$editor $file'",$?>>8,":",$?&127,"\n"); sysopen(*FH, $file, O_RDONLY, 0600) || die("a:$!\n"); sysseek(*FH, 0, SEEK_SET) || die("d:$!\n"); defined(sysread(*FH, $cmd, -s($file))) || die("e:$!\n"); sysseek(*FH, 0, SEEK_SET) || die("d:$!\n"); $sh->add_command($cmd); $retval = $sh->exec_script(*FH); close(*FH); unlink($file); unlink($file.'~'); #emacs, vim et al... }; if($@) { $sh->{ERRNO} = int($!); chomp($sh->{ERROR} = $@); return 0; } return $retval; } sub quit_cleanup ($) { my $sh = shift(@_); $sh->set_dot_history(); if($sh->{ENGINE}) { $sh->{ENGINE}->disconnect() } # zap the extra references: $sh->{TERM}->set_completion_function( undef() ); $sh->{ADDHISTORY} = undef(); if(WINDOWING) { my $title = length($sh->{ORIG_ITITLE}) ? $sh->{ORIG_TITLE} : join('', $ENV{TERM}, ':', ($ENV{USER}||$ENV{LOGNAME}||$ENV{USERNAME}), ($ENV{HOSTNAME} ? ('@',$ENV{HOSTNAME}) : '') ); my $ititle = length($sh->{ORIG_ITITLE}) ? $sh->{ORIG_ITITLE} : $title; DBIShell::Term_CTL::set_ititle($ititle); DBIShell::Term_CTL::set_title($title); } return 1; } sub is_terminated ($$) { my $sh = $_[0]; local($_) = $_[1]; my $e = $sh->getvar('EOL',undef()); my $one = (@{$sh->{ACMT}} == 1); # / is special, since it can crop up in comments, which we would like # to ignore. So trap it here, in case the user set EOL to '/' chomp($e); ($e eq '/') && ($e = undef()); # Conditions for command completeness: docs moved to pod section (defined($e) ? (m(^$e\s*$) || m([^\\]$e\s*$)) : (m(^/\s*$) || m([^*\\]/\s*$))) || ($one && (/^[\!\$]/ || /^\s*show\b /xi || /^\s*describe\b /xi || /^\s*cd\b /xi || /^\s*read\b /xi || /^\s*help\b /xi || /^\s*edit\b /xi || /^\s*spool\b /xi || /^\s*license\b /xi || /^\s*(?:dis)?connect\b /xi || /^\s*interpolat(?:e|ion)\b/xi || /^\s*escap(?:e|ing)\b /xi || /^\s*quit\b /xi ) ); } sub parse_loop ($) { my($eol); my($q, $i); my $sh = shift(@_); local( $CUR_EOL ); $CUR_EOL = $sh->getvar( EOL => '/' ); if(WINDOWING) { $sh->{LAST_TITLE} = $sh->{ENGINE}->title(); $sh->{ORIG_TITLE} = DBIShell::Term_CTL::set_ititle('dbishell'); $sh->{ORIG_ITITLE} = DBIShell::Term_CTL::set_title($sh->{LAST_TITLE}); } CMD_LINE: while(defined($q = $sh->readline($sh->{ENGINE}->prompt))) { if(WINDOWING) { my $title = $sh->{ENGINE}->title(); #warn("$title/",$sh->{LAST_TITLE},"\n"); if($title ne $sh->{LAST_TITLE}) { DBIShell::Term_CTL::set_title($title); $sh->{LAST_TITLE} = $title; } } # I can't see how this ever worked without this next line... # Quantum bugs. I hate them. $i = undef(); # return value from dispatched call # user hit ^D, or stdin otherwise cut off: defined($q) || last CMD_LINE; # ignore leading blank lines only: (scalar(@{$sh->{ACMT}}) == 0) && ($q !~ /\S/) && next CMD_LINE; # store the line push(@{$sh->{ACMT}}, $q); # did we terminate the command? # ($q =~ /\/$/) || next CMD_LINE #naive $sh->is_terminated($q) || next CMD_LINE; # Aha, we did terminate the command - Very well, make it so: $i = $sh->exec_command(); # abort! abort! if( $sh->{QUIT} ) { last CMD_LINE } } $sh->quit_cleanup(); return 1; } sub connect # did we remember to declare this in a 'use sub' ? { my $dbh; my $engine; my $sh = shift(@_); my $opt = $sh->{OPT}; if($opt->{help}) { $sh->errputf(CONTEXT_NIL, '%s', DBIShell::Help->help( undef() )); return undef(); } if ($sh->{OPT}{noconn}) { $sh->set_dbi_nullp_parameters() } else { $sh->get_dbi_parameters() } my $real_driver = $opt->{'shell-driver'} || $opt->{driver}; # jump through hoops to avoid doing a string eval: # this is a try/catch equivalent, not an evil eval. # see perldoc perlfunc, the section on 'eval BLOCK' # if there is a symbol table entry for our package, do not # try to reload it. $engine = join('::','DBIShell::dr',$real_driver); $engine = DBIShell::UTIL::dynaload($engine); $engine ||= DBIShell::UTIL::dynaload('DBIShell::dr::DEFAULT'); if ($engine) { $sh->errputf(CONTEXT_INFO, "Using %s engine\n",$engine) } else { $sh->{ERRNO} = int($!); $sh->{ERROR} = $@; return undef(); } # create a new engine object, or else go splat # don't actually replace the current engine object unless # we are sure we have connected to the database at the other end # the option must be the DBI driver we will be using # directly: eg 'Proxy' for DBI::Proxy - 'shell-driver' # controls which behaviours we'll get, but the engine must still use # a DBI driver of 'DBD::Proxy' [for example]. eval { $engine = $engine->new($opt->{driver},$sh) }; if($@ || !$engine) { $sh->{ERRNO} = int($!); $sh->{ERROR} = "Failed to initialise $engine: $@"; return undef(); } #warn("$engine\n"); unless($engine->connect($opt)) { $sh->{ERRNO} = $engine->errno(); $sh->{ERROR} = $engine->error(); return undef(); } $sh->{ENGINE} = $engine; $sh->get_dot_history(); $sh->read_rc_script(); return $sh->{ENGINE}->dbh(); } sub complete { $_[0]->{ENGINE}->complete( $_[0], $_[1], $_[2], $_[3] ) } sub get_dot_history ($) { my $fh = local(*FH); my $sh = shift(@_); my $hfile = $sh->dotfile(HISTORY); my $readl = $sh->{TERM}; # zero out the history anyway: $readl->set_history(); # don't actually care if the file is there yet or not: if(sysopen($fh, $hfile, O_RDONLY)) { my $line; flock($fh, LOCK_SH); while($line = CORE::readline($fh)) { chomp($line); $readl->add_history($line); } flock($fh, LOCK_UN); close($fh); } } sub set_dot_history ($) { my $fh = local(*FH); my $sh = shift(@_); my $hfile = $sh->dotfile(HISTORY); my $readl = $sh->{TERM}; #$sh->_strip_eol_from_history(); if(sysopen($fh, $hfile, O_WRONLY|O_CREAT, 0600)) { my $s = 0; flock($fh, LOCK_EX); seek($fh, 0, SEEK_SET); foreach my $line ($readl->get_history()) { $s += print($fh $line) ? length($line) : 0; $s += print($fh "\n") ? 1 : 0; } select((select($fh),$!=1)[0]); print($fh ""); truncate($fh, $s); flock($fh, LOCK_UN); close($fh); } } sub dotfile ($$) { my $sh = shift(@_); my $section = shift(@_); my $dotdir = $sh->{OPT}{dotdir}; my $dbi_drv = $sh->{OPT}{'shell-driver'} || $sh->{OPT}{driver}; return join('/', $dotdir, join('.', $dbi_drv, $section)); } sub tmpfile ($$) { my $sh = shift(@_); my $tag = shift(@_); my $dotdir = $sh->{OPT}{dotdir}; my $tmpfile = sprintf('%s.%3.3d-%5.5d-%9.9d',$tag,rand(1000),seqno(),time()); return join('/', $dotdir, TMPDIR, $tmpfile); } sub set_dbi_nullp_parameters ($) { my $sh = $_[0]; $sh->{OPT}{driver} = ''; $sh->{OPT}{dsn} = 'dbi:NullP'; $sh->{OPT}{user} = ''; $sh->{OPT}{pass} = ''; } # read in DBI parameters that weren't passsed in on the command line sub get_dbi_parameters ($) { my $sh = $_[0]; foreach my $s (DBI_OPT_SPEC) { unless(defined($sh->{OPT}{$s->[DBIOPT_NAME]})) { $sh->{OPT}{$s->[DBIOPT_NAME]} = DBIShell::UTIL::get_param($s->[DBIOPT_PRMT], $s->[DBIOPT_MODE]); } } } sub variables ($) { my $sh = shift(@_); my @env = map { join(':','env',$_) } keys(%ENV); my @dbi = map { join(':','dbi',$_) } DBIShell::UTIL::DBI_ATTR_NAMES; my @grl = map { join(':','grl',$_) } keys( %{$sh->{TERM}->attr()} ); my @var = keys( %{$sh->{VAR}} ); return (@env, @dbi, @var, @grl); } sub interpolate ($$;$) { my $sh = shift(@_); my $thing = shift(@_); my $nullrep = @_ ? shift(@_) : 'NULL'; my $umap = eval { $sh->{ENGINE}->UNIVERSAL::can('unslash_map') ? $sh->{ENGINE}->unslash_map : undef(); }; #warn("interpolating '$thing'\n"); if($sh->{OPT}{intp}) { #warn("Doing variable interpolation\n"); #$thing =~ /\$([\w:]+)/ && warn("variable \$'$2' found\n"); $thing =~ s/([^\\])\$((?:[\w]+:)?\w+)/join('',$1,$sh->getvar($2,"\x00"))/egs; $thing =~ s/^\$((?:[\w]+:)?\w+)/$sh->getvar($1,"\x00")/egs; # make sure that if we _really_ meant we wanted undef, we get it: if($thing =~ /^\x00\s*/) { $thing = $nullrep } else { $thing =~ s/['"]?\x00["']?/$nullrep/g } } #warn("escaping '$thing'\n"); # extra paranoia... if($sh->{OPT}{escp} && defined($thing) && ($thing ne $nullrep)) { $thing =~ s/\\(.)/unslash($umap,$1)/ges; } #warn("returning '$thing'\n"); return $thing; } __END__ # TLF: Nikola Tesla died for you.... =pod =head1 NAME DBIShell - Core of the dbishell program =head1 SYNOPSIS use DBIShell; =head1 DESCRIPTION This is the core of dbishell: It handles everything not directly database related, like parsing parameters, reading config files, loading drivers and managing the user interface. It provide mpst of the actual funcionality used by the F<dbishell> script =head1 DATA MEMBERS Data members are documented for developers of DBIShell.pm only: they should not be directly accessed from outside this package. Don't make me come over there =head2 TMPQ Gets set if the next bit of output should be skipped by dbishell. Used internally by a couple of things to skip some unneccesary verbiage. Anything that cheks this variable should promptly set it back to zero =head2 VAR Hash reference that provides storage for dbishell variables. There are 3 special keys used for DBI in/out parameter handling, but they have 'illegal' names, so we should never see a collision =head2 ERRNO Last system error number generated [like ENOENT or EINVAL, for example] =head2 ERROR Last error message generated by dbishell =head2 OPT Storage for the command line options =head2 ACMT Unfinished multiline commands are accumulated here before being interpreted =head2 TERM This is where the readline object is stored =head2 OUTFH Output file handle [probably *STDOUT] =head2 ERRFH Error file handle [probably *STDERR] =head2 INFH Input file handle [probably *STDIN] =head2 SPOOL A Multilevel hash: The first level has 3 keys: IN, OUT and ERR Each of these keys corresponds to a hash ref which contains filename => filehandle pairs for all the active output spools =head2 ENGINE Contains the DBIShell::dr::<FOO> object [the engine] =head2 PAGER Boolean. Whether the pager is currently active or not =head2 CMDBUF Buffer of multiline commands. [as opposed to the readline history, which is a list of individual lines as opposed to full commands] =head2 CMDLIM Maximum size of the CMDBUF. Should probably be more configurable than it is =head2 DFLT_SIGPIPE Default SIGPIPE handler =head2 LAST_TITLE The last window title that was set. [only in xterm/rxvt right now] =head2 ORIG_TITLE Original title of window =head2 ORIG_ITITLE Original icon title of window =head1 METHODS =head2 $sh->error() Return the last error string that was generated =head2 $sh->errno() Return last system error number that cropped up =head2 $sh->readline([PROMPT]) Fetch a line from the user, logging/spooling it as required by the current state of the spooler =head2 $sh->outputf(CONTEXT, FORMAT, [ARGS...]) Write the data in ARGS out onto the output filehandle according to the printf style format FORMAT, logging the output as required by the state of the spooler. The CONTEXT argument is not used yeet, but is intended for use with the GTK front end, or other cases where we want to pass more information about the kind of output we are emitting. =head2 $sh->errputf(CONTEXT, FORMAT,[ARGS]) As above [L</$sh-E<gt>outputf(CONTEXT, FORMAT,[ARGS...])>] but for the error filehandle instead =head2 DBIShell->new() Create a new DBIShell object, and set it up to work with whatever readline functionality is available =head2 $sh->add_command(COMMAND) Add a complete command to the command stack thingy =head2 $sh->command(NUMBER) Fetch the command NUMBER back from the end of the command stack thingy =head2 $sh->paging(NLINES) Return boolean value. based on NLINES [number of lines], indicating whether the pager should be fired up. if the dbishell variable PAGE_SIZE is set, then use that as the trigger size, otherwise attempt to determine the page size in some other way. Will not return true if dbishell variable PAGING is not true =head2 $sh->start_pager(NLINES) If paging is required for NLINES of data [L</$sh-E<gt>paging(NLINES)>] then fire up the pager command in the dbishell variable PAGER [or, if there's nothing there, then try $ENV{PAGER}, otherwise fall back to 'C<less -S>' cache the current output and error filehandles, and set them to the PAGER pipe just opened. If the pager is already running, do nothing =head2 $sh->stop_pager() Close the PAGER pipe and put the old output and error filehandles back =head2 $sh->set_signal_handlers() OBSOLETE+DEAD Set up the SIGWINCH, SIGTSTP and SIGTTIN handlers. SIGWINCH works with both Term::ReadLine::Gnu [rl version >= 4 only] and ::Perl [all versions, afaik] SIGSTP and SIGTTIN only seem to work with ::Perl - ::Gnu seems to block them in some arcane way, and only deliver them at exit time. If someone can wants to sort this out, or at least explain to me what;s going on, that would be just peachy =head2 $sh->set_dotdir() Create the dot-directory [~/.dbishell, by default] if neccessary =head2 $sh->set_completion_function(FUNC) OBSOLETE+DEAD Set up the tab completion function [Term::ReadLine::Gnu and ::Perl do this in different ways] =head2 $sh->getopts() Parse the command line args in @ARGV =head2 $sh->disconnect() Tell the driver to disconnect, zap the DBIShell::dr::<foo> object, flush the readline history, clear the connection parameters and drop back to a DBD::NullP connection =head2 $sh->reconnect(ARGS) Kill the current connection and open a new one, asking for any parameters that weren't supplied in ARGS =head2 $sh->spool(SPOOL_COMMAND_STRING) Interpret a 'spool {in|out|err} target-file {on|off}' command, and set the state of the spooler appropriately =head2 $sh->exec_script(FILE) FILE can be a filename or a filehandle. In either case, the indicated file is read and interpreted almost exactly as if the user had typed in the commands therein, with 2 minor differences: 1) The individual commands don't make it into the readline history 2) The last command need not be terminated with the EOL character =head2 $sh->set_show_var(VARCMD) VARCMD is of the form: $NAME=VALUE or: $!NAME set dbishell variable NAME to VALUE, [or unset it] and echo the resulting value back to the user =head2 $sh->setvar(VARCMD) Implements the actual interpretation of VARCMD [See L</$sh-E<gt>set_show_var(VARCMD)>] Returns (NAME, VALUE) =head2 $sh->subshell(CMD) Execute shell command CMD, or if CMD is not true, spwan an inferior shell =head2 $sh->getvar(NAME,NULL_REPRESENTATION) Get the value of dbishell variable NAME. If it is not defined, return NULL_REPRESENTATION instead. [This maybe slightly inaccurate for $dbi:NAME variables, but they're kind of a special case anyway] =head2 $sh->getvar_ref(NAME) Get a reference to the storage for dbishell variable NAME. Does not allow you to fetch references to $dbi:NAME variables, though =head2 $sh->put_parameter(NAME, IO) Store the vriable name, reference and in-outness of a ? style placeholder parameter =head2 $sh->clear_parameters() Clear the stored parameter list =head2 $sh->get_parameter(N) Return a reference to the storage of the Nth parameter =head2 $sh->get_parameter_name(N) Return the name of the dbishell variable in which the Nth parameter is stored =head2 $sh->get_parameter_io(N) Return the in/out-ness of the Nth parameter =head2 $sh->interpolation(BOOL) Set the flag that indicates whether or not dbishell should do variable interpolation or not =head2 $sh->escaping(BOOL) Set the flag that indicates whether dbishell should interpret \X style escapes, or pass them through unaltered =head2 $sh->show_license() Show the license =head2 $sh->pre_interpret(COMMAND) Attempt to interpret and deal with user input COMMAND. Return true on success, false on failure and undef to indicate 'I decline the command, pass on the next interpreter' =head2 $sh->interpret(COMMAND) Pass the command on to the DBIShell::dr::<foo> engine. Return true on success, false on failure and undef to indicate 'I decline the command, pass on the next interpreter' =head2 $sh->post_interpret(COMMAND) Attempt to interpret and deal with user input COMMAND. Return true on success, false on failure and undef to indicate 'I decline the command, pass on the next interpreter' =head2 $sh->edit(NUMBER) Launch the editor command in the dbishell variable EDITOR, or if that is empty $ENV{EDITOR}, or, failing that, 'ed', to edit the Nth command back in the command stack thingy. Execute the edited command when the editor returns =head2 $sh->quit_cleanup() Flush the readline history to appropriate dotfile, disconnect, and generally clean up prior to shutdown =head2 $sh->is_terminated(COMMAND) Determine whether COMMAND is ready for interpretation/execution, taking context into account, and return true or false, as appropriate. Some commands are considered to be one liners, and therefore always complete, others must be terminated by the character(s) in the dbishell variable EOL [or '/', if EOL is unset] The rules for termination, insofar as I can reconstruct my thoughts from the code, are as follows: Conditions for command completeness: We shall treat a command as having been completed when: GIVEN that the terminator sequence <E> shall have a single trailing newline sequence [$/] removed: ( (The terminator sequence '<E>' is set) AND ( (occurs at the start of a line, containing nothing else, other than whitespace) OR ( (occurs at the end of the line, discounting whitespace) AND (is NOT preceded by a '\' character) ) ) ) OR ( ((The terminator sequence <E> is unset) OR (<E> is set to '/')) AND ( ('/' occurs at the start of a line, containing nothing else, other than whitespace) OR ( ('/' occurs at the end of the line, discounting whitespace) AND (is NOT preceded by a '\' or a '*') ) ) ) OR ( (COMMAND consists of only one line) AND ( (COMMAND begins with the subshell command character '!') OR (COMMAND begins with the variable indicator character '$') OR (COMMAND begins with one of the special one line command keywords [show describe license quit help edit spool disconnect connect read interpolate interpolation escape escaping]) ) ) On reflection, this will mean that a terminator set by the user to contain multiple newline sequences will not work. I have not decided whether this constitutes a bug or a limitation =head2 $sh->parse_loop() Keep collecting user input, and shunt it along to the interpretes, as and when appropriate. Similar to exec_script, [See L</$sh-E<gt>exec_script(FILE)>] except that commands B<do> end up in the readline history =head2 $sh->connect() Collect any connection parameters that weren't supplied in the OPT storage from the user, and then load the appropriate modules and set up a connection =head2 $sh->complete() Call the tab completion function in the DBIShell::dr::<foo> driver =head2 $sh->get_dot_history() Load the readline history for the current dr:: driver into the readline history array from the appropriate dotfile =head2 $sh->set_dot_history() Flush the readline array to the appropriate dotfile =head2 $sh->dotfile(SECTION) Return the name of the dotfile for the section specified, for the current dr:: driver =head2 $sh->tmpfile(TAG) Return a tmpfile name. The tmpfile will lie in the 'draft' sub directory of your dotdir ['~/.dbishell' by default], and is guaranteed to be unique. [Unless you are uding threads, then all bets are off]. he tmfile will contain the contents of TAG in its name [so don't put a '/' in there]. =head2 $sh->set_dbi_nullp_parameters() Set the OPT connection parameters to dummy NullP connction values =head2 $sh->get_dbi_parameters() Get any connection parameters that haven't been set yet =head2 $sh->variables() Return a list of all the dbishell variables currently in existence =head2 $sh->interpolate(THING, NULLREP) Interpolate dbishell variables and transform escape sequences in THING, replacing anything that transforms to undef with the value of NULLREP. Any quotes immediately surrounding an interpolated undef value will be removed from THING eg: if $NARF is undef: update foo set bar = '$NARF' where poinkt = 32 becomes: update foo set bar = NULL where poinkt = 32 =head1 dbishell variables =head2 Introduction These fall into 4 categories, and have names of the form: dbi:NAME DBI attributes, (Database Handle+Universal ones) eg dbi:AutoCommit env:NAME environment variables grl:NAME GRL (GNU Read Line) attributes NAME normal dbishell variables Neither dbi nor grl domain variables may be bound as parameters or undefined, as it is unlikely to be helpful to do so, and may (imo) trigger bad XS - XS or Perl XS interactions, as that is not what they were provided for. =head2 Setting values calling $sh->setvar() [See L</$sh-E<gt>setvar(VARCMD)>] with a VARCMD of the form $NAME=VALUE will set the variable =head2 Unsetting values calling $sh->setvar() [See L</$sh-E<gt>setvar(VARCMD)>] with a VARCMD of the form $!NAME will unset the variable =head2 Special variables: EOL : The command termination character If unset [the default] '/' is used Set to \n to disable multiline commands PAGING : Boolean. Whether or not paging is allowed PAGER : Command to open a pipe to when paging is called for PAGE_SIZE : Number of lines at which paging will be triggered Set to -1 for 'always' EDITOR : EDITOR command PRESCAN_ROWS : Whether to prefetch all select data and work out max col width before printing If false, just use the precision/scale to calculate the max width for each column TRUNCATE_COLUMN_NAMES: Whether or not to truncate column names which are wider than the max data width in the column they refer to FUSSY_COMPLETION_WORKAROUND: Handle a quirk in the tab completion, or not CASE_SENSITIVE : Is the database case sensitive or not? FIELD_SEPARATOR : Character to use to separate columns in output =head1 BUGS =over 4 =item paging need to fix it so that it fails gracefully when it can't open a pipe to the pager. =item signal handling Seem to be some issues w. SIGTSTP and SIGTTIN handling when Term::ReadlIne::Gnu is used: they seem to get blocked till we exit. Also, if the readline version is < 4.00, SIGWINCH doesn't seem to get handled at all: I think this is an actual limitation in the old rl library. =item terminal settings When Term::Readline::Perl is used as the rl implementation, M-<foo> sequences don't seem to work, you just get an 8-bit character instead: so to get emacs style Meta-sequences, you have to use the ESC versions instead. eg 'ESC >' for go to end of cmd. history instead of M->. I'm sure this is configureable in some way, I just don't know how (yet). =back =head1 AUTHOR Vivek Dasmohapatra <vivek@etla.org> =cut |
|
|
|