fix: handle stdio properly

This commit is contained in:
2025-08-17 13:55:24 +02:00
parent 4ae0545ab4
commit 5f6f5bf04b
15 changed files with 311 additions and 120 deletions

View File

@@ -6,9 +6,10 @@ namespace Nih\CommandBuilder;
use RuntimeException;
use Stringable;
use TypeError;
use ValueError;
final class Command
final class Command implements Stringable
{
public readonly string $command;
@@ -48,7 +49,7 @@ final class Command
return $this;
}
public function cwd(string|Stringable $cwd): static
public function currentDir(string|Stringable $cwd): static
{
$this->cwd = (string) $cwd;
return $this;
@@ -72,43 +73,118 @@ final class Command
return $this;
}
public function stdin(Stdio $in): static
public function stdin(Stdio $stdin): static
{
$this->stdin = $in;
$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 $out): static
public function stdout(Stdio $stdout): static
{
$this->stdout = $out;
$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 $err): static
public function stderr(Stdio $stderr): static
{
$this->stderr = $err;
$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
{
$fd[0] = $this->stdin instanceof Stdio
? $this->stdin->getDescriptorSpec()
: ['pipe', 'r'];
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,
]);
}
$fd[1] = $this->stdout instanceof Stdio
? $this->stdout->getDescriptorSpec()
: ['pipe', 'w'];
/**
* @param bool $shell Run the command with or without a shell
*/
public function status(bool $shell = true): ExitStatus
{
return $this->spawn($shell)->wait();
}
$fd[2] = $this->stderr instanceof Stdio
? $this->stderr->getDescriptorSpec()
: ['pipe', 'w'];
/**
* @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 = implode(' ', array_map(escapeshellarg(...), $this->args));
$command = (string) $this;
} else if (is_executable($this->command)) {
$command = $this->args;
} else {
@@ -118,9 +194,10 @@ final class Command
));
}
$proc = proc_open($command, $fd, $pipes, $this->cwd, $this->envs);
$proc = proc_open($command, $descriptorSpec, $pipes, $this->cwd, $this->envs);
if ($proc === false) {
throw new RuntimeException();
throw new RuntimeException('Failed proc_open');
}
$stdin = array_key_exists(0, $pipes)
@@ -137,28 +214,4 @@ final class Command
return new Child($stdin, $stdout, $stderr, $proc);
}
/**
* @param bool $shell Run the command with or without a shell
*/
public function output(bool $shell = true): Output
{
return $this->spawn($shell)->output();
}
/**
* @param bool $shell Run the command with or without a shell
*/
public function status(bool $shell = true): int
{
return $this->spawn($shell)->status();
}
/**
* @param bool $shell Run the command with or without a shell
*/
public function wait(bool $shell = true): void
{
$this->spawn($shell)->wait();
}
}