NAME
App::Actions::Process - run other programs, the same way on Unix and Windows
SYNOPSIS
Run a program and read what it printed:
use App::Actions::Process;
my ($output, $status) = App::Actions::Process::capture(['git', 'rev-parse', 'HEAD']);
die "not a git checkout\n" if $status != 0;
Run a program and show its output to the user:
my $status = App::Actions::Process::stream(['make', 'test']);
Run a program and hide everything it prints:
my $status = App::Actions::Process::run_quiet(['docker', 'info']);
Run several programs at the same time:
my $statuses = App::Actions::Process::run_parallel(
[ ['docker', 'pull', 'alpine'], ['docker', 'pull', 'perl'] ],
concurrency => App::Actions::Process::cpu_count(),
);
Send the output to a file:
my $status = App::Actions::Process::run(['ls', '-l'], stdout => 'listing.txt');
DESCRIPTION
This module starts other programs and tells you how they ended. It works on Linux, macOS and Windows with the same code.
Every function takes the command as a reference to a list: the program first, then each argument. A shell is never used. Each argument reaches the program exactly as you wrote it, even if it contains spaces, quotes, $, ; or |. So text from a workflow cannot run a second command.
Windows has no separate arguments: a program gets one command line and splits it up itself. On Windows this module quotes each argument with "quote_win32" so that it is split back correctly. On other systems the arguments are passed as they are.
Every function returns an exit status as a shell shows it:
0 - the program succeeded;
1 to 125 - the program failed, and this is the number it chose;
255 - the program could not be started (for example, it does not exist);
128 + N - the program was stopped by signal N (130 for Ctrl-C).
FUNCTIONS
No function is exported. Call them with their full names, for example App::Actions::Process::capture(...).
exit_code
my $status = App::Actions::Process::exit_code($?);
Turns Perl's $? into an exit status, as described above.
ARGUMENTS
$status- the value of$?aftersystemorwaitpid.
RETURNS
The exit status.
SIDE EFFECTS
None.
EXAMPLE
system { 'true' } 'true';
print App::Actions::Process::exit_code($?); # 0
API SPECIFICATION
INPUT
{ status => { type => 'integer', optional => 1 } }
OUTPUT
{ type => 'integer', min => 0, max => 255 }
MESSAGES
None.
cpu_count
my $count = App::Actions::Process::cpu_count();
Returns how many processors this computer can use now. action-run uses it as the default for --parallel.
ARGUMENTS
None.
RETURNS
A whole number, 1 or more. If the computer does not say, 1.
SIDE EFFECTS
Reads NUMBER_OF_PROCESSORS on Windows, /proc/cpuinfo on Linux, or asks the system with sysconf.
EXAMPLE
my $jobs = App::Actions::Process::cpu_count();
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'integer', min => 1 }
MESSAGES
None.
capture
my ($output, $status) = App::Actions::Process::capture(\@command, %options);
Runs a program and returns what it printed.
ARGUMENTS
\@command- the program and its arguments. Not empty.quiet_stderr- optional. True to throw away the error output. Otherwise it goes to your terminal.merge_stderr- optional. True to include the error output in the returned text, in the order the program wrote it (like the shell's2>&1).
RETURNS
A list of two values: the output, and the exit status. Windows line ends (\r\n) in the output become \n.
SIDE EFFECTS
Writes the output to a temporary file, which is deleted afterwards.
EXAMPLE
my ($version) = App::Actions::Process::capture(['perl', '-e', 'print $]']);
API SPECIFICATION
INPUT
{
command => { type => 'arrayref', min => 1 },
quiet_stderr => { type => 'boolean', optional => 1 },
merge_stderr => { type => 'boolean', optional => 1 },
}
OUTPUT
{ type => 'array', min => 2, max => 2 } # (string, integer)
MESSAGES
+------------------------------------------+---------------------------------+---------------------------+
| Exception (croak) | Meaning | What to do |
+------------------------------------------+---------------------------------+---------------------------+
| No command given | not a non-empty array reference | pass [program, args...] |
| Unable to rewind captured output: ERR | the temporary file failed | check the temp directory |
| Unable to redirect command output: ERR | the output file could not be | check the temp directory |
| | opened | and disk space |
| Unable to duplicate STDOUT/STDERR: ERR | the program's own handles are | do not close STDOUT or |
| Unable to restore STDOUT/STDERR: ERR | broken | STDERR before calling |
+------------------------------------------+---------------------------------+---------------------------+
run
my $status = App::Actions::Process::run(\@command, %options);
Runs a program and sends its output to a file.
ARGUMENTS
\@command- the program and its arguments. Not empty.stdout- optional. The file for the output. Default: the null device (the output is thrown away).stderr- optional. The file for the error output. Default: your terminal.merge_stderr- optional. True to send the error output to thestdoutfile too. It wins overstderr.
RETURNS
The exit status.
SIDE EFFECTS
Replaces the files named by stdout and stderr. While the program runs, this process's own STDOUT and STDERR point to those files; they are put back afterwards.
EXAMPLE
my $status = App::Actions::Process::run(['make'], stdout => 'build.log', merge_stderr => 1);
API SPECIFICATION
INPUT
{
command => { type => 'arrayref', min => 1 },
stdout => { type => 'string', optional => 1 },
stderr => { type => 'string', optional => 1 },
merge_stderr => { type => 'boolean', optional => 1 },
}
OUTPUT
{ type => 'integer', min => 0, max => 255 }
MESSAGES
As for "capture", except the "rewind" message.
run_quiet
my $status = App::Actions::Process::run_quiet(\@command);
Runs a program and throws away everything it prints.
ARGUMENTS
\@command- the program and its arguments.
RETURNS
The exit status.
SIDE EFFECTS
None, apart from what the program does.
API SPECIFICATION
INPUT
{ command => { type => 'arrayref', min => 1 } }
OUTPUT
{ type => 'integer', min => 0, max => 255 }
MESSAGES
As for "run".
stream
my $status = App::Actions::Process::stream(\@command);
Runs a program with its output and error output going straight to your terminal, as it happens.
ARGUMENTS
\@command- the program and its arguments.
RETURNS
The exit status.
SIDE EFFECTS
Flushes STDOUT and STDERR first, so that what you printed before appears before the program's output.
API SPECIFICATION
INPUT
{ command => { type => 'arrayref', min => 1 } }
OUTPUT
{ type => 'integer', min => 0, max => 255 }
MESSAGES
+------------------+------------------------------------+-------------------------+
| Exception (croak)| Meaning | What to do |
+------------------+------------------------------------+-------------------------+
| No command given | not a non-empty array reference | pass [program, args...] |
+------------------+------------------------------------+-------------------------+
run_parallel
my $statuses = App::Actions::Process::run_parallel(\@commands, %options);
Runs several programs, some at the same time.
ARGUMENTS
\@commands- a list of commands, each a list reference as for "stream".concurrency- optional. The most programs that run at the same time. Default 4.stdout,stderr- optional. Files for every program's output, as for "run". Without them, the output goes to your terminal, mixed together.
RETURNS
An array reference of exit statuses, in the same order as \@commands. An empty list gives an empty array reference.
SIDE EFFECTS
On Unix, uses fork. On Windows, or with concurrency below 2, the programs run one after the other. If this process is interrupted while waiting, the programs still running are killed.
EXAMPLE
my $statuses = App::Actions::Process::run_parallel(
[ map { ['gzip', '-9', $_] } @files ],
concurrency => 2,
);
my @failed = grep { $statuses->[$_] != 0 } 0 .. $#files;
API SPECIFICATION
INPUT
{
commands => { type => 'arrayref' },
concurrency => { type => 'integer', min => 1, optional => 1, default => 4 },
stdout => { type => 'string', optional => 1 },
stderr => { type => 'string', optional => 1 },
}
OUTPUT
{ type => 'arrayref', element_type => 'integer' }
MESSAGES
+--------------------------+----------------------------------+------------------------------+
| Message | Meaning | What to do |
+--------------------------+----------------------------------+------------------------------+
| No commands given (croak)| \@commands is not an array ref | pass an array reference |
| Unable to run 'P': ERR | (printed by the child) program P | install P or fix its path; |
| | could not start; status 127 | the status is 127 |
| Unable to redirect | (printed by the child) the file | check the path; status 127 |
| STREAM to 'F': ERR | could not be opened | |
+--------------------------+----------------------------------+------------------------------+
quote_win32
my $argument = App::Actions::Process::quote_win32($text);
Quotes one argument for a Windows command line, using the rules Windows programs use to split it again. The other functions call it for you on Windows; you need it only to build a Windows command line yourself.
ARGUMENTS
$text- one argument.undefis treated as the empty string.
RETURNS
The argument, quoted if it needs to be. An argument without spaces or double quotes is returned unchanged. An empty argument becomes "".
SIDE EFFECTS
None.
EXAMPLE
print App::Actions::Process::quote_win32('say "hi"'); # "say \"hi\""
print App::Actions::Process::quote_win32('C:\dir\\'); # C:\dir\
print App::Actions::Process::quote_win32('a b\\'); # "a b\\"
API SPECIFICATION
INPUT
{ argument => { type => 'string', optional => 1 } }
OUTPUT
{ type => 'string', min => 1 }
MESSAGES
None.
COMMON PITFALLS
Pass a list reference, not a string.
capture('ls -l')is an error, andcapture(['ls -l'])looks for a program called "ls -l". Writecapture(['ls', '-l']).There is no shell. Pipes (
|), redirection (>), wildcards (*.txt) and variables ($HOME) are not expanded. If you need them, run the shell yourself:['sh', '-c', 'ls *.txt'], and never put untrusted text inside that string.capture returns a list.
my $text = capture(...)gives the status, not the text. Writemy ($text, $status) = capture(...).A status is not Perl's
$?. Compare it with 0, not with$? >> 8.A missing program is status 255, not an exception. Perl also prints a "Can't exec" warning, except in "run", "run_quiet" and "capture", which hide it.
Output from parallel programs is mixed. Without
stdout, "run_parallel" lets every program write to the same terminal, so their lines can be interleaved.Captured output is held in memory. Do not capture a program that prints gigabytes.
ENCODING
Commands, arguments and captured output are bytes. Encode text with non-ASCII characters or emoji (for example with Encode::encode('UTF-8', $text)) before you put it in a command, and decode captured output if you need characters. On Windows, the program receives the command line through the Windows "ANSI" code page, so characters outside it may be lost. An argument cannot contain a NUL character.
AUTHOR
Nigel Horne, njh@nigelhorne.com
FORMAL SPECIFICATION
This section describes each routine in the Z notation, a precise mathematical language for describing programs. You do not need to read it to use the module: everything here is also said in plain words in the sections above.
How to read it: a box names a routine. The lines above the middle rule declare the values it works with. A name ending in ? is an input, and a name ending in ! is an output. The lines below the rule are always true when the routine returns. The module keeps no state of its own; Host is the computer, whose processes and files the routines change.
[ARG, FILE]
STATUS == 0 .. 255
COMMAND == seq₁ ARG
exec : COMMAND → STATUS × seq BYTE × seq BYTE status, output, error output
exec(c) runs program head c with arguments tail c, without a shell
exit_code
┌─ ExitCode ────────────────────────────────────────
│ status? : optional ℤ
│ r! : STATUS
├───────────────────────────────────────────────────
│ status? = 0 ∨ status? = ⊥ ⇒ r! = 0
│ status? mod 128 ≠ 0 ⇒ r! = 128 + (status? mod 128)
│ otherwise r! = status? div 256
└───────────────────────────────────────────────────
cpu_count
┌─ CpuCount ────────────────────────────────────────
│ ΞHost
│ r! : ℕ₁
├───────────────────────────────────────────────────
│ r! = first defined of ⟨NUMBER_OF_PROCESSORS (Windows),
│ #processor lines in /proc/cpuinfo, sysconf(NPROCESSORS_ONLN), 1⟩
└───────────────────────────────────────────────────
capture
┌─ Capture ─────────────────────────────────────────
│ ΔHost
│ command? : COMMAND
│ quiet?, merge? : 𝔹
│ r! : seq BYTE × STATUS
├───────────────────────────────────────────────────
│ (s, out, err) = exec(command?)
│ merge? ⇒ r! = (crlf_to_lf(out ⋈ err), s) ⋈ : in time order
│ ¬merge? ⇒ r! = (crlf_to_lf(out), s)
│ ¬merge? ∧ ¬quiet? ⇒ err goes to the caller's STDERR
└───────────────────────────────────────────────────
run
┌─ Run ─────────────────────────────────────────────
│ ΔHost
│ command? : COMMAND
│ stdout?, stderr? : optional FILE
│ merge? : 𝔹
│ r! : STATUS
├───────────────────────────────────────────────────
│ (s, out, err) = exec(command?)
│ r! = s
│ contents'(stdout? or null) = out ⋈ (if merge? then err else ⟨⟩)
│ ¬merge? ∧ stderr? ≠ ⊥ ⇒ contents'(stderr?) = err
│ STDOUT' = STDOUT ∧ STDERR' = STDERR
└───────────────────────────────────────────────────
run_quiet
┌─ RunQuiet ────────────────────────────────────────
│ Run[stdout? := null, stderr? := null]
└───────────────────────────────────────────────────
stream
┌─ Stream ──────────────────────────────────────────
│ ΔHost
│ command? : COMMAND
│ r! : STATUS
├───────────────────────────────────────────────────
│ (s, out, err) = exec(command?) ∧ r! = s
│ out goes to STDOUT and err to STDERR, after what was already buffered
└───────────────────────────────────────────────────
run_parallel
┌─ RunParallel ─────────────────────────────────────
│ ΔHost
│ commands? : seq COMMAND
│ concurrency? : ℕ₁
│ r! : seq STATUS
├───────────────────────────────────────────────────
│ #r! = #commands?
│ ∀ i ∈ dom commands? • r!(i) = first(exec(commands?(i)))
│ at no time do more than min(concurrency?, #commands?) commands run
│ Windows ∨ concurrency? < 2 ⇒ the commands run one after another
└───────────────────────────────────────────────────
quote_win32
┌─ QuoteWin32 ──────────────────────────────────────
│ arg? : optional seq CHAR
│ r! : seq₁ CHAR
├───────────────────────────────────────────────────
│ arg? = ⊥ ∨ arg? = ⟨⟩ ⇒ r! = "\"\""
│ arg? contains no white space and no '"' ⇒ r! = arg?
│ otherwise r! = '"' ⁀ escape(arg?) ⁀ '"' where
│ a run of n '\' before '"' becomes 2n+1 '\' then '"',
│ a run of n '\' at the end becomes 2n '\',
│ any other run of '\' is unchanged
│ CommandLineToArgv(r!) = ⟨arg?⟩
└───────────────────────────────────────────────────
STATE DIAGRAM
The module keeps no state between calls. Each command goes through these states:
capture / run / run_quiet / stream / run_parallel
|
v
+------------------+ not a non-empty list +-------------+
| CHECK ARGUMENTS |------------------------>| croak |
+------------------+ +-------------+
|
v
+------------------+ (run, capture, run_quiet)
| REDIRECT | STDOUT/STDERR -> files; open fails
| (if asked) |------------------------------+
+------------------+ |
| |
v |
+------------------+ Windows: quote_win32 each |
| START PROGRAM | argument; never a shell |
+------------------+ |
| | |
cannot start | | started |
v v |
status 255 +------------------+ |
| | RUNNING | run_parallel: up to |
| +------------------+ "concurrency" here |
| | |
| | exits, or killed by signal N |
| v |
| status = code, or 128 + N |
| | |
+------+-----+ |
v |
+------------------+ |
| RESTORE HANDLES |<-----------------------------+
+------------------+
| |
| +--- redirect had failed --> croak
v
return status (capture: and the output)
LICENSE AND COPYRIGHT
Copyright 2026 Nigel Horne.
This program is released under the GNU General Public License, version 2. See the LICENSE file that comes with this distribution. If you use it, please let me know.