BATsh
view release on metacpan or search on metacpan
- TEST FIX (t/0020-tilde-expansion.t): a failing TE01 or TE02 now
prints $HOME, the reference path, the reached path, the marker, the
shell diagnostic captured on STDERR, and (TE02) whether the target
directory still existed, as TAP comments. A "not ok" that says
nothing else cannot be diagnosed from a CPAN Testers report, which is
why the 0.10 FAIL could not be resolved without another release.
- TEST FIX (t/0030-expansion-literals.t): EL16 carried the same
pathname-string comparison, against Cwd::realpath() -- the exact form
that had already failed once on Win32 -- and had simply not been
reached by a smoker yet. It now uses the marker probe as well. No
call to Cwd::realpath() is left in the suite.
- TEST FIX (t/0020-tilde-expansion.t): the file used File::Spec without
loading it, relying on FindBin to pull it in. It is now imported
explicitly.
- NEW FILE t/lib/BATsh_TestOS.pm: the platform predicates the suite
needs, named once and reviewed once, instead of re-derived inline in
each .t file. BATsh is a cmd.exe emulator, so its main platform is
the one the author cannot smoke locally, and every Windows FAIL this
distribution has received so far has been a test defect of exactly
that shape: an assumption about Win32 written into a single case and
never looked at again. The module carries posix_file_semantics()
(Win32 keeps neither the O_CREAT|O_EXCL-does-not-follow-a-symlink
promise nor the mode bits), have_getpwnam(), fs_can_hold_name()
(asks the file system whether a byte sequence can be a name at all,
rather than assuming), dir_marker() / in_marked_dir() /
drop_marker() / existing_entry() (the marker probe described above),
tap_diag(), and is_windows() -- from which cygwin is deliberately
absent, since it does keep the POSIX guarantees.
- The file also states, as its header, the five rules those failures
produced: R1 never compare pathnames as strings; R2 never assert on
text the operating system produced (Windows error strings are
localised -- assert on exit status); R3 never write an assertion
that prints nothing when it fails; R4 skip with a printed reason
where the platform cannot support the premise, and probe rather than
test $^O at the call site; R5 a newly enabled case has never run on
Windows. They are in the test tree rather than in CONTRIBUTING
because CONTRIBUTING is generated by pmake and would be overwritten,
and because the place to find them is next to the code they govern.
- t/0015, t/0019, t/0020 and t/0030 now use those predicates. This is
a consolidation, not a change of behaviour: t/0019 keeps the same
skips through posix_file_semantics(), t/0015 CP12 replaces its
hand-rolled probe with fs_can_hold_name(), t/0020 and t/0030 take
the marker helpers and tap_diag(). One real gap closes with it --
t/0020 TE03 resolves ~user and therefore needs getpwnam(), which
native Windows perl does not implement; it now skips there with a
reason instead of relying on the shell to fail for the right reason.
- NEW FILE xt/win32_matrix.pl: a maintainer tool that runs the suite
from one Windows box over the environments the smokers actually
differ in -- five shapes of $HOME (backslash, forward slash,
trailing separator, embedded space, and not set at all), a working
directory on the same drive as $HOME and on another one (Windows
keeps a current directory per drive), ANSI code page 932 and 65001,
and any number of perls given with --perl=. Each of the Windows
FAILs this distribution has received would have been visible here
before upload. It is not part of "make test" and no installer or
smoker runs it.
- DOC FIX (lib/BATsh.pm, README): the SYNOPSIS and the description of
BATsh->main() both stated that bin/batsh.pl "is installed as batsh".
That holds only on Windows, where MakeMaker runs pl2bat over the
installed script; EXE_FILES keeps the basename otherwise, so a Unix
install puts it in the path as batsh.pl. Confirmed with a DESTDIR
install. Both places now read "installed as batsh.pl; on Windows
MakeMaker's pl2bat also provides batsh", and the command-line
examples in the SYNOPSIS use the name that actually exists.
- NEW FILE t/0000-environment.t: a report, not an assertion. It has
one case, that case always passes, and all it does is print what
the machine actually is -- perl, archname, the spelling of $HOME
and of the working directory and whether they share a volume,
which of the environment variables that decide behaviour are set,
whether STDIN and STDOUT are a console, whether getpwnam() and
symlink() work, whether the file system folds case, whether it
will accept a name carrying a CP932 trail byte of 0x5C -- as TAP
comments, which every harness carries into the CPAN Testers
report. It is numbered 0000 so the fingerprint stands above
whatever failed below it. Four Windows FAILs have now arrived
without any of this, and each cost a release to guess at; from
here a report says what it was run on. Nothing in this file
should ever be turned into a check: a fingerprint that can fail is
one more thing to diagnose rather than the means of diagnosing.
- TEST FIX (t/0020-tilde-expansion.t): TE03 reads the EXIT STATUS of
the failed "cd" instead of matching the text of its diagnostic.
The text was BATsh's own literal and not the operating system's, so
matching it did not breach rule R2 -- but it is indistinguishable
from a breach at a glance, and it tied a passing case to a wording
that is documentation rather than interface. The getpwnam() skip
goes with it: BATsh resolves ~user inside an eval and leaves an
unresolvable one literal either way, so the case now exercises the
same behaviour on a native Windows perl as on Unix instead of
skipping there.
- TEST FIX (t/lib/BATsh_TestOS.pm): existing_entry() now refuses any
name that is also reachable from the current directory. Such a
name would be found by the relative probe whether the shell had
moved or not, so t/0020 TE01 could pass without having tested
anything -- and that is a real configuration, not a hypothesis: a
smoker that sets $HOME to the build directory produces it. Where
no distinguishing entry exists the case now skips with a reason.
A vacuous pass hides a case forever; a skip hides it for one run.
- Two more rules in t/lib/BATsh_TestOS.pm, both drawn from the same
four failures. R6: "it passed on my Windows box" is not evidence,
because every one of those FAILs passed a local run first -- the
author's console has PERL5LIB, PERL5OPT, an interactive STDIN and
a $HOME he chose, and a smoker has none of them. R7: a green
report must still say what it was green on.
- NEW PREDICATES (t/lib/BATsh_TestOS.pm): have_symlink() (asks
whether a link can actually be made in a given directory, since
Win32 perl has had the function since 5.16 but fails without the
privilege), writable_dir() (-w reports the read-only attribute on
Win32 and says nothing about the ACL that decides), and
path_shape() (names the properties of a pathname that have changed
- QA (pmake.bat 0.44 -> 0.45): runtime prerequisites were collected
from lib/*.pm only, so a dependency that appeared solely in an
installed bin/ script never reached META -- BATsh shipped without
declaring FindBin, which bin/batsh.pl loads. The scan now covers
bin/*.pl as well, and the "lib" pragma is dropped from the result
alongside strict, warnings and vars.
- QA (pmake.bat 0.45 -> 0.46): the prerequisite scan matched "use
Module" only, so a module reached through "require Module" was
invisible to META. BATsh loads Cwd and POSIX that way and declared
neither. Both spellings are now collected, and they are not treated
alike: "use Module" and a bare "require Module" are unconditional and
become runtime requires, while a require inside eval is the pure Perl
way of writing "use this if it is here" -- the caller has a fallback
-- and becomes a recommendation instead. META.yml gains a
"recommends" block (spec 1.4) and META.json a
prereqs/runtime/recommends block (spec 2); Makefile.PL is unchanged,
since a recommendation must not enter PREREQ_PM. BATsh now declares
Cwd and POSIX under recommends, which is what the code actually
means. The scan also skips POD and full-line comments, so prose that
merely names a module can no longer invent a dependency.
- QA (pmake.bat 0.46): "pmake dist" runs its own copy of the K1 rule
over the sources listed in MANIFEST, and that copy did not say what
INA_CPAN_Check::check_K says. It exempted a comma followed by $, a
single quote or a double quote, on the reasoning that stripping a
string literal leaves a quote character behind -- but f("a","b")
really is missing the space, and so is f($x,$y), so the exemption was
hiding exactly the lines the rule exists to find. The two are now
spelled identically. Nothing in BATsh changes: the strict form finds
no violation anywhere in the set "pmake dist" scans.
- QA (t/lib/INA_CPAN_Check.pm 0.42 -> 0.43): style group K ran over
lib/*.pm alone. Unlike D, E and G -- which demand a shape only a
module is obliged to have -- K1, K2 and K3 ask for a space after a
comma and for [ @array ] and { %hash } in place of \@array and
\%hash, which are questions of spelling that apply to any Perl
source. Restricting them to lib/ left most of a pure-Perl
distribution unchecked, so K now runs over lib/, bin/ and t/ as
listed in MANIFEST. Five lines needed respacing, none of which
changes what the code does: t/0003 208, t/0005 40 and t/0009 273
open(STDOUT,'>&...') in the STDOUT-capture helpers, and t/9080 55 68
ok(0,"..."). With group K widened the suite runs 2071 tests.
- QA (t/lib/INA_CPAN_Check.pm 0.43): new check B8. B1 to B7 tie
$VERSION to META.yml, META.json, Makefile.PL and Changes, but the
"Version 0.00" line printed under =head1 VERSION was verified by
nothing -- G2 asks only that the heading exists. That line is what a
reader sees on metacpan and it is a hand-edited copy of a number kept
elsewhere, so it is precisely what goes stale unnoticed. B8 requires
every shipped Perl source carrying the heading -- lib/*.pm and the
bin/*.pl installed as EXE_FILES -- to state the distribution version
under it. It is one check for the distribution, so check_B goes from
six per module plus one to six per module plus two.
- bin/batsh.pl: gains the =head1 VERSION section the five modules
already carry, so the installed launcher documents which release it
belongs to. B8 above keeps it from drifting.
- Changes: this entry is dated 2026-08-01 JST, the day of the upload.
0.09 2026-07-27 JST (Japan Standard Time)
- FIX (SH, Windows): the result of an expansion is literal data, not
shell source. A line is expanded first and has its quotes removed
afterwards, and the quote-removal stage could not tell text written
in the script from text an expansion had just produced: every
backslash coming out of a variable, a command substitution or a
tilde expansion was re-read as a shell escape and deleted. With
HOME set to C:\home\flower,
cd ~ tried to enter C:homeflower
d="C:\Users\x" ; cd $d tried to enter C:Usersx
so practically every Windows pathname that travelled through a
variable was destroyed. This is what made t/0020-tilde-expansion.t
(TE01 TE02 TE04 TE07) fail in the CPAN Testers report for
BATsh-0.08 on Windows with perl 5.8.9.
POSIX quote removal applies to the source word only, never to the
result of an expansion, and BATsh now follows that rule. A
backslash produced by an expansion is held behind a NUL-delimited
sentinel -- a NUL can never occur in shell source -- until quote
removal has finished, while a backslash written in the script keeps
its escaping meaning. Protected are all of $VAR, ${VAR} and the
whole ${VAR...} family, array elements and ${arr[@]}, $1..$9, $@,
$*, $0, $( ... ), `...` and tilde expansion; the literal backslash
is restored again for the consumers that never dequote (external
command lines, here-document bodies, the variable store).
A tilde expansion is additionally protected against field
splitting, so a home directory whose name contains a space stays a
single word, as POSIX requires.
- FIX (SH): "test" / "[" now removes quotes from its operands the
same way command words do, instead of stripping one leading and one
trailing double quote. A half-stripped operand such as
"C:\dir"/sub could never name an existing file.
- FIX (SH): filename globbing keeps only names that exist. When
nothing matches, glob() hands the pattern back with its escape
backslashes already removed, which silently turned an unmatched
C:\dir\*.txt into C:dir*.txt. POSIX leaves an unmatched pattern
untouched, and so does BATsh now.
- FIX (SH): "echo -e" interprets \n, \t, \r and \\ arriving from an
expansion ("v='a\tb' ; echo -e $v") as well as from the source.
- FIX (SH): "export VAR=value" stores the value rather than the quote
characters around it. "export V='C:\x'" used to export the quotes
as part of the value, and "export V='a b'" was truncated at the
space; the operands are now split on unquoted whitespace and
dequoted exactly like an ordinary assignment.
- FIX (SH and CMD, Windows): pathname expansion no longer goes
through Perl's glob(). glob() reads a BACKSLASH AS AN ESCAPE
character, so an ordinary Windows pattern
d="C:\dir" ; for f in $d/*.txt ; do ... done
( run in 0.857 second using v1.01-cache-2.11-cpan-b16cb0d3907 )