fix: never use a shell
This commit is contained in:
17
README.md
17
README.md
@@ -10,23 +10,16 @@ use Nih\CommandBuilder\Stdio;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$child = (new Command('/usr/bin/cat'))
|
||||
$child = (new Command('cat'))
|
||||
->stdin(Stdio::piped())
|
||||
->spawn(shell: false);
|
||||
->stdout(Stdio::piped())
|
||||
->spawn();
|
||||
|
||||
$child->stdin?->write('Hello, this is pretty cool.');
|
||||
$child->stdin?->close();
|
||||
|
||||
$output = $child->output();
|
||||
|
||||
var_dump($output)
|
||||
echo $output->stdout;
|
||||
|
||||
// object(Nih\CommandBuilder\Output)#4 (3) {
|
||||
// ["stdout"]=>
|
||||
// string(27) "Hello, this is pretty cool."
|
||||
// ["stderr"]=>
|
||||
// string(0) ""
|
||||
// ["code"]=>
|
||||
// int(0)
|
||||
// }
|
||||
// Hello, this is pretty cool.
|
||||
```
|
||||
|
||||
@@ -8,17 +8,6 @@ $output = (new Command('echo'))
|
||||
->arg('Hello, World!')
|
||||
->output();
|
||||
|
||||
var_dump($output);
|
||||
echo $output->stdout;
|
||||
|
||||
// object(Nih\CommandBuilder\Output)#9 (3) {
|
||||
// ["stdout"]=>
|
||||
// string(14) "Hello, World!
|
||||
// "
|
||||
// ["stderr"]=>
|
||||
// string(0) ""
|
||||
// ["code"]=>
|
||||
// object(Nih\CommandBuilder\ExitStatus)#8 (1) {
|
||||
// ["code"]=>
|
||||
// int(0)
|
||||
// }
|
||||
// }
|
||||
// Hello, World!
|
||||
|
||||
@@ -10,8 +10,8 @@ $echo = (new Command('echo'))
|
||||
->stdout(Stdio::piped())
|
||||
->spawn();
|
||||
|
||||
$cat = (new Command('cat'))
|
||||
(new Command('cat'))
|
||||
->stdin(Stdio::stream($echo->stdout))
|
||||
->status();
|
||||
|
||||
// Prints "Hello, World!\n"
|
||||
// Hello, World!
|
||||
|
||||
@@ -15,7 +15,7 @@ final class Command implements Stringable
|
||||
{
|
||||
public readonly string $program;
|
||||
|
||||
private array $args;
|
||||
private array $args = [];
|
||||
private ?array $environment = null;
|
||||
private bool $environmentInherit = true;
|
||||
private ?string $cwd = null;
|
||||
@@ -36,6 +36,8 @@ final class Command implements Stringable
|
||||
*
|
||||
* Builder methods are provided to change these defaults and otherwise
|
||||
* configure the process.
|
||||
*
|
||||
* If program is not an absolute path, the `PATH` will be searched.
|
||||
*/
|
||||
public function __construct(string $program)
|
||||
{
|
||||
@@ -44,7 +46,6 @@ final class Command implements Stringable
|
||||
}
|
||||
|
||||
$this->program = $program;
|
||||
$this->args = [$this->program];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +66,11 @@ final class Command implements Stringable
|
||||
* ```
|
||||
*
|
||||
* To pass multiple arguments see {@see Command::args()}.
|
||||
*
|
||||
* Note that the arguments are not passed through a shell, but given
|
||||
* literally to the program. This means that shell syntax like quotes,
|
||||
* escaped characters, word splitting, glob patterns, variable substitution,
|
||||
* etc. have no effect.
|
||||
*/
|
||||
public function arg(string|Stringable $arg): static
|
||||
{
|
||||
@@ -77,6 +83,11 @@ final class Command implements Stringable
|
||||
*
|
||||
* To pass a single argument see {@see Command::arg()}.
|
||||
*
|
||||
* Note that the arguments are not passed through a shell, but given
|
||||
* literally to the program. This means that shell syntax like quotes,
|
||||
* escaped characters, word splitting, glob patterns, variable substitution,
|
||||
* etc. have no effect.
|
||||
*
|
||||
* @param iterable<string|Stringable> $args
|
||||
*/
|
||||
public function args(iterable $args): static
|
||||
@@ -259,9 +270,9 @@ final class Command implements Stringable
|
||||
*
|
||||
* @param bool $shell Run the command with or without a shell
|
||||
*/
|
||||
public function spawn(bool $shell = true): Child
|
||||
public function spawn(): Child
|
||||
{
|
||||
return $this->spawnWithDescriptorSpec($shell, [
|
||||
return $this->spawnWithDescriptorSpec([
|
||||
$this->stdin instanceof Stdio
|
||||
? $this->stdin->descriptorSpec
|
||||
: STDIN,
|
||||
@@ -282,9 +293,9 @@ final class Command implements Stringable
|
||||
*
|
||||
* @param bool $shell Run the command with or without a shell
|
||||
*/
|
||||
public function status(bool $shell = true): ExitStatus
|
||||
public function status(): ExitStatus
|
||||
{
|
||||
return $this->spawn($shell)->wait();
|
||||
return $this->spawn()->wait();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,9 +307,9 @@ final class Command implements Stringable
|
||||
*
|
||||
* @param bool $shell Run the command with or without a shell
|
||||
*/
|
||||
public function output(bool $shell = true): Output
|
||||
public function output(): Output
|
||||
{
|
||||
return $this->spawnWithDescriptorSpec($shell, [
|
||||
return $this->spawnWithDescriptorSpec([
|
||||
$this->stdin instanceof Stdio
|
||||
? $this->stdin->descriptorSpec
|
||||
: ['pipe', 'r'],
|
||||
@@ -326,7 +337,7 @@ final class Command implements Stringable
|
||||
*/
|
||||
public function getArgs(): array
|
||||
{
|
||||
return array_slice($this->args, 1);
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,11 +367,15 @@ final class Command implements Stringable
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode(' ', array_map(escapeshellarg(...), $this->args));
|
||||
return implode(' ', [
|
||||
escapeshellarg($this->program),
|
||||
...array_map(escapeshellarg(...), $this->args)
|
||||
]);
|
||||
}
|
||||
|
||||
private function spawnWithDescriptorSpec(bool $shell, 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(
|
||||
@@ -371,17 +386,29 @@ final class Command implements Stringable
|
||||
}
|
||||
}
|
||||
|
||||
if ($shell) {
|
||||
$command = (string) $this;
|
||||
} else if (is_executable($this->program)) {
|
||||
$command = $this->args;
|
||||
} else {
|
||||
// Find executable if path is not absolute.
|
||||
$program = $this->program;
|
||||
if ($program[0] !== DIRECTORY_SEPARATOR) {
|
||||
$path = getenv('PATH');
|
||||
if (is_string($path)) {
|
||||
foreach (explode(':', $path) as $path) {
|
||||
$path = $path . '/' . $program;
|
||||
if (is_executable($path)) {
|
||||
$program = $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_executable($program)) {
|
||||
throw new CommandException(sprintf(
|
||||
'Program "%s" is not executable',
|
||||
$this->program,
|
||||
$program,
|
||||
));
|
||||
}
|
||||
|
||||
// Handle environment inheritance.
|
||||
$environment = $this->environment;
|
||||
if (is_array($environment) && $this->environmentInherit) {
|
||||
foreach (getenv() as $key => $val) {
|
||||
@@ -391,7 +418,14 @@ final class Command implements Stringable
|
||||
}
|
||||
}
|
||||
|
||||
$proc = proc_open($command, $descriptorSpec, $pipes, $this->cwd, $environment);
|
||||
$proc = proc_open(
|
||||
[$program, ...$this->args],
|
||||
$descriptorSpec,
|
||||
$pipes,
|
||||
$this->cwd,
|
||||
$environment,
|
||||
);
|
||||
|
||||
if ($proc === false) {
|
||||
throw new CommandException(sprintf(
|
||||
'Program "%s" failed to start',
|
||||
|
||||
Reference in New Issue
Block a user