#!/usr/bin/perl # Name: psh: Perl Shell # Author: David Ljung Madison # See License: http://MarginalHacks.com/License # Description: Just a perl-writing test - see also mish.c # Problems: # Try 'cd /*/apps/sh' # We reprint the line with the longest completion, but # this may be shorter than our current, pathwise. We # should include those lost path-elements in the printout, not # just no path at all. # Ideas: # Don't use - use perl regexp! <-------------- # Have () be used for setting $1, $2, etc.. # cpx and mvx would just be part of the shell! :) # (Using something like 'cp s/foo.*/bar.*/' would # exec 'cp foo.a bar.a foo.b bar.b foo.c bar.c.....' # Perl functions! (eval = eval??) # Shell ideas: # = cd (implement 'which' at the same time) # Do a regular check (or watch an interrupt) that causes this # process to run arbitrary commands (i.e., you could have a all-psh # command, such as to get all psh's to do a cd <- That would be so cool) # SEE: /opt/perl5/lib/IPC/Open3.pm or Open2.pm # Features: # History (!# and !substr) # aliases # Environment Variables (setting) # Dynamic, settable prompts (with most of the tcsh %char escapes) # Sourcing, .pshrc, .pshlogout # Globbing (wildcards in filenames) # Completion! # Job control! # # Missing: # Environment variable expansion in line parsing and completion # Quoting (of any type) # I/O redirection (>,<,|,etc..) # (In other words, real parsing) # Options: # tcsh-style word completion (words are separated by '_' '-' '.' not just '/' $WORD_COMPLETION=1; ######################### # Char-by-char mode ######################### sub char_mode { return if ($CHAR_MODE); $CHAR_MODE=1; return if $^O =~ /Win/; $ttyname=`tty`; system "/bin/stty -icanon -echo min 1 < $ttyname " if (! $?); } sub line_mode { return if (!$CHAR_MODE); $CHAR_MODE=0; return if $^O =~ /Win/; $ttyname=`tty`; `tty -s`; system "/bin/stty icanon echo < $ttyname " if (! $? ); } # Setup select masks $RIN=$WIN=$EIN=''; vec($RIN, fileno(STDIN), 1) = 1; $EIN=$RIN; sub get_char { my($ans); # Poll for character while watching interrupts while(!select($rout=$RIN, undef, undef, 0)) { if ($TOOK_INTERRUPT) { $TOOK_INTERRUPT=0; return -1; } } # Actually get the character read(STDIN,$ans,1); return $ans; } ######################### # Init the system ######################### my $WINDOWS = ($^O =~ /Win/) ? 1 : 0; sub init { chomp($hostname=`hostname`); if ($WINDOWS) { $ENV{HOME} ||= $ENV{USERPROFILE}; $ENV{USER} ||= $ENV{USERNAME}; $ENV{tty} = '??'; } else { chomp($ENV{USER}=`whoami`); chomp($ENV{tty}=`tty`); } ($PROGNAME=$0) =~ s|.*/||; # Interrupts $SIG{'INT'}='interrupt'; $SIG{'TERM'}='interrupt'; $SIG{'HUP'}='interrupt'; $SIG{'SUSP'}='interrupt'; $SIG{'QUIT'}='interrupt'; # Some settings $MAX_ALIASES=30; # Max aliases loop $MAX_HISTORY=10; $CURR_HISTORY=1; $ENV{prompt}="[%~] %t %n@%m:\n[%!] psh%# "; #%aliases=("logout","exit","ls","ls -sF","last","!-1","..","cd .."); $last_ret=0; @JOBS=(); &char_mode; my $pshrc = $WINDOWS ? "$ENV{HOME}\\pshrc" : "$ENV{HOME}/.pshrc"; $pshrc = "pshrc" if $WINDOWS && ! -r $pshrc; # Windows has problems with -r and drives source($pshrc) if -r $pshrc; } ######################### # Interrupts ######################### sub interrupt { alarm(0); # Turn off alarms # Usually happens during the get_char routine. # Best place to check is get_char anyways $TOOK_INTERRUPT=1 if (!$EXECUTING_COMMAND); return; } ######################### # All done ######################### sub all_done { if (!$JOBS_WARNED && keys %JOBS_RUNNING) { $JOBS_WARNED=2; print STDERR "You have running jobs:\n"; &show_jobs; return 0; } my $pshlogout = $WINDOWS ? "$ENV{HOME}/pshlogout" : "$ENV{HOME}/.pshlogout"; source($pshlogout) if -r $pshlogout; &line_mode; exit; } ######################### # Build and show the prompt ######################### sub prompt { $cut=-1; $lastcut=0; $prompt=$ENV{prompt}; $prompt=$ENV{prompt}="?> " if (!$prompt); if (index($prompt,"%")==-1) { $pr=$prompt; } else { # Consider using substr(...)="replace" method # OR: Andrew's idea: # $pr=~ s/%(.)/&{$percsubs{$1}}/eg; # # %percsubs = ( "t" => sub { time;... }, "u" => sub { $user }, ... ); # # But we only want to calc time once, so pass it in as a hash: # # $pr=~ s/%(.)/&{$percsubs{$1}}(\%timehash)/eg; # %percsubs = ( "d" => sub { ${$_[0]}{day}; }, ... ); $pr=""; while(($cut=index($prompt,"%",$cut+1)) != -1) { $pr=$pr.substr($prompt,$lastcut,$cut-$lastcut); $code=substr($prompt,++$cut,1); # Do time stuff ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$idst)=localtime; $ampm=($hour>12)?"pm":"am"; $ampmhour=($hour>12)?$hour-12:$hour; if ($min<10) { $min="0".$min; } if ($sec<10) { $sec="0".$sec; } if ($code eq "!" || $code eq "h") { $pr=$pr.$CURR_HISTORY; } elsif ($code eq "#") { $pr=$pr.($ENV{USER} eq "root" ?"#":">"); } elsif ($code eq "n") { $pr=$pr.$ENV{USER}; } elsif ($code eq "m") { $pr=$pr.$hostname; } elsif ($code eq "l") { $pr=$pr.$ENV{tty}; } elsif ($code eq "/" || $code eq "~") { # There must be a better way! :( $pwd= $WINDOWS ? `echo %CD%` : `pwd`; chomp($pwd); $pwd =~ s/^$ENV{HOME}/~/ if $code eq "~"; $pr=$pr.$pwd; } elsif ($code eq "t" || $code eq "@") { $pr=$pr.$ampmhour.":".$min."pm"; } elsif ($code eq "T") { $pr=$pr.$hour.":".$min; } elsif ($code eq "p") { $pr=$pr.$ampmhour.":".$min.":".$sec."pm"; } elsif ($code eq "P") { $pr=$pr.$hour.":".$min.":".$sec; } elsif ($code eq "d") { $pr=$pr.("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$wday]; } elsif ($code eq "D") { $pr=$pr.($mday<10?"0".$mday:$mday); } elsif ($code eq "w") { $pr=$pr.("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$mon]; } elsif ($code eq "W") { $pr=$pr.($mon<10?"0".$mon:$mon); } elsif ($code eq "y") { $pr=$pr.$year; } elsif ($code eq "Y") { $pr=$pr.($year>71?"19".$year:"20".$year); } elsif ($code eq "?") { $pr=$pr.$last_ret; } else { $pr=$pr."%".$code; } $lastcut=++$cut; } $pr=$pr.substr($prompt,$lastcut); } print STDERR "$pr"; } ######################### # Create the command array ######################### sub parse_comm { # SNIFTY TRICK TO TRY: (saves whitespace) # @arr= $line =~ /\S+|\s+/g; # Splits up whitespace and non-ws # my($str)=@_; $str =~ s/^\s+//; # Cheat - put whitespace before '&' if it doesn't have it $str =~ s/([^\s])&/$1 &/g; my(@a)=split(/\s+/,$str); return @a; } ######################### # Do the command ######################### sub Do { my(@a)=@_; # 'exit' twice will ignore the job warning $JOBS_WARNED-- if ($JOBS_WARNED>0); $cnt=0; # Alias loop counter ######################### # Expand aliases ######################### # ZZZZZZZZZZZZZZZZZZZZz # ZZZZZZZZZZZZZZZZZZZZz # Try using substr(...)="replace" method # Perhaps don't break into array until done with expansions # ZZZZZZZZZZZZZZZZZZZZz # ZZZZZZZZZZZZZZZZZZZZz while ($al=$aliases{$a[0]}) { $old=$a[0]; $a[0]=$al; @a=&parse_comm(join(" ",@a)); ( ($cnt++ > $MAX_ALIASES) || ($old eq (split(/\s+/,$al))[0]) ) && last; } ######################### # Expand history (assume no aliases in history) ######################### if (substr($a[0],0,1) eq "!") { $cnt=$CURR_HISTORY; $event=substr($a[0],1); if ($event < 0) { $event += $CURR_HISTORY; } if ($history{$event}) { $a[0]=$history{$event}; @a=&parse_comm(join(" ",@a)); } else { while(--$cnt>$CURR_HISTORY-$MAX_HISTORY-1) { if ($history{$cnt} =~ /^$event/) { @a=split(/\s+/,$history{$cnt}); last; } #if } # while } # else } # If we still have a !command, then we didn't find a history expansion if (substr($a[0],0,1) eq "!") { print substr($a[0],1).": Event not found.\n"; return; } ######################### # Add to history ######################### $history{$CURR_HISTORY}=join(" ",@a); delete $history{($CURR_HISTORY++)-$MAX_HISTORY}; ($comm,@args)=@a; ######################### # Evaluate environment variables # (We could do better than this...) ######################### map { s/\$(\w+)/$ENV{$1}/eg } @args; ######################### # Glob # (This kind of cheats because I believe perl fires off a csh to do the glob) ######################### @comm=($comm); # Start up the comm[] array for $arg (@args) { if (!/[\*\?\[]/) { @comm=(@comm,$arg); next; } @tmp=<${arg}>; # Glob if ($#tmp >= 0) { @comm=(@comm,@tmp); } else { @comm=(@comm,$arg); } } my $fullComm = join(' ',@comm); (undef,@args) = @comm; ######################### # Evaluate command # Check for built-ins first ######################### if (($comm eq "exit" || $comm eq "logout") && !(&all_done)) { ; # If all_done fails then we do nothing } elsif ($comm eq "jobs") { &show_jobs; } elsif ($comm eq "kill" && (@comm=&kill_wrapper(@comm)) && @comm[0] ne "kill") { ; # We just use the kill_wrapper and keep going } elsif ($comm eq "source") { shift(@comm); # Get rid of the 'source' &source(@comm); } elsif ($comm eq "env") { for $key (sort keys %ENV) { my $val = $ENV{$key}; $val =~ s/\n/\\n/mg; print " $key\t$val\n"; } } elsif ($fullComm =~ /^\s*(\S+)\s*=\s*(.*)$/) { # Set an environment variable my ($key,$val) = ($1,$2); $val=$1 if $val =~ /^"(.*)"$/; $ENV{$key}=$val; } elsif ($comm eq "echo") { print "@args\n"; } elsif ($comm eq "history") { for $key (sort {$a<=>$b;} keys %history) { print " $key\t$history{$key}\n"; } } elsif ($comm eq "unalias") { delete $aliases{$comm[1]}; } elsif ($comm eq "alias") { if ($#a == 0) { for $key (sort keys %aliases) { print "$key\t$aliases{$key}\n"; } } elsif ($#a==1) { $aliases{$comm[1]} && print "$aliases{$comm[1]}\n"; } else { if ($comm[1] eq "unalias") { print "$comm[1]: Too dangerous to alias that.\n"; } else { $a=shift(@args); # This is hokey kludgy crap. This should be from the original line $aliases{$a}=join(" ",@args); } } } elsif ($comm eq "cd") { print "$comm[1]: No such file or directory\n" if (!chdir($comm[1])); } elsif ($comm) { $EXECUTING_COMMAND=1; $run_background=(@comm[$#comm] eq "&"); pop(@comm) if ($run_background); # Check first if we can execute this command # This could have the benefit of timing out on hung PATH checks # (we can't just chdir after we fork...) unless ($pid=fork) { $WINDOWS ? exec join(" ",@comm) : exec @comm; print STDERR "$PROGNAME: $comm: Command not found.\n"; exit; } if ($run_background) { &add_job($pid,@comm); } else { waitpid($pid,0); } $last_ret=$?; $EXECUTING_COMMAND=0; } } ######################### # Erase a string (do as many ^H as needed) ######################### sub erase { my($str)=@_; my($i); for($i=0;$i $max); } $cols=($ENV{COLUMNS}?$ENV{COLUMNS}:80)-1; $col_size=$max+($files?3:2); $num_cols=int $cols/$col_size; # Print them out my($rows)=int $#options/$num_cols; my($i,$j); print STDERR "\n"; for($i=0;$i<=$rows;$i++) { for($j=0;$j<$num_cols;$j++) { if ($el = $options[$i+$j*($rows+1)]) { $s=$el; if ($files) { # Prune path info if they're files $s =~ s|.*/|| if ($files); if (-d $el) { $s.="/"; } elsif (-x $el) { $s.="*"; } } printf STDERR "%-${col_size}s",$s; } } print STDERR "\n"; } } ######################### # Get completion list # Second argument is the command. # This is a command completion if $command=0 ######################### sub get_completions { my($arg,$command)=@_; my(@tmp)=(); $arg.="*"; $arg=~s/\./*./g if ($WORD_COMPLETION); # Deal with words that start with '.' globbing treats them special :( # (echo *.t* doesn't show .t* files) # This solution is incomplete. # /*.a/*.b should become '/*.a/*.b /*.a/.b/ /.a/*.b/ /.a/.b' # Stupid globbing # (For each '*.stuff' add '.stuff') $arg.=" $1" if ($arg =~ /^\*(\..*)/); # (For each 'path/*.stuff' add 'path/.stuff') $arg.=" $`/$1" if ($arg =~ /\/\*(\..*)/); if ($command) { # This is an arg @tmp=<${arg}>; # Specific completion example: @tmp=grep(-d,@tmp) if ($command eq "cd"); } else { # This is a command foreach $p ( split(/:/,$ENV{PATH}) ) { next if (! -d $p); @new=<${p}/${arg}>; # This grep should be okay since all path elements will have / push(@tmp,grep(s|.*/||,@new)); } } return @tmp; } ######################### # Read a line, do completion, etc.. ######################### sub get_line { my ($c,$line)=(0,""); my $last_c = 0; while(1) { return if ( ($c=&get_char)==-1 ); # Backspace (wipe a character) if ($c eq "") { next if (!$line); $line=substr($line,0,length($line)-1); print STDERR " "; next; } # Tab: completion # EOF: (like tab but just list possible completions) if ($c eq "\t" || $c eq "") { $comm = $prev = $arg = 0; ($prev,$arg) = $line =~ /^(.*?)(\S+)$/; $prev=$line if (!$arg); ($comm) = $prev =~ /^\s*(\S+)/; @comps=&get_completions($arg,$comm); next if ($#comps == -1); if ($#comps == 0 && $c eq "\t") { # We only found one possibility $rest=$comps[0]; $rest.=(-d $comps[0]?"/":" "); $line="$prev$rest"; &erase($arg); print STDERR "$rest"; } else { $line=$prev.&longest_match(@comps) if ($c eq "\t"); # Print out the possible completions &column_print(($prev?1:0),@comps); &prompt; print STDERR "$line"; } next; } # End of line if ($c eq "\n" || $c eq "\r") { # Check for \ before end of line return "$line" if ($last_c ne "\\"); # Prompt 2 - should be settable? chop($line); # Remove the \ char print STDERR "\n...> "; next; } # Normal case print STDERR "$c"; $line.=$c; $last_c=$c; } } ######################### # Sourcing ######################### sub source { my (@files) = @_; foreach $file (@files) { my($HANDLE)="SOURCE_$file"; if (! open($HANDLE,$file)) { print STDERR "$PROGNAME: $file not found\n"; return 0; } while(<$HANDLE>) { s/#.*//; # No comments Do(@arr) if (@arr=&parse_comm($_)); # No check_jobs here? Guess not.. } close($HANDLE); } } ######################### # Job control ######################### sub kill_wrapper { my(@comm)=@_; for($i=1;$i<=$#comm;$i++) { if ($comm[$i] =~ /^%([0-9]*)$/) { if (!$JOBS_PID{$1}) { print STDERR "kill: No such job $1\n"; return 0; # This will skip the exec of kill } $comm[$i]=$JOBS_PID{$1}; } } return @comm; } sub add_job { # Pick next number $number=1; foreach $pid ( keys %JOBS_NUMBER ) { $number=$JOBS_NUMBER{$pid}+1 if ($JOBS_NUMBER{$pid}>=$number); } $JOBS_NUMBER{$pid}=$number; $JOBS_PID{$number}=$pid; $JOBS_RUNNING{$pid}=1; $JOBS_NAME{$pid}=join(" ",@comm); print STDERR "[$JOBS_NUMBER{$pid}] $pid\n"; } sub job_string { my($pid) = @_; return sprintf("%-6s%s %-30s%-30s%s", "[".$JOBS_NUMBER{$pid}."]", # pid " ", # ?? Is +- in most shells # What about Stopped and Terminated? ($JOBS_RUNNING{$pid}?"Running":"Done"), $JOBS_NAME{$pid}, (length($JOBS_NAME{$pid})>30?" ...":"") ); } sub show_jobs { foreach $pid ( sort keys %JOBS_NUMBER ) { print STDERR &job_string($pid)."\n"; } } sub check_jobs { foreach $pid ( keys %JOBS_RUNNING ) { if (waitpid($pid,1)) { $JOBS_RUNNING{$pid}=0; print STDERR &job_string($pid)."\n"; delete $JOBS_RUNNING{$pid}; delete $JOBS_NAME{$pid}; $number=$JOBS_NUMBER{$pid}; delete $JOBS_PID{$number}; delete $JOBS_NUMBER{$pid}; } } } ######################### # Main - Our actual shell code ######################### &init; while (1) { &prompt; $line=&get_line; print STDERR "\n"; Do(@arr) if (@arr=&parse_comm($line)); &check_jobs; }