Changes for version 0.04 - 2026-06-07
- Inline-Perl portability: every shelled-out Perl one-liner in the distribution is rewritten into a form that satisfies BOTH external shells that BATsh dispatches to -- cmd.exe (system STRING on Win32) and /bin/sh -c (system STRING on Unix/BSD). The previous forms hit two distinct, OS-specific foot-guns: (A) Unix: a dollar token inside the code (e.g. "$_") was expanded by /bin/sh from the environment variable "_" (the last-arg / path that the shell exports), which is unpredictable on CPAN smokers and produced random failures such as "Bareword found where operator expected ... 1EERDtQcrK". (B) Win32: cmd.exe does not honour single quotes, so a one-liner wrapped in '...' was split on whitespace and Perl died with "Can't find string terminator". The portable form uses DOUBLE quotes and no shell-expandable dollar token, e.g. perl -e "..." -> perl -ne "print uc" (the default variable is consumed implicitly by uc, so nothing leaks to the shell). Rewritten in: lib/BATsh.pm POD, lib/BATsh/SH.pm POD, README, all 21 doc/batsh_cheatsheet.*.txt files, eg/05_cmd_comprehensive.batsh, eg/06_sh_comprehensive.batsh, and t/0006-new-features.t. The stderr sample in eg/06_sh_comprehensive.batsh is likewise switched from a single-quoted body to a double-quoted "print STDERR qq(...)" form.
- t/0007-extcmd-env.t: new regression test that locks in the portability fix above. EE01/EE02 run the pipeline and here-document patterns under several hostile values of the environment variable "_" and confirm correct uppercase output (this passes on every OS and actively defeats vector (A) on Unix). EE03 is the clean- environment baseline. EE04 is a static guard against vector (B): no inline "perl -e/-ne/-pe" anywhere in t/ eg/ doc/ lib/ README may be wrapped in single quotes. EE05 is a static guard against vector (A): no double-quoted inline Perl may contain a shell-expandable dollar token ($name, $_, ${...}, $1..$9); the harmless numeric $$ is exempt. Both static guards run on any OS, so a Windows run still catches a Unix-introduced regression and vice versa. Added to MANIFEST.
- External-Perl PATH portability (vector C): the test suite and the eg/ examples shell out to a bareword "perl", but a CPAN smoker frequently does NOT have the perl under test on PATH as "perl" (perlbrew/plenv, or perl invoked by absolute path). The bareword then resolves to nothing ("perl: not found", empty output), which failed t/0007 EE01/EE02 and t/0006 NF23/NF60 and -- worse -- let t/0006 NF07/NF21/NF22 report a corrupted, empty-named "ok" when the failed pipe disturbed the captured-STDOUT save/restore; in eg/06 it also hung (an empty "perl" command substitution fed a "while read ... < $EMPTY" redirect whose read fell back to terminal STDIN and blocked). Fixed WITHOUT touching the command strings or the examples (a bareword "perl" is the correct thing for an end user to type, and embedding an absolute $^X path would expose a Win32 backslash path to SH-mode quote/escape processing): each affected test now prepends the directory of the running interpreter ($^X) to PATH so the bareword "perl" resolves to the very perl now running the suite. The prepend is installed before the first BATsh::Env::init() (init() snapshots %ENV into STORE and sync_to_env() copies STORE back to %ENV before each external command). Touched: t/0006-new-features.t, t/0007-extcmd-env.t, t/9070-examples.t (the last for the eg/ child process). Verified on Linux with perl deliberately removed from the child PATH (and under a hostile "_"): all 606 tests pass, no failures, no hangs; the command strings and all eg/*.batsh examples are byte-for-byte unchanged.
- t/0006-new-features.t: the END block now sets "$? = 1 if $fail" instead of calling "exit 1", matching the INA_CPAN_Check.pm END-block convention adopted in 0.03 (an END block must not call exit, so that the harness sees the real plan/ok reconciliation).
- Documentation: the "self-contained" qualifier is removed from lib/BATsh.pm (header comment, module description, the run() banner, and the NAME / DESCRIPTION POD) and from README. BATsh dispatches external commands to a real shell, so describing the interpreter itself as "self-contained" was misleading; "bilingual shell interpreter written in pure Perl" is retained and accurate.
- SH nested command substitution fixed (lib/BATsh/SH.pm). $( ... ) has been advertised as supporting full nesting since 0.02, but a nested $( ... $( ... ) ) -- especially with a pipeline at each level -- collapsed to an empty string, and on Unix a nested pipeline could hang; the failure mode also differed between Windows and Unix. Three independent defects were responsible: (1) _cmd_subst() named its stdout-capture temp file with the process id alone (batsh_cap_$$.tmp). An inner $(...) reused the same path and unlink()'d it, so the outer level captured nothing. The capture file is now tagged with the active substitution-nesting depth. (2) _split_sh_pipe() counted the "(" of a "$(" twice, leaving the $( nesting depth stuck at 1 after a nested $(...); a bare "|" that followed it was then not recognised as a pipe. "$(" now consumes both characters and bumps the depth exactly once. (3) _exec_sh_pipe() named its per-stage temp files with the process id alone (batsh_shp_$$) and left its dup STDOUT/STDIN globs un-local()ised. A nested pipeline therefore clobbered the outer pipeline's stage file and saved handles; the outer's final segment found no input file and blocked on the real STDIN (a hang on Unix). The stage files are now tagged with the active pipeline-nesting depth and the handle globs are local()ised. All fixes are Perl 5.005_03 compatible (use vars package globals, bareword filehandles, 2-argument open). The command strings and the externally-visible API are unchanged.
- t/0008-nested-subst.t: new regression test for the fix above. NS01/ NS02 prove the capture-file depth fix with pure builtins (no "perl" on PATH required); NS03 is the single-level pipeline-in-$() baseline; NS04/NS05 cover nested $() with a pipeline at each level (defects 2 and 3); NS06 checks that two sibling $() pipelines on one line do not collide; NS07 covers an assignment from a nested pipeline substitution and its reuse. Like t/0006/0007 it prepends the running interpreter's directory to PATH so the bareword "perl" resolves on a smoker. Added to MANIFEST.
- eg/05_cmd_comprehensive.batsh: the SET/IF demonstration variable was renamed LANG -> GREETING. As LANG, the example exported LANG=BATsh into %ENV, and the external "perl" it later spawns then emitted a glibc "Setting locale failed ... LANG = BATsh" warning to STDERR on Unix (harmless, and invisible during "make test" because t/9070-examples.t captures and discards child STDERR, but visible when the example is run by hand; Windows perl does not warn). The rename keeps the example's behaviour identical and silences the Unix-only noise.
- eg/00_hello.pl: normalised from a CRLF line ending to LF, matching the rest of eg/ (the other examples are already LF). Perl tolerates the trailing CR on Unix, so this is a cosmetic consistency fix.
- Version bumped to 0.04 in lib/BATsh.pm, lib/BATsh/CMD.pm, lib/BATsh/SH.pm, lib/BATsh/Env.pm, Makefile.PL, META.yml and META.json. BATsh::CMD and BATsh::Env carry no changes other than the version; BATsh::SH changes are the version, the two POD one-liner rewrites noted above, and the nested command-substitution fix noted above.
Documentation
Modules
Bilingual Shell for cmd.exe and bash in one script
Pure Perl cmd.exe interpreter for BATsh
Shared variable store for BATsh
Pure Perl bash/sh interpreter for BATsh