BATsh

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      the loop variable is substituted per-iteration via an internal placeholder.
    - IF /I (case-insensitive comparison) is now parsed before plain == so
      that "/I" is not consumed as part of the left-hand operand.
    - IF EXIST now handles quoted paths that contain spaces.
    - ECHO no longer resets ERRORLEVEL to 0 after printing.
    - FOR /F fully implemented:
        tokens=N,M-P  select specific token columns
        tokens=N*     select token N and put the remainder in the next variable
        delims=CHARS  field delimiters (default space/tab)
        skip=N        skip the first N lines of the source
        eol=C         skip lines beginning with character C (default ;)
        usebackq      swap quoting: "file" reads a file, 'cmd' runs a command
      Sources: bare filename, quoted filename, 'command' (backtick output),
      and ("literal string").
    - & (sequential), && (conditional-success), || (conditional-failure)
      compound commands are now supported.
    - SET VAR=value: variable name regex relaxed to accept any non-'=' prefix,
      matching cmd.exe's permissive variable naming.

    [BATsh::SH]
    - Full bash/sh interpreter implemented as Pure Perl (no external shell).

README  view on Meta::CPAN

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)

  Variable Expansion

lib/BATsh.pm  view on Meta::CPAN


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

lib/BATsh/CMD.pm  view on Meta::CPAN

#
# BATsh::CMD - Pure Perl cmd.exe interpreter
#
# v0.02 changes (cmd.exe compatibility fixes):
#   1. Environment variable case-insensitivity (via Env.pm)
#   2. ^ escape character: protects & | < > and line continuation
#   3. Redirect/pipe: > >> 2> 2>> < | parsed before dispatch
#   4. SETLOCAL ENABLEDELAYEDEXPANSION + !VAR! (via Env.pm)
#   5. IF block pre-expansion: entire IF block expanded at parse time
#      (matching cmd.exe's "parse before execute" semantics)
#   6. FOR /F: tokens= delims= skip= eol= usebackq
#   7. IF /I must be parsed BEFORE plain == to avoid shadowing
#   8. ECHO no longer resets ERRORLEVEL
#   9. SETLOCAL passes option string to Env::setlocal()
#  10. IF EXIST handles quoted paths with spaces
#  11. Pipeline (|): _split_compound detects |, _exec_pipe chains via tmpfile
#  12. SET /P VAR=Prompt: reads one line from STDIN
#  13. SHIFT / SHIFT /N: shifts %1..%9 and %* positional parameters
#  14. Batch-parameter tilde modifiers via Env::expand_cmd():
#      %~0 %~f1 %~d0 %~p0 %~n1 %~x1 %~dp0 %~nx1 (f d p n x combinable)
#  15. & && || compound commands (_exec_compound)

lib/BATsh/CMD.pm  view on Meta::CPAN

            _exec_line($class, $do_line, $lines_ref, $labels_ref, $i_ref, $opts_ref, 1);
        }
        last if $_GOTO_LABEL ne '';
    }
    return 0;
}

# ----------------------------------------------------------------
# FOR /F
#
# Options string (inside quotes): tokens= delims= skip= eol= usebackq
# Source:
#   "filename"    -- iterate lines of file (or usebackq: command output)
#   'command'     -- command output (or usebackq: literal filename)
#   ("string")    -- tokenize the string itself
# ----------------------------------------------------------------
sub _cmd_for_f {
    my ($class, $opts_str, $var, $source_str, $do_part, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;

    # Strip outer quotes from opts_str
    $opts_str =~ s/\A"//; $opts_str =~ s/"\z//;

    # Parse options
    my $tokens_spec = '1';       # default: first token only
    my $delims      = " \t";     # default delimiters
    my $skip        = 0;
    my $eol         = ';';       # default: skip lines starting with ;
    my $usebackq    = 0;

    $usebackq = 1 if $opts_str =~ /usebackq/i;
    if ($opts_str =~ /tokens=(\S+)/i) {
        $tokens_spec = $1;
        $tokens_spec =~ s/,\z//;
    }
    if ($opts_str =~ /delims=([^\s"]*)/i) {
        $delims = $1;
        $delims = ' ' if $delims eq '';  # delims= (empty) means no split? No: empty = space only
    }
    elsif ($opts_str =~ /delims=\s*\z/i) {
        $delims = '';  # delims= with nothing = no delimiter (whole line = one token)
    }
    if ($opts_str =~ /skip=(\d+)/i)  { $skip = int($1) }
    if ($opts_str =~ /eol=(.)/i)     { $eol = $1 }

    # Parse tokens spec: e.g. "1,2,3" "1-3" "1,2*" "*"
    my @token_indices = _parse_tokens_spec($tokens_spec);
    my $want_star = ($tokens_spec =~ /\*/) ? 1 : 0;

    # Determine source lines
    my @lines_to_process;
    $source_str =~ s/\A\s+//; $source_str =~ s/\s+\z//;

    if ($source_str =~ /\A'([^']*)'\z/ || ($usebackq && $source_str =~ /\A`([^`]*)`\z/)) {

lib/BATsh/CMD.pm  view on Meta::CPAN

    # Determine variable names: %%a and following letters for extra tokens
    my @var_names;
    for my $i (0 .. $#token_indices) {
        push @var_names, chr(ord($var) + $i);
    }
    # Star token goes to the next letter after the listed ones
    my $star_var = chr(ord($var) + scalar @token_indices);

    for my $src_line (@lines_to_process) {
        $src_line =~ s/\r?\n\z//;
        # Skip eol lines
        next if $eol ne '' && $src_line =~ /\A\Q$eol\E/;
        next if $src_line =~ /\A\s*\z/;

        # Tokenize
        my @tokens;
        if ($delims eq '') {
            @tokens = ($src_line);
        }
        else {
            my $escaped_delims = quotemeta($delims);
            @tokens = split /[$escaped_delims]+/, $src_line;

lib/BATsh/CMD.pm  view on Meta::CPAN

  cmd1 & cmd2     run cmd2 unconditionally after cmd1
  cmd1 && cmd2    run cmd2 only if cmd1 succeeded (ERRORLEVEL 0)
  cmd1 || cmd2    run cmd2 only if cmd1 failed    (ERRORLEVEL != 0)

=head2 FOR /F Options

  tokens=1,2-4   which token columns to capture (1-based)
  tokens=1*       token 1 to %%a; remainder to %%b
  delims=CHARS    field separator characters (default: space and tab)
  skip=N          skip first N lines
  eol=C           skip lines beginning with C (default: ;)
  usebackq        "file" reads a file; 'cmd' runs a command

Sources: bare filename, C<"quoted filename">, C<'command'>,
or C<("literal string")>.

=head2 ERRORLEVEL

C<IF ERRORLEVEL n> is true when ERRORLEVEL E<gt>= n (not equality).
ECHO does B<not> reset ERRORLEVEL (unlike some broken implementations).

t/0005-verify_compat.t  view on Meta::CPAN

######################################################################
#
# 0005-verify_compat.t  cmd.exe compatibility: 6 items
#
# Verifies the 6 compatibility fixes implemented in v0.02:
#   1. Environment variable case-insensitivity
#   2. ^ escape character
#   3. I/O redirection (> >> 2> <)
#   4. SETLOCAL ENABLEDELAYEDEXPANSION / !VAR!
#   5. IF/FOR block %VAR% parse-time expansion
#   6. FOR /F (tokens= delims= skip= eol= usebackq)
#
# COMPATIBILITY: Perl 5.005_03 and later
#
######################################################################
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 FindBin ();

t/0005-verify_compat.t  view on Meta::CPAN


    sub {
        my $out = _capture_cmd(
            'FOR /F "tokens=1*" %%a IN ("one two three") DO ECHO [%%a][%%b]',
        );
        _ok($out eq "[one][two three]\n", '6-4: FOR /F tokens=1* (star=remainder)');
    },

    sub {
        my $tmp = _tmpfile('ff5', "#comment\n", "data_line\n");
        my $out = _capture_cmd("FOR /F \"eol=#\" %%a IN ($tmp) DO ECHO %%a");
        unlink $tmp;
        _ok($out eq "data_line\n", '6-5: FOR /F eol=# skips lines');
    },

    sub {
        my $tmp = _tmpfile('ff6', "key1:val1\n", "key2:val2\n");
        my $out = _capture_cmd(
            "FOR /F \"tokens=1,2 delims=:\" %%a IN ($tmp) DO ECHO %%a=%%b",
        );
        unlink $tmp;
        _ok($out eq "key1=val1\nkey2=val2\n", '6-6: FOR /F multi-line file key:value');
    },



( run in 0.911 second using v1.01-cache-2.11-cpan-98e64b0badf )