BATsh

 view release on metacpan or  search on metacpan

lib/BATsh/SH.pm  view on Meta::CPAN

        if ($stripped =~ /\A(?:function\s+[A-Za-z_]|[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\))/) {
            ($status, $i) = _parse_function($class, \@lines, $i - 1, $opts_ref);
            next;
        }

        # Here-document: cmd << [-] [QUOTE]DELIM[QUOTE]
        # Detected on a simple command line; body is read from following
        # lines up to a line equal to DELIM (after optional tab strip for <<-).
        my @hd = _hd_detect($line);
        if (@hd) {
            my ($cmd_part, $dash, $delim, $quoted) = @hd;
            my @body = ();
            my $terminated = 0;
            while ($i <= $#lines) {
                my $bl = $lines[$i];
                $i++;
                $bl =~ s/\r?\n\z//;
                my $probe = $bl;
                $probe =~ s/\A\t+// if $dash;   # <<- strips leading tabs
                if ($probe eq $delim) { $terminated = 1; last }
                $bl =~ s/\A\t+// if $dash;       # also strip tabs from body
                push @body, $bl;
            }
            if (!$terminated) {
                warn "sh: unexpected EOF while looking for here-document delimiter \`$delim'\n";
                $LAST_STATUS = 2;
                $status = 2;
                next;
            }
            $status = _hd_run($class, $cmd_part, \@body, $quoted, $opts_ref);
            next;
        }

        $status = _exec_line($class, $line, $opts_ref);
        $_CONTINUE = 0 if $_CONTINUE;
    }
    return $status;
}

# ----------------------------------------------------------------
# Execute one SH line
# ----------------------------------------------------------------
sub _exec_line {
    my ($class, $raw, $opts_ref) = @_;

    my $line = $raw;
    $line =~ s/\A\s+//;
    return 0 if $line =~ /\A\s*\z/;
    return 0 if $line =~ /\A\s*#/;

    # Shebang: treat as comment
    return 0 if $line =~ /\A#!/;

    # ----------------------------------------------------------------
    # Background execution: an unquoted trailing & (v1).
    # Detected here, BEFORE _split_sh_compound, so that the bare & is
    # never mistaken for && and so that an internal & (e.g. in 2>&1 or
    # >&2) is left untouched.  Only the single & at the very end of the
    # line is consumed.  Builtins / functions / control words / variable
    # assignments run in the FOREGROUND (the & is ignored); only external
    # commands are launched asynchronously.
    # ----------------------------------------------------------------
    my ($_is_bg, $_bg_line) = _split_trailing_bg($line);
    if ($_is_bg) {
        $line = $_bg_line;
        my $probe = $line;
        $probe =~ s/\A\s+//;
        my $w0 = '';
        ($w0) = ($probe =~ /\A(\S+)/);
        $w0 = '' unless defined $w0;
        if (_sh_word_is_foreground($w0)) {
            # & ignored: run the stripped line in the foreground.
            return _exec_line($class, $line, $opts_ref);
        }
        my $exp = _expand($class, $line);
        $exp =~ s/\A\s+//;
        $exp =~ s/\s+\z//;
        return _bg_launch($class, $exp);
    }

    # Detect && / || / ; compound commands BEFORE expansion.
    # These must be split before _expand so that short-circuit logic works.
    my @compound = _split_sh_compound($line);
    if (@compound > 1) {
        return _exec_sh_compound($class, \@compound, $opts_ref);
    }

    # Detect pipeline BEFORE variable expansion to avoid expanding
    # pipe-like characters inside command substitutions prematurely.
    # _split_sh_pipe returns >1 segment only when bare | is present.
    my @pipe_segs = _split_sh_pipe($line);
    if (@pipe_segs > 1) {
        return _exec_sh_pipe($class, \@pipe_segs, $opts_ref);
    }

    # Array / associative-array operations (v0.06).  Detected on the RAW line
    # (before _expand) so that the "(a b c)" literal and the "[sub]" subscripts
    # are not mangled by variable / command-substitution expansion.
    {
        my @h = _sh_try_array_op($class, $line, $opts_ref);
        return $h[1] if @h;
    }

    # trap (v0.06).  Detected on the RAW line so that the handler command is
    # captured literally and (re-)expanded only when the trap fires, matching
    # shell semantics for e.g. trap 'rm $tmp' EXIT.
    {
        my $probe = $line;
        $probe =~ s/\A\s+//;
        if ($probe =~ /\Atrap(\s.*|)\z/is && $probe !~ /\Atrap\s*=/) {
            return _cmd_trap($class, $1, $opts_ref);
        }
    }

    # POSIX assignment prefix on the RAW line: `VAR=value command args`.
    # Detected before expansion so that a value containing $(...) or quoted
    # spaces is not mistaken for a trailing command.  Pure assignments (no
    # command following) fall through to the post-expansion handler below.
    {
        my ($pairs_ref, $remainder) = _sh_assign_prefix($line);
        if ($pairs_ref && defined $remainder && $remainder ne '') {

lib/BATsh/SH.pm  view on Meta::CPAN

sub _sh_word_is_foreground {
    my ($w) = @_;
    return 0 unless defined $w && $w ne '';

    # VAR=value assignment
    return 1 if $w =~ /\A[A-Za-z_][A-Za-z0-9_]*=/;

    # test bracket and no-op
    return 1 if $w eq '[' || $w eq ':' || $w eq '.';

    my $lc = lc($w);

    my %builtin = (
        export => 1, unset => 1, echo => 1, printf => 1, cd => 1,
        pwd => 1, exit => 1, 'true' => 1, 'false' => 1, read => 1,
        test => 1, source => 1, 'return' => 1, 'break' => 1,
        'continue' => 1, shift => 1, local => 1, set => 1,
    );
    return 1 if $builtin{$lc};

    # Control keywords (defensive; these are normally handled in _run_lines)
    my %kw = (
        'if' => 1, then => 1, 'else' => 1, elif => 1, fi => 1,
        'for' => 1, 'while' => 1, until => 1, 'do' => 1, done => 1,
        case => 1, esac => 1, function => 1, in => 1,
    );
    return 1 if $kw{$lc};

    # Defined SH function (case-sensitive, as in _exec_line dispatch)
    return 1 if exists $_SH_FUNCTIONS{$w};

    return 0;
}

# _bg_tempfile: create a unique, empty temp file (O_CREAT|O_EXCL to avoid
# symlink races) for capturing a background job's PID on Unix-like systems.
# Returns the path, or undef on failure.
sub _bg_tempfile {
    my $dir = $ENV{'TMPDIR'} || $ENV{'TEMP'} || $ENV{'TMP'} || '';
    $dir = '/tmp' if $dir eq '' && -d '/tmp';
    $dir = '.'    if $dir eq '';
    $dir =~ s{[\\/]+\z}{};
    $dir = '.' if !(-d $dir && -w $dir);

    my $attempt = 0;
    while ($attempt < 1000) {
        $_BG_SEQ++;
        $attempt++;
        my $path = $dir . '/' . 'batsh_bg_' . $$ . '_' . $_BG_SEQ;
        if (sysopen(_BG_TMP, $path, O_WRONLY | O_CREAT | O_EXCL, 0600)) {
            close(_BG_TMP);
            push @_BG_TMPFILES, $path;
            return $path;
        }
        # EEXIST or transient error: retry with next sequence number
    }
    warn "sh: cannot create background pidfile in $dir: $!\n";
    return undef;
}

# _bg_launch: start $cmdline asynchronously.
#   Win32      : system(1, STRING) spawns via the command shell (P_NOWAIT)
#                and returns the PID directly.
#   Unix-like  : delegate to /bin/sh so the job is backgrounded without a
#                Perl fork; the shell's $! (the job PID) is written to a
#                temp file and read back into BATsh's own $!.
# On a successful launch $? (LAST_STATUS) is 0; the exit code of the
# background job itself is not awaited (sh semantics).
sub _bg_launch {
    my ($class, $cmdline) = @_;
    $cmdline = '' unless defined $cmdline;
    return 0 if $cmdline =~ /\A\s*\z/;
    BATsh::Env->sync_to_env();

    if ($^O =~ /MSWin32/i) {
        my $pid = system(1, $cmdline);
        if (defined $pid && $pid > 0) {
            $_LAST_BG_PID = $pid;
            $LAST_STATUS  = 0;
        }
        else {
            warn "sh: failed to start background process\n";
            $LAST_STATUS = 1;
        }
        return $LAST_STATUS;
    }

    # Unix-like
    my $pidfile = _bg_tempfile();
    my $rc;
    if (defined $pidfile) {
        # Group the command so that the whole list (pipelines, &&, ...) is
        # backgrounded as a unit, then echo the job PID ($!) to the file.
        $rc = system("{ $cmdline ; } & echo \$! > '$pidfile'");
        if (open(_BG_PIDFH, "< $pidfile")) {
            local $/;
            my $buf = <_BG_PIDFH>;
            close(_BG_PIDFH);
            $buf = '' unless defined $buf;
            my $pid = '';
            ($pid) = ($buf =~ /(\d+)/);
            $_LAST_BG_PID = $pid if defined $pid && $pid ne '';
        }
        unlink $pidfile;
        @_BG_TMPFILES = grep { $_ ne $pidfile } @_BG_TMPFILES;
    }
    else {
        $rc = system("{ $cmdline ; } &");
    }
    $LAST_STATUS = (defined $rc && $rc != -1) ? 0 : 1;
    return $LAST_STATUS;
}


# ----------------------------------------------------------------
# Split "cmd rest" honouring quoted strings
# ----------------------------------------------------------------
sub _split_sh {
    my ($line) = @_;
    if ($line =~ /\A(\S+)\s*(.*)\z/s) {
        return ($1, $2);

lib/BATsh/SH.pm  view on Meta::CPAN

The body is written to a uniquely named temporary file created with
C<sysopen(...,O_CREAT|O_EXCL,...)> to avoid symlink races, and that file
is supplied as standard input through the same redirection path used by
C<E<lt> file>.  Both built-ins (e.g. C<read>) and external commands run
via C<system()> therefore see the body on STDIN.  The temporary file is
removed immediately after the command finishes, with an C<END> block as a
failsafe.  This implementation is Pure Perl and Perl 5.005_03 compatible.

The closing delimiter must appear on a line by itself and match exactly
(after tab stripping for C<<-E<lt>E<lt>->>); trailing whitespace is not
ignored.  If no matching delimiter is found before end of input, a
warning is issued and C<$?> is set to a non-zero value.

=head3 Here-Document Limitations

The following are B<not> supported in this release and are documented as
known limitations:

=over 4

=item *

Here-documents are recognised only in SH mode.  The C<E<lt>E<lt>>
sequence has no special meaning in CMD mode (C<BATsh::CMD>) and is left
untouched there.

=item *

Only a single here-document per command line is handled.  Multiple
here-documents on one line (C<cmd E<lt>E<lt>A E<lt>E<lt>B>) are not
supported.

=item *

Here-strings (C<E<lt>E<lt>E<lt> word>) are not supported; C<E<lt>E<lt>E<lt>>
is deliberately not treated as a here-document opener.

=item *

Combining a here-document with a pipeline or compound operator on the
same line (e.g. C<cmd E<lt>E<lt>EOF | other>) is best-effort only and not
guaranteed; use a separate command for portable behaviour.

=item *

The delimiter word is matched literally; the C<E<lt>E<lt>"a b"> form with
an embedded space in the delimiter is not supported.

=item *

A here-document body line that looks like a BATsh subroutine marker
(a line of the form C<:LABEL> later followed by C<RET>/C<RETURN>) may be
consumed by subroutine extraction, which runs before mode dispatch.
Avoid such lines inside here-document bodies.

=back

=head2 Background Execution

An unquoted C<&> at the very end of an SH command line starts the command
asynchronously and returns control immediately, in the style of POSIX
shells:

  longjob &
  echo "next prompt"

Only the single C<&> at the end of the line is consumed.  An C<&> that is
part of C<&&>, of an fd-duplication such as C<2E<gt>&1> or C<1E<gt>&2>,
inside single or double quotes, or backslash-escaped (C<\&>) is B<not>
treated as a background operator and is left in place.

The launch is Pure Perl and Perl 5.005_03 compatible, with a portable
split by platform:

=over 4

=item *

On Win32, the command is spawned through the command shell with
C<system(1, ...)> (P_NOWAIT), which returns the process id directly.

=item *

On Unix-like systems the command is started by delegating to F</bin/sh>
(no Perl C<fork> is used), and the background job's process id is captured
through the shell's own C<$!> into a uniquely named temporary file created
with C<sysopen(...,O_CREAT|O_EXCL,...)>.  The temporary file is removed
immediately, with an C<END> block as a failsafe.

=back

On a successful launch C<$?> is set to C<0>; the exit status of the
background job itself is B<not> awaited.  The process id of the most
recently started background job is available through C<$!>, which expands
to the empty string before any background job has been started.

=head3 Background Execution Limitations

The following are B<not> supported in this release and are documented as
known limitations:

=over 4

=item *

Background execution applies only to B<external> commands in SH mode.
A trailing C<&> on a built-in, a defined function, a variable assignment,
or a control keyword is ignored and the command runs in the foreground.
In CMD mode (C<BATsh::CMD>) C<&> keeps its cmd.exe meaning as a sequential
command separator and is unchanged.

=item *

Only a trailing C<&> is recognised.  A mid-line C<&> that backgrounds part
of a line (e.g. C<a & b>) is not supported; write C<a> on its own line
with a trailing C<&> instead.

=item *

There is no job control: C<jobs>, C<wait>, C<wait %n>, C<fg>, C<bg> and
job-specification (C<%n>) syntax are not implemented.  Signals are not



( run in 0.513 second using v1.01-cache-2.11-cpan-9581c071862 )