The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl
#: sh
#: Perl simulator for /bin/sh (Bourne Shell)
#: 2006-02-01 2006-02-13

use strict;
use warnings;

use FindBin;
use lib "$FindBin::Bin/..";
use lib "$FindBin::Bin/../t/lib";
use Test::Util::Base;
#use Smart::Comments;

use Getopt::Std;
use Time::HiRes qw( sleep );

my %opts;
getopts('c', \%opts);

my $ExitCode = 0;
my $RedirectIndex;

$| = 1;

if ($opts{c}) {
    eval {
        process_shell(join ' ', @ARGV);
    };
    warn $@ if $@;
    exit($ExitCode);
} else {
    print '$ ';
    process_prompt();
    exit(0);
}

sub process_prompt {
    while (<STDIN>) {
        chomp;
        ### Got shell: $_
        last if /^\s*exit(?:\s+\d+)?\s*$/;
        eval {
            process_shell($_);
        };
        if ($@) {
            warn "$@\n";
            warn "[Error code $ExitCode returned.]\n";
        }
        print '$ ';
    }
}

sub process_shell {
    my $cmd = shift;
    #warn Dumper($cmd);

    $cmd =~ s/\n+/ /gso;
    my @raw_args = split_arg($cmd);
	### @raw_args
    my @args = process_args(0, @raw_args);
    return eval_cmd(@args);
}

sub process_args ($$) {
    #warn "!!!!!!!!!!!!!!!!!!!!!!";
    my $level = shift;
    my @raw_args = @_;
    my @args;
    foreach (@raw_args) {
        #warn "----------\n";
        #warn Dumper(@args, $_, @raw_args);
        #warn "----------\n";
        if ($_ eq ';') {
            eval_cmd(@args);
            @args = ();
        } elsif ($_ eq '>' or $_ eq '>>' or $_ eq '<') {
            $RedirectIndex = $#args;
            push @args, $_;
        } elsif ($_ eq '&&') {
            eval_cmd(@args);
            return if $ExitCode != 0;
            @args = ();
        } elsif ($_ eq '||') {
            eval_cmd(@args);
            return if $ExitCode == 0;
            $ExitCode = 0;
            @args = ();
        } elsif (/^"(.*)"$/o) {
            #warn "---------";
            #warn qq{Pusing "$1" into args\n};
            my $s = $1;
            process_escape( $s, q{\\@$\#} );
            #warn "$s";
            subs_env($s);
            push @args, $s if $s ne '';
        } elsif (/^'(.*)'$/o) {
            #warn "  Pusing '$1' into args\n";
            push @args, $1 if $1 ne '';
        } elsif (/^['"]/o) {
            $ExitCode = 1;
            die "sh: unexpected EOF while looking for matching `$&'\n";
        } else {
            #warn "  Remaining: $_\n";
            my $s = $_;
            if ($level == 0 and $s =~ /^\#/o) {
                return @args;
            }
            process_unquoted($s, $level, \@args);
            #warn "~~~~~~~~~~~~~~\n";
            #warn Dumper(@args, $_, @raw_args);
            #warn "~~~~~~~~~~~~~~\n";
        }
    }
    return @args;
}

sub process_unquoted {
    my ($s, $level, $rargs) = @_;
    return if $s eq '';
	## before: $s
    $s =~ s/\\(.)/$1/gso;
	## after: $s
    subs_env($s);
    my @subargs = split_arg($s) if $s =~ / /o;
    #warn Dumper(@subargs);
    if (@subargs > 1) {
        push @$rargs, process_args ($level+1, @subargs);
    } else {
        my @files = glob $s;
        if (@files > 1) {
            push @$rargs, @files;
        } else {
            push @$rargs, $s if $s ne '';
        }
    }
}

sub subs_env {
    $_[0] =~ s/\$(\w+)/defined $ENV{$1} ? "$ENV{$1}" : ''/geo;
}

sub touch (@) {
    my @files = @_;
    foreach my $file (@files) {
        my $in;
        open $in, ">>$file" and
            print $in '' and close $in or
            die "Can't touch $file: $!";
    }
}

sub eval_cmd {
    my @args = @_;
    return 0 if not @args;
    my $redir = $RedirectIndex;
    undef $RedirectIndex;
	my $exec = shift @args;
	if ($exec =~ /^\s+$/) {
		$exec = shift @args;
	}
    ### Got exec: $exec
	### Got args: @args
	if ($exec eq 'echo') {
        if ($redir) {
            my $op = $args[$redir];
            my @elems = @args[0..$redir-1];
            if ($op eq '>') {
                my $file = $args[$redir+1];
                open my $out, "> $file" or
                    die "Can't open $file for writing: $!\n";
                print $out "@elems\n";
                close $out;
            } elsif ($op eq '>>') {
                my $file = $args[$redir+1];
                open my $out, ">> $file" or
                    die "Can't open $file for writing: $!\n";
                print $out "@elems\n";
                close $out;
            } elsif ($op eq '<') {
                print "@elems\n";
            } else {
                die "Unexpected redirection operator: $op";
            }
        } else {
            print "@args\n";
        }
        return 0;
    } elsif ($exec eq 'rm') {
        foreach my $file (@args) {
            $file =~ s/\s+$//;
            if (not unlink $file) {
                warn "rm: cannot lstat `$file': $!\n";
                $ExitCode = 1;
                return;
            }
        }
        return 0;
    } elsif ($exec eq 'sleep') {
        sleep ($args[0]);
    } elsif ($exec eq 'pwd') {
        require 'Cwd.pm';
        print Cwd::cwd(), "\n";
    } elsif ($exec eq 'cd') {
        chdir $args[0];
    } elsif ($exec eq 'touch') {
        touch(@args);
        $ExitCode = 0;
        return;
    } elsif ($exec eq ':') {
        return;
    } elsif ($exec eq 'exit') {
        $ExitCode = $args[0] if defined $args[0];
        exit($ExitCode);
    } elsif ($exec =~ /make/i or ($args[0] and $args[0] =~ /make/i)) {
        $ExitCode = system $exec, @args;
        return;
    } else {
        warn "sh: unknown shell command: $exec";
        $ExitCode = 1;
        return;
    }
}