Revision history for BATsh

0.06  2026-06-27 JST (Japan Standard Time)

    - SH indexed and associative arrays (BATsh::SH). The single most useful
      missing piece of bash-script compatibility is now implemented:

        arr=(a b c)            indexed array literal
        arr+=(d e)             append at the next index
        arr[i]=v, arr[i]+=v    element assignment / string append
        declare -a arr         declare an (empty) indexed array
        declare -A map         declare an associative array
        typeset -a / -A        accepted as an alias of declare
        map[key]=value         associative element assignment
        map=([k1]=v1 [k2]=v2)  associative (or sparse indexed) literal
        ${arr[i]}, ${map[key]} element access; $arr is short for ${arr[0]}
        ${arr[-1]}             negative index counts back from the last element
        ${arr[@]}, ${arr[*]}   all element values
        ${#arr[@]}             number of set elements
        ${#arr[i]}             length of one element's value
        ${!arr[@]}             list of indices (indexed) or keys (associative)
        unset arr, unset arr[i] remove whole array / single element

      Indexed subscripts are evaluated arithmetically (so ${arr[$i]} and
      ${arr[i+1]} work) and indexed arrays may be sparse; associative
      subscripts are literal strings. Array names are case-insensitive, like
      scalar variables, and a name is either a scalar or an array, never both.
      Element order for ${arr[@]} is ascending numeric index for indexed
      arrays and sorted key order for associative arrays -- bash leaves the
      associative order unspecified, so a deterministic order is used for
      portable output. In a for list, "${arr[@]}" and "${!arr[@]}" word-split
      to one item per element or key, so elements containing spaces survive.
      Array operations are detected on the raw line before expansion so the
      "(a b c)" literal and the "[sub]" subscripts are never mangled by
      variable or command substitution. All Perl 5.005_03 compatible (no
      prototypes; storage via plain package-level hashes).

    - SH for-loop word list: the list of a "for VAR in LIST" header is now
      variable- and command-substitution-expanded (previously the words were
      taken literally), with quote-aware word splitting and filename globbing.
      This fixes "for x in $LIST" and enables "for x in "${arr[@]}"".

    - SH echo: quote removal is now structural (per word) rather than only
      stripping one pair of surrounding quotes, so e.g.
      echo "${arr[@]}" tail no longer prints the literal double quotes.

    - t/0010-sh-arrays.t: new regression suite (26 checks) covering indexed
      and associative arrays, append, sparse arrays, negative indexing,
      arithmetic and variable subscripts, ${#arr[@]} / ${#arr[i]} / ${!arr[@]},
      unset of whole arrays and single elements, declare/typeset -a/-A,
      inline associative initialisers, and for-loop iteration over both
      "${arr[@]}" and "${!arr[@]}".

    - lib/BATsh.pm, lib/BATsh/SH.pm: POD updated to document array support and
      to move arrays out of the "not implemented" list.

    - CMD CALL :label argument passing and subroutine call frame
      (BATsh.pm, BATsh::CMD). CALL :label arg1 arg2 ... now installs the
      subroutine's own positional parameters -- %0 is the :label token,
      %1..%9 are the call arguments, and %* is their join -- and the caller's
      %0..%9 / %* frame is saved on entry and restored on return, so a
      subroutine no longer clobbers the caller's parameters and a shorter call
      no longer inherits the caller's stale arguments. Arguments are
      %-expanded before the call and split with double-quote awareness, so
      CALL :sub "a b" %FILE% passes "a b" as a single argument. The two CALL
      code paths (the BATsh-level directive interceptor and BATsh::CMD's
      _cmd_call) now both delegate to BATsh->call_sub, which performs the
      save/install/restore, so they behave identically. The same arguments
      are mirrored to BATSH_ARG1.. so an SH-mode subroutine body sees them as
      $1..$9 / $@. Previously CALL set BATSH_ARG* but not %1..%9 (so %1 and
      %~dp1 inside the subroutine saw the caller's arguments), set neither %0
      nor %* for the callee, and never restored the caller's frame.

    - CMD %~N path modifiers on CALL arguments: because %1..%9 are now
      populated by CALL, the tilde modifiers %~f1 %~d1 %~p1 %~n1 %~x1 (and
      combinations such as %~dp1, %~nx1) operate on a subroutine's passed
      path arguments, not just on %0.

    - CMD SHIFT / SHIFT /N is now an actual dispatched builtin (BATsh::CMD).
      It was documented but unimplemented, so SHIFT inside a subroutine fell
      through to an external command ("Can't exec SHIFT"). SHIFT now moves
      %2 into %1, %3 into %2, ..., clears %9, rebuilds %*, and keeps the
      BATSH_ARG* mirror in step; SHIFT /N begins the shift at %N.

    - t/0011-cmd-call-args.t: new regression suite (14 checks) for CALL
      argument passing (%1..%9, %*, %0 = label), caller-frame save/restore,
      quoted and %-expanded arguments, %~nx1 / %~n1 / %~x1 / %~dp1 on passed
      arguments, SHIFT and SHIFT /N, nested CALL frames, SH-mode subroutine
      bodies seeing $1..$9, and non-inheritance of unused parameters.

    - lib/BATsh.pm, lib/BATsh/CMD.pm: POD documents the CALL argument frame,
      %~N modifiers on passed arguments, and SHIFT / SHIFT /N.

    - CMD subroutine-internal GOTO labels (BATsh.pm). A subroutine body may
      now contain its own :labels as GOTO targets -- loops, forward skips,
      or an early GOTO :EOF return -- so idioms such as a SHIFT loop that
      sums a variable-length argument list work inside a CALL'd subroutine.
      _extract_subroutines() previously used only a "label ... RET" heuristic
      that mis-split such a subroutine: an internal label appearing before
      the RET demoted the real entry label, so CALL :SUB reported "undefined
      subroutine" and only the last internal label was registered. The
      extractor now (a) treats any label named by a "CALL :LABEL" anywhere in
      the script as a subroutine entry point (unioned with the existing RET
      heuristic for backward compatibility), and (b) opens a subroutine only
      at the top level: once inside a body, every :label is an internal label
      that travels with the body and only RET/RETURN closes it. Internal
      labels are resolved by the CMD interpreter when the body runs.

    - CALL is no longer intercepted in _exec_cmd_section; it is handled by the
      CMD interpreter's _cmd_call (which already delegates to call_sub /
      source_file). The old interception flushed and split the CMD batch at
      each CALL, so a GOTO whose loop body contained a CALL landed in a
      different exec_block than its label and failed with "label not found".
      Keeping CALL inside the batch lets each CMD section -- and each
      subroutine body -- run as one block with a complete label index, so
      GOTO across a CALL now resolves (in main code and in subroutines
      alike). The now-unused BATsh::_split_call_args helper was removed.

    - t/0012-cmd-sub-labels.t: new regression suite (7 checks) for internal
      GOTO loops with SHIFT, subroutine reuse, internal forward GOTO skips,
      coexistence of top-level GOTO labels with internal-label subroutines,
      multiple subroutines each with their own internal labels, early
      GOTO :EOF return from a subroutine, and nested CALL where both the
      caller and callee subroutines use internal labels.

    - eg/09_cmd_subroutines.batsh: the SHIFT demonstration now uses the
      idiomatic internal GOTO loop (:SUMARGS_LOOP / :SUMARGS_DONE) to sum a
      variable-length argument list.

    - SH case..esac pattern branching (BATsh::SH). _parse_case() and
      _match_pattern() were rewritten to parse clauses structurally rather
      than line by line, adding:
        * the bash fall-through terminators ;& (run the next clause's body
          unconditionally) and ;;& (keep testing the remaining patterns),
          alongside the normal ;;;
        * character-class patterns [abc], ranges [a-z], and negation
          [!abc] / [^abc], in addition to the existing * and ? globs;
        * quoted and backslash-escaped patterns that match literally (e.g.
          "*") matches a literal asterisk);
        * a fully-inline construct on one physical line
          (case $x in a) echo a ;; *) echo b ;; esac);
        * an optional leading "(" before the pattern list ((pattern)).
      Multiple |-separated patterns and the default *) clause already worked
      and remain supported; clause and pattern splitting is now quote- and
      [class]-aware so a | or ) inside quotes or a [...] class is not a
      delimiter.  The closing esac is recognised only at a clause boundary,
      so the word "esac" appearing inside a body no longer ends the construct
      prematurely.  break / continue / return / exit inside a clause body
      stop the case and propagate as before.  All Perl 5.005_03 compatible
      (hand-rolled scanners; no regex features beyond \A \z and character
      classes).

    - t/0013-sh-case.t: new regression suite (17 checks) for |-patterns, the
      *) default, glob and [class] patterns including negation and ranges,
      quoted/literal patterns, ;& and ;;& fall-through, fully-inline and
      multi-line forms, leading-paren clauses, empty bodies, first-match
      semantics, and esac-inside-a-body.

    - SH trap / signal handling (BATsh::SH, BATsh.pm): a minimal trap built-in
      bridged to Perl's %SIG.

        trap 'commands' SIGSPEC...   register a handler
        trap - SIGSPEC...            reset to the default action
        trap '' SIGSPEC...           ignore the signal
        trap        / trap -p        list the current traps

      A SIGSPEC is a signal name with or without a leading SIG (INT, SIGINT),
      a number (2), or the EXIT pseudo-signal (also 0).  Real signals are
      bridged to %SIG: trap 'cmd' INT installs a %SIG{INT} handler that runs
      cmd, trap '' INT sets IGNORE, and trap - INT restores DEFAULT.  The EXIT
      trap runs internally when the script finishes or when exit is called
      (exactly once), wired in via BATsh.pm's run / run_string / run_lines and
      BATsh::SH::fire_exit_trap(), plus _cmd_exit().  The handler command is
      captured on the raw line (a new interceptor in _exec_line, before
      expansion) and expanded only when it fires, so trap 'rm -f $tmp' EXIT
      removes the file named by $tmp as it stood at exit.  EXIT/ERR/DEBUG/
      RETURN are treated as pseudo-signals and never touch %SIG (only EXIT
      runs a handler today); %SIG assignment is eval-guarded so signal names
      unsupported by the host (common on Windows) degrade quietly.  trap was
      previously listed as unimplemented.  Assigning a handler for a signal
      the platform lacks (e.g. HUP/USR1/USR2 on Windows) no longer prints a
      "No such signal" warning: _sh_set_os_sig() filters just that warning
      while the best-effort assignment still succeeds.

    - t/0014-sh-trap.t: new regression suite (14 checks) for the EXIT trap on
      normal end, on exit, with deferred expansion, cancellation, and
      single-fire; %SIG handler install / IGNORE / DEFAULT / clearing;
      handler execution and real signal delivery (kill); trap listing;
      multiple signals per command; SIG-prefix and numeric normalization.

    - eg/12_cmd_vs_sh.batsh: new teaching example that writes the same nine
      tasks twice -- once in cmd.exe (CMD) style and once in bash/sh (SH)
      style -- as a side-by-side parallel-translation reference for students:
      printing, variables, arithmetic, an if/else string test, a counted
      loop, a list loop, a subroutine/function, a multi-way branch (CMD
      IF-chain vs SH case), and the shared variable bridge between the two
      modes.  A quick CMD-vs-SH syntax table heads the file; output lines are
      labelled CMD:/SH: so the two notations line up.

    - doc/ cheat sheets (all 21 languages): added three new sections covering
      the 0.06 SH features -- "15. SH Arrays", "16. SH case Statements", and
      "17. SH trap and Signals" -- with the same language-independent code
      examples in every file and consecutive section numbering preserved
      (t/9080 still passes).  Inline comments and prose are fully localized
      for EN and JA, and additionally for ZH, KO, FR, ID, VI and TR; the
      remaining 12 languages (BM BN HI KM MN MY NE SI TL TW UR UZ) carry the
      translated section headings with the comment text left in English as a
      draft pending native-speaker review.

0.05  2026-06-14 JST (Japan Standard Time)

    - SH single-line loops: "for VAR in LIST; do BODY; done" and
      "while/until COND; do BODY; done" written entirely on one physical
      line are now executed correctly. Previously _parse_for() captured the
      whole inline tail (BODY; done) into the iteration list and collected
      no body, so the loop produced no output and -- because the body
      collector then consumed the rest of the script looking for a "done"
      that never arrived as its own line -- it re-ran the remainder of the
      file once per whitespace token in the list; the single-line while
      form died with a shell "do unexpected" / "Can't exec done" error.
      _parse_for() and _parse_while() now detect an inline ";do" (matched
      with \b after "do" so a "; done" terminator is never taken for the
      "do" keyword), split the header from the inline body, and recognise
      an inline "; done" terminator (greedy, so the LAST "; done" closes the
      loop and a body that legitimately contains the word "done" or a nested
      single-line loop is handled). The inline while form also honours a
      redirect on the closing done, e.g. "while read L; do ...; done < FILE".
      Multi-line loops are byte-for-byte unaffected. All forms are Perl
      5.005_03 compatible (no regex features beyond \A \z \b and /s). This
      makes the single-line "for i in 1 2 3; do ... done" shown in the
      doc/*.txt cheat-sheet MIXED-MODE SAMPLE run as documented.

    - t/0002-sh-interpreter.t: added regression tests for the inline
      single-line for/while/until forms, nested single-line loops, a body
      that contains the literal word "done", inline filename globbing, and
      an inline while-read with a redirect on the closing done.

    - Section splitter: BATsh::_sh_depth_delta() now computes the NET block
      depth change for an ENTIRE SH line instead of inspecting only the first
      token. A single-line "for ...; do ...; done" (or while/until/case)
      counted the leading "for" (+1) but never the inline "done" (-1), so the
      SH section was left open; the FOLLOWING CMD line was then absorbed into
      the SH section and executed by the SH interpreter, where "%VAR%" is not
      expanded -- e.g. the cheat-sheet sample's "ECHO Back in CMD: %result%"
      printed "%result%" literally instead of bridging the SH-side value. The
      new scanner counts opener (if for while until case function select, "{")
      and closer (fi done esac, "}") keywords only in command position and
      skips quoted text, $( ) substitutions and backticks, so an inline loop
      nets to zero and the section boundary is detected correctly. Pure
      Perl 5.005_03 (substr-based scan, no regex features). Multi-line input
      is unaffected (the fast path returns the previous single-token result).

    - Section splitter: a CMD-style comment (":: ..." or "REM ...") that
      follows an SH section is no longer handed to the SH interpreter
      verbatim. _process_lines() routed every comment line into the current
      section unchanged; because the SH interpreter treats only "#" as a
      comment, a ":: ..."/"REM ..." line was dispatched to the external shell,
      and any "( )", "|" or other metacharacter in the comment then made
      /bin/sh fail with 'Syntax error: "(" unexpected'. Comment (and empty)
      lines are now routed into a section as a BLANK line, which both
      interpreters skip identically at every nesting depth and which also
      avoids the real cmd.exe quirk of "::" inside a "( )" block.

    - t/0004-bridge.t: added regression tests proving (a) a single-line
      for/while does not leave the SH section open so a following CMD
      "%VAR%" still bridges, and (b) a ":: ..."/"REM ..." comment containing
      shell metacharacters after an SH section is not dispatched to the
      shell (STDERR is captured and checked for no shell syntax error).

    - CMD variable substring/substitution modifiers: %VAR:~n,m% and
      %VAR:str1=str2% in-place substitution are now supported in
      BATsh::Env::expand_cmd() via a new _expand_var_modifier() helper.
      Substring forms: %VAR:~n% (from offset), %VAR:~n,m% (n+m chars),
      %VAR:~-n% (last n chars), %VAR:~n,-m% (all but last m chars).
      Substitution forms: %VAR:str1=str2% (replace first, case-insensitive),
      %VAR:*str1=str2% (replace from start through first str1).
      All forms are processed before the plain %VAR% pass and are
      Perl 5.005_03 compatible (substr, index, no regex features).

    - CMD dynamic pseudo-variables: %DATE%, %TIME%, %CD%, %RANDOM%,
      %ERRORLEVEL%, and %CMDCMDLINE% are now resolved at expansion time
      by a new _expand_named_var() dispatcher inside expand_cmd().
      %DATE% returns YYYY-MM-DD, %TIME% returns HH:MM:SS.cc, %CD% is the
      current working directory, %RANDOM% is a pseudo-random integer 0-32767,
      %ERRORLEVEL% reads the current value from BATsh::CMD via the new
      public accessor BATsh::CMD::_get_errorlevel(), and %CMDCMDLINE%
      returns an empty string (not meaningful in pure-Perl mode).

    - SH filename globbing: unquoted words that contain *, ?, or [...]
      metacharacters are now expanded to matching pathnames using Perl's
      built-in glob(). Expansion occurs in BATsh::SH::_parse_args() (for
      echo and external-command arguments) and in BATsh::SH::_parse_for()
      (for the word list of for VAR in GLOB; do ... done). Single-quoted
      and double-quoted patterns are NOT expanded (POSIX behaviour).
      If no file matches the pattern, the literal pattern is returned
      unchanged (nullglob-off behaviour, which is the shell default).
      Two new helpers are added to BATsh::SH: _glob_expand() (expand one
      word) and _glob_expand_args() (convenience wrapper over a list).

    - t/0009-new-vars.t: new test file, 25 tests (NV01-NV25) covering
      all three feature areas above. Added to MANIFEST.

    - POD updated: BATsh.pm BUGS AND LIMITATIONS now records that
      %VAR:~n,m% / %VAR:str1=str2% and all dynamic pseudo-variables are
      supported; the SH filename-globbing limitation item is removed.
      BATsh::Env Variable Expansion section documents all new forms.
      BATsh::SH Supported Features table lists glob expansion.

    - Version bumped to 0.05 in lib/BATsh.pm, lib/BATsh/CMD.pm,
      lib/BATsh/SH.pm, lib/BATsh/Env.pm, Makefile.PL, META.yml,
      META.json, and README.

0.04  2026-06-07 JST (Japan Standard Time)

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

0.03  2026-06-06 JST (Japan Standard Time)

    - t/lib/INA_CPAN_Check.pm: emit exactly one TAP plan line per test
      file. Each check_* helper previously called plan_tests() itself,
      while the .t files also called plan_tests(count_*); this produced
      multiple "1..N" lines in a single file. Under a real TAP harness
      (prove / Test::Harness, as used by CPAN Testers) this raised
      "More than one plan found in TAP output" and made the affected
      files FAIL, even though every individual "ok" line passed when the
      scripts were run by hand. Affected files: t/9010-encoding.t,
      t/9030-distribution.t, t/9040-style.t. The plan_tests() call is
      now removed from every check_* helper, leaving the .t file as the
      sole owner of the plan line.
    - t/lib/INA_CPAN_Check.pm: count_A() now returns the actual number
      of MANIFEST entries instead of a fixed 1, so that the plan
      computed by t/9030-distribution.t matches the number of A1 checks
      that check_A() emits.
    - t/lib/INA_CPAN_Check.pm: remove the mid-stream plan_skip() calls
      from check_A() and check_C(); the MANIFEST-absent guard is handled
      by the .t file and by count_C() before any plan line is printed.
    - t/lib/INA_CPAN_Check.pm: check_K() now honours the k3_exempt
      option passed by t/9040-style.t. The argument was previously
      discarded by "my ($root) = @_;", so the intended exemption of
      accessor-style hash names (%env, %opts, %args) never took effect;
      the K3 detector also did not capture the hash name. check_K() now
      accepts "k3_exempt => REGEX", captures the returned hash name via
      /return \%(\w*)/, and skips a "return \%name" only when the name
      matches the supplied pattern. Behaviour is unchanged when
      k3_exempt is not passed (every "return \%..." is still flagged),
      so distributions that call check_K($root) without the option are
      unaffected.
    - t/lib/INA_CPAN_Check.pm: add a regression guard for the two TAP
      defect classes above. plan_tests() now refuses to emit a second
      "1..N" line, and an end-of-run reconciliation reports
      "planned X but ran Y" (setting a non-zero exit) when the emitted
      plan does not match the number of ok()/not-ok() lines. Both
      problems now FAIL immediately on a plain "perl t/foo.t", not only
      under a real harness.
    - t/lib/INA_CPAN_Check.pm: add selfcheck_suite(), which runs t/*.t
      (and xt/*.t) in a child Perl and verifies one plan line per file,
      plan == number of ok/not-ok lines, and no failures.
    - pmake.bat: at "pmake dist" time, after the existing source checks,
      run INA_CPAN_Check::selfcheck_suite() as check3 and abort the
      build if any test file fails the plan-sanity check (disable with
      --no-check3). Bump $PMAKE_BAT_VERSION to 0.34.
    - t/lib/INA_CPAN_Check.pm: pass \@files / \@pm_files (a reference)
      instead of [ @files ] (an anonymous copy) to _find_pm_t() in
      _scan_code(), check_D(), check_E(), and check_K(). The copy form
      meant the collected file list never reached the caller, so E1
      (no shebang in lib/*.pm) and K3 (return { %hash } form) silently
      scanned zero files and always passed.

    - Documentation: BATsh.pm BUGS AND LIMITATIONS corrected. It no longer
      claims SH-mode background execution is unsupported (it is supported
      for external commands; see above and BATsh::SH), and it now clarifies
      that non-builtin commands (FINDSTR, SORT, etc.) are invoked as
      external programs rather than "unsupported". README and BATsh.pm POD
      additionally enumerate previously undocumented limitations: CMD
      "%VAR:~n,m%" / "%VAR:str1=str2%" and dynamic "%RANDOM%/%DATE%/%TIME%/
      %CD%" variables; SH arrays, filename globbing, "~" tilde expansion,
      brace expansion, and the trap/getopts/select/alias/declare/eval/exec
      builtins and set -e/-u/-x options; and the shared (no sub-shell)
      "( ... )" grouping common to both modes.

    - SH expansion: a backslash-escaped "\$", "\`" or "\\" inside double
      quotes is now preserved literally and no longer triggers variable
      or command substitution (e.g. "\$_" yields a literal "$_").

    - SH read: the "read" built-in now returns a non-zero status at end of
      input so that "while read VAR; do ...; done < FILE" terminates
      instead of looping. Leading option flags such as "-r" are skipped
      and are no longer treated as target variable names.

    - SH assignment prefix: "VAR=value command args" (POSIX) now applies
      the assignment and then runs the command (e.g. "IFS= read -r LINE",
      "LC_ALL=C sort"); multiple prefixes are supported. A standalone
      assignment whose value merely contains spaces or a "$(...)"
      substitution (e.g. UPPER=$(echo "a b")) keeps the full value and is
      no longer mistaken for a prefix.

    - SH while/until: an input redirection on the "done" line
      ("while read L; do ...; done < FILE") now reopens STDIN from FILE for
      the duration of the loop so the loop's "read" consumes the file.

    - eg/06_sh_comprehensive.batsh: I/O-redirection section simplified to a
      plain "while read" loop now that the loop terminates correctly.

    - Tests: t/9070-examples.t now executes each eg/*.batsh in a child
      process and guards against runaway output and "syntax error"
      breakage (E4). t/9060-readme.t verifies the README advertises every
      eg/ example by name (R5). README gains an EXAMPLES section.

    - SH background execution: an unquoted trailing "&" starts an external
      command asynchronously and returns immediately. On Win32 the job is
      spawned via system(1, ...) (P_NOWAIT, PID returned); on Unix it is
      started through /bin/sh without a Perl fork, capturing the job PID
      via the shell's $! into a sysopen O_CREAT|O_EXCL temp file (Pure
      Perl, 5.005_03). The new $! parameter expands to the most recent
      background PID (empty before any job); $? is 0 on a successful
      launch (the job's own exit status is not awaited). Built-ins,
      functions, assignments and control words ignore the trailing "&"
      and run in the foreground; "&&", ">&"/"2>&1", quoted and escaped
      "\&" are not treated as background. No job control; CMD-mode "&"
      remains a sequential separator (see BUGS AND LIMITATIONS).

    - eg/05_cmd_comprehensive.batsh: the "IF ERRORLEVEL" diagnostic line
      "ECHO   ERRORLEVEL>=0: ELTEST=%ELTEST%" contained a bare ">", which
      CMD mode correctly treats as output redirection (matching cmd.exe).
      As written, the message was silently redirected to a file named
      "=0:" instead of being printed, and that stray file was created in
      the current directory each time the example ran (including under
      "make test" via t/9070-examples.t). The ">" is now caret-escaped
      ("ECHO   ERRORLEVEL ^>= 0: ELTEST=%ELTEST%"), so the line prints
      as intended and no file is written.

0.02  2026-04-28 JST (Japan Standard Time)

    [Highlights]
    - Full bash/sh interpreter implementation: if/for/while/until/case,
      function definitions (name() { ... }), local variable scoping,
      && / || / ; compound commands, pipelines (|), I/O redirection
      (> >> < 2> 2>> 2>&1), variable expansion (${var%pat}, ${var#pat},
      ${#var}, ${var^^}, ${var,,}, ${var:N:L}, ${var/p/r}, ${var//p/r}),
      positional parameters $1..$9 / $@ / $* / $#, shift, read, source.
    - cmd.exe pipeline (|) support via temporary file (Pure Perl, 5.005_03).
    - I/O redirection: stdout overwrite (>), append (>>), stdin (<),
      stderr (2>), stderr-to-stdout (2>&1), stdout-to-stderr (1>&2).
      Supported in both CMD mode and SH mode.
    - SH here-documents on STDIN: cmd <<DELIM ... DELIM, <<-DELIM (strip
      leading tabs), and <<'DELIM' (literal, no expansion). Body is
      materialised to a temp file created with sysopen O_CREAT|O_EXCL
      (Pure Perl, 5.005_03) and fed through the existing "< file" path,
      so both built-ins (read) and external commands see it on STDIN.
      Top-level mode dispatch is here-document aware, so uppercase body
      lines are not misrouted to CMD mode. Single here-document per line;
      here-strings (<<<) and same-line pipeline/compound combos are not
      supported (see BUGS AND LIMITATIONS).
    - cmd.exe batch-parameter tilde modifiers: %~0, %~f1, %~dp0, %~nx1,
      %~n0, %~x0, %~p1 etc. (f d p n x modifiers, combinable).
    - SET /P VAR=Prompt  interactive prompt input from STDIN.
    - $0 normalised to absolute path via File::Spec on run().

    [BATsh::Env]
    - Variable names are now stored and looked up in uppercase, matching
      cmd.exe's case-insensitive environment variable behaviour.
      SET myvar=x  followed by  ECHO %MYVAR%  now correctly outputs "x".
    - Added $DELAYED_EXPANSION package variable (default 0).
    - setlocal() now accepts an options string and parses
      ENABLEDELAYEDEXPANSION / DISABLEDELAYEDEXPANSION.
      The delayed-expansion flag is saved/restored with the variable store.
    - expand_cmd() now expands !VAR! references when $DELAYED_EXPANSION is on.

    [BATsh::CMD]
    - Implemented ^ escape character:
        ^X          -> literal X (protects & | < > etc.)
        ^^          -> literal ^
        trailing ^  -> line continuation (joins next line)
    - Implemented I/O redirection parsed before command dispatch:
        >file       stdout overwrite
        >>file      stdout append
        2>file      stderr overwrite
        2>>file     stderr append
        <file       stdin redirect
      Redirects with ^> are correctly treated as escaped > (not a redirect).
      fd-digit stripping limited to isolated '1' or '2' before '>' to avoid
      consuming trailing digits of command arguments (e.g. "ECHO line1 >f").
    - SETLOCAL now passes its option string to BATsh::Env::setlocal() so
      ENABLEDELAYEDEXPANSION and DISABLEDELAYEDEXPANSION take effect.
    - !VAR! delayed expansion: pre_expanded block bodies now call expand_cmd()
      at runtime when delayed expansion is active, so SET inside an IF/FOR
      block followed by ECHO !VAR! correctly reflects the updated value.
    - IF block pre-expansion: %VAR% in parenthesised IF/ELSE bodies is now
      expanded at parse time (matching cmd.exe semantics), so a SET inside the
      block does not affect %VAR% references in the same block.
    - FOR block pre-expansion: %VAR% in parenthesised FOR bodies is expanded
      once before the first iteration (at FOR-line parse time) and cached;
      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).
    - Control structures: if/then/elif/else/fi, for/do/done, while/do/done,
      until/do/done, case/esac with glob-pattern matching.
    - Function definitions: name() { ... } and function name { ... } syntax,
      including inline single-line bodies.  Functions receive positional
      arguments $1..$9; caller's arguments are saved and restored on return.
    - local variable scoping: local VAR=value saves the caller's value and
      restores it when the function returns.
    - Compound commands: cmd1 && cmd2, cmd1 || cmd2, cmd1 ; cmd2.
    - Pipeline: cmd1 | cmd2 [| cmd3 ...] implemented via temporary files
      (Perl 5.005_03 compatible bareword filehandles, no fork/exec).
    - I/O redirection: > >> < 2> 2>> 2>&1 1>&2, parsed after variable
      expansion so that filenames may contain variables.
    - Variable expansion: $VAR, ${VAR}, $1..$9, $@, $*, $#, $?, $$, $0.
      Parameter expansion forms: ${VAR:-default}, ${VAR:=default},
      ${VAR:+alt}, ${VAR%pat}, ${VAR%%pat}, ${VAR#pat}, ${VAR##pat},
      ${VAR/pat/rep}, ${VAR//pat/rep}, ${VAR^^}, ${VAR^}, ${VAR,,}, ${VAR,},
      ${VAR:N:L}, ${VAR:N}, ${#VAR}.
      Glob patterns in %/%%/#/## support *, ?, and [abc] character classes.
    - Arithmetic expansion: $(( expr )) with +, -, *, /, %, and positional
      parameters $1..$9 inside the expression.
    - Command substitution: $( cmd ) with full nesting/quoting support, and
      backtick `cmd` form.
    - shift [N]: shifts positional parameters left by N positions (default 1),
      updating both %N and %* in BATsh::Env.
    - read VAR: reads one line from STDIN, chomps it, stores in VAR.
    - source / . file: executes an external file in the current SH context.
    - Builtin commands: echo, printf, cd, pwd, exit, true, false, :, export,
      unset, set (noop), test / [ ... ] with -f -d -e -r -w -x -s -z -n
      and string (= == != < >) and integer (-eq -ne -lt -le -gt -ge) ops.
    - _cmd_subst uses fixed bareword filehandles (_SUBST_SAVOUT etc.) to
      avoid collision under Perl strict and recursive invocations.
    - _sh_strip_redirects and _sh_exec_with_redirs added for I/O redirection.
    - _replace_cmd_subst added: walks $( ) depth-tracking the nesting so that
      $( cmd | perl -e "print uc" ) parses the closing ) correctly.
    - _split_sh_compound and _exec_sh_compound added for && / || / ; handling.
    - _split_sh_pipe and _exec_sh_pipe added for pipeline handling.
    - $0 is set to the absolute path of the running script by BATsh::run().

    [BATsh.pm]
    - _exec_cmd_section no longer intercepts SETLOCAL/ENDLOCAL itself;
      both are passed through to BATsh::CMD::_dispatch so the option string
      (ENABLEDELAYEDEXPANSION etc.) is correctly forwarded.

0.01  2026-04-26 JST (Japan Standard Time)

    - Initial CPAN release.
