Zoidberg - A modular perl shell


Zoidberg documentation Contained in the Zoidberg distribution.

Index


Code Index:

NAME

Top

Zoidberg - A modular perl shell

SYNOPSIS

Top

You should use the zoid system command to start the Zoidberg shell. To embed the Zoidberg shell in another perl program use the Zoidberg::Shell module.

DESCRIPTION

Top

This page contains devel documentation, if you're looking for user documentation start with the zoid(1) and zoiduser(1) man pages.

This module contains the core dispatch and event logic of the Zoidberg shell. Also it is used as a 'main object' so other objects can find each other here; all other objects are nested below this object. Also it contains some parser code.

This object inherits from both Zoidberg::Contractor and Zoidberg::Shell.

METHODS

Top

new(\%attr)

Initialize secondary objects and sets config. %attr contains non-default attributes and is used to set runtime settings.

You probably don't want to use this to construct a new Zoidberg shell object, better use Zoidberg::Shell.

main_loop()

Spans interactive shell reading from a secondary ReadLine object or from STDIN.

To quit this loop the routine exit() of this package should be called. Most common way to do this is pressing ^D.

mode [mode]

Without arguments prints the current mode. With arguments sets the mode.

plug

TODO

unplug

TODO

exit()

Called by plugins to exit zoidberg -- this ends a interactive main_loop() loop. This does not clean up or destroy any objects, main_loop() can be called again to restart it.

round_up()

This method should be called to clean up the shell objects. A round_up() method will be called recursively for all secondairy objects.

AUTOLOADING

Top

Routines not recognised by this object are understood to be either the name of a plugin, in which case a reference to that object is returned, or a shell command, in which case Zoidberg tries to execute it.

AUTHOR

Top

Jaap Karssenberg || Pardus [Larus] <pardus@cpan.org>

R.L. Zwart, <carl0s@users.sourceforge.net>

Copyright (c) 2002 Jaap G Karssenberg. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

See http://www.perl.com/language/misc/Artistic.html and http://www.gnu.org/copyleft/gpl.html

SEE ALSO

Top

zoid(1), zoiddevel(1), Zoidberg::Shell, http://zoidberg.sourceforge.net


Zoidberg documentation Contained in the Zoidberg distribution.
package Zoidberg;

our $VERSION = '0.96';
our $LONG_VERSION = "Zoidberg $VERSION

Copyright (c) 2002 - 2004 Jaap G Karssenberg. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl.

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.

http://zoidberg.sourceforge.net";

use strict;
use vars qw/$AUTOLOAD/;
no warnings; # yes, undefined == '' == 0

require Cwd;
require File::Glob;
require Zoidberg::Contractor;
require Zoidberg::Shell;
require Zoidberg::PluginHash;
require Zoidberg::StringParser;

use Zoidberg::DispatchTable;
use Zoidberg::Utils
	qw/:error :output :fs read_data_file merge_hash regex_glob getopt/;

our @ISA = qw/Zoidberg::Contractor Zoidberg::Shell/;

our %OBJECTS; # used to store refs to ALL Zoidberg objects in a process
our $CURRENT; # current Zoidberg object

our $_base_dir; # relative path for some settings
our @_parser_settings = qw/
	split_script split_words
	parse_env parse_fd parse_aliases parse_def_contexts
	expand_comm expand_param expand_path
/;

our %_settings = ( # default settings
	output => { error => 'red', debug => 'yellow' },
	clothes => {
		keys => [qw/settings commands aliases events error/],
		subs => [qw/shell alias unalias setting set source mode plug unplug/],
	},
	perl => {
		keywords => [qw/
			if unless for foreach while until 
			print
			push shift unshift pop splice
			delete
			do eval
			tie untie
			my our use no sub package
			import bless
		/],
		namespace => 'Zoidberg::Eval',
		opts => 'Z',
	},
	hide_private_method => 1,
	hide_hidden_files => 1,
	naked_zoid => 0,
	( map {($_ => 1)} @_parser_settings ),
	##Insert defaults here##
);
our %_grammars = ( # default grammar
	_base_gram => {
	        esc => '\\',
	        quotes => {
	                '"' => '"',
	                "'" => "'",
			'`' => '`',
	        },
	        nests => {
	                '{' => '}',
			'(' => ')',
	        },
	},
	script_gram => {
	        tokens => [
			[ ';',	'EOS'  ],
			[ "\n",	'EOL'  ],
	                [ '&&',	'AND'  ],
			[ '||',	'OR'   ],
	                [ '|',	'_CUT' ],
			[ qr/(?<![<>])&/ , 'EOS_BG' ],
			[ '==>', 'XFW' ],
			[ '<==', 'XBW' ],
	        ],
	},
	word_gram => qr/\s/,
	redirect_gram => {
		s_esc  => qr/[\\\-\=]/,
		tokens => [
			[ qr/<\S+>/, '_SELF' ],
			[ '>&', 'DUP_OUT'  ],
			[ '>|', 'CLOB_OUT' ],
			[ '>!', 'CLOB_OUT' ],
			[ '>>', 'APP_OUT'  ],
			[ '<&', 'DUP_IN'   ],
			[ '<<', 'ERROR'    ],
			[ '<>', 'RW'       ],
			[ '>',  'OUT'      ],
			[ '<',  'IN'       ],
		],
	},
	dezoid_gram => {
		tokens => [
			[ qr/->/, 'ARR' ], # ARRow
			[ qr/[\$\@][A-Za-z_][\w\-]*(?<!\-)/, '_SELF' ], # env var
		],
		quotes => { "'" => "'" }, # interpolate also between '"'
		nests => {},
	},
	expand_comm_gram => {
		tokens => {
			'$(' => {
				token  => 'COMM',
				tokens => {')' => '_CUT'},
			},
			'`'  => {
				token  => 'COMM',
				tokens => {'`' => '_CUT'},
			}
		},
	},
#	expand_braces_gram => {
#		tokens => {
#			'{' => {
#				token => 'BRACE',
#				tokens => { '}' => '_CUT' },
#			},
#		},
#	},
);

sub new { # FIXME maybe rename this to init ?
	my $class = shift;
	my $self = @_ ? { @_ } : {};
	$$self{$_} ||= {} for qw/settings commands aliases events objects/;
	$$self{no_words} ||= [];
	push @{$$self{no_words}}, qw/PERL SUBZ/; # parser stuff
	$$self{round_up}++;
	$$self{topic} ||= '';

	bless($self, $class);

	$OBJECTS{"$self"} = $self;
	$CURRENT = $self unless ref( $CURRENT ) eq $class; # could be autovivicated
	$self->{shell} = $self; # for Contractor

	## settings
	$$self{_settings} = merge_hash(\%_settings, $$self{settings});
	$$self{_settings}{data_dirs}
		|| error 'You should at least set a config value for \'data_dirs\'';

	my %set;
	tie %set, 'Zoidberg::SettingsHash', $$self{_settings}, $self;
	$$self{settings} = \%set;

	## commands
	$$self{commands} = Zoidberg::DispatchTable->new(
		$self, {
			exit	 => '->exit',
			plug	 => '->plug',
			unplug	 => '->unplug',
			mode	 => '->mode',
			readline => "->stdin('zoid-$VERSION\$ ')",
			readmore => "->stdin('> ')",
			builtin  => '->builtin',
			command	 => '->command',
			( %{$$self{commands}} )
		}
	);

	## events
	$$self{events} = Zoidberg::DispatchTable->new($self, $$self{events});
	$$self{events}{envupdate} = sub {
		my $pwd = Cwd::cwd();
		return if $pwd eq $ENV{PWD};
		@ENV{qw/OLDPWD PWD/} = ($ENV{PWD}, $pwd);
		$self->broadcast('newpwd');
		$self->builtin('log', $pwd, 'pwd') if $$self{settings}{interactive};
	};

	## parser
	$$self{parser} = Zoidberg::DispatchTable->new($self, $$self{parser});

	## stringparser 
	$$self{grammars} ||= \%_grammars;
	$$self{stringparser} = Zoidberg::StringParser->new(
		$$self{grammars}{_base_gram}, $$self{grammars},
	       	{allow_broken => 1, no_esc_rm => 1} );

	## initialize contractor
	$self->shell_init;

	## plugins
	my %objects;
	tie %objects, 'Zoidberg::PluginHash', $self;
	$self->{objects} = \%objects;

	# autoloading of contexts after plugin loading
	# because of bootstrapping issues
	$$self{parser}{_AUTOLOAD} = sub {
		my $c = shift;
		debug "trying to autoload $c";
		if ($c =~ /::/) {
			$c =~ m#(.*?)(::|->)$#;
			my ($class, $type) = ($1, $2);
			debug "loading class $class";
			$$self{parser}{$c} = {};
			$$self{parser}{$c}{handler} = sub {
				my (undef, $sub, @args) = @{ shift() };
				unshift @args, $class if $type eq '->';
				no strict 'refs';
				$sub = $class.'::'.$sub;
				$sub->(@args);
			};
			$$self{parser}{$c}{intel} = sub {
				my $block = shift;
				return undef if @$block > 2;
				no strict 'refs';
				my @p = grep m/^$$block[1]/,
					grep defined *{$class.'::'.$_}{CODE}, keys %{$class.'::'};
				push @p, grep m/^$$block[1]/, keys %{$$self{aliases}{'mode_'.$c}}
					if exists $$self{aliases}{'mode_'.$c};
				$$block[0]{poss} = \@p;
				return $block;
			};
		}
		else { eval { $self->plug($c) } }
		debug 'did you know 5.6.2 sucks ?' if $] < 5.008; # don't ask ... i suspect another vivication bug
		return exists($$self{parser}{$c}) ? $$self{parser}{$c} : undef ;
	};

	## let's load the rcfiles
	$$self{events}{loadrc} = sub {
		$self->source(grep {-f $_} @{$$self{_settings}{rcfiles}});
	};
	$self->broadcast('loadrc');

	$self->broadcast('envupdate'); # set/log pwd and maybe init other env stuff

	return $self;
}

sub import { bug "You should use Zoidberg::Shell to import from" if @_ > 1 }

# hooks overloading Contracter # FIXME these are not used !?
*pre_job = \&parse_block;
*post_job = \&broadcast;

# ############ #
# Main routine #
# ############ #

sub main_loop {
	my $self = shift;

	$$self{_continue} = 1;
	while ($$self{_continue}) {
		$self->reap_jobs();
		$self->broadcast('prompt');
		my ($cmd) = $self->builtin('readline');
		if ($@) {
			complain "\nInput routine died. (You can interrupt zoid NOW)";
			local $SIG{INT} = 'DEFAULT';
			sleep 1; # infinite loop protection
		}
		else {
			$self->reap_jobs();

			unless (defined $cmd || $$self{_settings}{ignoreeof}) {
				debug 'readline returned undef .. exiting';
				$self->exit();
			}
			else { $$self{_warned_bout_jobs} = 0 }

			last unless $$self{_continue};

			$self->shell_string({interactive => 1}, $cmd) if length $cmd;
		}
	}
}

# ############ #
# Parser stuff #
# ############ #

sub shell_string {
	my ($self, $meta, $string) = @_;
	($meta, $string) = ({}, $meta) unless ref($meta) eq 'HASH';
	local $CURRENT = $self;

	PARSE_STRING:
	my @list = $$self{_settings}{split_script}
       		? ($$self{stringparser}->split('script_gram', $string)) : ($string) ;
	my $b = $$self{stringparser}{broken} ? 1
		: (@list and ! ref $list[-1] and $list[-1] !~ /^EO/) ? 2 : 0 ;
	if ($b and ! $$self{_settings}{interactive}) { # FIXME should be STDIN on non interactive
		error qq#Operator at end of input# if $b == 2;
		my $gram = $$self{stringparser}{broken}[1];
		error qq#Unmatched $$gram{_open}[1] at end of input: $$gram{_open}[0]#;
	}
	elsif ($b) {
		($string) = $self->builtin('readmore');
		debug "\n\ngot $string\n\n\n";
		if ($@) {
			complain "\nInput routine died.\n$@";
			return;
		}
		goto PARSE_STRING;
	}

	if ($$meta{interactive}) {
		$self->broadcast('cmd', $string);
		$$self{previous_cmd} = $string;
		print STDERR $string if $$self{_settings}{verbose}; # posix spec
	}

	debug 'block list: ', \@list;
	$$self{fg_job} ||= $self;
	$$self{fg_job}->shell_list($meta, @list); # calling a contractor
}

sub prepare_block {
	my ($self, $block) = @_;
	my $t = ref $block;
	if ($t eq 'SCALAR') { $block = [{env => {pwd => $ENV{PWD}}}, $$block] }
	elsif ($t eq 'ARRAY') {
		if (ref($$block[0]) eq 'HASH') { $$block[0]{env}{pwd} ||= $ENV{PWD} }
		else { unshift @$block, {env => {pwd => $ENV{PWD}}} }
	}
	else {
		bug $t ? "prepare_block can't handle type $t"
		       : "block ain't a ref !??" ;
	}
	return $block;
}

sub parse_block { # call as late as possible before execution
 	# FIXME can this be more optimised for builtin() call ?
	my $self = shift;
	my $meta = (ref($_[0]) eq 'HASH') ? shift : {};
	my $block = shift;

	# check settings
	$$meta{$_} = $$self{_settings}{$_} for grep {! defined $$meta{$_}} @_parser_settings;
	# FIXME mode settings, uc || lc ?
	
	# decipher block
	PARSE_BLOCK:
	my @words;
	my $t = ref $block;
	if (!$t or $t eq 'SCALAR') {
		($meta, @words) = @{ $self->parse_env([$meta, $t ? $$block : $block]) };
		++$$meta{no_mode} and (length $words[0] or shift @words) if @words && $words[0] =~ s/^\!\s*//;
	}
	elsif ($t eq 'ARRAY') {
		$meta = { %$meta, %{shift @$block} } if ref($$block[0]) eq 'HASH';
		unless (@$block > 1 or $$meta{plain_words}) {
				debug "block aint a word block";
				$block = shift @$block;
				goto PARSE_BLOCK;
		}
		@words = @$block;
		++$$meta{no_mode} and shift @words if @words && $words[0] eq '!';
	}
	elsif ($t eq 'CODE') { return [{context => 'PERL', %$meta}, $block] }
	else { bug "parse tree contains $t reference" }

	# do aliases
	debug 'meta: ', $meta; # , 'words: ', [[@words]];
	if (@words and ! $$meta{pretend} and $$meta{parse_aliases}) {
		my @blocks = $self->parse_aliases($meta, @words);
		if (@blocks > 1) { return @blocks } # probably an alias contained pipe or logic operator
		elsif (! @blocks) { return undef }
		else {
			($meta, @words) = @{ shift(@blocks) };
		}
	}
	# post alias stuff
	$$meta{zoidcmd} = join ' ', @words; # unix haters guide pdf page 60 
	#FIXME how does this hadle escaped whitespacec ?
	$$meta{no_mode}++ if $words[0] eq 'mode'; # explicitly after alias expansion .. ! is before alias expansion

	# check custom filters
	for my $sub ($$self{parser}->stack('filter')) {
		my $r = $sub->([$meta, @words]);
		($meta, @words) = @$r if $r; # skip on undef
	}
	return undef unless $$meta{context} or @words;

	$$meta{context} = 'SUBZ' if $$meta{zoidcmd} =~ /^\s*\(.*\)\s*$/s; # check for subshell

	# check builtin contexts/filters
	unless ($$meta{context} or ! $$meta{parse_def_contexts}) {
		debug 'trying builtin contexts';
		my $perl_regexp = join '|', @{$self->{_settings}{perl}{keywords}};
		if (
			$$meta{zoidcmd} =~ s/^\s*(\w*){(.*)}(\w*)\s*$/$2/s or $$meta{pretend} and
			$$meta{zoidcmd} =~ s/^\s*(\w*){(.*)$/$2/s
		) { # all kinds of blocks with { ... }
			unless (length $1) { @$meta{qw/context opts/} = ('PERL', $3 || '') }
			elsif (grep {$_ eq $1} qw/s m tr y/) {
				$$meta{zoidcmd} = $1.'{'.$$meta{zoidcmd}.'}'.$3; # always the exceptions
				@$meta{qw/context opts/} = ('PERL', ($1 eq 'm') ? 'g' : 'p')
			}
			else {
				@$meta{qw/context opts/} = (uc($1), $3 || '');
				@words = $$self{stringparser}->split('word_gram', $$meta{zoidcmd});
			}
		}
		elsif ($$meta{zoidcmd} =~ s/^\s*(\w+):\s+//) { # little bit o psh2 compat
			$$meta{context} = uc $1;
			shift @words;
		}
		elsif (@words == 1 and $words[0] =~ /^%/) { unshift @words, 'fg' } # and another exception
		elsif ($words[0] =~ /^\s*(->|[\$\@\%\&\*\xA3]\S|\w+::|\w+[\(\{]|($perl_regexp)\b)/s) {
			$$meta{context} = 'PERL';
		}
	}

	$$meta{env}{ZOIDCMD} = $$meta{zoidcmd}; # unix haters guide, pdf page 60
	if ($$self{_settings}{mode} and ! $$meta{no_mode}) {
		my $m = $$self{_settings}{mode};
		$$meta{context} ||= ($m =~ /::/) ? $m : uc($m);
	}

	return [$meta, @words] if $$meta{pretend} and @words == 1;

	# check custom contexts
	unless ($$meta{context}) {
		debug 'trying custom contexts';
		for my $pair ($$self{parser}->stack('word_list', 'TAGS')) {
			my $r = $$pair[0]->([$meta, @words]);
			unless ($r) { next }
			elsif (ref $r) { ($meta , @words) = @$r }
			else { $$meta{context} = length($r) > 1 ? $r : $$pair[1] }
			last if $$meta{context};
		}
	}

	# use default builtin context
	unless ($$meta{context} or ! $$meta{parse_def_contexts}) {
		debug 'using default context';
		$$meta{context} = 'CMD';
	}

	if (
		exists $$self{parser}{$$meta{context}} and
		exists $$self{parser}{$$meta{context}}{parser}
	) { # custom parser
		($meta, @words) = @{ $$self{parser}{$$meta{context}}{parser}->([$meta, @words]) };
	}
	elsif (grep {$$meta{context} eq $_} @{$$self{no_words}}) { # no words
		@words = $$meta{pretend} 
			? $$self{stringparser}->split('word_gram', $$meta{zoidcmd})
			: ( $$meta{zoidcmd} ) ;
		$$meta{fork_job} = 1 if $$meta{context} eq 'SUBZ';
		($meta, @words) = @{ $self->parse_perl([$meta, @words]) }
			if ! $$meta{pretend} and $$meta{context} eq 'PERL';
	}
	elsif (@words and ! $$meta{pretend}) { # expand and set topic
		($meta, @words) = @{ $self->parse_words([$meta, @words]) } unless $$meta{plain_words};
		$$self{topic} =
# FIXME			exists($$meta{fd}{0})               ? $$meta{fd}{0}[0] :
			(@words > 1 and $words[-1] !~ /^-/) ? $words[-1]       : $$self{topic};
		$$meta{fork_job} = 1 if $$meta{context} eq 'CMD' and
			$$meta{cmdtype} ne 'builtin' and ! exists $$self{commands}{$words[0]};
	}
	return [$meta, @words];
}

our %_redir_ops = (
	IN => '<', OUT => '>',
       	CLOB_OUT => '>!', APP_OUT => '>>',
       	RW => '+<', DUP_OUT => '>&', DUP_IN => '<&'
);

sub parse_env {
	my ($self, $block) = @_;
	my ($meta, @words) = @$block;

	if (@words > 1 or ! $$meta{split_words}) {
		$$meta{string} = join ' ', @words;
	}
	else {
		$$meta{string} = $words[0];
		@words = $$self{stringparser}->split('word_gram', $words[0])
	}
	# FIXME parse word_gram and redir_gram at same time

	# parse environment
	if ($$meta{parse_env}) {
		my $_env = delete $$meta{env}; # PWD and SHELL
		while ($words[0] =~ /^(\w[\w\-]*)=(.*)/s) {
			$$meta{compl} = shift @words;
			$$meta{env}{$1} = $2
		}
		if (! @words and $$meta{env}) { # special case
			@words = ('export', map $_.'='.$$meta{env}{$_}, keys %{$$meta{env}});
			delete $$meta{env}; # duplicate would make var local
		}
		elsif ($$meta{env}) {
			delete $$meta{compl}; # @words > 0
			for (keys %{$$meta{env}}) {
				my (undef, @w) = @{ $self->parse_words([$meta, $$meta{env}{$_}]) };
				$$meta{env}{$_} = join ':', @w;
			}
		}
		for (keys %$_env) {
			$$meta{env}{$_} = $$_env{$_} unless defined $$meta{env}{$_};
		}
	}

	# parse redirections
	return [$meta, @words] unless $$meta{parse_fd};
	my @s_words = map [ $$self{stringparser}->split('redirect_gram', $_) ], @words;
	return [$meta, @words] if ! grep {! ref $_} map @$_, @s_words;
	$$meta{fd} ||= [];
	my @re;

	PARSE_REDIR_S_WORD:
	my @parts = @{shift @s_words};
	my $last = $#parts; # length of @parts changes later on
	for (0 .. $#parts) {
		next unless defined $parts[$_] and ! ref $parts[$_];
		my $op = delete $parts[$_];
		if ($op =~ /[^A-Z_]/) { # _SELF escape for "<fh>"
			$parts[$_] = \$op;
			next;
		}
		elsif ($op eq 'ERROR') { 
			error 'redirection operation not supported'
				unless $$meta{pretend};
		}

		my ($n, $word);
		if ($_ > 0 and ref $parts[$_-1]) { # find file descriptor number
			if (${$parts[$_-1]} =~ /^\d+$/) { $n = ${delete $parts[$_-1]} }
			else {
				${$parts[$_-1]} =~ s/(\\\\)|(\\\d+)$|(\d+)$/$1 || $2/eg;
				$n = $3;
			}
		}

		if ($_ < $#parts and ref $parts[$_+1]) { # find argument
			$word = ${ delete $parts[$_+1] };
			$$meta{compl} = $word if $_+1 == $last and ! @s_words; # complete last word
		}
		elsif (@s_words and ref $s_words[0][0]) {
			$word = ${ delete $s_words[0][0] };
			$$meta{compl} = $word if @s_words == 1 and ! @{$s_words[0]};
		}
		else {
			error 'redirection needs argument'
				unless $op =~ /^DUP/ or $$meta{pretend};
			$$meta{compl} = '';
		}

		unless ($$meta{pretend}) {
			$n ||= ($op =~ /OUT$/) ? 1 : 0;
			my (undef, @w) = @{ $self->parse_words([$meta, $word]) };
			if (@w == 1) { push @{$$meta{fd}}, $n.$_redir_ops{$op}.$w[0] }
			elsif (@w > 1) { error 'redirection argument expands to multiple words' }
			else { error 'redirection needs argument' } # @w < 1
		}
	}
	push @re, map $$_, @parts;
	goto PARSE_REDIR_S_WORD if @s_words;

	return [$meta, @re];
}

sub parse_aliases { # recursive sub (aliases are 3 way recursive, 2 ways are in this sub)
	my ($self, $meta, @words) = @_;
	my $aliases = ($$self{_settings}{mode} && ! $$meta{no_mode})
		? $$self{aliases}{'mode_'.$$self{_settings}{mode}}
		: $$self{aliases};
	return [$meta, @words] unless ref $aliases and exists $$aliases{$words[0]};
	$$meta{alias_stack} ||= [];
	return [$meta, @words] if grep {$_ eq $words[0]} @{$$meta{alias_stack}};
	push @{$$meta{alias_stack}}, $words[0];

	my $string = $$aliases{$words[0]};
	debug "$words[0] is aliased to: $string";
	shift @words;

	my @as = @{$$meta{alias_stack}}; # force copy
	my @l = map {
		ref($_) ? [ 
			{ alias_stack => [@as] },
			$$self{stringparser}->split('word_gram', $$_)
		] : $_
	} $$meta{split_script} ? ($$self{stringparser}->split('script_gram', $string)) : ($string);

	if ( my ($firstref) = grep ref($_), @l ) {
		$$firstref[0]  = $meta; # re-insert %meta
		++$$meta{no_mode} and (length $$firstref[1] or delete $$firstref[1])
	       		if @$firstref > 1 and $$firstref[1] =~ s/^\!\s*//; # check mode
	}

	if ($string =~ /\s$/) { # recurs for 2nd word - see posix spec
		my @l1 = $self->parse_aliases({}, @words); # recurs
		push @{$l[-1]}, splice(@{ shift(@l1) }, 1) if ref $l[-1] and ref $l1[0];
		push @l, @l1;
	}
	elsif (@l == 1) { return $self->parse_aliases(@{$l[0]}, @words) } # recurs
	else {
		if (ref $l[-1]) { push @{$l[-1]}, @words }
		else { push @l, \@words }
	}

	return @l;
}

sub parse_words { # expand words etc.
	my ($self, $block) = @_;

	# custom stack
	for ($$self{parser}->stack('word_expansion')) {
		my $re = $_->($block);
		$block = $re if $re;
	}

	# default expansions
	# expand_comm resets zoidcmd, all other stuff is left for appliction level re-parsing
	@$block = $self->$_(@$block)
		for grep $$block[0]{$_}, qw/expand_param expand_comm expand_path/;

	# remove quote
	my ($meta, @words) = @$block;
	for (@words) {
		if (/^([\/\w]+=)?(['"])(.*)\2$/s) {
		       	# quote removal and escape removal within quotes
			$_ = $1.$3;
			if ($2 eq '\'') { $_ =~ s/\\([\\'])/$1/ge }
			else            { $_ =~ s/\\(.)/$1/ge     }
		}
		# FIXME also do escape removal here
		# is now done by File::Glob
	}

	return [$meta, @words];
}

sub expand_param {
	# make sure $() and @() remain untouched ... `` are considered quotes
	no strict 'refs';
	my ($self, $meta, @words) = @_;
	my ($e);
	
	my $class = $$self{_settings}{perl}{namespace};
	@words = map { # substitute vars
		next if /^([\/\w]+=)?'.*'$/s; # skip quoted words
		my $old = $_;
		s{(?<!\\)\$\?}{ ref($$self{error}) ? $$self{error}{exit_status} : $$self{error} ? 1 : 0 }ge;
		s{ (?<!\\) \$ (?: \{ (.*?) \} | ([\w-]+) ) (?: \[(-?\d+)\] )? }{
						my ($w, $i) = ($1 || $2, $3);
						$e ||= "no advanced expansion for \$\{$w\}" if $w =~ /[^\w-]/;
						if ($w eq '_') { $w = $$self{topic} }
						elsif (exists $$meta{env}{$w} or exists $ENV{$w}) {
								$w = exists( $$meta{env}{$w} ) ? $$meta{env}{$w} : $ENV{$w} ;
								$w = $i ? (split /:/, $w)[$i] : $w;
						}
						elsif ($i ? defined(*{$class.'::'.$w}{ARRAY}) : defined(*{$class.'::'.$w}{SCALAR})) {
								$w = $i ? ${$class.'::'.$w}[$i] : ${$class.'::'.$w};
						}
						else { $w = '' }
						$w =~ s/\\/\\\\/g; # literal backslashes
						$w;
				}exg;
		if ($_ eq $old or $_ =~ /^".*"$/) { $_ }
		else { $$self{stringparser}->split('word_gram', $_) }
		# TODO honour IFS here -- POSIX tells us so
	}

	@words = map { # substitute arrays
		if (m/^ \@ (?: \{ (.*?) \} | ([\w-]+) ) $/x) {
			my $w = $1 || $2;
			$e ||= "no advanced expansion for \@\{$w\}" if $w =~ /[^\w-]/;
			$e ||= '@_ is reserved for future syntax usage' if $2 eq '_';
			if (exists $$meta{env}{$w} or exists $ENV{$w}) {
				$w = (exists $$meta{env}{$w}) ? $$meta{env}{$w}  : $ENV{$w};
				map {s/\\/\\\\/g; $_} split /:/, $w;
			}
			elsif (defined *{$class.'::'.$w}{ARRAY}) {
				map {s/\\/\\\\/g; $_} @{$class.'::'.$w};
			}
			else { () }
		}
		else { $_ }
	} @words;
	error $e if $e; # "Attempt to free unreferenced scalar" when dying inside the map !?
	return ($meta, @words);
}

sub expand_comm {
	my ($self, $meta, @words) = @_;
	my @re;
	my $m = {capture => 1, env => $$meta{env}};
	for (@words) {
		if (/^([\/\w]+=)?'.*'$/s) {
			push @re, $_;
		}
		elsif (/^\@\((.*?)\)$/s) {
			debug "\@() subz: $1";
			push @re, $self->shell($m, $1); # list context
		}
		else {
			my $quote = $1 if s/^(")(.*)\1$/$2/s;
			my @parts = $$self{stringparser}->split('expand_comm_gram', $_);
			error $$self{stringparser}{broken} if $$self{stringparser}{broken};
			# FIXME let stringparser do the error throwing ?
			unless (@parts > 1) {
				push @re, $quote ? $quote.$_.$quote : $_;
				next;
			}
			for (0 .. $#parts) {
				if ($parts[$_] eq 'COMM') {
					debug '$() subz: '.$parts[$_+1];
					$parts[$_] = $self->shell($m, ${delete $parts[$_+1]}); # scalar context
					if ($_ < $#parts-1 and ${$parts[$_+2]} =~ s/^\[(\d*)\]//) {
						$parts[$_] = $parts[$_][$1];
						chomp $parts[$_];
					}
					else { $parts[$_] = "$parts[$_]" } # just to be sure bout overload
				}
				elsif (ref $parts[$_]) { $parts[$_] = ${$parts[$_]} }
			}
			my $word = join '', @parts; # map {ref($_) ? (@$_) : $_} @parts;
			if ($quote) { push @re, $quote.$word.$quote }
			else { push @re, $$self{stringparser}->split('word_gram', $word) }
			# TODO honour IFS here - POSIX says so
		}
	}
	$$meta{env}{ZOIDCMD} = $$meta{zoidcmd} = join ' ', @re;
	return $meta, @re;
}

# See File::Glob for explanation of behaviour
our $_GLOB_OPTS = File::Glob::GLOB_TILDE() | File::Glob::GLOB_QUOTE() | File::Glob::GLOB_BRACE();
our $_NC_GLOB_OPTS = $_GLOB_OPTS | File::Glob::GLOB_NOCHECK();

sub expand_path { # path expansion
	# FIXME add 'failglob' setting (useful in scripts)
	my ($self, $meta, @files) = @_;
	return $meta, @files if $$self{_settings}{noglob};
	my $opts = $$self{_settings}{nullglob} ? $_GLOB_OPTS : $_NC_GLOB_OPTS;
	$opts |= File::Glob::GLOB_NOCASE() if $$self{_settings}{nocaseglob};
	return $meta, map {
		if (/^([\/\w]+=)?(['"])/) { $_ } # quoted
		elsif (/^m\{(.*)\}([imsx]*)$/) { # regex globs
			my @r = regex_glob($1, $2);
			if (@r) { @r }
			else { $_ =~ s/\\\\|\\(.)/$1||'\\'/eg; $_ }
		}
		elsif (/^~|[*?\[\]{}]/) { # normal globs
			# TODO: {x..y} brace expansion
			$_ =~ s#(\\\\)|(?<!\\){([^,{}]*)(?<!\\)}#$1?$1:"\\{$2\\}"#ge
				unless $$self{_settings}{voidbraces}; # brace pre-parsing
			my @r = File::Glob::doglob($_, $opts);
			debug "glob: $_ ==> ".join(', ', @r);
			($_ !~ /^-/) ? (grep {$_ !~ /^-/} @r) : (@r);
			# protect against implict switches as file names
		}
		else { $_ =~ s/\\\\|\\(.)/$1||'\\'/eg; $_ } # remove escapes # FIXME should be done in parse_words like quote removal
	} @files ;
}

sub parse_perl { # parse switches
	my ($self, $block) = @_;
	my ($meta, $string) = @$block;
	my %opts = map {($_ => 1)} split '', $$self{_settings}{perl}{opts};
	$opts{z} = 0 if delete $opts{Z};
	$opts{$_}++ for split '', $$meta{opts};
	$opts{z} = 0 if delete $opts{Z};
	debug 'perl block options: ', \%opts;

	($meta, $string) = $self->_expand_zoid($meta, $string) unless $opts{z};

	if ($opts{g}) { $string = "\nwhile (<STDIN>) {\n\tif (eval {".$string."}) { print \$_; }\n}" }
	elsif ($opts{p}) { $string = "\nwhile (<STDIN>) {\n\t".$string.";\n\tprint \$_\n}" }
	elsif ($opts{n}) { $string = "\nwhile (<STDIN>) {\n\t".$string.";\n}" }

	$string = "no strict;\n".$string unless $opts{z};

	return [$meta, $string];
}

sub _expand_zoid {
	my ($self, $meta, $code) = @_;

	my @parts = $$self{stringparser}->split('dezoid_gram', $code);
	my @idx = grep {! ref $parts[$_]} 0 .. $#parts;
	@parts = map {ref($_) ? $$_ : $_} @parts;

	my $pre = '';
	for (@idx) { # probably could be done much cleaner
		my $token = delete $parts[$_];
		my $next = ($_ < $#parts) ? $parts[$_+1] : '';
		my $prev = $_ ? $parts[$_-1] : '';

		my $class = $$self{_settings}{perl}{namespace};
		if ($token =~ /^([\@\$])(\w+)/) {
			my ($sigil, $name) = ($1, $2);
			if ( # global, reserved or non-env var
				$next =~ /^::/
				or grep {$name eq $_} qw/_ ARGV ENV SIG INC JOBS/
				or ! exists $ENV{$name} and ! exists $$meta{env}{$name}
			) { $parts[$_] = $token }
			elsif ($sigil eq '@' or $next =~ /^\[/) { # array
				no strict 'refs';
				$pre .= "Env->import('$token');\n"
					unless defined *{$class.'::'.$name}{ARRAY} and @{$class.'::'.$name};
				$parts[$_] = $token;
			}
			else { $parts[$_] = '$ENV{'.$name.'}' } # scalar
		}
		# else token eq 'ARR'
		elsif ($prev =~ /[\w\}\)\]]$/) { $parts[$_] = '->' }
		else { $parts[$_] = '$shell->' }
	}

	return $meta, $pre . join '', grep defined($_), @parts;
}

# ########## #
# Exec stuff #
# ########## #

sub eval_block { # real exec code
	my ($self, $ref) = @_;
	my $context = $$ref[0]{context};

	if ($$self{parser}{$context}{handler}) {
		debug "going to call handler for context: $context";
		$$self{parser}{$context}{handler}->($ref);
	}
	elsif ($self->can('_do_'.lc($context))) {
		my $sub = '_do_'.lc($context);
		debug "going to call sub: $sub";
		$self->$sub(@$ref);
	}
	else {
		$context
			? error "No handler defined for context $context"
			: bug   'No context defined !'
	}
}

# FIXME FIXME remove _do_* subs below and store them in {parser}

sub _do_subz { # sub shell, forked if all is well
	my ($self, $meta) = @_;
	my $cmd = $$meta{zoidcmd};
	$cmd = $1 if $cmd =~ /^\s*\((.*)\)\s*$/s;
	%$meta = map {($_ => $$meta{$_})} qw/env/; # FIXME also add parser opts n stuff
	# FIXME reset mode n stuff ?
	$self->shell_string($meta, $cmd);
	error $$self{error} if $$self{error}; # forward the error
}

sub _do_cmd {
	my ($self, $meta, $cmd, @args) = @_;
	# exec = exexvp which checks $PATH for us
	# the block syntax to force use of execvp, not shell for one argument list
	# If a command is not found, the exit status shall be 127. If the command name is found,
	# but it is not an executable utility, the exit status shall be 126.
	$$meta{cmdtype} ||= '';
	if ($cmd =~ m|/|) { # executable file
		error 'builtin should not contain a "/"' if $$meta{cmdtype} eq 'builtin';
		error {exit_status => 127}, $cmd.': No such file or directory' unless -e $cmd;
		error {exit_status => 126}, $cmd.': is a directory' if -d _;
		error {exit_status => 126}, $cmd.': Permission denied' unless -x _;
		debug 'going to exec file: ', join ', ', $cmd, @args;
		exec {$cmd} $cmd, @args or error {exit_status => 127}, $cmd.': command not found';
	}
	elsif ($$meta{cmdtype} eq 'builtin' or exists $$self{commands}{$cmd}) { # built-in, not forked I hope
		error {exit_status => 127}, $cmd.': no such builtin' unless exists $$self{commands}{$cmd};
		debug 'going to do built-in: ', join ', ', $cmd, @args;
		local $Zoidberg::Utils::Error::Scope = $cmd;
		$$self{commands}{$cmd}->(@args);
	}
	else { # command in path ?
		debug 'going to exec: ', join ', ', $cmd, @args;
		exec {$cmd} $cmd, @args or error {exit_status => 127}, $cmd.': command not found';
	}
}

sub _do_perl {
	my ($shell, $_Meta, $_Code) = @_;
	my $_Class = $$shell{_settings}{perl}{namespace} || 'Zoidberg::Eval';
	$_Code .= ";\n\$_Class = __PACKAGE__;" if $_Code =~ /package/;
	$_Code  = "package $_Class;\n$_Code";
	undef $_Class;
	debug "going to eval perl code: << '...'\n$_Code\n...";

	local $Zoidberg::Utils::Error::Scope = ['zoid', 0];
	$_ = $$shell{topic};
	$? = $$shell{error}{exit_status} if ref $$shell{error};
	ref($_Code) ? eval { $_Code->() } : eval $_Code;
	if ($@) { # post parse errors
		die if ref $@; # just propagate the exception
		$@ =~ s/ at \(eval \d+\) line (\d+)(\.|,.*\.)$/ at line $1/;
		error { string => $@, scope => [] };
	}
	else {
		$$shell{topic} = $_;
		$$shell{settings}{perl}{namespace} = $_Class if $_Class;
		print "\n" if $$shell{_settings}{interactive}; # ugly hack
	}
}

# ############## #
# some functions #
# ############## #

sub mode {
	my $self = shift;
	unless (@_) {
		output $$self{_settings}{mode} if $$self{_settings}{mode};
		return;
	}
	my $mode = shift;
	if ($mode eq '-' or $mode eq 'default') {
		$$self{settings}{mode} = undef;
	}
	else {
		my $m = ($mode =~ /::/) ? $mode : uc($mode);
		error $mode.': No such context defined'
			unless grep {lc($mode) eq $_} qw/perl cmd sh/
			or     $$self{parser}{$m}{handler} ; # allow for autoloading
		$$self{settings}{mode} = $mode;
	}
}

sub plug {
	my $self = shift;
	my ($opts, $args) = getopt 'list,l verbose,v @', @_;
	if ($$opts{list}) { # list info
		my @items = keys %{$$self{objects}};
		if (@$args) {
			my $re = join '|', @$args;
			@items = grep m/$re/i, @items;
		}
		if ($$opts{verbose}) { # FIXME nicer PLuginHash interface for this
			my ($raw, $meta) = @{ tied( %{$$self{objects}} ) };
			@items = map {
				$_ .' '. $$meta{$_}{module}
			       	. (exists($$raw{$_}) ? ' (loaded)' : '')
			} @items;
		}
		output \@items;
	}
	else { # load plugin
		error 'usage: plug name [args]' unless @$args;
		error $$args[0].': no such plugin'
			unless exists $$self{objects}{ $$args[0] };
		tied( %{$$self{objects}} )->load(@$args);
	}
}

sub unplug {
	my $self = shift;
	my ($opt, $args) = getopt 'all,a @', @_;
	if ($$opt{all}) { tied( %{$$self{objects}} )->CLEAR() }
	else {
		error "usage: unplug name" unless @$args == 1;
		delete $$self{objects}{$$args[0]};
	}
}

sub dev_null {} # does absolutely nothing

sub stdin { # stub STDIN input
	my (undef, $prompt, $preput) = @_;
	local $/ = "\n";
	print $prompt if length $prompt;
	my $string = length($preput) ? $preput . <STDIN> : <STDIN> ;
	output $string;
};

sub list_clothes {
	my $self = shift;
	my @return = map {'{'.$_.'}'} sort @{$self->{_settings}{clothes}{keys}};
	push @return, sort @{$self->{_settings}{clothes}{subs}};
	return [@return];
}

# ########### #
# Event logic #
# ########### #

sub broadcast { # eval to be sure we return
	my ($self, $event) = (shift(), shift());
	return unless exists $self->{events}{$event};
	debug "Broadcasting event: $event";
	for my $sub ($$self{events}->stack($event)) {
		eval { $sub->($event, @_) };
		complain("$sub died on event $event ($@)") if $@;
	}
}

sub call { bug 'deprecated routine used' }

# ########### #
# auto loader #
# ########### #

our $ERROR_CALLER;

sub AUTOLOAD {
	my $self = shift;
	my $call = (split/::/,$AUTOLOAD)[-1];

	local $ERROR_CALLER = 1;
	error "Undefined subroutine &Zoidberg::$call called" unless ref $self;
	debug "Zoidberg::AUTOLOAD got $call";

	if (exists $self->{objects}{$call}) {
		no strict 'refs';
		*{ref($self).'::'.$call} = sub { return $self->{objects}{$call} };
		goto \&{$call};
	}
	else { # Shell like behaviour
		debug "No such method or object: '$call', trying to shell() it";
		@_ = ([$call, @_]); # force words parsing
		goto \&Zoidberg::Shell::shell;
	}
}

# ############# #
# Exit routines #
# ############# #

sub exit {
	my $self = shift;
	if (@{$$self{jobs}} and ! $$self{_warned_bout_jobs}) {
		complain "There are unfinished jobs";
		$$self{_warned_bout_jobs}++;
	}
	else {
		message join ' ', @_;
		$self->{_continue} = 0;
	}
	# FIXME this should force ReadLine to quit
}

sub round_up {
	my $self = shift;
	$self->broadcast('exit');
	if ($self->{round_up}) {
		tied( %{$$self{objects}} )->round_up(); # round up loaded plugins
		Zoidberg::Contractor::round_up($self);
		undef $self->{round_up};
	}
}

sub DESTROY {
	my $self = shift;
	if ($$self{round_up}) {
		warn "Zoidberg was not properly cleaned up.\n";
		$self->round_up;
	}
	delete $OBJECTS{"$self"};
}

package Zoidberg::SettingsHash;

sub TIEHASH {
	my ($class, $ref, $shell) = @_;
	bless [$ref, $shell], $class;
}

sub STORE {
	my ($self, $key, $val) = @_;
	my $old = $$self[0]{$key};
	$$self[0]{$key} = $val;
	$$self[1]->broadcast('set_'.$key, $val, $old); # new, old
	1;
}

#sub set_default {
#	my ($self, $key, @list) = @_;
#	$$self[0]{_SettingsHash_def}{$key} = \@list;
#}

sub DELETE {
	my ($self, $key) = @_;
	my $val = delete $$self[0]{$key};
	$$self[1]->broadcast('set_'.$key, undef, $val); # new, old
	return $val;
}

sub CLEAR { $_[0]->DELETE($_) for keys %{$_[0][0]} }

sub FETCH {
	return $_[0][0]{$_[1]}
#		unless !defined $_[0][0]{$_[1]}
#		and exists $_[0][0]{_SettingsHash_def}{$_[1]};
	# check for default (environment) values
#	for my $def (@{$_[0][0]{_SettingsHash_def}{$_[1]}}) {
#		$def = $ENV{$1} if $def =~ /^\$(.*)/;
#		return $def if defined $def;
#	}
}

sub EXISTS { exists $_[0][0]{$_[1]} }

sub FIRSTKEY { my $a = scalar keys %{$_[0][0]}; each %{$_[0][0]} }

sub NEXTKEY { each %{$_[0][0]} }

package Zoidberg::Eval;

# included to bootstrap a bit of default environment
# for the perl syntax

use strict;
use vars qw/$AUTOLOAD/;

use Data::Dumper;
use Zoidberg::Shell qw/:all/;
use Zoidberg::Utils qw/:error :output :fs regex_glob/;
require Env;

$| = 1;
$Data::Dumper::Sortkeys = 1;

sub pp { # pretty print
	local $Data::Dumper::Maxdepth = shift if $_[0] =~ /^\d+$/;
	if (wantarray) { return Dumper @_ }
	else { print Dumper @_ }
}

{
	no warnings;
	sub AUTOLOAD {
		## Code inspired by Shell.pm ##
		my $cmd = (split/::/, $AUTOLOAD)[-1];
		return undef if $cmd eq 'DESTROY';
		shift if ref($_[0]) eq __PACKAGE__;
		debug "Zoidberg::Eval::AUTOLOAD got $cmd";
		@_ = ([$cmd, @_]); # force words
		unshift @{$_[0]}, '!'
			if lc( $Zoidberg::CURRENT->{settings}{mode} ) eq 'perl';
		goto \&shell;
	}
}

1;

__END__