Le Shell (It’s French)
A massive expansion on previous trials, this is an Perl implementation of an entire basic shell. It’s also on Github.
Brief overview of non-standard features:
- ‘!’ executes last command, ‘!#’ executes a specific command from history, specified by #.
- ‘* newprompt’ changes prompt to newprompt.
- ‘file filename.leshell’ executes the semicolon separated commands in the file.
There are two main issues and I’m offering an immediate 10 bonus points for an explanation!
- ‘bye’ doesn’t exit immediately if any error occurred.
- catching ^c (interupt) only works once.
#!/opt/local/bin/perl
# Brief overview of features:
# - '!' executes last command, '!#' executes a specific number command from history
# - '* prompt' changes prompt to prompt
# - 'file filename.leshell' executes the semicolon separated commands in the file
# KNOWN ISSUES:
# bye doesn't exit if any error occurred
# catching ^c only works once
use strict;
# use warnings;
use Term::ReadLine;
use Term::ReadLine::Gnu;
use Cwd;
use String::Similarity;
use POSIX;
use File::Glob;
use Env;# Makes environment vars work automagically
#$SIG{INT} = \&hiya;
#$SIG{CHLD} = "IGNORE";
my $debug = 0;
my $prompt = 'orig ';
my $term = new Term::ReadLine 'leShell';
my $pid;
$term->Attribs->ornaments( 0 );
sub generatePrompt {
if( $prompt eq 'orig ' ) {
return getlogin() . '@' . getcwd . '> ';
} else {
return $prompt;
}
}
# Return an array of array refs. Each subarray will be a command separated on words and
# stripped of extra whitespace.
sub cmdToArrayOfArrays {
my @cmd = split( /;/, shift );
my @commands;
foreach( @cmd ) {
$_ =~ s/^\s*//;
$_ =~ s/\s*$//;
my @tempCmdSplitOnWhite = split( /\s+/, $_ );
if( @tempCmdSplitOnWhite ) {
push( @commands, \@tempCmdSplitOnWhite );
}
}
return @commands;
}
# Passed a reference to an array of references to arrays. Trippy.
# Look for *'s in the commands that aren't changing prompt and substitute the appropriate expansion.
# Returns false if there was supposed to be an expansion and nothing was found.
sub expandGlob {
my $bigRef = shift;
foreach( @$bigRef ) {
my $littleRef = $_;
my $index = 0;
foreach( @$littleRef ) {
if( $_ =~ qr/\*/ and $index > 0 ) {
my @blob = glob( $_ );
unless( @blob ) {
print "Nothing found on expansion.\n";
return 0;
}
splice( @$littleRef, $index, 1, @blob );
last;
}
$index ++;
}
}
return 1;
}
# Pass app, returns directory/app_name on success. Prints failed otherwise.
sub searchPathForApp {
my $token = shift;
my @path = split( /:/, $ENV{'PATH'} );
my $bestFit = 0;
my $closestApp = "wtf", my $closestDir = "not/even/close";
unless( @path ) {
print "Broken path! Quitting.\n";
exit 0;
}
foreach( @path ) {
if( opendir( my $dir, $_ ) ) {
my $currentDir = $_;
while( readdir( $dir ) ) {
if( $_ eq $token ) {
closedir( $dir );
return $currentDir . "/" . $_;
} elsif( similarity( $_, $token ) > $bestFit ) {
$bestFit = similarity( $_, $token );
$closestApp = $_;
$closestDir = $currentDir;
}
}
closedir( $dir );
}
}
unless( $debug ) {
print( "$token not found. Did you mean $closestApp at $closestDir/$closestApp?\n" );
}
return 0;
}
# Pass an array of the command. Comes out with app name as arg[1] and dir as arg[0]
sub makeBeginningOfArrayExecName {
my $ref = shift;
if( @$ref[0] !~ qr/\// ) { # Not a dir, find dir and put on @cmd
my $dir = searchPathForApp( @$ref[0] );
if( $dir ) {
unshift( @$ref, $dir );
}
} else { # 0 is executable, parse for name and add at 1
@$ref[0] =~ qr/\/?([[:alpha:]]*)$/;
splice( @$ref, 1, 0, $1 );
}
}
# Takes a reference to an array of a command.
sub frkExec {
my $cmdRef = shift;
my @cmd = @$cmdRef;
$pid = fork();
if( $pid ) {
# Parent
return waitpid( $pid, 0 );
} elsif( $pid == 0 ) {
# Child
exec{ $cmd[0] } splice( @cmd, 1 ) or print STDERR "Couldn't execute " . $cmd[0] . " - " . $! . "\n";
} else {
die "Error forking.\n";
}
}
sub runCommands {
my $commandsRef = shift;
COMMAND: foreach( @$commandsRef ) {
# Here check for special chars separately. Prolly glob too.
if( @$_ ~~ qr/^\$/ ) {
subEnvVariable( \@$_ );
}
if( @$_ ~~ qr/.*~.*/ ) {
subSquiggly( \@$_ );
}
if( @$_[0] eq 'bye' ) {
byebye();
} elsif( @$_[0] eq '*' ) {
setPrompt( splice( @$_, 1 ) );
next COMMAND;
} elsif( @$_[0] =~ qw/!/ ) {
next COMMAND unless substituteHistory( \@$_ );
my @tempCmd = cmdToArrayOfArrays( "@$_" );
runCommands( \@tempCmd );
next COMMAND;
} elsif( @$_[0] eq 'file' ) {
runFile( @$_[1] );
next COMMAND;
} elsif( @$_[0] eq 'cd' ) {
my @dir = splice( @$_, 1 );
my $string = "@dir";
$string =~ s/^\s*//;
$string =~ s/\s*$//;
if( $string eq "" ) { print "No dir?\n"; next COMMAND; }
print "That's not a directory.\n" unless chdir( $string );
next COMMAND;
}
makeBeginningOfArrayExecName( \@$_ );
chomp( @$_ );
frkExec( \@$_ );
}
}
sub subSquiggly {
my $cmdRef = shift;
my $index = 0;
foreach( @$cmdRef ) {
@$cmdRef[$index] =~ s/~/$ENV{'HOME'}/g;
$index++;
}
}
sub subEnvVariable {
my $cmdRef = shift;
my $index = 0;
foreach( @$cmdRef ) {
if( $_ =~ qr/^\$([[:alpha:]]+)/ ) {
@$cmdRef[$index] =~ s/\$(([A-Z]|_)+)/$ENV{$1}/g;
}
$index++;
}
}
sub runFile {
my $fileName = shift;
$fileName =~ qr/\.([[:alpha:]]+)$/;
if( $1 eq 'leshell' ) {
my @command;
my $file;
if( !open( $file, "<", $fileName ) ) {
print "Cannot open file.\n";
return;
}
foreach( <$file> ) {
push( @command, $_ );
}
@command = cmdToArrayOfArrays( "@command" );
runCommands( \@command );
} else {
print "file command needs an argument file with '.leshell' extension.\n";
}
}
sub byebye {
print "See ya.\n";
POSIX:_exit( 0 );
}
sub getPrompt {
return $prompt;
}
sub setPrompt {
$prompt = "@_" . " "; # Mandatory space..
}
sub substituteHistory {
my $cmdRef = shift;
$term->remove_history( $term->where_history );
if( $term->where_history == 0 ) {
print "No history.\n";
return 0;
}
if( length( @$cmdRef[0] ) == 1 ) {
return 0 unless getLastHistory( \@$cmdRef );
} else {
return 0 unless getSpecificHistory( \@$cmdRef );
}
return 1;
}
sub getLastHistory {
my $cmdRef = shift;
my @history = $term->GetHistory();
my $size = @history;
@$cmdRef = split( /\s/, $history[ $size - 1 ] );
return "america";
}
sub getSpecificHistory {
my $cmdRef = shift;
my $requestNumber = substr( @$cmdRef[0], 1 );
if( $requestNumber > $term->where_history() ) {
print "Too big.\n";
return 0;
}
@$cmdRef = split( /\s/, $term->history_get( $requestNumber ) );
return "awesome";
}
sub hiya {
print "Got to hiya\n";
kill( 9, $pid );
showPrompt();
}
sub showPrompt {
if( !$debug ) {
PROMPT: while( defined( $_ = $term->readline( generatePrompt() ) ) ) {
my @commands = cmdToArrayOfArrays( $_ );
next PROMPT unless expandGlob( \@commands );
runCommands( \@commands );
}
}
}
showPrompt();
1
Leave a Comment