NAME

BATsh - Bilingual Shell for cmd.exe and bash in one script

VERSION

Version 0.07

SYNOPSIS

use BATsh;

# Run a bilingual .batsh script; the return value is the script's
# exit status ("exit 3" -> 3, "EXIT /B 5" -> 5, else last command)
my $rc = BATsh->run('myscript.batsh');
BATsh->run('myscript.batsh', args => ['arg1', 'arg2']);
print BATsh->last_status;    # same value, queried later

# From the command line (bin/batsh.pl is installed as "batsh"):
#   batsh script.batsh arg1 arg2      exit code = script status
#   batsh -e 'echo hi'                run inline source
#   ... | batsh - arg1                read the script from STDIN
#   batsh --help / --version

# CP932 (Shift_JIS) scripts on Japanese Windows: auto-detected,
# or select the encoding explicitly
BATsh->run('nihongo.batsh', encoding => 'cp932');
BATsh->set_encoding('cp932');    # also: sjis gbk uhc big5 utf8 auto

# 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 -ne "print uc"');
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 -ne "print uc")
echo \$x
echo out > /tmp/out.txt
BATSH

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

DESCRIPTION

Executive Summary

BATsh is a bilingual shell interpreter written in pure Perl. It runs cmd.exe batch syntax and bash/sh syntax in the 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.

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 -ne "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.

FULL DESCRIPTION

BATsh is a 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.

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

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

Inside parenthesised IF and FOR blocks, %VAR% is expanded 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

Batch Parameters

%0 is the script path (absolute); %1..%9 are positional arguments; %* is all arguments joined by space.

CALL :label arg1 arg2 ... invokes a subroutine as a true call frame: the subroutine receives its own %0 (the :label token), %1..%9 (the call arguments) and %* (their join), and the caller's parameters are saved before the call and restored on return. Arguments are %-expanded before the call and split with double-quote awareness, so CALL :sub "a b" %FILE% passes a b as one argument and the expanded value of %FILE% as the next. Nested calls each get an independent frame. The same arguments are also visible as $1..$9 / $@ when the subroutine body is written in SH mode.

SHIFT moves %2 into %1, %3 into %2, and so on, clears %9, and rebuilds %*; SHIFT /N begins the shift at %N (%1..%(N-1) are left unchanged).

Batch-parameter tilde modifiers expand %0..%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

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 ^ 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)

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 pat1|pat2) ... ;; *) ... ;; esac
  (|-patterns, * ? [abc] [a-z] [!abc] globs, ;& and ;;& fall-through)
test / [ ... ]  (file, string, and integer comparisons)
cd, pwd, exit, true, false, :, read, shift [N], local VAR=value
eval  (quote removal + re-execution with a second expansion)
set -e / -u / -x, set +e/+u/+x, set -o errexit|nounset|xtrace
trap 'cmd' SIG... / trap - SIG / trap '' SIG / trap [-p]  (EXIT + %SIG)
$(( arithmetic )) -- full C-style operator set:
  + - * / % **  (** 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

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 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 ${#VAR}, ${VAR:N:L} and %VAR:~n,m% count characters rather than bytes. A UTF-8 BOM on the first line is stripped. See BATsh::MB for the mechanism.

EXIT STATUS

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

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

BATsh->main(@ARGV) implements the command-line interface used by the modulino (perl lib/BATsh.pm ...) and by bin/batsh.pl (installed as batsh): --help, --version, -e 'source', a script filename, or - to read the script from STDIN; remaining arguments become %1..%9 / $1..$9. The modulino calls exit(BATsh->main(@ARGV)), so the OS-level exit code of the process is the script's own status. In the REPL, exit N / EXIT N ends the session.

REQUIREMENTS

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

BUGS AND LIMITATIONS

Commands that are not built in -- FINDSTR, SORT, MORE, CHOICE, TIMEOUT, XCOPY, ROBOCOPY and the like in CMD mode, and any non-builtin program in SH mode -- are not reimplemented in Perl. They are invoked as external programs (via Perl's system), so they work only where the host operating system provides the corresponding executable (e.g. 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:

  • FOR /F with usebackq backtick-quoted commands on Windows (the cmd /c subprocess path is untested on Windows).

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

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

Indexed and associative arrays -- arr=(a b c), arr+=(...), arr[i]=v, declare -A map, map=([k]=v ...), ${arr[i]}, ${arr[@]}, ${#arr[@]}, ${!arr[@]}, and unset arr[i] -- are now supported as of version 0.06 (see BATsh::SH). Element ordering for ${arr[@]} is ascending numeric index for indexed arrays and sorted key order for associative arrays (bash leaves the latter unspecified); "${arr[@]}" word-splits to one item per element in for lists.

Tilde expansion ~/path and ~user/path are supported as of version 0.07: word-initial, unquoted ~ in cd, in unquoted words produced by word-splitting (external command arguments, echo, eval), in test/[ file-test operands, and in the right-hand side of a plain VAR=value or prefix VAR=value command assignment. ~user resolves via getpwnam and is therefore Unix-like only (a no-op on Win32, where the word is left literal, matching bash's behaviour for an unresolvable login name). Not implemented: tilde expansion after : in colon-list assignments such as PATH=~/a:~/b (bash expands each colon-separated tilde in PATH/CDPATH/MAILPATH specifically); such values pass through unexpanded.

Brace expansion {a,b,c} and {1..5}/{a..e}[..step], extended pattern matching (shopt -s extglob; ?(), *(), +(), @(), !() in case patterns and in ${VAR%pat}-family patterns), here-strings (<<< word), process substitution (<(cmd), >(cmd)), and the select, alias/unalias, and exec builtins are now supported as of version 0.07 (see BATsh::SH).

The builtin getopts is supported as of version 0.07: it parses single-character options with the usual OPTIND/OPTARG protocol, clustered flags (-abc), attached (-oVALUE) and separate (-o VALUE) option arguments, the -- end-of-options marker, and both the default (diagnostic on STDERR) and silent (leading : in the optstring) error-reporting modes. See "getopts" in BATsh::SH.

The shell options set -e (errexit), set -u (nounset) and set -x (xtrace) are supported as of version 0.07, including the long forms set -o errexit|nounset|xtrace, the +e/+u/+x off switches, and combined letters (set -eux). Known limitations: set -x traces the raw pre-expansion command line (tracing an expanded copy would execute $(...) substitutions twice), and under set -u the offending command first completes with the empty expansion before the script stops with status 1. The options are reset at the start of each top-level run/run_string/run_lines, so set -e does not leak into a later run in the same process.

The builtin eval is supported as of version 0.07: one level of quote removal, concatenation, and re-execution with a second round of expansion (POSIX semantics).

trap is supported in SH mode: trap 'cmd' SIGSPEC... registers a handler, trap - SIGSPEC resets to default, trap '' SIGSPEC ignores, and trap / trap -p lists. Real signals are bridged to Perl's %SIG; the EXIT pseudo-signal (also 0) runs when the script ends or on exit. The handler is expanded when it fires. See "Traps and Signals" in BATsh::SH.

In SH mode, a parenthesised group ( ... ) is a subshell command group as of version 0.07: variable, array, function, and alias changes, and cd, made inside it do not affect the calling shell (approximated by snapshot/restore around the body, since this interpreter never forks -- see "Subshell Command Groups" in BATsh::SH). In CMD mode, ( ... ) is only recognised as an IF/FOR block delimiter (as in cmd.exe); it is not a general-purpose command group and has no associated variable-scope isolation.

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

Here-documents (<<EOF, <<'EOF', <<-EOF) are supported in SH mode, with the limitations described in BATsh::SH: one here-document per command line, and best-effort behaviour when combined with a pipeline or compound operator on the same line. Here-strings (<<< word) are a separate feature (also supported as of version 0.07; see above).

Background execution (a trailing &) is supported in SH mode for external commands only, with the limitations described in BATsh::SH: only a trailing & is recognised, built-ins/functions/ assignments/control words ignore it, there is no job control (jobs, wait, fg, bg, %n), and no signals are delivered to background jobs. In CMD mode & 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: https://github.com/ina-cpan/BATsh/issues

SEE ALSO

BATsh::CMD, BATsh::SH, BATsh::Env

AUTHOR

INABA Hitoshi <ina.cpan@gmail.com>

LICENSE

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