BATsh
view release on metacpan or search on metacpan
lib/BATsh/SH.pm view on Meta::CPAN
$i++;
while ($i < $n && $c[$i] ne "'") { $re .= quotemeta($c[$i]); $i++ }
$i++; next;
}
if ($ch eq '"') { # literal double-quoted run
$i++;
while ($i < $n && $c[$i] ne '"') {
if ($c[$i] eq '\\' && $i + 1 < $n) {
$i++; $re .= quotemeta($c[$i]); $i++; next;
}
$re .= quotemeta($c[$i]); $i++;
}
$i++; next;
}
if ($ch eq '\\') { # escaped literal
$i++; $re .= quotemeta($c[$i]) if $i < $n; $i++; next;
}
if ($ch eq '*') { $re .= '.*'; $i++; next }
if ($ch eq '?') { $re .= '.'; $i++; next }
if ($ch eq '[') { # character class
my $j = $i + 1;
my $neg = 0;
if ($j < $n && ($c[$j] eq '!' || $c[$j] eq '^')) { $neg = 1; $j++ }
my $body = '';
if ($j < $n && $c[$j] eq ']') { $body .= '\\]'; $j++ } # leading ] literal
while ($j < $n && $c[$j] ne ']') {
my $cc = $c[$j];
if ($cc eq '\\' && $j + 1 < $n) {
$body .= '\\' . $c[$j + 1]; $j += 2; next;
}
if ($cc eq '\\' || $cc eq '^' || $cc eq ']') { $body .= '\\' . $cc }
else { $body .= $cc }
$j++;
}
if ($j < $n && $c[$j] eq ']') {
$re .= '[' . ($neg ? '^' : '') . $body . ']';
$i = $j + 1; next;
}
$re .= '\\['; $i++; next; # unterminated [ : literal
}
$re .= quotemeta($ch);
$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.
# ----------------------------------------------------------------
sub _extglob_scan {
my ($chars_ref, $i, $convert_sub) = @_;
my @c = @{$chars_ref};
my $n = scalar @c;
return () unless $i+1 < $n && $c[$i+1] eq '(';
my $op = $c[$i];
my $depth = 1;
my $j = $i + 2;
my $body = '';
while ($j < $n && $depth > 0) {
my $cc = $c[$j];
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;
my $i = 0;
my $depth = 0;
while ($i < $n) {
my $ch = $c[$i];
if ($ch eq '\\' && $i+1 < $n) { $cur .= $ch . $c[$i+1]; $i += 2; next }
if ($ch eq '(') { $depth++; $cur .= $ch; $i++; next }
if ($ch eq ')') { $depth--; $cur .= $ch; $i++; next }
if ($ch eq '|' && $depth == 0) { push @out, $cur; $cur = ''; $i++; next }
$cur .= $ch; $i++;
}
push @out, $cur;
return @out;
}
# ----------------------------------------------------------------
# External command
# ----------------------------------------------------------------
# ----------------------------------------------------------------
lib/BATsh/SH.pm view on Meta::CPAN
=item *
When the command word is supplied by a variable (e.g. C<$CMD &>), the
foreground/background decision is made on the literal first token before
expansion; such lines are treated as external and backgrounded.
=back
=head2 Compound Commands
cmd1 && cmd2 run cmd2 only if cmd1 exits with status 0
cmd1 || cmd2 run cmd2 only if cmd1 exits with non-zero status
cmd1 ; cmd2 run cmd2 unconditionally after cmd1
These are detected B<before> variable expansion to ensure short-circuit
logic works correctly. Quoting (C<'>, C<">) and C<$(...)> nesting are
respected when splitting.
=head2 Function Definitions
name() { body }
function name { body }
name() { cmd1; cmd2; } # inline single-line body
Functions are registered in a package-level hash C<%_SH_FUNCTIONS>.
The caller's positional parameters (C<$1>..C<$9>, C<$*>) are saved before
the call and restored on return. C<local VAR=value> saves the existing
value of C<VAR> in the function's stack frame and restores it on return.
=head2 Brace Expansion
echo a{b,c,d}e # abe ace ade
echo {1..5} # 1 2 3 4 5
echo {5..1} # 5 4 3 2 1
echo {01..03} # 01 02 03 (zero-padded from the wider operand)
echo {a..e} # a b c d e
echo {1..10..2} # 1 3 5 7 9 (numeric step)
echo {a..e..2} # a c e (alpha step)
echo pre{a,b}mid{c,d}post # preamidcpost preamidcpost ...
Brace expansion (v0.07) runs lexically on the raw source line, before any
other expansion, exactly like tilde expansion. A brace group is only
expanded when it contains a top-level comma or a valid C<..> range;
otherwise it -- and any earlier literal braces on the same word -- is
left untouched (C<echo x{foo}y> prints C<x{foo}y>). Quoted text and
C<${...}>, C<$(...)>, C<$((...))>, C<`...`>, C<E<lt>(...)>, C<E<gt>(...)>
regions are protected and copied through unexpanded, matching the fact
that these are not brace-expansion syntax even though some of them also
use C<{> C<}> or C<(> C<)>. Nested and nested nested groups are
supported (each alternative is itself recursively brace-expanded).
=head2 Extended Pattern Matching (extglob)
shopt -s extglob # enable; "shopt -u extglob" disables (the default)
shopt extglob # query; "shopt" alone lists all known options
shopt -p extglob # print in "shopt -s/-u extglob" form
case $f in
@(*.tar.gz|*.tgz)) echo archive ;;
!(*.jpg|*.png)) echo not-an-image ;;
esac
echo ${name%%+([0-9])} # strip a trailing run of digits
While C<shopt -s extglob> is active, C<?(list)>, C<*(list)>, C<+(list)>,
C<@(list)>, and C<!(list)> pattern-list operators (C<|>-separated
alternatives, each itself an ordinary glob or a nested extglob group) are
recognised in case patterns and in the C<${VAR%pat}> / C<${VAR%%pat}> /
C<${VAR#pat}> / C<${VAR##pat}> / C<${VAR/pat/rep}> / C<${VAR//pat/rep}>
pattern operand. C<extglob> is off by default, matching bash, and is
reset to off by C<reset_sh_options()> between top-level runs, alongside
C<set -e> / C<-u> / C<-x>.
=head3 Extended Pattern Matching Limitations
=over 4
=item *
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"
read LINE <<< hello
A here-string (C<E<lt>E<lt>E<lt> word>, v0.07) supplies I<word> -- after
tilde, parameter, command, and arithmetic expansion, and quote removal,
exactly like any other word -- as the command's standard input, with a
trailing newline appended. Unlike a here-document body, I<word> is not
further word-split. Implementation-wise this reuses the here-document
temporary-file machinery: the expanded content is written to a uniquely
named C<sysopen(...,O_CREAT|O_EXCL,...)> temp file and supplied through
the same redirection path as C<E<lt> file>, removed immediately after the
command finishes. As with here-documents, only one here-string (or
here-document) per command line is handled.
=head2 Process Substitution
diff <(sort a.txt) <(sort b.txt)
generate | tee >(gzip > out.gz)
This interpreter never forks (see L</Background Execution> above), so
neither form of process substitution (v0.07) uses a real named pipe:
=over 4
=item C<E<lt>(cmd)>
I<cmd> is run immediately, its standard output captured into a fresh
temporary file (exactly like C<$(cmd)>, but the file is kept rather than
read back into a scalar), and C<E<lt>(cmd)> is replaced by that file's
path -- suitable for anything that wants a filename to read from.
=item C<E<gt>(cmd)>
An empty temporary file is created immediately and C<E<gt>(cmd)> is
replaced by its path; I<cmd> itself is deferred and run with that file as
its standard input only after the current simple command has finished.
Because I<cmd> runs after, rather than concurrently with, the writer,
this is a best-effort approximation of real streaming C<E<gt>(...)> and
does not suit a writer that expects the reader to keep up in real time.
=back
Both temporary files are removed once the current simple command (and,
for C<E<gt>(cmd)>, its deferred job) has finished; a process substitution
( run in 1.622 second using v1.01-cache-2.11-cpan-788537b7465 )