BATsh
view release on metacpan or search on metacpan
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).
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.633 second using v1.01-cache-2.11-cpan-98e64b0badf )