refactor!: clean up stdio interface
with an interface no less!
This commit is contained in:
@@ -11,7 +11,7 @@ $echo = (new Command('echo'))
|
|||||||
->spawn();
|
->spawn();
|
||||||
|
|
||||||
(new Command('cat'))
|
(new Command('cat'))
|
||||||
->stdin(Stdio::stream($echo->stdout))
|
->stdin($echo->stdout)
|
||||||
->status();
|
->status();
|
||||||
|
|
||||||
// Hello, World!
|
// Hello, World!
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
final class ChildStderr
|
use Override;
|
||||||
|
|
||||||
|
final class ChildStderr implements StdioInterface
|
||||||
{
|
{
|
||||||
use StreamReadable;
|
use StreamReadable;
|
||||||
|
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
final class ChildStdin
|
use Override;
|
||||||
|
|
||||||
|
final class ChildStdin implements StdioInterface
|
||||||
{
|
{
|
||||||
use StreamWritable;
|
use StreamWritable;
|
||||||
|
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
final class ChildStdout
|
use Override;
|
||||||
|
|
||||||
|
final class ChildStdout implements StdioInterface
|
||||||
{
|
{
|
||||||
use StreamReadable;
|
use StreamReadable;
|
||||||
|
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use Override;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
use ValueError;
|
use ValueError;
|
||||||
|
|
||||||
@@ -20,9 +21,9 @@ final class Command implements Stringable
|
|||||||
private bool $environmentInherit = true;
|
private bool $environmentInherit = true;
|
||||||
private ?string $cwd = null;
|
private ?string $cwd = null;
|
||||||
|
|
||||||
private ?Stdio $stdin = null;
|
private ?StdioInterface $stdin = null;
|
||||||
private ?Stdio $stdout = null;
|
private ?StdioInterface $stdout = null;
|
||||||
private ?Stdio $stderr = null;
|
private ?StdioInterface $stderr = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new Command for launching the program at path program, with
|
* Constructs a new Command for launching the program at path program, with
|
||||||
@@ -202,14 +203,8 @@ final class Command implements Stringable
|
|||||||
* Defaults to inherit when used with spawn or status, and defaults to piped
|
* Defaults to inherit when used with spawn or status, and defaults to piped
|
||||||
* when used with output.
|
* when used with output.
|
||||||
*/
|
*/
|
||||||
public function stdin(Stdio $stdin): static
|
public function stdin(StdioInterface $stdin): static
|
||||||
{
|
{
|
||||||
$stdin = match ($stdin->type) {
|
|
||||||
Stdio::INHERIT => Stdio::stream(STDIN),
|
|
||||||
Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'r']),
|
|
||||||
default => $stdin,
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->stdin = $stdin;
|
$this->stdin = $stdin;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -220,14 +215,8 @@ final class Command implements Stringable
|
|||||||
* Defaults to inherit when used with spawn or status, and defaults to piped
|
* Defaults to inherit when used with spawn or status, and defaults to piped
|
||||||
* when used with output.
|
* when used with output.
|
||||||
*/
|
*/
|
||||||
public function stdout(Stdio $stdout): static
|
public function stdout(StdioInterface $stdout): static
|
||||||
{
|
{
|
||||||
$stdout = match ($stdout->type) {
|
|
||||||
Stdio::INHERIT => Stdio::stream(STDOUT),
|
|
||||||
Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'w']),
|
|
||||||
default => $stdout,
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->stdout = $stdout;
|
$this->stdout = $stdout;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -238,14 +227,8 @@ final class Command implements Stringable
|
|||||||
* Defaults to inherit when used with spawn or status, and defaults to piped
|
* Defaults to inherit when used with spawn or status, and defaults to piped
|
||||||
* when used with output.
|
* when used with output.
|
||||||
*/
|
*/
|
||||||
public function stderr(Stdio $stderr): static
|
public function stderr(StdioInterface $stderr): static
|
||||||
{
|
{
|
||||||
$stderr = match ($stderr->type) {
|
|
||||||
Stdio::INHERIT => Stdio::stream(STDERR),
|
|
||||||
Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'w']),
|
|
||||||
default => $stderr,
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->stderr = $stderr;
|
$this->stderr = $stderr;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -267,20 +250,18 @@ final class Command implements Stringable
|
|||||||
* Executes the command as a child process, returning a handle to it.
|
* Executes the command as a child process, returning a handle to it.
|
||||||
*
|
*
|
||||||
* By default, stdin, stdout and stderr are inherited from the parent.
|
* By default, stdin, stdout and stderr are inherited from the parent.
|
||||||
*
|
|
||||||
* @param bool $shell Run the command with or without a shell
|
|
||||||
*/
|
*/
|
||||||
public function spawn(): Child
|
public function spawn(): Child
|
||||||
{
|
{
|
||||||
return $this->spawnWithDescriptorSpec([
|
return $this->spawnWithDescriptorSpec([
|
||||||
$this->stdin instanceof Stdio
|
$this->stdin instanceof StdioInterface
|
||||||
? $this->stdin->descriptorSpec
|
? $this->stdin->getDescriptionSpec(0)
|
||||||
: STDIN,
|
: STDIN,
|
||||||
$this->stdout instanceof Stdio
|
$this->stdout instanceof StdioInterface
|
||||||
? $this->stdout->descriptorSpec
|
? $this->stdout->getDescriptionSpec(1)
|
||||||
: STDOUT,
|
: STDOUT,
|
||||||
$this->stderr instanceof Stdio
|
$this->stderr instanceof StdioInterface
|
||||||
? $this->stderr->descriptorSpec
|
? $this->stderr->getDescriptionSpec(2)
|
||||||
: STDERR,
|
: STDERR,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -290,8 +271,6 @@ final class Command implements Stringable
|
|||||||
* collecting its status.
|
* collecting its status.
|
||||||
*
|
*
|
||||||
* By default, stdin, stdout and stderr are inherited from the parent.
|
* By default, stdin, stdout and stderr are inherited from the parent.
|
||||||
*
|
|
||||||
* @param bool $shell Run the command with or without a shell
|
|
||||||
*/
|
*/
|
||||||
public function status(): ExitStatus
|
public function status(): ExitStatus
|
||||||
{
|
{
|
||||||
@@ -304,20 +283,18 @@ final class Command implements Stringable
|
|||||||
*
|
*
|
||||||
* By default, stdout and stderr are captured (and used to provide the
|
* By default, stdout and stderr are captured (and used to provide the
|
||||||
* resulting output). Stdin is not inherited from the parent.
|
* resulting output). Stdin is not inherited from the parent.
|
||||||
*
|
|
||||||
* @param bool $shell Run the command with or without a shell
|
|
||||||
*/
|
*/
|
||||||
public function output(): Output
|
public function output(): Output
|
||||||
{
|
{
|
||||||
return $this->spawnWithDescriptorSpec([
|
return $this->spawnWithDescriptorSpec([
|
||||||
$this->stdin instanceof Stdio
|
$this->stdin instanceof StdioInterface
|
||||||
? $this->stdin->descriptorSpec
|
? $this->stdin->getDescriptionSpec(0)
|
||||||
: ['pipe', 'r'],
|
: ['pipe', 'r'],
|
||||||
$this->stdout instanceof Stdio
|
$this->stdout instanceof StdioInterface
|
||||||
? $this->stdout->descriptorSpec
|
? $this->stdout->getDescriptionSpec(1)
|
||||||
: ['pipe', 'w'],
|
: ['pipe', 'w'],
|
||||||
$this->stderr instanceof Stdio
|
$this->stderr instanceof StdioInterface
|
||||||
? $this->stderr->descriptorSpec
|
? $this->stderr->getDescriptionSpec(2)
|
||||||
: ['pipe', 'w'],
|
: ['pipe', 'w'],
|
||||||
])->waitWithOutput();
|
])->waitWithOutput();
|
||||||
}
|
}
|
||||||
@@ -365,6 +342,7 @@ final class Command implements Stringable
|
|||||||
return $this->cwd;
|
return $this->cwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return implode(' ', [
|
return implode(' ', [
|
||||||
@@ -375,17 +353,6 @@ final class Command implements Stringable
|
|||||||
|
|
||||||
private function spawnWithDescriptorSpec(array $descriptorSpec): Child
|
private function spawnWithDescriptorSpec(array $descriptorSpec): Child
|
||||||
{
|
{
|
||||||
// Validate stream resources in descriptor spec.
|
|
||||||
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),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find executable if path is not absolute.
|
// Find executable if path is not absolute.
|
||||||
$program = $this->program;
|
$program = $this->program;
|
||||||
if ($program[0] !== DIRECTORY_SEPARATOR) {
|
if ($program[0] !== DIRECTORY_SEPARATOR) {
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ namespace Nih\CommandBuilder;
|
|||||||
* their exit status is exposed through the status method, or the wait method of
|
* their exit status is exposed through the status method, or the wait method of
|
||||||
* a Child process.
|
* a Child process.
|
||||||
*/
|
*/
|
||||||
final class ExitStatus
|
final readonly class ExitStatus
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ?int $code,
|
private ?int $code,
|
||||||
private readonly ?int $signal = null,
|
private ?int $signal = null,
|
||||||
private readonly ?int $stoppedSignal = null,
|
private ?int $stoppedSignal = null,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
final class Output
|
final readonly class Output
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly ?string $stdout,
|
public ?string $stdout,
|
||||||
public readonly ?string $stderr,
|
public ?string $stderr,
|
||||||
public readonly ExitStatus $status,
|
public ExitStatus $status,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
src/Stdio.php
116
src/Stdio.php
@@ -4,78 +4,128 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Override;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes what to do with a standard I/O stream for a child process when
|
* Describes what to do with a standard I/O stream for a child process when
|
||||||
* passed to the stdin, stdout, and stderr methods of {@see Command}.
|
* passed to the stdin, stdout, and stderr methods of {@see Command}.
|
||||||
|
*
|
||||||
|
* @psalm-suppress UnusedClass
|
||||||
*/
|
*/
|
||||||
final class Stdio
|
abstract class Stdio
|
||||||
{
|
{
|
||||||
public const INHERIT = 0;
|
|
||||||
public const PIPE = 1;
|
|
||||||
public const FILE = 2;
|
|
||||||
public const STREAM = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param null
|
|
||||||
* |resource
|
|
||||||
* |array{0: 'file', 1: string, 2: string}
|
|
||||||
* |array{0: 'pipe',1: string} $descriptorSpec
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public readonly int $type,
|
|
||||||
public readonly mixed $descriptorSpec,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The child inherits from the corresponding parent descriptor.
|
* The child inherits from the corresponding parent descriptor.
|
||||||
*/
|
*/
|
||||||
public static function inherit(): self
|
public static function inherit(): StdioInterface
|
||||||
{
|
{
|
||||||
return new self(self::INHERIT, null);
|
return new class implements StdioInterface {
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
return match ($fd) {
|
||||||
|
0 => STDIN,
|
||||||
|
1 => STDOUT,
|
||||||
|
2 => STDERR,
|
||||||
|
default => ['php://fd/' . $fd, 'w+'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A new pipe should be arranged to connect the parent and child processes.
|
* A new pipe should be arranged to connect the parent and child processes.
|
||||||
*/
|
*/
|
||||||
public static function piped(): self
|
public static function piped(): StdioInterface
|
||||||
{
|
{
|
||||||
return new self(self::PIPE, ['pipe', 'r']);
|
return new class implements StdioInterface {
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
return match ($fd) {
|
||||||
|
0 => ['pipe', 'r'],
|
||||||
|
1, 2 => ['pipe', 'w'],
|
||||||
|
default => ['pipe', 'w+'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This stream will be ignored. This is the equivalent of attaching the
|
* This stream will be ignored. This is the equivalent of attaching the
|
||||||
* stream to `/dev/null`.
|
* stream to `/dev/null`.
|
||||||
*/
|
*/
|
||||||
public static function null(): self
|
public static function null(): StdioInterface
|
||||||
{
|
{
|
||||||
return self::file('/dev/null', 'a+');
|
return new class implements StdioInterface {
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
// TODO: Support windows (I think you just write to `nul` in any
|
||||||
|
// directory?)
|
||||||
|
return match ($fd) {
|
||||||
|
0 => ['file', '/dev/null', 'r'],
|
||||||
|
1, 2 => ['file', '/dev/null', 'w'],
|
||||||
|
default => ['file', '/dev/null', 'w+'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like piped, but instead of capturing the stream into a handle, read
|
* Like piped, but instead of capturing the stream into a handle, read
|
||||||
* and/or write from/to a file.
|
* and/or write from/to a file.
|
||||||
*/
|
*/
|
||||||
public static function file(string|Stringable $file, string $mode): self
|
public static function file(string|Stringable $filename, string $mode): StdioInterface
|
||||||
{
|
{
|
||||||
return new self(self::FILE, ['file', (string) $file, $mode]);
|
return new class((string) $filename, $mode) implements StdioInterface {
|
||||||
|
public function __construct(
|
||||||
|
private string $filename,
|
||||||
|
private string $mode,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
return ['file', $this->filename, $this->mode];
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like piped, but instead of capturing the stream into a handle, read
|
* Like piped, but instead of capturing the stream into a handle, read
|
||||||
* and/or write from/to a stream.
|
* and/or write from/to a stream resource.
|
||||||
*
|
*
|
||||||
* @param resource|StreamReadable|StreamWritable $stream
|
* @param resource $stream
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException When stream is not a live stream resource
|
||||||
*/
|
*/
|
||||||
public static function stream($stream): self
|
public static function stream($stream): StdioInterface
|
||||||
{
|
{
|
||||||
if (is_object($stream)) {
|
$isResource = is_resource($stream)
|
||||||
$stream = $stream->stream;
|
|| ($stream !== null
|
||||||
|
&& !is_scalar($stream)
|
||||||
|
&& !is_array($stream)
|
||||||
|
&& !is_object($stream));
|
||||||
|
|
||||||
|
if (!$isResource) {
|
||||||
|
throw new InvalidArgumentException('not a resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new self(self::STREAM, $stream);
|
if (get_resource_type($stream) !== 'stream') {
|
||||||
}
|
throw new InvalidArgumentException('resource is not a stream or was closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new class($stream) implements StdioInterface {
|
||||||
|
public function __construct(private mixed $stream) {}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
|
public function getDescriptionSpec(int $fd): mixed
|
||||||
|
{
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/StdioInterface.php
Normal file
20
src/StdioInterface.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes what to do with a standard I/O stream for a child process when
|
||||||
|
* passed to the stdin, stdout, and stderr methods of {@see Command}.
|
||||||
|
*/
|
||||||
|
interface StdioInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return resource|array{0: 'file', 1: string, 2: string}|array{0: 'piped', 1: string}
|
||||||
|
*/
|
||||||
|
public function getDescriptionSpec(int $fd): mixed;
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Nih\CommandBuilder;
|
|
||||||
|
|
||||||
final class StdioPiped extends Stdio
|
|
||||||
{
|
|
||||||
public function __construct(public readonly string $mode)
|
|
||||||
{}
|
|
||||||
|
|
||||||
public function getDescriptorSpec(): array
|
|
||||||
{
|
|
||||||
return ['pipe', $this->mode];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user