fix: never use a shell

This commit is contained in:
2025-08-17 18:45:40 +02:00
parent 25e4c6a151
commit ad740afb5f
4 changed files with 61 additions and 45 deletions

View File

@@ -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.
```

View File

@@ -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!

View File

@@ -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!

View File

@@ -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',