package BATsh;
######################################################################
#
# BATsh - Bilingual Shell for cmd.exe and bash in one script
#
# Version 0.03 -- Self-contained interpreter
#
# https://metacpan.org/dist/BATsh
#
# Copyright (c) 2026 INABA Hitoshi <ina.cpan@gmail.com>
#
# This version implements both cmd.exe and sh/bash command sets
# entirely in Perl.  No external cmd.exe, bash, or sh is required.
#
######################################################################

use 5.00503;
use strict;
BEGIN { if ($] < 5.006 && !defined(&warnings::import)) { $INC{'warnings.pm'} = 'stub'; eval 'package warnings; sub import {}' } }
use warnings; local $^W = 1;
BEGIN { pop @INC if $INC[-1] eq '.' }

use File::Spec ();
BEGIN { eval { require Cwd } }
use Carp qw(croak);
use vars qw($VERSION);
$VERSION = '0.03';
$VERSION = $VERSION;

require BATsh::Env;
require BATsh::CMD;
require BATsh::SH;

###############################################################################
# Architecture
###############################################################################
#
# BATsh is a self-contained bilingual shell interpreter.
#
# It splits a script into CMD sections and SH sections, then executes
# each section using its own pure-Perl interpreter:
#
#   BATsh::CMD  -- cmd.exe command set (SET, ECHO, IF, FOR, GOTO, ...)
#   BATsh::SH   -- sh/bash command set (echo, export, if/fi, for/done, ...)
#   BATsh::Env  -- shared variable store (bridge between both modes)
#
# MODE DETECTION: first non-empty, non-comment token of each section.
#   CMD: token is [A-Z 0-9 _ - \ / : . @ %]+ with at least one A-Z
#   SH:  anything else
#
# SECTION BOUNDARY:
#   CMD: parenthesis ( ) depth returns to 0
#   SH:  keyword depth (if/fi, for/done, ...) returns to 0
#
# ENV BRIDGE:
#   BATsh::Env::STORE is the single variable table.
#   CMD %VAR% and SH $VAR both read/write the same store.
#
###############################################################################

###############################################################################
# Global state
###############################################################################
my $_TMPCOUNT = 0;

# Subroutine registry: { LABEL => \@lines }
my %_SUBROUTINES = ();

###############################################################################
# Constructor
###############################################################################
sub new {
    my ($class, %args) = @_;
    BATsh::Env::init();
    return bless { verbose => $args{verbose} || 0 }, $class;
}

###############################################################################
# Public run interface
###############################################################################
sub run {
    my ($class_or_self, $file, %args) = @_;
    unless (-f $file) { croak "BATsh->run: file not found: $file" }
    local *SRCFH;
    open(SRCFH, $file) or croak "BATsh->run: cannot open $file: $!";
    my @lines = <SRCFH>;
    close(SRCFH);
    _ensure_env_init();
    # Set batch positional parameters: %0 = script path, %1..%9 = args, %* = all args
    my @script_args = defined($args{args}) ? @{$args{args}} : ();
    _set_batch_args($file, @script_args);
    _process_lines(@lines);
    return 1;
}

sub run_string {
    my ($class_or_self, $source) = @_;
    croak "BATsh->run_string: source required" unless defined $source;
    my @lines = map { "$_\n" } split(/\n/, $source, -1);
    _ensure_env_init();
    _process_lines(@lines);
    return 1;
}

sub run_lines {
    my ($class_or_self, @lines) = @_;
    _ensure_env_init();
    _process_lines(@lines);
    return 1;
}

sub _ensure_env_init {
    # Init only once per process
    BATsh::Env::init() unless %BATsh::Env::STORE;
}

###############################################################################
# _set_batch_args -- populate %0..%9 and %* in the Env store
#   %0  = script path (as passed to run())
#   %1  = first argument, ..., %9 = ninth argument
#   %*  = all arguments joined by single space (does not include %0)
###############################################################################
sub _set_batch_args {
    my ($script, @args) = @_;
    # Normalise $0: resolve to absolute path using File::Spec
    my $abs_script = defined $script ? $script : '';
    if ($abs_script ne '' && !File::Spec->file_name_is_absolute($abs_script)) {
        my $cwd = defined(&Cwd::cwd) ? Cwd::cwd() : '.';
        $abs_script = File::Spec->catfile($cwd, $abs_script);
    }
    BATsh::Env->set('%0', $abs_script);
    for my $n (1 .. 9) {
        BATsh::Env->set("%$n", defined($args[$n - 1]) ? $args[$n - 1] : '');
    }
    BATsh::Env->set('%*', join(' ', @args));
}

###############################################################################
# classify_token
###############################################################################
sub classify_token {
    my ($class_or_token, $token) = @_;
    unless (defined $token) { $token = $class_or_token }
    if ($token =~ /\A[A-Z0-9_\-\\\/\.:@%]+\z/ && $token =~ /[A-Z]/) {
        return 'CMD';
    }
    return 'SH';
}

###############################################################################
# Line parser
# Returns ($mode, $stripped_line, $first_token)
###############################################################################
sub _parse_line {
    my ($line) = @_;
    (my $s = $line) =~ s/\r?\n\z//;
    return ('EMPTY', $s, '')   if $s =~ /\A\s*\z/;
    return ('COMMENT', $s, '') if $s =~ /\A\s*(?:::|\@?REM(?:\s|\z))/i;
    return ('COMMENT', $s, '') if $s =~ /\A\s*#(?!!)/;
    (my $t = $s) =~ s/\A\s+//;
    my $first = ($t =~ /\A(\S+)/) ? $1 : '';
    return (classify_token($first), $s, $first);
}

###############################################################################
# CMD section depth: count unquoted ( )
###############################################################################
sub _cmd_paren_delta {
    my ($line) = @_;
    my ($delta, $in_q) = (0, 0);
    for my $ch (split //, $line) {
        if ($ch eq '"')     { $in_q = !$in_q }
        elsif (!$in_q) {
            $delta++ if $ch eq '(';
            $delta-- if $ch eq ')';
        }
    }
    return $delta;
}

###############################################################################
# SH section depth
###############################################################################
my %_SH_OPEN  = map { $_ => 1 } qw(if for while until case function select);
my %_SH_CLOSE = map { $_ => 1 } qw(fi done esac);

sub _sh_depth_delta {
    my ($first) = @_;
    my $l = lc($first);
    return  1 if exists $_SH_OPEN{$l} || $first eq '{';
    return -1 if exists $_SH_CLOSE{$l} || $first eq '}';
    return  0;
}

###############################################################################
# Subroutine extraction
###############################################################################
sub _extract_subroutines {
    my (@lines) = @_;
    my @out = (); my $in_sub = ''; my @sub_body = ();

    # Two-pass: first identify which :LABEL lines have a matching RET/RETURN.
    # Only those are BATsh subroutines; pure GOTO labels stay in the stream.
    my %is_sub_label = ();
    {
        my $cur = '';
        for my $line (@lines) {
            (my $s = $line) =~ s/\r?\n\z//;
            $s =~ s/\A\s+//;
            if ($s =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
                $cur = uc($1);
            }
            elsif ($cur ne '' && $s =~ /\A(?:RET|RETURN)\s*\z/i) {
                $is_sub_label{$cur} = 1;
                $cur = '';
            }
            elsif ($cur ne '' && $s =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
                # New label before RET: previous one is a GOTO label, not sub
                $cur = uc($1);
            }
        }
    }

    for my $line (@lines) {
        (my $s = $line) =~ s/\r?\n\z//;
        $s =~ s/\A\s+//;
        if ($s =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
            my $lbl = uc($1);
            if ($is_sub_label{$lbl}) {
                # This is a BATsh subroutine definition
                $_SUBROUTINES{$in_sub} = [@sub_body] if $in_sub ne '';
                $in_sub = $lbl; @sub_body = ();
                next;   # remove label line from stream
            }
            else {
                # This is a GOTO label: keep in stream for CMD interpreter
                push @out, $line if $in_sub eq '';
                push @sub_body, $line if $in_sub ne '';
                next;
            }
        }
        if ($in_sub ne '') {
            if ($s =~ /\A(?:RET|RETURN)\s*\z/i) {
                $_SUBROUTINES{$in_sub} = [@sub_body];
                $in_sub = ''; @sub_body = ();
            }
            else {
                push @sub_body, $line;
            }
            next;
        }
        push @out, $line;
    }
    $_SUBROUTINES{$in_sub} = [@sub_body] if $in_sub ne '';
    return @out;
}

###############################################################################
# call_sub / source_file
###############################################################################
sub call_sub {
    my ($class_or_self, $label, @args) = @_;
    $label = uc($label); $label =~ s/^://;
    croak "BATsh->call_sub: undefined subroutine :$label"
        unless exists $_SUBROUTINES{$label};
    $BATsh::Env::STORE{'BATSH_ARGC'} = scalar @args;
    for my $i (1 .. scalar @args) {
        $BATsh::Env::STORE{"BATSH_ARG$i"} = $args[$i-1];
    }
    _process_lines(@{$_SUBROUTINES{$label}});
    return 1;
}

sub source_file {
    my ($class_or_self, $file) = @_;
    croak "BATsh->source_file: file not found: $file" unless -f $file;
    local *SFHH;
    open(SFHH, $file) or croak "BATsh->source_file: cannot open $file: $!";
    my @src = <SFHH>;
    close(SFHH);
    _process_lines(@src);
    return 1;
}

###############################################################################
# SETLOCAL / ENDLOCAL  (public API)
###############################################################################
sub setlocal  { BATsh::Env::setlocal()  }
sub endlocal  { BATsh::Env::endlocal()  }

###############################################################################
# _exec_cmd_section -- run CMD lines through BATsh::CMD
###############################################################################
sub _exec_cmd_section {
    my (@lines) = @_;
    # Handle BATsh-native directives before CMD interpreter
    my @batch = ();
    for my $line (@lines) {
        (my $s = $line) =~ s/\r?\n\z//;
        $s =~ s/\A\s+//;
        if ($s =~ /\ASETLOCAL(?:\s+(.*))?\z/i) {
            my $opts = defined $1 ? $1 : '';
            # Pass the whole line through to CMD interpreter so it sees
            # ENABLEDELAYEDEXPANSION etc; also update Env flags here.
            push @batch, $line;
            next;
        }
        if ($s =~ /\AENDLOCAL\s*\z/i) {
            # Handled inside CMD.pm / _dispatch
            push @batch, $line;
            next;
        }
        if ($s =~ /\ACALL\s+:([A-Za-z_][A-Za-z0-9_]*)(.*)/i) {
            my ($lbl, $rest) = (uc($1), $2);
            _flush_cmd(\@batch) if @batch; @batch = ();
            $rest =~ s/\A\s+//;
            my @args = split /\s+/, $rest;
            eval { call_sub('', $lbl, @args) };
            warn $@ if $@;
            next;
        }
        if ($s =~ /\ACALL\s+(\S+\.batsh)(.*)/i) {
            my $bfile = $1;
            _flush_cmd(\@batch) if @batch; @batch = ();
            eval { source_file('', $bfile) };
            warn $@ if $@;
            next;
        }
        push @batch, $line;
    }
    _flush_cmd(\@batch) if @batch;
}

sub _flush_cmd {
    my ($lines_ref) = @_;
    return unless @{$lines_ref};
    BATsh::CMD::exec_block('BATsh::CMD', $lines_ref,
        _batsh => __PACKAGE__,
        _pushd_stack => [],
    );
}

###############################################################################
# _exec_sh_section -- run SH lines through BATsh::SH
###############################################################################
sub _exec_sh_section {
    my (@lines) = @_;
    my @batch = ();
    for my $line (@lines) {
        (my $s = $line) =~ s/\r?\n\z//;
        $s =~ s/\A\s+//;
        if ($s =~ /\A(?:source|\.)\s+(\S+\.batsh)/) {
            my $bfile = $1;
            _flush_sh(\@batch) if @batch; @batch = ();
            eval { source_file('', $bfile) };
            warn $@ if $@;
            next;
        }
        push @batch, $line;
    }
    _flush_sh(\@batch) if @batch;
}

sub _flush_sh {
    my ($lines_ref) = @_;
    return unless @{$lines_ref};
    BATsh::SH::exec_block('BATsh::SH', $lines_ref,
        _batsh => __PACKAGE__,
    );
}

###############################################################################
# _process_lines -- main dispatcher
###############################################################################
sub _process_lines {
    my (@source) = @_;
    @source = _extract_subroutines(@source);

    my @pending = (); my $cur_mode = ''; my $depth = 0;

    # Here-document tracking: while a here-document body is being collected,
    # body lines are appended to the current section verbatim and are NOT
    # reclassified (so an uppercase-leading body line is not misrouted to CMD).
    # Activation is deferred by one line so the trigger line itself is
    # classified normally.
    my $hd_delim         = undef;   # active delimiter (collecting body)
    my $hd_dash          = 0;
    my $pending_hd_delim = undef;   # set on the trigger line, activated next iter
    my $pending_hd_dash  = 0;

    for my $raw (@source) {
        chomp $raw;

        # Promote a pending here-document to active (trigger line already pushed)
        if (defined $pending_hd_delim) {
            $hd_delim         = $pending_hd_delim;
            $hd_dash          = $pending_hd_dash;
            $pending_hd_delim = undef;
        }

        # Collecting a here-document body: pass through unclassified.
        if (defined $hd_delim) {
            push @pending, $raw;
            my $probe = $raw;
            $probe =~ s/\A\t+// if $hd_dash;
            $hd_delim = undef if $probe eq $hd_delim;
            next;
        }

        my ($mode, $line, $first) = _parse_line($raw);

        # Detect a here-document opener on an SH line; defer activation so the
        # trigger line is classified normally this iteration.
        if ($mode eq 'SH') {
            my @hd = BATsh::SH::_hd_detect($raw);
            if (@hd) {
                $pending_hd_delim = $hd[2];
                $pending_hd_dash  = $hd[1];
            }
        }

        if ($mode eq 'EMPTY' || $mode eq 'COMMENT') {
            push @pending, $line if $cur_mode ne '';
            next;
        }

        if ($cur_mode eq '') {
            $cur_mode = $mode; $depth = 0;
            push @pending, $line;
            $depth += ($mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($first);
        }
        elsif ($mode eq $cur_mode) {
            push @pending, $line;
            $depth += ($mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($first);
            $depth = 0 if $depth < 0;
        }
        else {
            if ($depth > 0) {
                push @pending, $line;
                $depth += ($cur_mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($first);
                $depth = 0 if $depth < 0;
            }
            else {
                _flush_section($cur_mode, @pending) if @pending;
                @pending = ($line); $cur_mode = $mode; $depth = 0;
                $depth += ($mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($first);
            }
        }
    }
    _flush_section($cur_mode, @pending) if @pending;
}

sub _flush_section {
    my ($mode, @lines) = @_;
    return unless @lines;
    if ($mode eq 'CMD') { _exec_cmd_section(@lines) }
    else                { _exec_sh_section(@lines) }
}

###############################################################################
# REPL
###############################################################################
sub repl {
    my ($class_or_self) = @_;
    _ensure_env_init();
    print "BATsh $VERSION - Self-contained Bilingual Shell\n";
    print "Uppercase => CMD mode, lowercase => SH mode. EXIT/exit to quit.\n\n";

    my (@buf, $depth, $cur_mode) = ((), 0, '');
    my ($hd_delim, $hd_dash, $pending_hd_delim, $pending_hd_dash)
        = (undef, 0, undef, 0);
    while (1) {
        print $depth > 0 || defined($hd_delim) ? '    +> ' : 'BATsh> ';
        my $line = <STDIN>;
        last unless defined $line;
        chomp $line;

        # Promote pending here-document (trigger already buffered)
        if (defined $pending_hd_delim) {
            $hd_delim = $pending_hd_delim; $hd_dash = $pending_hd_dash;
            $pending_hd_delim = undef;
        }
        # Collecting here-document body: buffer verbatim, do not flush/classify
        if (defined $hd_delim) {
            push @buf, $line;
            my $probe = $line;
            $probe =~ s/\A\t+// if $hd_dash;
            $hd_delim = undef if $probe eq $hd_delim;
            if (!defined($hd_delim) && $depth == 0) {
                _flush_section($cur_mode, @buf);
                @buf = (); $cur_mode = ''; $depth = 0;
            }
            next;
        }

        if ($line =~ /\A\s*(?:EXIT|exit)\s*\z/) { print "Bye.\n"; last }
        next if $depth == 0 && $line =~ /\A\s*\z/;
        push @buf, $line;
        my (undef, undef, $first) = _parse_line($line);
        $cur_mode = classify_token($first) if $depth == 0 && $cur_mode eq '';
        # Detect here-document opener (SH only); defer activation one line
        if ($cur_mode eq 'SH') {
            my @hd = BATsh::SH::_hd_detect($line);
            if (@hd) { $pending_hd_delim = $hd[2]; $pending_hd_dash = $hd[1] }
        }
        $depth += ($cur_mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($first);
        $depth = 0 if $depth < 0;
        if ($depth == 0 && !defined($pending_hd_delim)) {
            _flush_section($cur_mode, @buf);
            @buf = (); $cur_mode = ''; $depth = 0;
        }
    }
}

###############################################################################
# Accessors
###############################################################################
sub version      { return $VERSION }
sub sh_available { return 1 }   # always: built-in SH interpreter

###############################################################################
# Run as script
###############################################################################
unless (caller) {
    BATsh::Env::init();
    if (@ARGV == 0) { BATsh->repl() }
    elsif ($ARGV[0] eq '-e') { shift @ARGV; BATsh->run_string(join("\n", @ARGV)) }
    else { BATsh->run($ARGV[0]) }
}

1;

__END__

=head1 NAME

BATsh - Bilingual Shell for cmd.exe and bash in one script (self-contained)

=head1 VERSION

Version 0.03

=head1 SYNOPSIS

  use BATsh;

  # Run a bilingual .batsh script
  BATsh->run('myscript.batsh');
  BATsh->run('myscript.batsh', args => ['arg1', 'arg2']);

  # Run source inline
  BATsh->run_string('echo hello from sh');
  BATsh->run_string("SET MSG=hello\nECHO %MSG%");

  # Interactive REPL
  BATsh->repl();

  # CMD features: pipe, tilde modifiers, SET /P
  BATsh->run_string('ECHO hello | perl -e "while(<STDIN>){chomp;print uc(\$_).chr(10)}"');
  BATsh->run_string("SET /P NAME=Enter name: ");

  # SH features: functions, expansions, pipelines, redirection
  BATsh->run_string(<<'BATSH');
  greet() {
      echo "Hello, \$1"
  }
  greet world
  x=\$(echo hello | perl -e 'while(<STDIN>){chomp;print uc}')
  echo \$x
  echo out > /tmp/out.txt
  BATSH

  # Perl 5.005_03 and later; pure-Perl, no external shell required.

=head1 DESCRIPTION

=head2 Executive Summary

BATsh is a self-contained bilingual shell interpreter written in pure Perl.
It runs cmd.exe batch syntax and bash/sh syntax in the B<same script file>,
switching automatically between CMD mode and SH mode on a line-by-line basis.
No external cmd.exe, bash, or sh is required -- everything runs inside Perl.

=head2 Mixed-Mode Sample

The following script demonstrates cmd.exe and bash sections coexisting and
sharing variables through the common BATsh::Env variable store.

  :: -- CMD section: sets a variable and calls a SH function via bridge --
  @ECHO OFF
  SET LANG=BATsh
  SET COUNT=3

  # -- SH section: reads CMD variables, uses functions and pipeline --
  greet() {
      echo "Hello from $1 (bash/sh mode)"
  }
  greet $LANG
  for i in 1 2 3; do echo "  item $i of $COUNT"; done
  result=$(echo "$LANG" | perl -e 'while(<STDIN>){chomp;print uc}')
  echo "Uppercase: $result"
  echo "log line" >> /tmp/batsh_demo.txt

  :: -- CMD section again: reads variable set by SH side --
  ECHO Back in CMD mode
  ECHO Uppercase result: %result%

BATsh features (both modes): pipelines (|), I/O redirection (> >> < 2>&1),
variable expansion (${var%pat} ${var^^} ${#var}), functions, shift, local.

=head1 FULL DESCRIPTION

BATsh is a self-contained bilingual shell interpreter written in pure Perl.
It implements both the cmd.exe command set and the sh/bash command set
entirely in Perl -- no external cmd.exe, bash, or sh is required.

Scripts are divided into CMD sections (uppercase first token) and SH sections
(lowercase first token). Both sections share a common variable store via
BATsh::Env, so variables set in a CMD section are immediately visible in the
next SH section and vice versa.

=head1 CMD MODE

Any line whose first token is all uppercase (A-Z, 0-9, path chars) is a CMD
line. CMD sections are executed by BATsh::CMD, which implements:

  ECHO, @ECHO OFF/ON
  SET VAR=value, SET /A expr (arithmetic)
  SET /P VAR=Prompt  (interactive prompt input from STDIN)
  IF "A"=="B" ... ELSE ..., IF /I (case-insensitive), IF NOT
  IF EXIST "path with spaces", IF DEFINED var, IF ERRORLEVEL n
  FOR %%V IN (list) DO ..., FOR /L %%V IN (s,step,e) DO ...
  FOR /F "tokens= delims= skip= eol= usebackq" %%V IN (src) DO ...
  GOTO :label, :label, GOTO :EOF
  CALL :label [args], CALL file.batsh
  SHIFT, SHIFT /N
  SETLOCAL [ENABLEDELAYEDEXPANSION|DISABLEDELAYEDEXPANSION], ENDLOCAL
  CD, DIR, COPY, DEL, MOVE, MKDIR, RMDIR, REN, TYPE
  PAUSE, EXIT [/B] [code], CLS, TITLE, VER, PUSHD, POPD
  cmd1 | cmd2  (pipeline via temporary file)
  &, &&, ||  (sequential, conditional-and, conditional-or)

=head2 Variable Expansion

C<%VAR%> references are expanded before each line is dispatched.
Variable names are B<case-insensitive> (C<SET foo=x> is visible as C<%FOO%>).

Inside parenthesised IF and FOR blocks, C<%VAR%> is expanded B<at parse time>
(before any commands in the block run), matching cmd.exe behaviour.  To see
a value updated inside a block, use delayed expansion:

  SETLOCAL ENABLEDELAYEDEXPANSION
  SET X=old
  IF 1==1 (
      SET X=new
      ECHO !X!       &:: prints "new" (delayed)
      ECHO %X%       &:: prints "old" (parse-time)
  )
  ENDLOCAL

=head2 Batch Parameters

C<%0> is the script path (absolute); C<%1>..C<%9> are positional arguments;
C<%*> is all arguments joined by space.  C<SHIFT> / C<SHIFT /N> shifts the
positional parameters.  C<CALL :label> saves and restores caller's arguments.

Batch-parameter tilde modifiers expand C<%0>..C<%9> components:

  %~0    dequote (strip surrounding "...")
  %~f1   full absolute path of %1
  %~d1   drive letter only   (e.g. C:)
  %~p1   directory path only (with trailing /)
  %~n1   filename without extension
  %~x1   extension only       (e.g. .bat)
  %~dp0  drive + directory    (most common usage)
  %~nx1  filename + extension

=head2 Redirection and Compound Commands

  ECHO text > file      stdout overwrite
  ECHO text >> file     stdout append
  prog 2> err.txt       stderr redirect
  & cmd                 sequential execution
  cmd1 && cmd2          run cmd2 only if cmd1 succeeded (ERRORLEVEL 0)
  cmd1 || cmd2          run cmd2 only if cmd1 failed   (ERRORLEVEL != 0)

The C<^> character escapes the next character:

  ECHO a^&b    prints  a&b   (& not treated as compound separator)
  ECHO a^^b    prints  a^b
  ECHO text^   next line is joined (line continuation)

=head1 SH MODE

Any line whose first token contains a lowercase letter is a SH line.
SH sections are executed by BATsh::SH, which implements:

  VAR=value, export VAR=value, unset VAR
  echo, printf
  if/then/elif/else/fi
  for VAR in list; do ... done
  while condition; do ... done
  until condition; do ... done
  case $var in pattern) ... ;; esac
  test / [ ... ]  (file, string, and integer comparisons)
  cd, pwd, exit, true, false, :, read, shift [N], local VAR=value
  $(( arithmetic )) -- +, -, *, /, %, and $1..$9 inside
  $( command ) and `command`  (command substitution, nested)
  cmd1 | cmd2 [| cmd3 ...]  (pipeline via temporary file)
  cmd1 && cmd2, cmd1 || cmd2, cmd1 ; cmd2  (compound commands)
  > >> < 2> 2>> 2>&1 1>&2  (I/O redirection)
  name() { ... }, function name { ... }  (function definitions)
  $VAR, ${VAR}, $1..$9, $@, $*, $#, $?, $$, $0
  ${VAR:-default}, ${VAR:=default}, ${VAR:+alt}
  ${VAR%pat}, ${VAR%%pat}   -- shortest/longest suffix removal
  ${VAR#pat}, ${VAR##pat}   -- shortest/longest prefix removal
  ${VAR/pat/rep}, ${VAR//pat/rep}  -- first/all substitution
  ${VAR^^}, ${VAR^}, ${VAR,,}, ${VAR,}  -- case conversion
  ${VAR:N:L}, ${VAR:N}  -- substring
  ${#VAR}  -- string length
  source / . file

=head1 REQUIREMENTS

Perl 5.005_03 or later. Core modules only. No external shell required.

=head1 BUGS AND LIMITATIONS

Commands that are not built in -- C<FINDSTR>, C<SORT>, C<MORE>, C<CHOICE>,
C<TIMEOUT>, C<XCOPY>, C<ROBOCOPY> and the like in CMD mode, and any
non-builtin program in SH mode -- are B<not> reimplemented in Perl. They
are invoked as external programs (via Perl's C<system>), so they work only
where the host operating system provides the corresponding executable
(e.g. F<FINDSTR.EXE> on Windows). This is by design: only the built-in
command set is guaranteed to run identically on every platform.

The built-in CMD interpreter does not implement:

=over

=item * Variable substring C<%VAR:~n,m%> and substitution
C<%VAR:str1=str2%> expansion.

=item * Dynamic pseudo-variables C<%RANDOM%>, C<%DATE%>, C<%TIME%>,
C<%CD%>, C<%CMDCMDLINE%>, and C<%ERRORLEVEL%> used as a variable reference.

=item * C<FOR /F> with C<usebackq> backtick-quoted commands on Windows
(the C<cmd /c> subprocess path is untested on Windows).

=back

The built-in SH interpreter does not implement:

=over

=item * Arrays, indexed and associative: C<arr[i]>, C<${arr[@]}>,
C<declare -A>.

=item * Filename (pathname) globbing such as C<echo *.txt> or
C<for f in *.pl>. (The C<*>, C<?> and C<[abc]> metacharacters are honoured
in C<case> patterns and in C<${VAR%pat}> / C<${VAR#pat}> expansion only,
not for expanding filenames on the command line.)

=item * Tilde expansion C<~/path> and C<~user> (only C<cd> with no
argument uses C<$HOME>).

=item * Brace expansion C<{a,b}> and C<{1..5}>.

=item * Here-strings (C<E<lt>E<lt>E<lt> word>) and process substitution
(C<E<lt>(cmd)>, C<E<gt>(cmd)>).

=item * The shell options C<set -e>, C<set -u>, C<set -x>, and the
builtins C<trap>, C<getopts>, C<select>, C<alias>, C<declare>/C<typeset>,
C<eval> and C<exec>.

=back

Common to both modes: a parenthesised group C<( ... )> does not run in a
separate sub-shell; it shares the one variable store, so there is no
variable-scope isolation.

Pipeline (C<|>), I/O redirection (C<E<gt>> C<E<gt>E<gt>> C<E<lt>>
C<2E<gt>> C<2E<gt>E<gt>> C<2E<gt>&1>), compound commands
(C<&&> C<||> C<;>), and function definitions are supported in both modes.

Here-documents (C<E<lt>E<lt>EOF>, C<E<lt>E<lt>'EOF'>, C<E<lt>E<lt>-EOF>)
B<are> supported in SH mode, with the limitations described in
L<BATsh::SH>: one here-document per command line, no here-strings, and
best-effort behaviour when combined with a pipeline or compound operator
on the same line.

Background execution (a trailing C<&>) B<is> supported in SH mode for
B<external> commands only, with the limitations described in
L<BATsh::SH>: only a trailing C<&> is recognised, built-ins/functions/
assignments/control words ignore it, there is no job control
(C<jobs>, C<wait>, C<fg>, C<bg>, C<%n>), and no signals are delivered to
background jobs. In CMD mode C<&> keeps its cmd.exe meaning as a
sequential separator.

Section boundary detection is token-based (uppercase vs. lowercase first
token). Mixed-case first tokens are treated as SH.

Please report bugs via the issue tracker:
L<https://github.com/ina-cpan/BATsh/issues>

=head1 SEE ALSO

L<BATsh::CMD>, L<BATsh::SH>, L<BATsh::Env>

=head1 AUTHOR

INABA Hitoshi E<lt>ina.cpan@gmail.comE<gt>

=head1 LICENSE

This software is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut
