BATsh
view release on metacpan or search on metacpan
directory contains spaces.
- SH `cd` QUOTING: the cd builtin now dequotes its argument like every
other command word, so cd "a b", cd 'dir', and cd "$VAR/x" reach
chdir() as a plain path. Previously the quote characters were kept
and cd failed with "No such file or directory". (t/0026 CD01-CD03.)
- SH SUBSHELL REDIRECT FAILURE: a subshell whose redirection cannot be
opened, e.g. "( read L ) < /no/such/file", now reports the error and
returns a non-zero status WITHOUT running its body. Previously it
fell through and ran the body against the inherited stdin, which
blocks forever when the body reads stdin (cat, read). Any handle
already redirected is restored before returning. (t/0026 RF01; RF02
guards that a working redirect still reads the file.) New test
t/0026-sh-cd-and-redir-fail.t (5 checks). Perl 5.005_03 compatible.
- NEW SH BUILTIN getopts: the POSIX single-character option parser,
the last remaining builtin previously listed as unimplemented.
getopts optstring name [arg ...]
Called in a loop, it sets the variable named by "name" to each
option letter in turn (and OPTARG to an option's argument), tracks
progress through the argument list in OPTIND (1-based, reset to 1
by the script before an independent parse), and returns non-zero
when the options are exhausted so a "while getopts ...; do ...;
done" loop ends by itself. Supports: a ':' after a letter in the
optstring to mark an argument-taking option; the separate
(-o value) and attached (-ovalue) option-argument forms; clustered
flags (-abc == -a -b -c); the "--" end-of-options marker (consumed)
and a leading non-option word (stops parsing, left in place);
parsing the positional parameters when no explicit args are given;
and both error-reporting modes -- the default (a diagnostic to
STDERR and name='?') and the silent mode selected by a leading ':'
in the optstring (no message; name='?' for an unknown option or
':' for a missing argument, with the offending letter in OPTARG).
The intra-argument character offset used for clustered options is
internal state that is restarted whenever the script resets OPTIND,
matching bash's behaviour for a fresh getopts loop; it is also
reset between top-level runs so one script's half-finished loop
cannot leak into the next. All Perl 5.005_03 compatible
(index/substr scanning, no prototypes, package-variable state).
New test t/0023-sh-getopts.t (23 checks).
- Fixed SH "shift N": the builtin accepted an optional count in its
implementation (shift 1..9 positions) but the dispatcher called it
with no argument, so "shift 3" and "shift $((OPTIND-1))" always
shifted by exactly one -- silently corrupting any script that
shifted more than one positional parameter at a time, including the
idiomatic "shift $((OPTIND - 1))" that follows a getopts loop. The
dispatcher now passes the count through. (t/0023: GO16/GO17.)
- Fixed inline single-line function bodies that contain a control
structure. A body such as
loop() { for x in a b c; do echo "$x"; done; }
count() { i=0; while [ $i -lt 3 ]; do echo $i; i=$((i+1)); done; }
chk() { if [ "$1" = yes ]; then echo Y; else echo N; fi; }
was split naively on ';' at definition time, tearing the loop or
conditional into fragments ("while C" / "do B" / "done") that no
longer reassembled into a runnable block -- the construct silently
produced no output, or ran "done"/"fi" as an external command.
_parse_function() now detects a control-structure keyword in
command position in an inline body (new helper
_inline_body_has_control(), quote/substitution aware) and, when
present, keeps the whole body as one line so _run_lines() applies
the same inline-control handling it already uses for a control
structure typed directly on one physical line (including the
"prefix; control" split, so "setup; while ...; do ...; done"
works). A trailing ';' before the closing brace is dropped so the
inline "; done"/"; fi"/"; esac" terminator sits where the block
parsers expect it. Bodies of only simple commands
(greet() { echo hi; echo bye; }) still split on ';' exactly as
before. All Perl 5.005_03 compatible. (t/0023: GO14/GO15 exercise
getopts inside such a function; the fix is general to all inline
function bodies.)
- Fixed the SH "$*" special parameter, which was never expanded and
printed literally: the expansion pass substituted "$@" but not
"$*", despite the code comment naming both. "$*" now expands to
the space-joined positional parameters, like "$@". This is the
parameter a script reads after "shift $((OPTIND - 1))" to see the
operands left by a getopts loop. (t/0023: GO19.)
- Fixed the SH "echo" builtin's glob decision, which inspected the
raw source text for *, ?, or [ with a plain pattern match. That
match also fired on the '*' of the "$*" parameter, on the '?' of
"$?", and on any metacharacter inside quotes -- so
echo " x: $*" was needlessly run through word-splitting/globbing,
collapsing its leading and internal whitespace, and a quoted
echo "*.txt" risked being globbed against the current directory.
A new quote- and parameter-aware scanner (_raw_has_glob) triggers
globbing only on a genuine unquoted glob metacharacter that was
actually written in the script, skipping quoted regions, $NAME /
$* / $? parameter references, and ${...} expansions. Unquoted
"echo *.txt" still globs as before. (t/0023: GO20/GO21.)
- Extended shell features: extglob, brace expansion, here-strings,
process substitution, select, alias/unalias, exec, and subshell
command groups (v0.07).
* shopt -s/-u extglob enables ?(),*(),+(),@(),!() pattern-list
operators in case patterns and in the ${VAR%pat}-family
parameter-expansion patterns (_extglob_scan/_extglob_split_alts,
shared by _case_glob_to_re() and _glob_to_re()). Fixed two
existing case-pattern parsers (_case_split_patterns and
_case_parse_clause) to be paren-depth-aware so an extglob
group's internal '|' and ')' are not mistaken for the
pattern-separator '|' or the clause-terminator ')'.
* {a,b,c} and {1..5}/{a..e}[..step] brace expansion runs
lexically on the raw line (_brace_expand_line and friends),
protecting quotes, ${...}, $(...), $((...)), `...`, and
<(...)/>(...) as opaque regions; wired into _exec_line and
_expand_word_list (for-loop lists).
* cmd <<< word here-strings (_sh_strip_herestring), reusing the
here-document temp-file machinery. Fixed a pre-existing
_hd_detect() bug where "<<<" was only rejected as a
here-document opener when matched starting at its FIRST '<';
the scan then re-tried from the second '<' and matched "<<"
there, silently mistreating "cmd <<< word" as a broken
here-document. _hd_detect() now skips all three characters.
- BATsh::Env::sync_to_env no longer copies the batch-parameter
pseudo keys (%0..%9, %*, %%V FOR variables) into %ENV; they are
not legal environment variable names and polluted the child
environment of external commands.
- t/0015-cp932.t: new regression suite (20 checks, US-ASCII
source with \xNN escapes) covering echo/ECHO of dame-moji,
%VAR% and Env-bridge round-trips, pipeline/backtick/caret
false-positive protection, uc() safety, character-based
${#VAR} / ${VAR:N:L} / %VAR:~n,m%, case patterns, redirects,
CP932 filenames (test -f, IF EXIST), FOR /F over a CP932 file,
${VAR^^}, IF string comparison, %ENV export, auto-detection,
UTF-8 pass-through, and enc/dec bijectivity.
- eg/13_cp932_demo.pl: runnable demonstration (US-ASCII source)
of a CP932 mixed CMD/SH script.
- t/9010-encoding.t: BATsh::MB added to the US-ASCII source
check list.
- Fixed a pre-existing echo bug: the SH "echo" builtin decided
whether to apply filename globbing by testing the already
variable-expanded $rest for * ? [ characters. This meant a
variable whose *value* happened to be a glob metacharacter
(e.g. getopts setting $opt to "?" on an unknown option, or ":"
on a missing argument) could be silently glob-matched against
the current directory and replaced by whatever single-character
filename happened to match, instead of being echoed literally.
The glob-or-not decision is now made from the raw, pre-expansion
source text instead, so only a glob metacharacter actually
written in the script (e.g. "echo *.txt") triggers globbing;
a variable's expanded value is never re-subject to globbing,
matching the same "expansion results are not re-expanded"
principle already used for tilde expansion. (t/0021:
EF25/EF26 regression tests.)
- Practical-level fixes to the pure-Perl SH and CMD interpreters
(bugs found by exercising real scripts; all covered by the new
t/0022-sh-compound-fixes.t regression suite, 16 checks):
* A simple-command prefix followed on the SAME physical line by
a control structure introduced with ';' now dispatches the
control structure through its block parser instead of handing
the pieces to /bin/sh -- e.g. x=""; if [ -z "$x" ]; then ...;
fi, i=0; while ...; done, v=cat; case ...; esac
(_run_lines/_find_control_split).
* Inline "if COND; then A; else B; fi" (and elif) now honours
the else/elif branch; the old single-line parser dropped it
(_parse_if via _inline_has_terminator/_inline_expand).
* Nested "if ... fi" is depth-aware so an inner fi no longer
closes the outer if, both multi-line and inline
(_parse_if body collector + _if_depth_delta).
* Escaped double quotes inside a double-quoted word are handled
per POSIX: echo "she said \"hi\"" -> she said "hi"
(_arr_dequote rewritten with backslash escaping).
* A for-loop / array list built from $(...) no longer leaks a
stray ")" token: for f in $(echo a b c); do ...; done, and
arr=($(echo x y z)) (_arr_split_words made substitution-aware).
* A trailing "# comment" is stripped from SH command lines when
the '#' begins a word and is unquoted, while $#, ${#var},
${var#pat}, an in-word '#' (a#b, http://h#frag) and quoted
'#' are left intact, and here-document bodies keep their '#'
(_strip_sh_comment, applied per physical line in _run_lines).
* A control structure used as a pipeline element or && / ||
operand now runs correctly: cmd | while read x; do ...; done,
cmd | for i in ...; do ...; done, true && for ...; done.
_split_sh_compound became control-structure-grouping aware so
a ';' / && / || inside a while/for/if/case/until/select block
is no longer treated as a top-level separator, and a single
compound command reaching _exec_line_impl as a pipe/operand is
routed through the block runner (_seg_is_control). (The
multi-line form with "do" on its own line after a pipe remains
a known limitation; the inline "; do" form is supported.)
* Single-line CMD "IF cond (body) ELSE (body)" (and IF EXIST /
IF DEFINED / negated forms) now parses both branches on one
physical line instead of echoing the raw ") ELSE (" text
(_parse_if_bodies via _match_paren).
- created by INABA Hitoshi
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
( run in 1.354 second using v1.01-cache-2.11-cpan-364913b4093 )