Proc-Spawner version 0.12 (EXPERIMENTAL)
========================================

Compiles and runs shell one-liners (or portable shell scripts) from Perl
without /bin/sh and "without" fork, matching dash/bash: the strings are
parsed by one of the Proc::ShellSubset parsers and run
by Proc::ShellSpawn, which starts programs with posix_spawn(3) and runs loops,
subshells, $( ) and pipelines of compound commands inside the Perl process.
(All the started programs are children of the initial Perl process, as opposed
to descendants.)

  Proc::Spawner          start children (every posix_spawn setting)
  Proc::Spawner::Loop    the event loop, one interface on POSIX and on Windows
  Proc::Spawner::Graph   the executable graph of children, pipes and files
  Proc::ShellSpawn       the shell layer
  Proc::ShellSubset, Proc::ShellSubset::Small, Proc::ShellSubset::Large
                         parsers of three sizes of the shell language

WHAT IS OPEN: the file TODO lists every postponed thing in one place, first
the decisions waiting for the author (with where to read and how to act),
then the work, then what to watch.

HOW CLOSE TO DASH AND BASH (0.12; doc/large-subset/large-subset-state.md)

Snippet by snippet, across environments, with every difference of ours and
the ruling that allows it: doc/large-subset/large-subset-environment.md,
section 11 (post-0.11).
To redo any of these measurements: make corpus-help (after perl Makefile.PL;
Linux, root for the runs; doc/test-pools.md, section 0).

Of 41,952 shell strings from six public collections (tldr, NL2Bash,
ShellCheck's tests, the bash Cookbook, the pure sh and bash bibles), the
large parser accepts 40,410.  The rest are mostly bash-only syntax, which the
subset rejects on purpose; 44 are forms both shells run alike, the work list
(section 4).  Each accepted string was then run by dash, by bash --posix and
by Proc::ShellSpawn, isolated, with every program but the plain text tools
replaced by a recorder of its arguments, each run with a fresh /tmp, a
fixed /dev/urandom and fixed file times, and background jobs still running
when a shell ends given up to 5 seconds (section 7, the run of post-0.11;
the limits: known issue X27).  Ours ran echo, printf, test, [, true, false
and pwd inside Perl, its default since 0.10:

                       run   same   only the    dash and bash  dash and bash
                                    messages    agree, ours    differ
                                    differ      differs
  coreutils involved  1,333    887     310            4            129
  no coreutils       39,077 14,899  23,080            2          1,084
  all                40,410 15,786  23,390            6          1,213
  (and 15 strings that never end, or not in ours: section 7)

"Coreutils involved": the string calls echo, printf, test, [, kill or pwd,
which the subset runs as programs where the shells have builtins.  "Only the
messages differ" is mostly dash's "x: not found" against bash's "x: command
not found".  Where dash and bash differ, the subset follows dash in 965,
bash in 98, and neither in 150 (mostly $0, process IDs, times, and the
interactive builtins).  The 6 where ours differs: printf -v (2, decided),
echo into a broken pipe in a subshell (1), exec command (1: known issue X56,
a bug of ours, open), a race (1), and ls -l of / (1: times outside the run).
Of the 40,410, 365+27=392 are shown not to depend on that environment at
all (N+M: N found by the programs, M by reading the snippets: section 1
there), and 34,304 more not under named assumptions (mostly that a program
does the same in each run), so their agreement is a real one; 2,140+14=2,154
more if also every value they take from the environment only passes through
the shell (664+7 unchanged, 1,476+7 split as well); the other 3,508+52=3,560
may depend on files, variables, programs' output, /tmp and the
like (known issue X27: doc/large-subset/large-subset-environment.md, and a
record by snippet ID,
doc/large-subset/large-subset-provenance.tsv).  Runs with the environment varied
(a second case of POSIXLY_CORRECT, the locale, SIGPIPE, /proc; a fresh /tmp,
a fixed /dev/urandom) are footnotes to these figures: of the samples tried,
few came out otherwise (section 9 there).
Across environments (post-0.11; section 11 there: two trees of every
snippet, over the base run and the example environments of steps 2 and 3):
with messages counted nowhere, wherever a redirection sends them,
ours = dash = bash in every environment for 38,603; in 1,793 ours differs
from a shell somewhere: 1,620 allowed by the author's rulings (mostly A5,
ours with dash where the shells differ), 88 awaiting a decision, 40 chance,
45 not yet allowed.  Each echo, printf, test, [, kill or pwd those snippets
ran (341 distinct calls) was also run alone by coreutils, dash's builtin,
bash's builtin and ours: ours = coreutils in all.

For the programs alone, each call the strings make was also run as dash's
builtin, as bash's builtin and as the program: of 650 calls (1,154 uses),
515 (995) agree, 41 (45) differ in messages only, and 94 (114) differ:
mostly echo -e and backslashes (dash's builtin against bash and the
program), unary -a -o -v in [ ] (bash's builtin against dash and the
program), and kill with a word that is no process ID (dash against bash and
the program).  Section 7 has them all, with the decisions taken so far.

Tested on Linux with glibc 2.39 (also built as if some or all of the
extensions were missing: PROC_SPAWNER_WITHOUT="PIDFD_SPAWN ..." perl Makefile.PL),
and with /proc hidden, as on FreeBSD (RT #182479; FreeBSD itself is not tested
here): without /proc, t/graph.t and two batteries of t/dash.t are skipped,
and Perl's own writes into a pipe the caller gave may block (known issue X32).  Parts that flip process-wide
state for a while (umask, signal actions) are bullet-proof only in
non-threaded execution.

INSTALLATION

To install this module type the following:

   perl Makefile.PL
   make
   make test
   make install

DEPENDENCIES

Only modules that come with Perl.  Building needs a C compiler.
Makefile.PL finds whether the libc has posix_spawn, and which of its
extensions (chdir, fchdir, closefrom, tcsetpgrp, pidfd_spawn, cgroup), by
compiling a call to each.  What the libc lacks is done in Perl's process
around the call where that can be undone (chdir, the fds, the signal mask,
...); the rest fails with ENOTSUP, or is dropped, as
%Proc::Spawner::UNSUPPORTED says.  Without posix_spawn the primitive is fork
and exec.  Resource limits, ignored signals and the nice value, which
posix_spawn has no option for, are set around the call too.

Tests: t/spawner.t (Proc::Spawner and the graph), t/graph.t (hand-written
graphs), t/parsers.t (1,117 strings through the parsers), and t/dash.t
(every battery string run by dash and by the shell layer; skipped without
dash; t/dash-known.txt lists the known differences).  Their fixtures are
in t/: t/batteries/, t/parser/, t/sgraph/, t/try.pl (runs one string: perl t/try.pl 'STRING').

COPYRIGHT AND LICENCE

Copyright (C) 2026 by Ilya Zakharevich

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.22.2 or,
at your option, any later version of Perl 5 you may have available.
