# # Copyright (c) 2004-2020 Christian Jaeger, copying@christianjaeger.ch # # This is free software, offered under either the same terms as perl 5 # or the terms of the Artistic License version 2 or the terms of the # MIT License (Expat version). See the file COPYING.md that came # bundled with this file. # =head1 NAME FP::Repl::Repl - read-eval-print loop =head1 SYNOPSIS my $repl = new FP::Repl::Repl; $repl->set_prompt("foo> "); # ^ if left undefined, "$package$perhapslevel> " is used $repl->set_historypath("somefile"); # default is ~/.fp-repl_history $repl->set_env_PATH ($safe_PATH); # default in taint mode is # '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', # $ENV{PATH} otherwise. $repl->run; # or $repl->run($skip) to skip $skip levels =head1 DESCRIPTION Enters an interactive read-eval-print loop. Term::ReadLine with history is active. The loop can be exited by typing ctl-d. Entering nothing re-evaluates the last entry. Some autocompletion exists. When the entered line starts with ':' or ',' (both characters are equivalent), then it is interpreted as containing special commands or modes. Enter ':h' or ':help' or ':?' (or the equivalents starting with the comma like ',?', from now on whenever the text days ':' you can also use the comma) to get a help text including the currently active settings. If the 'Maybe_keepResultIn' field is set to a string, the scalar with the given mae is set to either an array holding all the result values (in :l mode) or the result value (in :1 mode). By default, in the :d and :s modes the results of a calculation are carried over to the next entry in $VAR1 etc. as shown by the display of the result. Those are lexical variables. This does not turn on the Perl debugger, hence programs are not slowed down. =head1 FEATURES Read the help text that is displayed by entering ":h", ",h", ":?" or ",?" in the repl. =head1 TODO - 'A::Class-> ' method completion - for '$Foo ->bar' completion, if $Foo contains a valid class name, use it - maybe '$ans->[1]->' method completion =head1 IDEAS - should stdout and stderr of the evaluation context go to the pager, too? - maybe handle ->SUPER:: completion? - differ between emptylistofcompletions (no &subs for example) (empty list) and no sigil there so dunno-how-to-complete (undef?, exception?, ??). - make package lexicals accessible when entering a package with :p =head1 BUGS Completion: - $ does not filter out scalars only, since perl is not able to do so - % and * make completion stop working unless you put a space after those sigils. (@ and & work as they should) - keep the last 10 or so completion lists, and use those in the -> case if the var's type could not be determined. :V breaks view of :e and similar (shows CODE..) =head1 SEE ALSO L: easy wrapper =head1 NOTE This is alpha software! Read the status section in the package README or on the L. =cut package FP::Repl::Repl; use strict; use warnings; use warnings FATAL => 'uninitialized'; # Copy from FP::Repl::WithRepl, to prevent circular dependency. This has # to be at the top before any lexicals are defined! so that lexicals # from this module are not active in the eval'ed code. sub WithRepl_eval (&;$) { my ($arg, $maybe_package) = @_; if (ref $arg) { eval { &$arg() } } else { my $package = $maybe_package // caller; eval "package $package; $arg" } } use Chj::Class::methodnames; use Chj::xoutpipe(); use Chj::xtmpfile; use Chj::xperlfunc qw(xexec); use Chj::xopen qw(fh_to_fh perhaps_xopen_read); use POSIX; use Chj::xhome qw(xhome); use Chj::singlequote 'singlequote'; use FP::HashSet qw(hashset_union); use FP::Hash qw(hash_xref); use FP::Repl::StackPlus; use FP::Lazy; use FP::Show; use Scalar::Util qw(blessed); use FP::Carp; sub maybe_tty { my $path = "/dev/tty"; if (open my $fh, "+>", $path) { $fh } else { warn "opening '$path': $!"; undef } } sub xone_nonwhitespace { my ($str) = @_; $str =~ /^\s*(\S+)\s*\z/s or die "exactly one non-quoted argument must be given"; $1 } my $HOME = xhome; our $maybe_historypath = "$HOME/.fp-repl_history"; our $maybe_settingspath = "$HOME/.fp-repl_settings"; our $maxHistLen = 100; our $doCatchINT = 1; our $doRepeatWhenEmpty = 1; our $doKeepResultsInVARX = 1; our $pager = $ENV{PAGER} || "less"; our $mode_context = 'l'; our $mode_formatter = 'd'; our $mode_viewer = 'a'; our $mode_lexical_persistence = 'X'; our $maybe_env_path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'; use Chj::Class::Array -fields => -publica => ( 'Maybe_historypath', # undef=none, but a default is set 'Maybe_settingspath', # undef=none, but a default is set 'MaxHistLen', 'Maybe_prompt', # undef= build fresh one from package&level 'Maybe_package', # undef= use caller's package 'DoCatchINT', 'DoRepeatWhenEmpty', 'Maybe_keepResultIn', 'DoKeepResultsInVARX', 'Pager', 'Mode_context', # char 'Mode_formatter', # char 'Mode_viewer', # char 'Mode_lexical_persistence', # char 'Maybe_input', # fh 'Maybe_output', # fh 'Maybe_env_PATH', # maybe string ); sub new { my $class = shift; my $self = $class->SUPER::new; $$self[Maybe_historypath] = $maybe_historypath; $$self[Maybe_settingspath] = $maybe_settingspath; $$self[MaxHistLen] = $maxHistLen; $$self[DoCatchINT] = $doCatchINT; $$self[DoRepeatWhenEmpty] = $doRepeatWhenEmpty; $$self[DoKeepResultsInVARX] = $doKeepResultsInVARX; $$self[Pager] = $pager; $$self[Mode_context] = $mode_context; $$self[Mode_formatter] = $mode_formatter; $$self[Mode_viewer] = $mode_viewer; $$self[Mode_lexical_persistence] = $mode_lexical_persistence; $$self[Maybe_env_PATH] = $maybe_env_path if ${^TAINT}; $self } my $maybe_setter = sub { my ($method) = @_; sub { @_ == 2 or fp_croak_arity 2; my ($self, $v) = @_; my $set_maybe_method = "set_maybe_${method}"; defined $v or die "set_${method} does not accept undef, use $set_maybe_method instead"; $self->$set_maybe_method($v); } }; for my $method ( qw(historypath settingspath prompt package keepResultIn input output env_PATH) ) { no strict 'refs'; my $var = "set_$method"; *$var = &$maybe_setter($method); } sub use_lexical_persistence { my $self = shift; hash_xref(+{ m => 1, M => 1, x => 0, X => 0 }, $self->mode_lexical_persistence) } sub use_strict_vars { my $self = shift; hash_xref(+{ m => 1, M => 0, x => 1, X => 0 }, $self->mode_lexical_persistence) } my $settings_version = "v2"; my $settings_fields = [ # these should remain caller dependent: #maxHistLen #doCatchINT #doRepeatWhenEmpty #maybe_keepResultIn #doKeepResultsInVARX #pager qw(mode_context mode_formatter mode_viewer mode_lexical_persistence) ]; sub possibly_save_settings { my $self = shift; if (my $path = $self->maybe_settingspath) { my $f = xtmpfile $path; $f->xprint( join("\0", $settings_version, map { $self->$_ } @$settings_fields)); $f->xclose; $f->xputback(0600); } } sub possibly_restore_settings { my $self = shift; if (my $path = $self->maybe_settingspath) { if (my ($f) = perhaps_xopen_read($path)) { my @v = split /\0/, $f->xcontent; $f->xclose; if (shift(@v) eq $settings_version) { for (my $i = 0; $i < @$settings_fields; $i++) { my $method = "set_" . $$settings_fields[$i]; $self->$method($v[$i]); } } else { warn "note: not reading settings of other version from '$path'"; } } } } sub saving { @_ == 2 or fp_croak_arity 2; my ($self, $proc) = @_; sub { &$proc(@_); $self->possibly_save_settings; } } # (move to some lib?) sub splitpackage { my ($package) = @_; # may be partial. if ($package =~ /(.*)::(.*)/s) { ($1, $2) } else { ("", $package) } } my $PACKAGE = qr/\w+(?:::\w+)*/; use FP::Repl::corefuncs(); our @builtins = FP::Repl::corefuncs; # whether to use Data::Dumper in perl mode our $Dumper_Useperl = 0; sub __signalhandler { die "SIGINT\n" } our $term; # local'ized but old value is reused if present. our $current_history; # local'ized; array(s). sub print_help { my $self = shift; my ($out) = @_; my $selection = sub { my ($meth, $val) = @_; my $method = "mode_$meth"; $self->$method eq $val ? "->" : " " }; my $L = &$selection(context => '1'); my $l = &$selection(context => 'l'); my $p = &$selection(formatter => 'p'); my $s = &$selection(formatter => 's'); my $d = &$selection(formatter => 'd'); my $V = &$selection(viewer => 'V'); my $v = &$selection(viewer => 'v'); my $a = &$selection(viewer => 'a'); my $m = &$selection(lexical_persistence => 'm'); my $M = &$selection(lexical_persistence => 'M'); my $x = &$selection(lexical_persistence => 'x'); my $X = &$selection(lexical_persistence => 'X'); print $out qq{Repl help: If a command line starts with a ':' or ',', then the remainder of the line is interpreted as follows: package \$package use \$package as new compilation package p \$package shortcut for package DIGITS shorthand for 'f n', see below - same as 'f n' where n is the current frameno - 1 + same as 'f n' where n is the current frameno + 1 (both are the same as 'f' if reaching the end) CMD args... one-time command MODES code... change some modes then evaluate code CMD is one of: e [n] print lexical environment at level n (default: 0) Note: currently this does *not* show the lexicals that were persisted when enabling m or M mode. b|bt print back trace f [n] move to stack frame number n (at start: 0) without n, shows the current frame again y [n] alias for f q quit the process (by calling `exit`) MODES are a combination of these characters, which change the previously used mode (indicated on the left): context: $L 1 use scalar context $l l use list context (default) formatter: $p P print stringification $s s show from FP::Show (experimental, does not show data sharing) $d d Data::Dumper (default) viewer: $V V no pager $v v pipe to pager ($$self[Pager]) $a a pipe to 'less --quit-if-one-screen --no-init' (default) lexical persistence: (Persisting lexicals means to carry over variables introduced with "my" into subsequent entries in the same repl. It prevents their values from being deallocated until new values are assigned or a new lexical with the same name is introduced. Currently these lexicals are carried over even when moving to another frame or package.) $m m persist lexicals, use strict 'vars' $M M persist lexicals, no strict 'vars' $x x do not persist lexicals, use strict 'vars' $X X do not persist lexicals, no strict 'vars' (default) Other features: \$FP::Repl::Repl::args is an array holding the arguments of the last subroutine call that led to the currently selected frame \$FP::Repl::Repl::argsn is an array holding the arguments of the subroutine call that *leaves* the currently selected frame }; } sub formatter { my $self = shift; my ($terse) = @_; # true for :e viewing my $mode = $self->mode_formatter; $mode = "d" if ($terse and $mode eq "p"); hash_xref( +{ p => sub { (join "", map { (defined $_ ? $_ : 'undef') . "\n" } @_) }, s => sub { my $z = 1; ( join "", map { my $VARX = ($$self[DoKeepResultsInVARX] and not $terse) ? '$VAR' . $z++ . ' = ' : ''; $VARX . show($_) . ";\n" } @_ ) }, d => sub { my @v = @_; # to survive into # WithRepl_eval below require Data::Dumper; my $res; WithRepl_eval { local $Data::Dumper::Sortkeys = 1; local $Data::Dumper::Terse = $terse; local $Data::Dumper::Useperl = $Dumper_Useperl; $res = Data::Dumper::Dumper(@v); 1 } || do { warn "Data::Dumper: " . show($@); }; $res } }, $mode ); } sub viewers { my $self = shift; my ($OUTPUT, $ERROR) = @_; my $port_pager_with_options = sub { my ($maybe_pager, @options) = @_; sub { my ($printto) = @_; local $SIG{PIPE} = "IGNORE"; my $pagercmd = $maybe_pager // $self->pager; eval { # XX this now means that no options # can be passed in $ENV{PAGER} ! # (stupid Perl btw). Ok hard code # 'less' instead perhaps! my $o = Chj::xoutpipe( sub { # set stdout and stderr in case they are # redirected (stdin is the pipe) my $out = fh_to_fh($OUTPUT); $out->xdup2(1); $out->xdup2(2); $ENV{PATH} = $self->maybe_env_PATH if defined $self->maybe_env_PATH; xexec $pagercmd, @options } ); &$printto($o); $o->xfinish; 1 } || do { my $estr = show($@); unless ($estr =~ /broken pipe/i) { print $ERROR "error piping to pager " . "$pagercmd: $estr\n" or die $!; } }; } }; my $string_pager_with_options = sub { my $port_pager = &$port_pager_with_options(@_); sub { my ($v) = @_; &$port_pager( sub { my ($o) = @_; $o->xprint($v); } ); } }; my $choosepager = sub { my ($pager_with_options) = @_; hash_xref( +{ V => sub { print $OUTPUT $_[0] or die "print: $!"; }, v => &$pager_with_options(), a => &$pager_with_options( qw(less --quit-if-one-screen --no-init)), }, $self->mode_viewer ); }; my $pager = sub { my ($pager_with_options) = @_; sub { my ($v) = @_; &$choosepager($pager_with_options)->($v); } }; (&$pager($port_pager_with_options), &$pager($string_pager_with_options)) } our $use_warnings = q{use warnings; use warnings FATAL => 'uninitialized';}; sub eval_code { my $self = shift; @_ == 5 or fp_croak_arity 5; my ($code, $in_package, $maybe_lexicals, $maybe_kept_results, $maybe_lexical_persistence) = @_; # merge with previous results, if any my $maybe_kept_results_hash = sub { return unless $maybe_kept_results; my %r; for (my $i = 0; $i < @$maybe_kept_results; $i++) { $r{ '$VAR' . ($i + 1) } = \($$maybe_kept_results[$i]); } \%r }; $maybe_lexicals = ($maybe_lexicals && $maybe_kept_results) ? hashset_union($maybe_lexicals, &$maybe_kept_results_hash) : ($maybe_lexicals // &$maybe_kept_results_hash); my $use_method_signatures = $Method::Signatures::VERSION ? "use Method::Signatures" : ""; my $use_functional_parameters_ = $Function::Parameters::VERSION ? "use Function::Parameters ':strict'" : ""; my $use_signatures = ($] >= 5.020) ? "use experimental 'signatures'" : ""; my $use_tail = $Sub::Call::Tail::VERSION ? "use Sub::Call::Tail" : ""; my $use_autobox = @FP::autobox::ISA ? "use FP::autobox" : ""; my $prelude = "package " . &$in_package() . ";" . "use strict; " . ($self->use_strict_vars ? "" : "no strict 'vars'; ") . "$use_warnings; " . "$use_method_signatures; $use_functional_parameters_; " . "$use_signatures; $use_tail; $use_autobox; "; if (my $lp = $maybe_lexical_persistence) { my $allcode = $prelude . $code; if (defined $maybe_lexicals) { $lp->lexicals(hashset_union($lp->lexicals, $maybe_lexicals)) } my $context = wantarray ? "list" : "scalar"; $lp->context($context); WithRepl_eval { $lp->eval($allcode) } } else { my @v = sort keys %$maybe_lexicals if defined $maybe_lexicals; my $allcode = $prelude . (@v ? 'my (' . join(", ", @v) . '); ' : '') . 'sub {' . $code . "\n" . '}'; my $thunk = &WithRepl_eval($allcode) // return; PadWalker::set_closed_over($thunk, $maybe_lexicals) if defined $maybe_lexicals; WithRepl_eval { &$thunk() } } } sub _completion_function { my ($attribs, $package, $lexicals) = @_; sub { my ($text, $line, $start, $end) = @_; my $part = substr($line, 0, $end); #reset to the default before deciding upon it: $attribs->{completion_append_character} = " "; my @matches = do { # arrow completion: my ($pre, $varnam, $brace, $alreadywritten); if (($pre, $varnam, $brace, $alreadywritten) = $part =~ /(.*)\$(\w+)\s*->\s*([{\[]\s*)?(\w*)\z/s or ($pre, $varnam, $brace, $alreadywritten) = $part =~ /(.*\$)\$(\w+)(?:\s+|(?:\s*([{\[]\s*)(\w*)))\z/s) { # need to know the class of that thing no strict 'refs'; my $r; # try to get the value, or at least the package. my $val = do { if (my $ref = $$lexicals{ '$' . $varnam }) { $$ref } else { my $v = ${ $package . "::" . $varnam }; if (defined $v) { $v } else { # (if I could run code side-effect free... or # compile-only and disassemble....) Try to # parse the perl myself if ( $part =~ /.* # force latest possible match (ok?) (?:^|;)\s* (?:(?:my|our)\s+)? # ^ optional for no 'use strict' \$$varnam \s* = \s* (?:new\w*\s+($PACKAGE) |($PACKAGE)\s*->\s*new) /sx ) { $r = $1; 1 } else { 0 } } } }; # Force any potential promises, since we want to # complete methods on the resulting value, OK? TODO # once typing framework is ready (FP::Type): mark type # in promise, thus no need to force it. Have to catch # (and ignore, OK?) exceptions. eval { $val = force $val; }; if (defined $val) { #warn "got value from \$$varnam"; if ($r ||= ref($val)) { if ($r eq 'HASH' or ($brace and UNIVERSAL::isa($val, 'HASH'))) { # Could also check `$val->isa('HASH')` if # we wanted to run isa overloads, but # would we want to do that? #("{") #("{foo}","{bar}") if ($brace) { map {"$_}"} keys %$val } else { map {"{$_}"} keys %$val } } elsif ($r eq 'ARRAY' or ($brace and UNIVERSAL::isa($val, 'ARRAY'))) { # ^ not sure this works here; see commit messages ("[") } elsif ($r eq 'CODE') { ("(") } elsif ($r eq 'SCALAR') { ("SCALAR") ## } elsif ($r eq 'IO') { ("IO") ## } elsif ($r eq 'GLOB') { ("GLOB") ## } else { # object my @a = methodnames($r); grep { # (no need to check for matching the # already-written part of the string # here (with something like # /^\Q$alreadywritten\E/), why? Maybe # $attribs->{completion_word} was # deleted or?) # exclude some of the possible methodnames: # - all-uppercase when characters are contained. not(/[A-Z]/ and uc($_) eq $_) } @a } } else { () } } else { #warn "no value from \$$varnam"; () } } elsif ($part =~ tr/"/"/ % 2) { # odd number of quotes means we are inside () } elsif ($part =~ tr/'/'/ % 2) { # odd number of quotes means we are inside () } elsif ( $part =~ /(^|.)\s*(${PACKAGE}(?:::)?)\z/s or $part =~ /([\$\@\%\*\&]) \s* (${PACKAGE}(?:::)?|) # ^ accept the empty string \z/sx ) { # namespace completion my ($sigil, $partialpackage) = ($1, $2); no strict 'refs'; my ($upperpackage, $localpart) = splitpackage($partialpackage); #warn "upperpackage='$upperpackage', localpart='$localpart'\n"; # if upperpackage is empty, it might also be a # non-fully qualified, i.e. local, partial identifier. my $globentry = ( $sigil and +{ '$' => 'SCALAR', '@' => 'ARRAY', '%' => 'HASH', # ^ (problem with readline library, with a space # after % it works too; need better completion # function than the one from gnu readline?) # (years later: what was this?) '*' => 'SCALAR', # ^ really 'GLOB', but that would make it # invisible. SCALAR matches everything, which is # what we want. '&' => 'CODE' }->{$sigil} ); #print $ERROR "<$globentry>"; my $symbols_for_package = sub { my ($package) = @_; grep { # only show 'usable' ones. /^\w+(?:::)?\z/ } do { if ($globentry) { #print $ERROR ".$globentry."; grep { ( /::\z/ # either it's a namespace which we # want to see regardless of type, or: # type exists or *{ $package . "::" . $_ }{$globentry} ) } keys %{ $package . "::" } } else { keys %{ $package . "::" } } } }; my @a = ( $symbols_for_package->($upperpackage), length($upperpackage) ? () : ( $symbols_for_package->($package), ($globentry ? () : @builtins), # and lexicals: map { /^\Q$sigil\E(.*)/s ? $1 : () } sort keys %$lexicals ) ); #print Data::Dumper::Dumper(\@a); # Now, if it ends in ::, or even generally, care about # it not appending space on completion: $attribs->{completion_append_character} = ""; ( map { if (/::\z/) { $_ } else { "$_ " } } ( length($upperpackage) ? map { $upperpackage . "::$_" } @a : @a ) ) } else { () } }; if (@matches) { #print $ERROR "<".join(",",@matches).">"; $attribs->{completion_word} = \@matches; # (no sorting necessary) return $term->completion_matches($text, $attribs->{list_completion_function}) } else { # restore defaults. $attribs->{completion_append_character} = " "; return () } } } our $clear_history = do { my $did = 0; sub { my ($term) = @_; # Term::ReadLine::Perl does not have clear_history, so, wrap # it. ->can doesn't work either (lazy loading?), so: eval { $term->clear_history; 1 } || do { warn $@ . "install Term::ReadLine::Gnu if you can" unless $did++; } } }; our ($maybe_input, $maybe_output); # dynamic parametrization of # filehandles our $repl_level; # maybe number of repl layers above our $args; # see '$FP::Repl::Repl::args' in help text our $argsn; # see '$FP::Repl::Repl::argsn' in help text # TODO: split this monstrosity into pieces. sub run { my ($self, $maybe_skip) = @_; my $skip = $maybe_skip // 0; my $stack = FP::Repl::StackPlus->get($skip + 1); local $repl_level = ($repl_level // -1) + 1; my $frameno = 0; my $get_package = sub { # (What is $$self[Maybe_package] for? Can set the maybe_prompt # independently. Security feature or just overengineering? # Ok, remember the ":p" setting; but why not use a lexical # within `run`? Ok how long-lived are the repl objects, same # duration? Then hm is the only reason for the object to be # able to set up things explicitely first? Thus is it ok after # all?) my $r = $$self[Maybe_package] || $stack->package($frameno); $r }; my $oldsigint = $SIG{INT}; eval { local $SIG{__DIE__}; # It seems this is the only way to make signal handlers work in # both perl 5.6 and 5.8: sigaction SIGINT, new POSIX::SigAction __PACKAGE__ . '::__signalhandler' or die "Error setting SIGINT handler: $!\n"; 1 } || do { if ($^O eq 'MSWin32') { # XX will that work? $SIG{INT} = \&__signalhandler; } else { warn "could not set up signal handler: $@ "; } }; { local $SIG{__DIE__}; require Term::ReadLine; } # only start one readline instance, do not nest (doing otherwise # seems to lead to segfaults). okay?. local our $term = $term || new Term::ReadLine 'Repl'; # This means that the history from nested repls will also show up # in the history of the parent repl. Not saved, but within the # readline instance. (Correct?) # XX: idea: add nesting level to history filename? my $attribs = $term->Attribs; my ($INPUT, $OUTPUT, $ERROR) = do { my $tty = lazy {maybe_tty}; my $in = $self->maybe_input // $maybe_input // force($tty) // $term->IN // *STDIN; my $out = $self->maybe_output // $maybe_output // force($tty) // $term->OUT // *STDOUT; $term->newTTY($in, $out); ($in, $out, $out) }; # carry over input/output to subshells: local $maybe_input = $INPUT; local $maybe_output = $OUTPUT; my $printerror_frameno = sub { my $max = $stack->max_frameno; print $ERROR "frame number must be between 0..$max", (@_ ? ", got @_" : ()), "\n"; }; my ($view_with_port, $view_string) = $self->viewers($OUTPUT, $ERROR); { my @history; local $current_history = \@history; # ^ this is what nested repl's will use to restore the history # in the $term object if (defined $$self[Maybe_historypath]) { # clean history of C based object before we re-add the # saved one: $clear_history->($term); if (open my $hist, "<", $$self[Maybe_historypath]) { @history = <$hist>; close $hist; for (@history) { chomp; $term->addhistory($_); } } } # do not add input to history automatically (which allows me # to do it myself): $term->MinLine(undef); my $myreadline = sub { DO: { my $line; eval { $line = $term->readline($$self[Maybe_prompt] // &$get_package() . ($repl_level ? " $repl_level" : "") . ($frameno ? "/$frameno" : "") . "> "); 1 } || do { if (!length ref($@) and $@ =~ /^SIGINT\n/s) { print $OUTPUT "\n"; redo DO; } else { die $@ } }; return $line; } }; # for repetitions: my $evaluator = sub { }; # noop my $maybe_kept_results; my $maybe_lexical_persistence; my $try_enable_lexical_persistence = sub { eval { require Eval::WithLexicals; $maybe_lexical_persistence = Eval::WithLexicals->new; eval { require strictures } or $maybe_lexical_persistence->prelude(""); 1 } || do { print $ERROR "Could not enable lexical persistence: " . show($@); 0 } }; &$try_enable_lexical_persistence() if $self->use_lexical_persistence; READ: { while (1) { local $attribs->{attempted_completion_function} = _completion_function($attribs, &$get_package, $stack->perhaps_lexicals($frameno) // {}); my $input = &$myreadline // last; if (length $input) { my ($cmd, $rest) = $input =~ /^ *[:,] *([?+-]|[a-zA-Z]+|\d+)(.*)/s ? ($1, $2) : (undef, $input); if (defined $cmd) { if ($cmd =~ /^\d+\z/) { # hacky way to allow ":5" etc. as ":f 5" $rest = "$cmd $rest"; $cmd = "f"; } # handle commands eval { my $set_package = sub { # (no package parsing, trust user) $$self[Maybe_package] = xone_nonwhitespace($rest); $rest = ""; # XX HACK }; my $help = sub { $self->print_help($OUTPUT) }; my $bt = sub { my ($maybe_frameno) = $rest =~ /^\s*(\d+)?\s*\z/ or die "expecting digits or no argument, got '$cmd'"; local $FP::Lazy::allow_access = 1; # ^ XX should be generalized, not just # for FP::Lazy; or alternatively, use # overload::StrVal instead of # stringification in the backtrace # library. print $OUTPUT $stack->backtrace($maybe_frameno // $frameno); $rest = ""; # XX HACK; also, really # silently drop stuff? }; my $chooseframe = sub { my ($maybe_frameno) = @_; $rest = ""; # still the hack, right? if (defined $maybe_frameno) { if ($maybe_frameno <= $stack->max_frameno) { $frameno = $maybe_frameno } else { &$printerror_frameno($maybe_frameno); return; } } # unset any explicit package as # we want to use the one of the # current frame undef $$self[Maybe_package] ; # even without frameno? mess # Show the context: (XX same # issue as with :e with overly # long data (need viewer, but # don't really want to, so should # really use shortener) print $OUTPUT $stack->desc( $frameno, $self->mode_formatter ), "\n"; }; my $select_frame = sub { my ($maybe_frameno) = $rest =~ /^\s*(\d+)?\s*\z/ or die "expecting frame number, " . "an integer, or nothing, got '$cmd'"; &$chooseframe($maybe_frameno) }; my %commands = ( h => $help, help => $help, '?' => $help, '-' => sub { &$chooseframe( ($frameno > 0) ? $frameno - 1 : undef) }, '+' => sub { &$chooseframe( ($frameno < $stack->max_frameno) ? $frameno + 1 : undef) }, package => $set_package, p => $set_package, 1 => saving( $self, sub { $$self[Mode_context] = "1" } ), l => saving( $self, sub { $$self[Mode_context] = "l" } ), P => saving( $self, sub { $$self[Mode_formatter] = "p" } ), s => saving( $self, sub { $$self[Mode_formatter] = "s" } ), d => saving( $self, sub { $$self[Mode_formatter] = "d" } ), V => saving( $self, sub { $$self[Mode_viewer] = "V" } ), v => saving( $self, sub { $$self[Mode_viewer] = "v" } ), a => saving( $self, sub { $$self[Mode_viewer] = "a" } ), m => saving( $self, sub { &$try_enable_lexical_persistence() and $$self[Mode_lexical_persistence] = "m" } ), M => saving( $self, sub { &$try_enable_lexical_persistence() and $$self[Mode_lexical_persistence] = "M" } ), x => saving( $self, sub { undef $maybe_lexical_persistence; $$self[Mode_lexical_persistence] = "x" } ), X => saving( $self, sub { undef $maybe_lexical_persistence; $$self[Mode_lexical_persistence] = "X" } ), e => sub { use Data::Dumper; # XX clean up: don't want i in the regex my ($maybe_frameno) = $rest =~ /^i?\s*(\d+)?\s*\z/ or die "expecting digits or no argument, got '$cmd'"; $rest = "" ; # can't s/// above when expecting value my $fno = $maybe_frameno // $frameno; if (my ($lexicals) = $stack->perhaps_lexicals($fno)) { &$view_with_port( sub { my ($o) = @_; my $format = $self->formatter(1); for my $key ( sort keys %$lexicals) { if ($key =~ /^\$/) { $o->xprint( "$key = " . &$format( ${ $$lexicals{ $key} } ) ); } else { $o->xprint( "\\$key = " . &$format( $$lexicals{$key} ) ); } } } ); } else { &$printerror_frameno($fno); } }, f => $select_frame, y => $select_frame, q => sub { # XX change exit code depending # on how the repl was called? # Well, at least make it a config # field? exit 0 }, bt => $bt, b => $bt, ); while (length $cmd) { # XX why am I checking with and without chopping here? if (my $sub = $commands{$cmd}) { &$sub; last; } else { my $subcmd = chop $cmd; if (my $sub = $commands{$subcmd}) { &$sub; last; # XX why last? shouldn't we # continue with what's left of # $cmd ? } else { print $ERROR "unknown command " . "or mode: '$subcmd'\n"; last; } } } 1 } || do { print $ERROR "error handling command $cmd: " . show($@); redo READ; } } # build up evaluator my $eval = do { my $eval_ = sub { $self->eval_code( $rest, $get_package, $stack->perhaps_lexicals($frameno) // {}, $maybe_kept_results, $maybe_lexical_persistence ) }; hash_xref( +{ 1 => sub { ([scalar &$eval_()], $@) }, l => sub { ([&$eval_()], $@) }, }, $self->mode_context ); }; my $format_vals = $self->formatter; $evaluator = sub { my ($results, $error) = do { # make it possible for the code entered in # the repl to access the arguments in the # last call leading to this position by # accessing $FP::Repl::Repl::args : my $getframe = sub { my ($i) = @_; if ( defined( my $frame = $stack->frame($frameno + $i) ) ) { $frame->args } else { "TOP" } }; local $argsn = &$getframe(0); local $args = &$getframe(1); &$eval() }; &$view_string( do { if (ref $error or $error) { my $err = ( (defined blessed $error) && $error->can("plain") ? # error in plaintext; XX: # change to better # thought-out protocol? $error->plain : show($error) ); chomp $err; $err . "\n" ; # no prefix? no safe way to differentiate. } else { if (my $varname = $$self[Maybe_keepResultIn]) { $varname = &$get_package() . "::$varname" unless $varname =~ /::/; no strict 'refs'; $$varname = $self->mode_context eq "1" ? $$results[0] : $results; } &$format_vals(@$results) } } ); $maybe_kept_results = $results if $$self[DoKeepResultsInVARX]; }; &$evaluator(); } elsif ($$self[DoRepeatWhenEmpty]) { &$evaluator(); } else { next; } if (length $input and ((!defined $history[-1]) or $history[-1] ne $input)) { push @history, $input; chomp $input; $term->addhistory($input); #splice @history,0,@history-$$self[MaxHistLen] = (); if ($$self[MaxHistLen] >= 0) { # <-prevent endless loop shift @history while @history > $$self[MaxHistLen]; } } } } print $OUTPUT "\n"; if (defined $$self[Maybe_historypath]) { eval { my $f = xtmpfile $$self[Maybe_historypath]; $f->xprint("$_\n") for @history; $f->xclose; $f->xputback(0600); }; if (ref $@ or $@) { warn "could not write history file: " . show($@) } } $SIG{INT} = defined($oldsigint) ? $oldsigint : "DEFAULT"; # (Is there no other return path from sub run? should I use # DESTROY objects for this? -> nope, no returns, but if # exceptions not trapped it would fail) } # restore previous history, if any if ($current_history) { $clear_history->($term); for (@$current_history) { chomp; $term->addhistory($_); } } } end Chj::Class::Array; # for backwards compatibility: *set_maxhistlen = *set_maxHistLen{CODE}; *set_docatchint = *set_doCatchINT{CODE}; *set_dorepeatwhenempty = *set_doRepeatWhenEmpty{CODE}; *set_maybe_keepresultin = *set_maybe_keepResultIn{CODE};