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 )