NAME

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

VERSION

Version 0.11

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 installs as "batsh.pl";
# on Windows MakeMaker's pl2bat also provides "batsh"):
#   batsh.pl script.batsh arg1 arg2   exit code = script status
#   batsh.pl -e 'echo hi'             run inline source
#   ... | batsh.pl - arg1             read the script from STDIN
#   batsh.pl --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
CHDIR, MD, RD, ERASE, RENAME  (cmd.exe spellings of CD, MKDIR,
  RMDIR, DEL, REN)
REM comment, :: comment
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
break [N], continue [N], return [N]
eval  (quote removal + re-execution with a second expansion)
let EXPR [EXPR ...]  (arithmetic evaluation; status 1 if last is zero)
type [-t|-p] NAME ...  (report how NAME resolves)
command [-v|-V] NAME [ARG ...]  (run bypassing functions; look up NAME)
umask [-S] [MODE]  (print/set the file-creation mask; octal or
  symbolic u=rwx,g=rx,o=rx)
hash [-r] [NAME ...]  (PATH lookup; no-op cache maintenance)
readonly [-p] [NAME[=VALUE] ...]  (mark a variable read-only)
mapfile / readarray [-t] [-d D] [-n N] [-O O] [-s S] [ARRAY]
  (read stdin lines into an indexed array)
declare -i NAME[=EXPR]  (integer attribute: assignments evaluate as
  arithmetic); declare -r NAME  (readonly attribute)
set -e / -u / -x, set +e/+u/+x, set -o errexit|nounset|xtrace
set -- [ARG ...] / set ARG ...  (replace $1..$9 / $@ / $#)
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
{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

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.pl; on Windows MakeMaker's pl2bat also provides batsh): --help, --version, -e 'source', a script filename, or - to read the script from STDIN. With a script filename or with -, the remaining arguments become %1..%9 / $1..$9. With -e they do not: every remaining argument is joined with newlines onto the inline source, so -e 'echo one' 'echo two' runs a two-line script. 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.

The result of an expansion is literal data as of version 0.09: a backslash arriving from a variable, from a command substitution or from a tilde expansion is no longer re-read as a shell escape by the quote-removal stage, so a Windows pathname held in a variable stays intact (d="C:\Users\x"; cd $d), while a backslash written in the script itself still quotes the character after it. A tilde expansion is also protected against field splitting, so a home directory whose name contains a space stays one word.

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.

set also takes operands as of version 0.09: set -- ARG ... replaces the positional parameters (and set -- clears them), and so does a first operand that is not an option, as in set a b c. The parameters are the ones a function call and shift use, so $1..$9, $@, $*, $#, shift and getopts all see them, which makes the usual

set -- -f value extra
while getopts f: opt; do ... done
shift $((OPTIND - 1))

idiom work; before 0.09 every operand was ignored and that loop never ran. set leaves OPTIND alone, as bash does. At most nine positional parameters are addressable.

Filename patterns (*, ?, [abc], [a-z], [!abc]) are matched by BATsh itself as of version 0.09, not by Perl's glob(), which reads a backslash as an escape character and so destroyed an ordinary Windows pattern (C:\dir\*.txt was searched for as C:dir*.txt and came back with its backslashes deleted). The separator is / everywhere and \ as well on Windows, where a path is written that way; on Unix a backslash stays an ordinary filename character. A leading . is matched only by a pattern that starts with ., every segment but the last has to be a directory, [!abc] negates, matches are sorted, case is ignored on Windows, and a pattern that matches nothing is left exactly as written. CMD-mode wildcards (FOR %f IN (...), DEL) use the same matcher.

How a Windows pattern is written differs between the two modes. A CMD-mode line has no escape character, so it is written as it looks (FOR %f IN (C:\dir\*.txt), DEL C:\dir\*.tmp). An SH-mode line follows bash, where a backslash quotes the next character, so a bare echo C:\dir\*.txt means C:dir*.txt there (in bash as well); keep the path in a variable, as a script does anyway, and the backslashes are data and stay separators:

d='C:\dir\'
for f in $d*.txt; do echo "$f"; done

The builtins let, type and command are supported as of version 0.08, evaluated internally in pure Perl (previously they fell through to an external shell and failed where none existed). let EXPR ... evaluates each argument as shell arithmetic, reusing the $(( )) evaluator, with exit status 0 when the last expression is non-zero and 1 when it is zero. type [-t|-p] NAME ... reports how each NAME resolves (alias, keyword, function, builtin, or a file on PATH). command [-v|-V] NAME [ARG ...] runs NAME bypassing a same-named shell function; -v prints how NAME would be invoked (the portable command -v foo feature test) and -V prints a verbose, type-style description.

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.

A control structure written entirely on one physical line may be followed by ;, &&, || or | and more commands (if ...; fi; echo done, for ...; done | sort) as of version 0.11; before that release everything after the closing fi/done/esac was silently discarded and the block collector went on to swallow the following lines. The same now holds for a one-line function definition and a parenthesised group (f(){ ...; }; f, ( ... ) && echo ok). A redirection still binds to the structure itself, as in bash.

The reserved word time is not implemented; time cmd is looked up as an external program and fails when none is installed.

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

Please report bugs to the author at <ina.cpan@gmail.com>, quoting the fingerprint that t/0000-environment.t prints at the top of the test output.

EXAMPLES

The eg/ directory contains runnable example scripts:

eg/00_hello.pl                Minimal Perl driver calling BATsh->run
eg/01_hello.batsh             Hello world in both modes
eg/02_env_bridge.batsh        Environment-variable bridge (CMD <-> SH)
eg/03_cmd_features.batsh      CMD-mode features and parameter modifiers
eg/04_sh_features.batsh       SH-mode features and expansions
eg/05_cmd_comprehensive.batsh Comprehensive CMD-mode tour
eg/06_sh_comprehensive.batsh  Comprehensive SH-mode tour
eg/07_mixed_comprehensive.batsh  Mixed CMD/SH comprehensive tour
eg/08_sh_arrays.batsh         SH indexed and associative arrays
eg/09_cmd_subroutines.batsh   CMD subroutines: CALL args, %~N, SHIFT
eg/10_sh_case.batsh           SH case..esac pattern branching
eg/11_sh_trap.batsh           SH trap / signal handling
eg/12_cmd_vs_sh.batsh         cmd and sh side by side (for students)
eg/13_cp932_demo.pl           CP932 (Shift_JIS) Japanese script demo
eg/14_sh_getopts.batsh        SH getopts option parsing (v0.07)

Run a .batsh example with:

perl -Ilib -MBATsh -e "BATsh->run(shift)" eg/01_hello.batsh

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.