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
* should be spawned.
*
* TODO: Wonky env handling.
*/
final class Command implements Stringable
{
public readonly string $program;
private array $args;
private ?array $envs = null;
private ?array $environment = null;
private bool $environmentInherit = true;
private ?string $cwd = null;
private ?Stdio $stdin = null;
@@ -26,7 +25,17 @@ final class Command implements Stringable
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)
{
@@ -40,6 +49,22 @@ final class Command implements Stringable
/**
* 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
{
@@ -50,6 +75,8 @@ final class Command implements Stringable
/**
* Adds multiple arguments to pass to the program.
*
* To pass a single argument see {@see Command::arg()}.
*
* @param iterable<string|Stringable> $args
*/
public function args(iterable $args): static
@@ -63,22 +90,45 @@ final class Command implements Stringable
/**
* 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
{
$this->envs[$key] = $val;
$this->environment[$key] = $val;
return $this;
}
/**
* 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
*/
public function envs(iterable $vars): static
{
foreach ($vars as $key => $val) {
$this->envs[$key] = (string) $val;
$this->environment[$key] = (string) $val;
}
return $this;
@@ -87,27 +137,42 @@ final class Command implements Stringable
/**
* Removes an explicitly set environment variable and prevents inheriting it
* 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
{
if (is_null($this->envs)) {
$this->envs = getenv();
}
if (isset($this->envs[$key])) {
unset($this->envs[$key]);
}
$this->environment[$key] = null;
return $this;
}
/**
* Clears all explicitly set environment variables and prevents inheriting
* 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
{
$this->envs = [];
$this->environmentInherit = false;
$this->environment = [];
return $this;
}
@@ -266,10 +331,17 @@ final class Command implements Stringable
/**
* 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
{
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) {
throw new CommandException(sprintf(
'Program "%s" failed to start',