BATsh

 view release on metacpan or  search on metacpan

lib/BATsh.pm  view on Meta::CPAN

    my $rc = _final_status();
    BATsh::SH::fire_exit_trap("BATsh::SH") if defined &BATsh::SH::fire_exit_trap;
    return $rc;
}

sub run_lines {
    my ($class_or_self, @lines) = @_;
    _prepare_source(\@lines, undef);
    _ensure_env_init();
    $_SCRIPT_EXIT = undef;
    BATsh::SH::reset_sh_options() if defined &BATsh::SH::reset_sh_options;
    _process_lines(@lines);
    my $rc = _final_status();
    BATsh::SH::fire_exit_trap("BATsh::SH") if defined &BATsh::SH::fire_exit_trap;
    return $rc;
}

# ----------------------------------------------------------------
# _final_status -- the exit status of the run just finished: the code of
# an executed exit/EXIT if any, else the last command's status.  After
# every section flush both interpreters hold the same value (see
# _flush_cmd/_flush_sh), so the SH side is authoritative here.
# ----------------------------------------------------------------
sub _final_status {
    return $_SCRIPT_EXIT if defined $_SCRIPT_EXIT;
    return BATsh::SH::get_status();
}

# ----------------------------------------------------------------
# last_status -- public accessor: the unified $? / %ERRORLEVEL% value.
# ----------------------------------------------------------------
sub last_status { return _final_status() }

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

###############################################################################
# set_encoding -- select the script encoding for multibyte-safe execution
#   BATsh->set_encoding('cp932');   # also: sjis gbk uhc big5 utf8 none auto
# The default is 'auto': a non-UTF-8 script containing bytes >= 0x80 is
# treated as CP932 and guarded (see BATsh::MB).  The environment variable
# BATSH_ENCODING, when set, overrides the default before the first run.
###############################################################################
my $_ENV_ENCODING_APPLIED = 0;

sub set_encoding {
    my ($class_or_self, $enc) = @_;
    $enc = $class_or_self
        if !defined($enc) && defined($class_or_self)
        && $class_or_self !~ /\ABATsh\b/;
    $_ENV_ENCODING_APPLIED = 1;   # explicit choice beats BATSH_ENCODING
    return BATsh::MB::set_encoding($enc);
}

sub encoding { return BATsh::MB::encoding() }

###############################################################################
# _prepare_source -- per-run encoding setup on the raw script lines
#   1. strip a UTF-8 BOM from the first line
#   2. apply an explicit per-run encoding, or BATSH_ENCODING (once),
#      or leave the current ('auto' by default) setting in place
#   3. under 'auto', detect the source encoding and activate the guard
#   4. guard-transform every line in place (identity when inactive)
###############################################################################
sub _prepare_source {
    my ($lines_ref, $encoding) = @_;
    $lines_ref->[0] = BATsh::MB::strip_bom($lines_ref->[0]) if @{$lines_ref};
    if (defined $encoding && $encoding ne '') {
        BATsh::MB::set_encoding($encoding);
        $_ENV_ENCODING_APPLIED = 1;
    }
    elsif (!$_ENV_ENCODING_APPLIED
        && defined $ENV{BATSH_ENCODING} && $ENV{BATSH_ENCODING} ne '') {
        BATsh::MB::set_encoding($ENV{BATSH_ENCODING});
        $_ENV_ENCODING_APPLIED = 1;
    }
    BATsh::MB::activate_for(join('', @{$lines_ref}));
    if (BATsh::MB::active()) {
        for my $l (@{$lines_ref}) { $l = BATsh::MB::enc($l) }
    }
    return 1;
}

###############################################################################
# _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) = @_;
    # Arguments arrive as RAW bytes from outside the interpreter
    # (command line / caller); guard them before they enter the store.
    @args = map { BATsh::MB::enc(defined $_ ? $_ : '') } @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', BATsh::MB::enc($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';
}

lib/BATsh.pm  view on Meta::CPAN

    + - * / % **  (** right-assoc; / % truncate toward zero)
    == != < <= > >=  && || !  (results 0/1)
    & ^ | ~ << >>  (bitwise; ~ is signed)
    = += -= *= /= %= <<= >>= &= ^= |=  (write back to the variable)
    ++ --  (prefix and postfix), ?: (ternary), comma
    0xNN hex and 0NN octal literals, $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
  arr=(a b c), arr+=(d e), arr[i]=v, arr[i]+=v  -- indexed arrays
  declare -a arr, declare -A map, typeset ...   -- array declaration
  map=([k]=v ...), map[k]=v                     -- associative arrays
  ${arr[i]}, ${map[key]}, $arr (== ${arr[0]})   -- element access
  ${arr[@]}, ${arr[*]}, ${#arr[@]}, ${#arr[i]}, ${!arr[@]}
  unset arr, unset arr[i]
  source / . file
  {a,b,c}, {1..5}, {a..e}[..step]  -- brace expansion
  shopt -s/-u extglob; ?(),*(),+(),@(),!()  -- extended pattern
    matching in case patterns and ${VAR%pat}-family patterns
  cmd <<< word  -- here-string
  <(cmd), >(cmd)  -- process substitution via temp file
  select VAR in list; do ... done  -- menu loop
  alias name=value, alias, unalias
  exec cmd, exec > file ...
  ( cmd1; cmd2 )  -- subshell command group, isolated scope

=head1 ENCODING (CP932 / Shift_JIS SUPPORT)

Scripts written in CP932 -- the ANSI encoding of Japanese Windows --
run correctly as of version 0.07, including the notorious "dame-moji"
whose second byte collides with an ASCII shell metacharacter:

  SO   (0x83 0x5C)  trail byte = backslash
  HYOU (0x95 0x5C)  trail byte = backslash
  PO   (0x83 0x7C)  trail byte = pipe
  CHI  (0x83 0x60)  trail byte = backtick
  DA   (0x83 0x5E)  trail byte = caret (the cmd.exe escape)

The encoding is B<auto-detected> by default: a non-UTF-8 source
containing bytes above 0x7F is treated as CP932.  Pure-ASCII and
UTF-8 scripts are unaffected.  Explicit selection:

  BATsh->run($file, encoding => 'cp932');   # per run
  BATsh->set_encoding('cp932');             # for the process
  set BATSH_ENCODING=cp932                  # environment variable
  perl lib/BATsh.pm --encoding=cp932 script.batsh

Supported names: cp932 (sjis), gbk (cp936), uhc (cp949), big5
(cp950), utf8, none, auto.  Under an active DBCS encoding the
substring and length operators C<${#VAR}>, C<${VAR:N:L}> and
C<%VAR:~n,m%> count characters rather than bytes.  A UTF-8 BOM on
the first line is stripped.  See L<BATsh::MB> for the mechanism.

=head1 EXIT STATUS

C<run>, C<run_string> and C<run_lines> return the script's B<final exit
status> as an integer: the argument of SH C<exit N> or CMD C<EXIT [/B] N>
if one was executed, otherwise the status of the last command.  C<EXIT>
with no code keeps the current C<ERRORLEVEL> (so C<false> then C<EXIT /B>
returns 1).  The same value is available afterwards as
C<BATsh-E<gt>last_status>.

At every CMD/SH section boundary the status is mirrored in both
directions, so an SH failure is immediately visible as C<%ERRORLEVEL%>
(and C<IF ERRORLEVEL n>) in the following CMD section, and a CMD failure
is visible as C<$?> in the following SH section.

C<BATsh-E<gt>main(@ARGV)> implements the command-line interface used by
the modulino (C<perl lib/BATsh.pm ...>) and by F<bin/batsh.pl> (installed
as C<batsh.pl>; on Windows MakeMaker's F<pl2bat> also provides
C<batsh>): C<--help>, C<--version>, C<-e 'source'>, a script filename,
or C<-> to read the script from STDIN.  With a script filename or with
C<->, the remaining arguments become C<%1>..C<%9> / C<$1>..C<$9>.  With
C<-e> they do B<not>: every remaining argument is joined with newlines
onto the inline source, so C<-e 'echo one' 'echo two'> runs a two-line
script.  The modulino calls
C<exit(BATsh-E<gt>main(@ARGV))>, so the OS-level exit code of the process
is the script's own status.  In the REPL, C<exit N> / C<EXIT N> ends the
session.

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

=back

Variable substring C<%VAR:~n,m%> / C<%VAR:~n%> / C<%VAR:~-n%> / C<%VAR:~n,-m%>
and in-place substitution C<%VAR:str1=str2%> / C<%VAR:*str1=str2%> are B<now
supported> as of version 0.05 (see L<BATsh::Env>).

Dynamic pseudo-variables C<%DATE%> (YYYY-MM-DD), C<%TIME%> (HH:MM:SS.cc),
C<%CD%> (current directory), C<%RANDOM%> (0-32767), C<%ERRORLEVEL%>, and
C<%CMDCMDLINE%> are B<now supported> as of version 0.05.



( run in 1.051 second using v1.01-cache-2.11-cpan-364913b4093 )