adachsoft / command-executor-lib
A robust PHP library for executing system commands with timeout support, output handling, and process management
Requires
- php: ^8.3
Requires (Dev)
- adachsoft/php-code-style: ^0.2.1
- friendsofphp/php-cs-fixer: ^3.89
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.4
- rector/rector: ^2.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A robust PHP library for executing system commands with timeout support, output handling, and process management.
Features
- Simple API - Easy to use interface for command execution
- Error Handling - Comprehensive exception handling with detailed error information
- Process Management - Robust process handling with non-blocking I/O and timeouts
- Output Capture - Captures both stdout and stderr streams and exposes them separately and combined
- Interactive Execution - Forwards process output and user input through an application-provided I/O interface
- PTY Support - Optionally allocates a pseudoterminal when supported by the platform
- Environment Variables - Support for custom environment variables
- Working Directory - Ability to set custom working directory
- PHP 8.3+ - Modern PHP with strict types and full type safety
Installation
Install via Composer:
composer require adachsoft/command-executor-lib
Requirements
- PHP 8.3 or higher
- No external system dependencies
Quick Start
Basic Usage
<?php
use AdachSoft\CommandExecutorLib\CommandExecutorException;
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
// Execute a simple command
try {
$output = $executor->execute('ls -la');
echo $output;
} catch (CommandExecutorException $e) {
echo 'Error: ' . $e->getMessage();
}
Advanced Usage
Execute with Environment Variables
<?php
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
// Set custom environment variables
$env = [
'PATH' => '/usr/local/bin:/usr/bin:/bin',
'DEBUG' => 'true',
];
$output = $executor->execute('echo $DEBUG', $env);
// Output: true
Execute in Specific Directory
<?php
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
// Execute command in specific directory
$output = $executor->execute('ls -la', [], '/var/www');
Handle Command Failures
<?php
use AdachSoft\CommandExecutorLib\CommandExecutorException;
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
try {
$output = $executor->execute('nonexistent-command');
} catch (CommandExecutorException $e) {
echo 'Command failed: ' . $e->getMessage() . "\n";
echo 'Exit code: ' . $e->getExitCode() . "\n";
echo 'Output: ' . $e->getCommandOutput() . "\n";
}
Get Exit Code Without Exception (array result)
<?php
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
// Execute command and get stdout, stderr, combined output and exit code
$result = $executor->executeWithExitCode('ls /nonexistent');
if ($result['exitCode'] === 0) {
echo 'Success (stdout): ' . $result['stdout'] . "\n";
} else {
echo 'Failed with code ' . $result['exitCode'] . ': ' . $result['output'] . "\n";
}
Get Detailed Result as DTO
<?php
use AdachSoft\CommandExecutorLib\CommandExecutionResult;
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
/** @var CommandExecutionResult $result */
$result = $executor->executeWithResult('ls /nonexistent');
if ($result->isSuccessful()) {
echo 'Success (stdout): ' . $result->stdout . "\n";
} else {
echo 'Failed with code ' . $result->exitCode . ': ' . $result->output . "\n";
}
Using Timeouts
<?php
use AdachSoft\CommandExecutorLib\CommandExecutorException;
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
try {
// This command will be terminated after 1 second
$output = $executor->execute('sleep 5', [], null, 1.0);
} catch (CommandExecutorException $e) {
echo 'Command timed out: ' . $e->getMessage();
}
Interactive execution
Interactive execution forwards stdout and stderr as soon as output is available and writes input supplied by an InteractiveIoInterface implementation to the process stdin. The library does not assume whether input comes from a terminal, a WebSocket, or another transport.
<?php
use AdachSoft\\CommandExecutorLib\\InteractiveIoInterface;
use AdachSoft\\CommandExecutorLib\\SimpleInteractiveCommandExecutor;
final class ConsoleInteractiveIo implements InteractiveIoInterface
{
private bool $inputClosed = false;
public function writeOutput(string $data): void
{
fwrite(STDOUT, $data);
}
public function readInput(float $timeoutSeconds): ?string
{
$read = [STDIN];
$write = null;
$except = null;
$seconds = (int) $timeoutSeconds;
$microseconds = (int) (($timeoutSeconds - $seconds) * 1_000_000);
$ready = stream_select($read, $write, $except, $seconds, $microseconds);
if ($ready === false || $ready === 0) {
return null;
}
$input = fgets(STDIN);
if ($input === false) {
$this->inputClosed = true;
return null;
}
return $input;
}
public function isInputClosed(): bool
{
return $this->inputClosed;
}
}
$executor = new SimpleInteractiveCommandExecutor();
$exitCode = $executor->executeInteractive(
command: 'php bin/console app:interactive',
interactiveIo: new ConsoleInteractiveIo(),
timeout: 60.0,
usePty: $executor->isPtySupported(),
);
// Non-zero exit codes are returned and do not throw an exception.
echo "Process exited with code {$exitCode}" . PHP_EOL;
readInput() must return within the requested timeout. When isInputClosed() returns true, the executor closes process stdin so the child process receives EOF. PTY mode is optional; check isPtySupported() before requesting it. Requesting PTY mode on an unsupported platform throws CommandExecutorException with code CODE_PTY_NOT_SUPPORTED.
Git Operations
<?php
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
// Get current git branch
$branch = trim($executor->execute('git rev-parse --abbrev-ref HEAD'));
echo "Current branch: $branch";
// Check git status
$status = $executor->execute('git status --porcelain', [], '/path/to/project');
if (empty($status)) {
echo 'Working directory is clean';
} else {
echo 'Uncommitted changes detected';
}
File Operations
<?php
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
// Create backup
$backupDir = '/backups/' . date('Y-m-d');
$executor->execute("mkdir -p $backupDir");
$executor->execute("cp -r /var/www $backupDir/");
// Check disk usage
$usage = $executor->execute('df -h / | tail -1');
echo "Disk usage: $usage";
Docker Operations
<?php
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
// Build Docker image
$executor->execute('docker build -t myapp:latest .', [], '/path/to/dockerfile');
// Run container
$result = $executor->executeWithExitCode('docker run --rm myapp:latest');
if ($result['exitCode'] === 0) {
echo 'Container executed successfully';
}
API Reference
CommandExecutorInterface
The main interface that all command executors must implement.
Methods
execute(string $command, array $env = [], ?string $cwd = null, ?float $timeout = null): stringexecuteWithExitCode(string $command, array $env = [], ?string $cwd = null, ?float $timeout = null): array{stdout: string, stderr: string, output: string, exitCode: int}executeWithResult(string $command, array $env = [], ?string $cwd = null, ?float $timeout = null): CommandExecutionResult
SimpleCommandExecutor
Default implementation using PHP's proc_open().
InteractiveCommandExecutorInterface
Executes a process while forwarding output and accepting input through an InteractiveIoInterface implementation.
executeInteractive(string $command, InteractiveIoInterface $interactiveIo, array $env = [], ?string $cwd = null, ?float $timeout = null, bool $usePty = false): int- Returns the process exit code. Non-zero exit codes do not throw an exception.isPtySupported(): bool- Reports whether PTY mode is available on the current platform.
InteractiveIoInterface
The application-provided I/O port used by the interactive executor.
writeOutput(string $data): void- Receives each raw stdout or stderr output chunk.readInput(float $timeoutSeconds): ?string- Returns an input chunk or null, without blocking longer than the timeout.isInputClosed(): bool- Tells the executor to close process stdin and send EOF.
SimpleInteractiveCommandExecutor is the built-in implementation and can be instantiated directly with new SimpleInteractiveCommandExecutor().
Constructor
$executor = new SimpleCommandExecutor();
CommandExecutionResult
Immutable data transfer object representing command execution result.
Properties
string $stdout- Standard output (STDOUT)string $stderr- Standard error output (STDERR)string $output- Combined stdout + stderrint $exitCode- Process exit code
Methods
isSuccessful(): bool- ReturnstruewhenexitCode === 0
CommandExecutorException
Exception thrown when command execution fails.
Error Codes
Use $exception->getCode() to distinguish the reason for a command execution failure:
CODE_GENERAL = 2000- General command execution failureCODE_TIMEOUT = 2001- Command execution exceeded the configured timeoutCODE_PROCESS_START_FAILED = 2002- The process could not be startedCODE_COMMAND_FAILED = 2003- The process completed with a non-zero exit codeCODE_PTY_NOT_SUPPORTED = 2004- PTY mode was requested on a platform that does not support it
Methods
getCommandOutput(): ?string- Returns the command outputgetExitCode(): ?int- Returns the exit codegetStdout(): ?string- Returns captured standard output (STDOUT), if availablegetStderr(): ?string- Returns captured standard error output (STDERR), if availablefromCommandFailure()- Static factory method for creating command failure exceptionstimeout()- Static factory method for creating timeout exceptionsprocessStartFailed()- Static factory method for creating process start failure exceptions
Error Handling
The library provides comprehensive error handling through CommandExecutorException:
<?php
use AdachSoft\CommandExecutorLib\CommandExecutorException;
use AdachSoft\CommandExecutorLib\SimpleCommandExecutor;
$executor = new SimpleCommandExecutor();
try {
$output = $executor->execute('invalid-command');
} catch (CommandExecutorException $e) {
// Handle the failure reason first
switch ($e->getCode()) {
case CommandExecutorException::CODE_TIMEOUT:
echo 'Command timed out';
break;
case CommandExecutorException::CODE_PROCESS_START_FAILED:
echo 'The process could not be started';
break;
case CommandExecutorException::CODE_COMMAND_FAILED:
echo 'Command failed with exit code ' . $e->getExitCode();
break;
default:
echo 'Command execution failed: ' . $e->getMessage();
}
}
CommandExecutorException exposes both the full combined output via getCommandOutput() and separate streams via getStdout()/getStderr(). Use getCode() for the failure category and getExitCode() for the process exit code when a command was started. When a command times out, the exception message may include a short stderr summary if stderr is not empty.
Testing
Run the test suite:
composer test
Run with coverage:
composer test-coverage
Run code quality checks:
composer quality
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run quality checks (
composer quality) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Merge Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Changelog
See CHANGELOG.md for a detailed list of changes.