php-standard-library/shell
Execute shell commands safely in PHP with built-in argument escaping and controlled handling of stdout/stderr. Part of PHP Standard Library, providing a simple API for running processes, capturing error output, and managing failures predictably.
composer require php-standard-library/shell
exec()):
use PHPStandardLibrary\Shell\Shell;
$shell = new Shell();
$result = $shell->run('ls -la');
echo $result->getOutput();
if ($result->isFailed()) {
throw new \RuntimeException('Command failed: ' . $result->getErrorOutput());
}
Replace unsafe exec() in a Laravel Artisan command:
use Illuminate\Console\Command;
use PHPStandardLibrary\Shell\Shell;
class DeployCommand extends Command
{
public function __construct(private Shell $shell) {
parent::__construct();
}
public function handle()
{
$result = $this->shell->run('git pull');
$this->info($result->getOutput());
}
}
Register in AppServiceProvider:
public function register()
{
$this->app->singleton(Shell::class);
}
// Run a command and capture output
$result = $shell->run('docker ps');
$output = $result->getOutput(); // stdout
$error = $result->getErrorOutput(); // stderr
$exitCode = $result->getExitCode();
// Validate exit code
$result->throwIfFailed(); // throws on non-zero exit
// Stream output line-by-line (PHP 8.1+)
$shell->run('tail -f logfile.log')
->stream(fn(string $chunk) => $this->output->writeln($chunk));
// For async jobs, buffer to database/Redis
$shell->run('docker build .')
->stream(fn($chunk) => DB::table('build_logs')->insert(['chunk' => $chunk]));
// Safe argument handling (prevents injection)
$shell->run('cp', ['source.txt', 'destination.txt']);
// Dynamic arguments
$userInput = 'file.txt';
$shell->run('mv', [$userInput])->escapeArguments(); // auto-escapes
// Set environment variables
$shell->run('php artisan queue:work')
->withEnvironment(['QUEUE_CONNECTION' => 'database']);
// Merge with existing env
$shell->run('command')->withEnvironment($_ENV);
// Pipe output between commands
$shell->run('cat file.log | grep error');
// Redirect stderr to stdout
$shell->run('command 2>&1');
use Illuminate\Console\Command;
use PHPStandardLibrary\Shell\Shell;
class DockerCommand extends Command
{
public function __construct(private Shell $shell) {}
public function handle()
{
$this->info('Starting containers...');
$result = $this->shell->run('docker-compose up -d');
$this->line($result->getOutput());
}
}
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use PHPStandardLibrary\Shell\Shell;
class BuildJob implements ShouldQueue
{
use Queueable, SerializesModels;
public function handle(Shell $shell)
{
$shell->run('docker build .')
->stream(fn($chunk) => Log::debug('Build: ' . $chunk));
}
}
// app/Services/SystemCommand.php
namespace App\Services;
use PHPStandardLibrary\Shell\Shell;
class SystemCommand
{
public function __construct(private Shell $shell) {}
public function runSafe(string $command): string
{
return $this->shell->run($command)
->throwIfFailed()
->getOutput();
}
public function streamLogs(string $command, callable $callback): void
{
$this->shell->run($command)->stream($callback);
}
}
Streaming on Windows (cmd.exe)
cmd.exe buffers output, causing delayed or partial chunks.--line-buffering flag if possible:
$shell->run('powershell -Command "Get-Content logfile.log -Wait"')
->stream(...);
Argument Escaping Overhead
* in filenames).escapeArguments(false) for trusted inputs:
$shell->run('mv', [$file])->escapeArguments(false);
Memory Leaks in Streaming
tail -f) can exhaust memory if not buffered.$shell->run('tail -f logfile.log')
->stream(fn($chunk) => $this->buffer->add($chunk));
Exit Code Misinterpretation
grep) return non-zero on "no match" but are successful.if ($result->isFailed() && $result->getExitCode() !== 1) {
throw new \RuntimeException('Command failed');
}
Environment Variable Leakage
$_ENV.$safeEnv = array_filter($_ENV, fn($key) => !str_starts_with($key, 'SECRET_'));
$shell->run('command')->withEnvironment($safeEnv);
Inspect Raw Output
Use getOutput() and getErrorOutput() to debug command behavior:
$result = $shell->run('ls /nonexistent');
$this->error("Output: " . $result->getOutput());
$this->error("Errors: " . $result->getErrorOutput());
Enable Verbose Mode
Add -v flags to commands for debugging:
$shell->run('docker-compose up -d -v');
Log Streaming Chunks For async jobs, log chunks to track progress:
$shell->run('docker build .')
->stream(fn($chunk) => Log::debug('Build chunk: ' . substr($chunk, 0, 100)));
Test Cross-Platform Verify commands work on both Linux/macOS and Windows:
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$shell->run('powershell -Command "Get-Process"');
} else {
$shell->run('ps aux');
}
Custom CommandResult
Extend CommandResult for Laravel-specific logic:
use PHPStandardLibrary\Shell\CommandResult;
class LaravelCommandResult extends CommandResult
{
public function toNotification(): Notification
{
return new CommandFailedNotification($this);
}
}
Stream Filtering Add middleware to filter streaming output:
$shell->run('command')
->stream(fn($chunk) => $this->filterSensitiveData($chunk));
Retry Logic Integrate with Laravel’s retry system:
use Illuminate\Support\Facades\Retry;
Retry::retry(3, fn() => $shell->run('command')->throwIfFailed());
Windows-Specific Wrappers Override shell behavior for Windows:
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$shell = new WindowsShellWrapper();
}
Default Timeout The package has no default timeout, which can cause hangs. Set explicitly:
$shell->run('slow-command')->timeout(30); // 30 seconds
Environment Inheritance
By default, the shell does not inherit $_ENV. Explicitly merge if needed:
$shell->run('command')->withEnvironment($_ENV);
Working Directory Change the working directory for commands:
$shell->run('ls')->workingDirectory('/path/to/dir');
Shell Integration
Force a specific shell (e.g., bash vs
How can I help you explore Laravel packages today?