218 lines
5.7 KiB
PHP
218 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Nih\CommandBuilder;
|
|
|
|
use RuntimeException;
|
|
use Stringable;
|
|
use TypeError;
|
|
use ValueError;
|
|
|
|
final class Command implements Stringable
|
|
{
|
|
public readonly string $command;
|
|
|
|
private array $args = [];
|
|
private ?array $envs = null;
|
|
private ?string $cwd = null;
|
|
|
|
private ?Stdio $stdin = null;
|
|
private ?Stdio $stdout = null;
|
|
private ?Stdio $stderr = null;
|
|
|
|
public function __construct(string $command)
|
|
{
|
|
if (strlen($command) === 0) {
|
|
throw new ValueError('Empty command');
|
|
}
|
|
|
|
$this->command = $command;
|
|
$this->args[] = $this->command;
|
|
}
|
|
|
|
public function arg(string|Stringable $arg): static
|
|
{
|
|
$this->args[] = (string) $arg;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param array<string|Stringable> $args
|
|
*/
|
|
public function args(array $args): static
|
|
{
|
|
foreach ($args as $arg) {
|
|
$this->args[] = (string) $arg;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function currentDir(string|Stringable $cwd): static
|
|
{
|
|
$this->cwd = (string) $cwd;
|
|
return $this;
|
|
}
|
|
|
|
public function env(string $var, string|Stringable $value): static
|
|
{
|
|
$this->envs[$var] = escapeshellarg((string) $value);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string|Stringable> $envs
|
|
*/
|
|
public function envs(array $envs): static
|
|
{
|
|
foreach ($envs as $var => $value) {
|
|
$this->envs[$var] = escapeshellarg((string) $value);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function stdin(Stdio $stdin): static
|
|
{
|
|
$stdin = match ($stdin->type) {
|
|
Stdio::INHERIT => Stdio::stream(STDIN),
|
|
Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'r']),
|
|
default => $stdin,
|
|
};
|
|
|
|
$this->stdin = $stdin;
|
|
return $this;
|
|
}
|
|
|
|
public function stdout(Stdio $stdout): static
|
|
{
|
|
$stdout = match ($stdout->type) {
|
|
Stdio::INHERIT => Stdio::stream(STDOUT),
|
|
Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'w']),
|
|
default => $stdout,
|
|
};
|
|
|
|
$this->stdout = $stdout;
|
|
return $this;
|
|
}
|
|
|
|
public function stderr(Stdio $stderr): static
|
|
{
|
|
$stderr = match ($stderr->type) {
|
|
Stdio::INHERIT => Stdio::stream(STDERR),
|
|
Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'w']),
|
|
default => $stderr,
|
|
};
|
|
|
|
$this->stderr = $stderr;
|
|
return $this;
|
|
}
|
|
|
|
// TODO: Allow capturing arbitrary descriptors (proc_open supports this)?
|
|
// public function descriptor(int $fd, Stdio $stdio): static
|
|
// {
|
|
// match ($fd) {
|
|
// 0 => $this->stdin($stdio),
|
|
// 1 => $this->stdout($stdio),
|
|
// 2 => $this->stderr($stdio),
|
|
// default => $this->fd[$fd] = $stdio,
|
|
// };
|
|
//
|
|
// return $this;
|
|
// }
|
|
|
|
/**
|
|
* @param bool $shell Run the command with or without a shell
|
|
*/
|
|
public function spawn(bool $shell = true): Child
|
|
{
|
|
return $this->spawnWithDescriptorSpec($shell, [
|
|
$this->stdin instanceof Stdio
|
|
? $this->stdin->descriptorSpec
|
|
: STDIN,
|
|
$this->stdout instanceof Stdio
|
|
? $this->stdout->descriptorSpec
|
|
: STDOUT,
|
|
$this->stderr instanceof Stdio
|
|
? $this->stderr->descriptorSpec
|
|
: STDERR,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param bool $shell Run the command with or without a shell
|
|
*/
|
|
public function status(bool $shell = true): ExitStatus
|
|
{
|
|
return $this->spawn($shell)->wait();
|
|
}
|
|
|
|
/**
|
|
* @param bool $shell Run the command with or without a shell
|
|
*/
|
|
public function output(bool $shell = true): Output
|
|
{
|
|
return $this->spawnWithDescriptorSpec($shell, [
|
|
$this->stdin instanceof Stdio
|
|
? $this->stdin->descriptorSpec
|
|
: ['pipe', 'r'],
|
|
$this->stdout instanceof Stdio
|
|
? $this->stdout->descriptorSpec
|
|
: ['pipe', 'w'],
|
|
$this->stderr instanceof Stdio
|
|
? $this->stderr->descriptorSpec
|
|
: ['pipe', 'w'],
|
|
])->waitWithOutput();
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
return implode(' ', array_map(escapeshellarg(...), $this->args));
|
|
}
|
|
|
|
private function spawnWithDescriptorSpec(bool $shell, array $descriptorSpec): Child
|
|
{
|
|
foreach ($descriptorSpec as $descriptor => $spec) {
|
|
if (!is_array($spec) && !is_resource($spec)) {
|
|
throw new CommandException(sprintf(
|
|
'Descriptor %d is not a valid stream resource: %s',
|
|
$descriptor,
|
|
get_debug_type($spec),
|
|
));
|
|
}
|
|
}
|
|
|
|
if ($shell) {
|
|
$command = (string) $this;
|
|
} else if (is_executable($this->command)) {
|
|
$command = $this->args;
|
|
} else {
|
|
throw new CommandException(sprintf(
|
|
'Command "%s" is not executable',
|
|
$this->command,
|
|
));
|
|
}
|
|
|
|
$proc = proc_open($command, $descriptorSpec, $pipes, $this->cwd, $this->envs);
|
|
|
|
if ($proc === false) {
|
|
throw new RuntimeException('Failed proc_open');
|
|
}
|
|
|
|
$stdin = array_key_exists(0, $pipes)
|
|
? new ChildStdin($pipes[0])
|
|
: null;
|
|
|
|
$stdout = array_key_exists(1, $pipes)
|
|
? new ChildStdout($pipes[1])
|
|
: null;
|
|
|
|
$stderr = array_key_exists(2, $pipes)
|
|
? new ChildStderr($pipes[2])
|
|
: null;
|
|
|
|
return new Child($stdin, $stdout, $stderr, $proc);
|
|
}
|
|
}
|