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 )