feat: first impl
This commit is contained in:
18
composer.lock
generated
Normal file
18
composer.lock
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "e6871b52f4fe4ae525e588a0924fdf18",
|
||||||
|
"packages": [],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {},
|
||||||
|
"platform-dev": {},
|
||||||
|
"plugin-api-version": "2.6.0"
|
||||||
|
}
|
||||||
26
example.php
Normal file
26
example.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Nih\CommandBuilder\Command;
|
||||||
|
use Nih\CommandBuilder\Stdio;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$output = (new Command('echo'))
|
||||||
|
->arg('-n')
|
||||||
|
->arg('-')
|
||||||
|
->arg("./ \$wow '''/stdout.txt")
|
||||||
|
->stderr(Stdio::null())
|
||||||
|
->output();
|
||||||
|
|
||||||
|
var_dump($output);
|
||||||
|
|
||||||
|
$child = (new Command('/usr/bin/cat'))
|
||||||
|
->stdin(Stdio::piped('r'))
|
||||||
|
->spawn(shell: true);
|
||||||
|
|
||||||
|
$child->stdin?->write('Hello, this is pretty cool.');
|
||||||
|
$child->stdin?->close();
|
||||||
|
|
||||||
|
$output = $child->output();
|
||||||
|
|
||||||
|
var_dump($output);
|
||||||
49
src/Child.php
Normal file
49
src/Child.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
final class Child
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param resource $proc The process handle.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly ?ChildStdin $stdin,
|
||||||
|
public readonly ?ChildStdout $stdout,
|
||||||
|
public readonly ?ChildStderr $stderr,
|
||||||
|
public $proc,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function id(): int
|
||||||
|
{
|
||||||
|
$status = proc_get_status($this->proc);
|
||||||
|
return $status['pid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function output(): Output
|
||||||
|
{
|
||||||
|
$stdout = $this->stdout?->getContents();
|
||||||
|
$stderr = $this->stderr?->getContents();
|
||||||
|
$code = proc_close($this->proc);
|
||||||
|
|
||||||
|
return new Output($stdout, $stderr, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status(): int
|
||||||
|
{
|
||||||
|
return proc_close($this->proc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(): void
|
||||||
|
{
|
||||||
|
proc_close($this->proc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kill(): bool
|
||||||
|
{
|
||||||
|
return proc_terminate($this->proc, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/ChildStderr.php
Normal file
10
src/ChildStderr.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
final class ChildStderr
|
||||||
|
{
|
||||||
|
use StreamReadable;
|
||||||
|
}
|
||||||
10
src/ChildStdin.php
Normal file
10
src/ChildStdin.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
final class ChildStdin
|
||||||
|
{
|
||||||
|
use StreamWritable;
|
||||||
|
}
|
||||||
10
src/ChildStdout.php
Normal file
10
src/ChildStdout.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
final class ChildStdout
|
||||||
|
{
|
||||||
|
use StreamReadable;
|
||||||
|
}
|
||||||
164
src/Command.php
Normal file
164
src/Command.php
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Stringable;
|
||||||
|
use ValueError;
|
||||||
|
|
||||||
|
final class Command
|
||||||
|
{
|
||||||
|
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 cwd(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 $in): static
|
||||||
|
{
|
||||||
|
$this->stdin = $in;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stdout(Stdio $out): static
|
||||||
|
{
|
||||||
|
$this->stdout = $out;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stderr(Stdio $err): static
|
||||||
|
{
|
||||||
|
$this->stderr = $err;
|
||||||
|
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'];
|
||||||
|
|
||||||
|
$fd[1] = $this->stdout instanceof Stdio
|
||||||
|
? $this->stdout->getDescriptorSpec()
|
||||||
|
: ['pipe', 'w'];
|
||||||
|
|
||||||
|
$fd[2] = $this->stderr instanceof Stdio
|
||||||
|
? $this->stderr->getDescriptorSpec()
|
||||||
|
: ['pipe', 'w'];
|
||||||
|
|
||||||
|
if ($shell) {
|
||||||
|
$command = implode(' ', array_map(escapeshellarg(...), $this->args));
|
||||||
|
} 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, $fd, $pipes, $this->cwd, $this->envs);
|
||||||
|
if ($proc === false) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/CommandException.php
Normal file
10
src/CommandException.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
final class CommandException extends RuntimeException
|
||||||
|
{}
|
||||||
15
src/Output.php
Normal file
15
src/Output.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
final class Output
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly ?string $stdout,
|
||||||
|
public readonly ?string $stderr,
|
||||||
|
public readonly int $code,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/Stdio.php
Normal file
32
src/Stdio.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
abstract class Stdio
|
||||||
|
{
|
||||||
|
abstract public function getDescriptorSpec(): array;
|
||||||
|
|
||||||
|
public static function file(string|Stringable $file, string $mode): self
|
||||||
|
{
|
||||||
|
return new StdioFile((string) $file, $mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function piped(string $mode): self
|
||||||
|
{
|
||||||
|
return new StdioPiped($mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function inherit(): null
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function null(): self
|
||||||
|
{
|
||||||
|
return new StdioFile('/dev/null', 'a+');
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/StdioFile.php
Normal file
25
src/StdioFile.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
use ValueError;
|
||||||
|
|
||||||
|
final class StdioFile extends Stdio
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $file,
|
||||||
|
public readonly string $mode,
|
||||||
|
) {
|
||||||
|
match ($mode) {
|
||||||
|
'r', 'w', 'a', 'x', 'r+', 'w+', 'a+', 'x+' => null,
|
||||||
|
default => throw new ValueError('Invalid mode: ' . $mode),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescriptorSpec(): array
|
||||||
|
{
|
||||||
|
return ['file', $this->file, $this->mode];
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/StdioPiped.php
Normal file
16
src/StdioPiped.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?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];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/StreamReadable.php
Normal file
28
src/StreamReadable.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
trait StreamReadable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param resource $stream
|
||||||
|
*/
|
||||||
|
public function __construct(private $stream)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(int $length): ?string
|
||||||
|
{
|
||||||
|
return fread($this->stream, $length) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContents(?int $length = null, int $offset = -1): ?string
|
||||||
|
{
|
||||||
|
$contents = stream_get_contents($this->stream, $length, $offset);
|
||||||
|
return $contents === false
|
||||||
|
? null
|
||||||
|
: $contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/StreamWritable.php
Normal file
31
src/StreamWritable.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nih\CommandBuilder;
|
||||||
|
|
||||||
|
trait StreamWritable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param resource $stream
|
||||||
|
*/
|
||||||
|
public function __construct(private $stream)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(string $data, ?int $length = null): ?int
|
||||||
|
{
|
||||||
|
$bytes = fwrite($this->stream, $data, $length) ?: null;
|
||||||
|
return $bytes === false ? null : $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): bool
|
||||||
|
{
|
||||||
|
return fflush($this->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close(): bool
|
||||||
|
{
|
||||||
|
return fclose($this->stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user