From ad740afb5ff6d8e508cdf4f74bfce2f77f11a104 Mon Sep 17 00:00:00 2001 From: Jonas Kattendick Date: Sun, 17 Aug 2025 18:45:40 +0200 Subject: [PATCH] fix: never use a shell --- README.md | 17 +++------- examples/hello-world.php | 15 ++------- examples/plumbing.php | 4 +-- src/Command.php | 70 +++++++++++++++++++++++++++++----------- 4 files changed, 61 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index bbff83c..21ba838 100644 --- a/README.md +++ b/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. ``` diff --git a/examples/hello-world.php b/examples/hello-world.php index 59a4cf1..de242fd 100644 --- a/examples/hello-world.php +++ b/examples/hello-world.php @@ -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! diff --git a/examples/plumbing.php b/examples/plumbing.php index d7d0eea..b8d331f 100644 --- a/examples/plumbing.php +++ b/examples/plumbing.php @@ -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! diff --git a/src/Command.php b/src/Command.php index 616c4a7..b762012 100644 --- a/src/Command.php +++ b/src/Command.php @@ -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 $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',