fix: env handling and inheritance

This commit is contained in:
2025-08-17 18:28:11 +02:00
parent 6b1b98b662
commit 25e4c6a151

View File

@@ -10,15 +10,14 @@ use ValueError;
/** /**
* A process builder, providing fine-grained control over how a new process * A process builder, providing fine-grained control over how a new process
* should be spawned. * should be spawned.
*
* TODO: Wonky env handling.
*/ */
final class Command implements Stringable final class Command implements Stringable
{ {
public readonly string $program; public readonly string $program;
private array $args; private array $args;
private ?array $envs = null; private ?array $environment = null;
private bool $environmentInherit = true;
private ?string $cwd = null; private ?string $cwd = null;
private ?Stdio $stdin = null; private ?Stdio $stdin = null;
@@ -26,7 +25,17 @@ final class Command implements Stringable
private ?Stdio $stderr = null; private ?Stdio $stderr = null;
/** /**
* Constructs a new Command for launching the program at path program, with
* the following default configuration:
* *
* - No arguments to the program
* - Inherit the current process's environment
* - Inherit the current process's working directory
* - Inherit stdin/stdout/stderr for spawn or status, but create pipes for
* output
*
* Builder methods are provided to change these defaults and otherwise
* configure the process.
*/ */
public function __construct(string $program) public function __construct(string $program)
{ {
@@ -40,6 +49,22 @@ final class Command implements Stringable
/** /**
* Adds an argument to pass to the program. * Adds an argument to pass to the program.
*
* Only one argument can be passed per use. So instead of:
*
* ```
* $command->arg('-C /path/to/repo');
* ```
*
* usage would be:
*
* ```
* $command
* ->arg('-C')
* ->arg('/path/to/repo');
* ```
*
* To pass multiple arguments see {@see Command::args()}.
*/ */
public function arg(string|Stringable $arg): static public function arg(string|Stringable $arg): static
{ {
@@ -50,6 +75,8 @@ final class Command implements Stringable
/** /**
* Adds multiple arguments to pass to the program. * Adds multiple arguments to pass to the program.
* *
* To pass a single argument see {@see Command::arg()}.
*
* @param iterable<string|Stringable> $args * @param iterable<string|Stringable> $args
*/ */
public function args(iterable $args): static public function args(iterable $args): static
@@ -63,22 +90,45 @@ final class Command implements Stringable
/** /**
* Inserts or updates an explicit environment variable mapping. * Inserts or updates an explicit environment variable mapping.
*
* This method allows you to add an environment variable mapping to the
* spawned process or overwrite a previously set value. You can use
* {@see Command::envs()} to set multiple environment variables
* simultaneously.
*
* Child processes will inherit environment variables from their parent
* process by default. Environment variables explicitly set using
* {@see Command::env()} take precedence over inherited variables. You can
* disable environment variable inheritance entirely using
* {@see Command::envClear()} or for a single key using
* {@see Command::envRemove()}.
*/ */
public function env(string $key, string|Stringable $val): static public function env(string $key, string|Stringable $val): static
{ {
$this->envs[$key] = $val; $this->environment[$key] = $val;
return $this; return $this;
} }
/** /**
* Inserts or updates multiple environment variable mappings. * Inserts or updates multiple environment variable mappings.
* *
* This method allows you to add multiple environment variable mappings to
* the spawned process or overwrite previously set values. You can use
* {@see Command::env()} to set a single environment variable.
*
* Child processes will inherit environment variables from their parent
* process by default. Environment variables explicitly set using
* {@see Command::envs()} take precedence over inherited variables. You can
* disable environment variable inheritance entirely using
* {@see Command::envClear()} or for a single key using
* {@see Command::envRemove()}.
*
* @param iterable<string, string|Stringable> $vars * @param iterable<string, string|Stringable> $vars
*/ */
public function envs(iterable $vars): static public function envs(iterable $vars): static
{ {
foreach ($vars as $key => $val) { foreach ($vars as $key => $val) {
$this->envs[$key] = (string) $val; $this->environment[$key] = (string) $val;
} }
return $this; return $this;
@@ -87,27 +137,42 @@ final class Command implements Stringable
/** /**
* Removes an explicitly set environment variable and prevents inheriting it * Removes an explicitly set environment variable and prevents inheriting it
* from a parent process. * from a parent process.
*
* This method will remove the explicit value of an environment variable set
* via {@see Command::env()} or {@see Command::envs()}. In addition, it will
* prevent the spawned child process from inheriting that environment
* variable from its parent process.
*
* After calling {@see Command::envRemove()}, the value associated with its
* key from {@see Command::getEnvs()} will be `NULL`.
*
* To clear all explicitly set environment variables and disable all
* environment variable inheritance, you can use {@see Command::envClear()}.
*/ */
public function envRemove(string $key): static public function envRemove(string $key): static
{ {
if (is_null($this->envs)) { $this->environment[$key] = null;
$this->envs = getenv();
}
if (isset($this->envs[$key])) {
unset($this->envs[$key]);
}
return $this; return $this;
} }
/** /**
* Clears all explicitly set environment variables and prevents inheriting * Clears all explicitly set environment variables and prevents inheriting
* any parent process environment variables. * any parent process environment variables.
*
* This method will remove all explicitly added environment variables set
* via {@see Command::env()} or {@see Command::envs()}. In addition, it will
* prevent the spawned child process from inheriting any environment
* variable from its parent process.
*
* After calling {@see Command::envClear()}, the array from
* {@see Command::getEnvs()} will be empty.
*
* You can use {@see Command::envRemove()} to clear a single mapping.
*/ */
public function envClear(): static public function envClear(): static
{ {
$this->envs = []; $this->environmentInherit = false;
$this->environment = [];
return $this; return $this;
} }
@@ -266,10 +331,17 @@ final class Command implements Stringable
/** /**
* Returns the environment variables set for the child process. * Returns the environment variables set for the child process.
*
* Environment variables explicitly set using {@see Command::env(),
* {@see Command::envs()}, and {@see Command::envRemove} can be retrieved
* with this method.
*
* Note that this output does not include environment variables inherited
* from the parent process.
*/ */
public function getEnvs(): array public function getEnvs(): array
{ {
return $this->envs; return $this->environment ?? [];
} }
/** /**
@@ -310,7 +382,16 @@ final class Command implements Stringable
)); ));
} }
$proc = proc_open($command, $descriptorSpec, $pipes, $this->cwd, $this->envs); $environment = $this->environment;
if (is_array($environment) && $this->environmentInherit) {
foreach (getenv() as $key => $val) {
if (!array_key_exists($key, $environment)) {
$environment[$key] = $val;
}
}
}
$proc = proc_open($command, $descriptorSpec, $pipes, $this->cwd, $environment);
if ($proc === false) { if ($proc === false) {
throw new CommandException(sprintf( throw new CommandException(sprintf(
'Program "%s" failed to start', 'Program "%s" failed to start',