*/ private array $args = []; /** * @var array */ private ?array $environment = null; private bool $environmentInherit = true; private ?string $cwd = null; private ?StdioInterface $stdin = null; private ?StdioInterface $stdout = null; private ?StdioInterface $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. * * If program is not an absolute path, the `PATH` will be searched. */ public function __construct(string $program) { if (strlen($program) === 0) { throw new ValueError('Empty program name'); } $this->program = $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()}. * * 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 { $this->args[] = (string) $arg; return $this; } /** * Adds multiple arguments to pass to the program. * * 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 { foreach ($args as $arg) { $this->args[] = (string) $arg; } return $this; } /** * 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->environment[$key] = (string) $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 $vars */ public function envs(iterable $vars): static { foreach ($vars as $key => $val) { $this->environment[$key] = (string) $val; } return $this; } /** * 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 { $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->environmentInherit = false; $this->environment = []; return $this; } /** * Sets the working directory for the child process. */ public function currentDir(string|Stringable $dir): static { $this->cwd = (string) $dir; return $this; } /** * Configuration for the child process’s standard input (stdin) handle. * * Defaults to inherit when used with spawn or status, and defaults to piped * when used with output. * * @param StdioInterface|resource $stdin * * @throws InvalidArgumentException * When the provided stdin handle is neither a StdioInterface nor a live * stream resource. */ public function stdin(mixed $stdin): static { $this->stdin = $stdin instanceof StdioInterface ? $stdin : Stdio::stream($stdin); return $this; } /** * Configuration for the child process’s standard output (stdout) handle. * * Defaults to inherit when used with spawn or status, and defaults to piped * when used with output. * * @param StdioInterface|resource $stdout * * @throws InvalidArgumentException * When the provided stdout handle is neither a StdioInterface nor a live * stream resource. */ public function stdout(mixed $stdout): static { $this->stdout = $stdout instanceof StdioInterface ? $stdout : Stdio::stream($stdout); return $this; } /** * Configuration for the child process’s standard error (stderr) handle. * * Defaults to inherit when used with spawn or status, and defaults to piped * when used with output. * * @param StdioInterface|resource $stderr * * @throws InvalidArgumentException * When the provided stderr handle is neither a StdioInterface nor a live * stream resource. */ public function stderr(mixed $stderr): static { $this->stderr = $stderr instanceof StdioInterface ? $stderr : Stdio::stream($stderr); return $this; } // TODO: Allow capturing arbitrary descriptors (proc_open supports this)? // public function descriptor(int $fd, Stdio $stdio): static // { // match ($fd) { // 0 => $this->stdin($stdio), // 1 => $this->stdout($stdio), // 2 => $this->stderr($stdio), // default => $this->fd[$fd] = $stdio, // }; // // return $this; // } /** * Executes the command as a child process, returning a handle to it. * * By default, stdin, stdout and stderr are inherited from the parent. * * @throws ChildException */ public function spawn(): Child { return $this->spawnWithDescriptorSpec([ $this->stdin instanceof StdioInterface ? $this->stdin->getDescriptiorSpec(0) : STDIN, $this->stdout instanceof StdioInterface ? $this->stdout->getDescriptiorSpec(1) : STDOUT, $this->stderr instanceof StdioInterface ? $this->stderr->getDescriptiorSpec(2) : STDERR, ]); } /** * Executes a command as a child process, waiting for it to finish and * collecting its status. * * By default, stdin, stdout and stderr are inherited from the parent. * * @throws ChildException */ public function status(): ExitStatus { return $this->spawn()->wait(); } /** * Executes the command as a child process, waiting for it to finish and * collecting all of its output. * * By default, stdout and stderr are captured (and used to provide the * resulting output). Stdin is not inherited from the parent. * * @throws ChildException */ public function output(): Output { return $this->spawnWithDescriptorSpec([ $this->stdin instanceof StdioInterface ? $this->stdin->getDescriptiorSpec(0) : ['pipe', 'r'], $this->stdout instanceof StdioInterface ? $this->stdout->getDescriptiorSpec(1) : ['pipe', 'w'], $this->stderr instanceof StdioInterface ? $this->stderr->getDescriptiorSpec(2) : ['pipe', 'w'], ])->waitWithOutput(); } /** * Returns the path to the program that was given to the constructor. */ public function getProgram(): string { return $this->program; } /** * Returns the arguments that will be passed to the program. * * This does not include the path to the program as the first argument. * * @return list */ public function getArgs(): array { return $this->args; } /** * 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. * * @return array */ public function getEnvs(): array { return $this->environment ?? []; } /** * Returns the working directory for the child process. * * This returns `NULL` if the working directory will not be changed. */ public function getCurrentDir(): ?string { return $this->cwd; } #[Override] public function __toString(): string { return implode(' ', [ escapeshellarg($this->program), ...array_map(escapeshellarg(...), $this->args) ]); } /** * @param array> $descriptorSpec */ private function spawnWithDescriptorSpec(array $descriptorSpec): Child { // 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 . DIRECTORY_SEPARATOR . $program; if (is_executable($path)) { $program = $path; break; } } } } // Handle environment inheritance. $environment = $this->environment; if (is_array($environment) && $this->environmentInherit) { foreach (getenv() as $key => $val) { if (!array_key_exists($key, $environment)) { $environment[$key] = $val; } } } $command = [$program]; foreach ($this->args as $arg) { $command[] = $arg; } try { set_error_handler(ChildException::handleError(...)); $proc = proc_open( $command, $descriptorSpec, $pipes, $this->cwd, $environment, ); } finally { restore_error_handler(); } assert($proc !== false); return new Child($proc, $pipes); } }