BATsh
view release on metacpan or search on metacpan
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
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
lib/BATsh/SH.pm view on Meta::CPAN
$i++;
}
return $re;
}
# ----------------------------------------------------------------
# extglob (v0.07): ?(list) *(list) +(list) @(list) !(list) pattern-list
# operators, active only while "shopt -s extglob" is on. Shared by
# _case_glob_to_re() (case patterns) and _glob_to_re() (${VAR%pat} and
# friends). $convert_sub converts one pattern-list alternative (which
# may itself contain nested extglob groups) to a regex fragment.
#
# Returns ($pos_after_close_paren, $regex_fragment), or () when the
# text at $i is not a well-formed extglob group (extglob is then left
# to fall through to its ordinary, literal meaning for that character).
#
# !(list) is approximated as "any run of characters that never forms a
# complete match of one of the alternatives" via a negative lookahead
# repeated per character; this matches the common "exclude these whole
# patterns" usage (e.g. !(*.jpg|*.png)) but, unlike real extglob, is not
# exact when !(...) is combined with more pattern after it in the same
# glob -- documented as a known limitation.
# ----------------------------------------------------------------
lib/BATsh/SH.pm view on Meta::CPAN
if ($cc eq '(') { $depth++; $body .= $cc; $j++ }
elsif ($cc eq ')') { $depth--; $j++; $body .= $cc if $depth > 0 }
elsif ($cc eq '\\' && $j+1 < $n) { $body .= $cc . $c[$j+1]; $j += 2 }
else { $body .= $cc; $j++ }
}
return () if $depth != 0;
my @alts = _extglob_split_alts($body);
my @re_alts = map { $convert_sub->($_) } @alts;
my $inner = '(?:' . join('|', @re_alts) . ')';
my $frag;
if ($op eq '?') { $frag = $inner . '?' }
elsif ($op eq '*') { $frag = $inner . '*' }
elsif ($op eq '+') { $frag = $inner . '+' }
elsif ($op eq '@') { $frag = $inner }
elsif ($op eq '!') { $frag = '(?:(?!' . $inner . ').)*' }
else { return () }
return ($j, $frag);
}
# _extglob_split_alts: split an extglob pattern-list body on top-level
# '|' (respecting nested parens and backslash escapes).
sub _extglob_split_alts {
my ($body) = @_;
my @out;
my $cur = '';
my @c = split //, $body;
my $n = scalar @c;
lib/BATsh/SH.pm view on Meta::CPAN
}
}
return @out;
}
# _strip_sh_comment: remove a trailing "# ..." comment from one SH
# physical line. A '#' introduces a comment only when it is unquoted,
# outside any $(...)/${...}/`...` region, and begins a word (preceded by
# the start of line or by whitespace / ; / & / | / '(' ). This leaves
# parameter forms such as $#, ${#var}, ${var#pat} and an in-word '#'
# (echo a#b, http://h#frag) untouched, matching POSIX shells. Pure Perl
# 5.005_03 (hand-rolled scan; no regex features).
sub _strip_sh_comment {
my ($line) = @_;
return $line unless defined $line && index($line, '#') >= 0;
my @c = split //, $line;
my $n = scalar @c;
my $in_sq = 0;
my $in_dq = 0;
my $in_bt = 0;
my $pdep = 0;
lib/BATsh/SH.pm view on Meta::CPAN
# ----------------------------------------------------------------
# Shell function registry { name => \@body_lines }
# ----------------------------------------------------------------
# ----------------------------------------------------------------
# _inline_body_has_control: true when a single-line function body
# (the text between the braces of "name() { ... }") contains a shell
# control-structure keyword in command position -- if/for/while/until/
# case/select as the first word, or after a ';', '&&', '||' or '|'.
# Such a body must not be torn apart on ';' (that would split
# "while C; do B; done" into unusable fragments); the caller keeps it
# as one line so _run_lines()'s inline-control handling parses it.
# Quotes, $(...), `...` and ${...} are skipped so a keyword appearing
# only inside them (echo "done", VAR=$(case ...)) does not count.
# Perl 5.005_03 compatible: character scan, no regex features beyond
# \A and \b.
# ----------------------------------------------------------------
sub _inline_body_has_control {
my ($body) = @_;
return 0 unless defined $body && $body =~ /\S/;
my @c = split //, $body;
lib/BATsh/SH.pm view on Meta::CPAN
Extglob operators are recognised in case patterns and in the
C<${VAR#pat}>-family parameter-expansion patterns only. Pathname
(filename) globbing (C<echo *.@(jpg|png)>) does not expand them: an
unquoted C<(> C<)> is read by the line parser as a subshell command
group long before the word reaches the pathname matcher.
=item *
C<!(list)> is approximated with a repeated negative-lookahead regex
fragment ("any run of characters that never forms a complete match of
one of the alternatives"). This matches the common "exclude these whole
patterns" usage exactly, but is not a byte-for-byte reimplementation of
bash's extglob matcher when C<!(...)> is combined with further pattern
text after it in the same glob.
=back
=head2 Here-Strings
cat <<< "$greeting"
t/0007-extcmd-env.t view on Meta::CPAN
# once. Two distinct foot-guns were observed:
#
# (A) Unix vector -- a dollar default-variable token inside the
# one-liner is expanded by /bin/sh using the environment variable
# "_" (the path/last-arg the shell exports), which is
# unpredictable on CPAN smokers. This produced random failures
# such as "Bareword found where operator expected ... 1EERDtQcrK".
#
# (B) Windows vector -- cmd.exe does NOT treat single quotes as
# quoting. A one-liner wrapped in single quotes is split on
# whitespace, so Perl receives a broken fragment and dies with
# "Can't find string terminator".
#
# The portable form satisfies both: wrap the code in DOUBLE quotes
# and use NO dollar sign the shell would expand, e.g.
# perl -ne "print uc"
# (the default variable is used implicitly by uc, so no token leaks).
#
# THIS TEST
# EE01/EE02 run the pipeline and here-document patterns under several
# hostile values of the environment variable "_" (the Unix
t/0031-set-positional.t view on Meta::CPAN
######################################################################
#
# 0031-set-positional.t set [--] ARG ... sets $1..$9 / $@ / $# (v0.09)
#
# BACKGROUND
# Until v0.09 the "set" builtin understood only its option letters
# (-e -u -x, -o NAME) and silently ignored every operand, so the
# standard way of feeding an argument list to a script fragment
#
# set -- -f value
# while getopts f: opt ; do ... done
#
# saw an empty argument list: getopts falls back to the positional
# parameters, and those were never set. "set" now implements the POSIX
# operand rules -- "set -- [ARG ...]" replaces the positional
# parameters (clearing them when no ARG follows), and so does a first
# operand that is not an option, as in "set a b c". The parameters are
# kept in the interpreter's existing %1..%9 / %* representation, the
( run in 1.424 second using v1.01-cache-2.11-cpan-364913b4093 )