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] = $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. */ public function stdin(Stdio $stdin): static { $stdin = match ($stdin->type) { Stdio::INHERIT => Stdio::stream(STDIN), Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'r']), default => $stdin, }; $this->stdin = $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. */ public function stdout(Stdio $stdout): static { $stdout = match ($stdout->type) { Stdio::INHERIT => Stdio::stream(STDOUT), Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'w']), default => $stdout, }; $this->stdout = $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. */ public function stderr(Stdio $stderr): static { $stderr = match ($stderr->type) { Stdio::INHERIT => Stdio::stream(STDERR), Stdio::PIPE => new Stdio(Stdio::PIPE, ['pipe', 'w']), default => $stderr, }; $this->stderr = $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. * * @param bool $shell Run the command with or without a shell */ public function spawn(): Child { return $this->spawnWithDescriptorSpec([ $this->stdin instanceof Stdio ? $this->stdin->descriptorSpec : STDIN, $this->stdout instanceof Stdio ? $this->stdout->descriptorSpec : STDOUT, $this->stderr instanceof Stdio ? $this->stderr->descriptorSpec : 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. * * @param bool $shell Run the command with or without a shell */ 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. * * @param bool $shell Run the command with or without a shell */ public function output(): Output { return $this->spawnWithDescriptorSpec([ $this->stdin instanceof Stdio ? $this->stdin->descriptorSpec : ['pipe', 'r'], $this->stdout instanceof Stdio ? $this->stdout->descriptorSpec : ['pipe', 'w'], $this->stderr instanceof Stdio ? $this->stderr->descriptorSpec : ['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. */ 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. */ 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; } public function __toString(): string { return implode(' ', [ escapeshellarg($this->program), ...array_map(escapeshellarg(...), $this->args) ]); } 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( 'Descriptor %d is not a valid stream resource: %s', $descriptor, get_debug_type($spec), )); } } // 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', $program, )); } // 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; } } } $proc = proc_open( [$program, ...$this->args], $descriptorSpec, $pipes, $this->cwd, $environment, ); if ($proc === false) { throw new CommandException(sprintf( 'Program "%s" failed to start', $this->program, )); } return new Child($proc, $pipes); } }