#!/usr/bin/perl use common::sense; use utf8; use Encode; use Data::Dumper; $Data::Dumper::Indent = $Data::Dumper::Sortkeys = 1; use Term::ReadLine; use File::Slurp qw(slurp); binmode STDIN, ":utf8"; binmode STDOUT, ":utf8"; if( $ARGV[0] eq "-h" or $ARGV[0] eq "--help" ) { exec "perldoc", $0; exit 900 } my $term = new Term::ReadLine 'psh'; my $OUT = $term->OUT || \*STDOUT; my @s = (); if( $ENV{TERM} =~ m/(?:xterm|)/ ) { local $| = 1; print "\e]0; PSH \x07"; } *d = *Dumper; eval 'use Data::Dump qw(dump)'; if( !$@ ) { *d = *dump; *Dumper = *dump; } BEGIN { eval "use List::Util qw(first max maxstr min minstr reduce shuffle sum)"; eval "use Scalar::Util qw(blessed dualvar isweak readonly refaddr reftype tainted weaken isvstring looks_like_number set_prototype);"; eval "use Math::Units::PhysicalValue qw(PV)"; unless( $@ ) { *pv = *PV; *p = *PV; } } $SIG{INT} = sub { exit 0 }; END { print "\n" } $SIG{HUP} = sub { exit 0 }; print "Welcome to Paul's perl shell (type 'help' for documentation)\n"; for my $file ("$ENV{HOME}/.pshrc") { if( -f $file and open my $n, $file ) { local $/; my $thefile = <$n>; no strict "vars"; eval $thefile; die "(while evaluating $file): $@" if $@; } } END { eval { # try it... don't freak out if it doesn't work $term->write_history("$ENV{HOME}/.psh_history"); }; } my $cmd = 0; eval { # try it, but don't freak out if it fails... my $file = "$ENV{HOME}/.psh_history"; $term->history_truncate_file($file, 100); $term->read_history($file); print "[loaded ", int ($term->GetHistory), " command(s) from history file]\n"; }; $SIG{INT} = sub { print "\n" }; print "\n"; $term->ornaments('', '', '', ''); our $PS1; $PS1 = "\\# psh> " unless $PS1; while ( defined ($_ = $term->readline(sub_ps1_vars($PS1))) ) { s/^\s*//; s/\s*$//; s/[\r\n]//g; # s/\bs(\d+)/\$s[$1]/g; print "\r\e[2K"; # move to start of line and erase it $cmd ++; my $less_next_command = 0; if( m/^(?:q|e|quit|exit)\b/ ) { exit; } elsif( m/^\/?(?:hist|last)\s*(\d*)/ ) { my @hist = reverse $term->GetHistory; my $max = ($1>0 ? $1-1 : $#hist); for my $i ( reverse 0 .. $max ) { print "$i - $hist[$i]\n"; } } elsif( m/^\/?less\s+(.+)/ ) { $_ = $1; $less_next_command = 1; goto EXECUTE_AFTER_TRANSFORM; } elsif( m/^\/?(?:help|h)/ ) { system("perldoc", $0); } elsif( $ENV{EDITOR} and $ENV{HOME} and m/^\/?(?:conf?i?g?u?r?a?t?i?o?n?)/ ) { system($ENV{EDITOR} => "$ENV{HOME}/.pshrc"); } elsif( m/^\/?(?:doc|mod|m)\b\s*(.+)/ ) { system("perldoc", argparse($1)); } elsif( m/^\s*'\s*(.+)/ and my $args = balanced_single_quotes($1) ) { system(argparse($args)); } elsif( m/^\/?(?:func|f)\b\s*(.+)/ ) { system("perldoc", "-f", argparse($1)); } elsif( m/^\/?(?:s|l|stack|list)\b\s*(\d*)/ ) { my $max = ($1>0 ? $1-1 : $#s); for my $i ( reverse 0 .. $max ) { my $r = ""; if( my $R = ref $s[$i] ) { $r = "\t\t[$R]"; } print "s$i = $s[$i]$r\n"; } } elsif( m/./ ) { EXECUTE_AFTER_TRANSFORM: my $less; my $ofh; if( $less_next_command ) { open $less, '|-', ($ENV{PAGER}||'less') or die "unable to open ENV{PAGER}||less: $!"; $ofh = select $less; $less_next_command = 0; } my $eval_line; my $val; my $dt; { my $__before = time; local $SIG{INT} = sub { die "interrupted\n"; }; no strict "vars"; $val = eval $_; $eval_line = __LINE__; $dt = time - $__before; } if( $dt > 1 ) { my $s = ""; $s = "s" if $dt != 1; print "(dt=$dt second$s)\n"; } if( $@ ) { if( $@ eq "interrupted\n" ) { warn "(operation interrupted with ^C)\n"; } else { $@ =~ s/at \s*$0\s+line $eval_line//; # rare, but can happen if the error uses caller() stuff $@ =~ s/at\s*\(eval\s*\d+\)/in command #$cmd/; # make eval into command $@ =~ s/\s*line\s+\d+\.$//; # take the line off, it's 1 warn "ERROR: $@\n"; } } else { do_val( $val ); my $res; if( ref $val and not blessed $val ) { local $Data::Dumper::Indent = 0; $res = Dumper($val); $res =~ s/\$VAR\d\s*=\s*//; } else { $res = $val; } $res =~ s/([^\n[:print:]])/sprintf('\x%02x', ord($1))/eg; print "\$s[0] = $res\n"; } if( $less ) { select $ofh; close $less; } # this is actually automatic # $term->addhistory($_) if /\S/; } print "\n"; } sub sub_ps1_vars { my $p = shift; $p =~ s/\\#/$cmd/eg; return $p; } sub do_val { my $v = shift; unshift @s, $v unless "$s[$#s]" eq "$v"; pop @s while @s > 50; } sub argparse { my $args = shift; #TODO: handle quotes return split /\s+/, $args; } sub balanced_single_quotes { my $v = shift; my $cnt = () = $v =~ m/\'/g; return undef unless ($cnt/2) == int ($cnt/2); return $v; } __END__ =head1 NAME psh -- yet another perl shell, complete with fun =head1 SYNOPSIS I wanted a hilfe (pike shell) or python shell like setup for perl. Because I designed the shell through actual use, I ended up including a few handy shortcuts and commands. =head1 THE STACK Everything returned from expressions you type is dumped into the stack (@s). The most recent value is s0. You can type the literal 's0' anywhere in an expression and psh will substitue '$s[0]' (which also works). You can similarly type 's15' for '$s[15]'. The @s never grows bigger than 50. You can view the stack with: 'list', 'l', 'stack', and 's'. These commands take an optional number (e.g. 'l 10') argument to limit the lines printed. =head1 STRICTNESS Your expressions are evaluated under 'use strict'; but also under no strict 'vars'. Warnings are not enabled, but you can 'use warnings' in your .pshrc. =head1 HISTORY The history is nothing fancy. I highly recommend installing Term::ReadLine::Gnu, but that is a personal preference I suppose. ** However, your history will NOT save until you install it ** You can list your history with 'last'. Presently there is no way to actually execute something from history other than the obvious arrow keys and/or vim keys (iff applicable). =head1 COMPLETE COMMAND LIST You can lead each command with a '/' if you desire. Why would you want to? IRC and TinyFugue habits? The '/' is optional. last, hist - show the history s, l, stack, list - show the stack config, conf - edit your config (iff $ENV{EDITOR} set) less - pipe the results to less, if found in path doc, mod, m - fork of perldoc func, f - fork of perldoc -f ' - arbitrary fork (checks for unbalanced 's) help - this document q, e, quit, exit - exit =head1 COMPLETE LIST OF SUBSTITUTIONS *p = *PV -- from Math::Units::PhysicalValue (if available) *pv = *PV -- from Math::Units::PhysicalValue (if available) *d = *Dumper -- from Data::Dumper (if available) =head2 GLOB SUB EXAMPLES (Disclaimer: This may be a plug for PV. Meh.) psh> p "3,000 ft" psh> p "2 minutes" psh> ($s[1]/$s[2]) + "0 miles/hour" Violla, $s[0] is now set to 17.05 miles/hour! Lastly, because you probably do not even have PV installed, but most likely do have Data::Dumper (since it's required): psh> [qw(lol dude!)] psh> d $s[0] psh> d [1, 2, 3] Oh and one more because strange attractors are neat. psh> 7 psh> sqrt $s[0] psh> sqrt $s[0] psh> sqrt $s[0] =head1 FILES $ENV{HOME}/.psh_history - contains your command history $ENV{HOME}/.pshrc - evaled at starttime if it exists =head1 PS1 I intend to add many bash substitutions, but for now only \# (cmd number) actually works. You can (and possibly should) set your PS1 in your .pshrc. I choose this because I like blue: $PS1 = "\e[1;34m\\# psh>\e[0;37m "; =head1 AUTHOR Paul Miller =head1 COPYRIGHT Public Domain! I relinquish all my rights to anything written in this document/program. However, I politely request that you leave my name on the project unless you rewrite, add, or alter the project in such a way that the diff -u is bigger than the original source file. =head1 SEE ALSO NOTE: In a few ways this is a reduplication of the perl debugger. In many other ways, it is most definitely not. perl(1), perldebug(1), perldebtut(1) =cut