Search by

adachsoft / command-executor-lib

Arkadiusz Adach

A robust PHP library for executing system commands with timeout support, output handling, and process management

Package info

gitlab.com/a.adach/CommandExecutorLib

Issues

pkg:composer/adachsoft/command-executor-lib

Statistics

Installs: 196

Dependents: 18

Suggesters: 0

Stars: 0

v2.1.0 2026-10-21 17:21 UTC

This package is not auto-updated.

Last update: 2026-10-21 15:22:55 UTC


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): string
  • executeWithExitCode(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 + stderr
  • int $exitCode - Process exit code

Methods

  • isSuccessful(): bool - Returns true when exitCode === 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 failure
  • CODE_TIMEOUT = 2001 - Command execution exceeded the configured timeout
  • CODE_PROCESS_START_FAILED = 2002 - The process could not be started
  • CODE_COMMAND_FAILED = 2003 - The process completed with a non-zero exit code
  • CODE_PTY_NOT_SUPPORTED = 2004 - PTY mode was requested on a platform that does not support it

Methods

  • getCommandOutput(): ?string - Returns the command output
  • getExitCode(): ?int - Returns the exit code
  • getStdout(): ?string - Returns captured standard output (STDOUT), if available
  • getStderr(): ?string - Returns captured standard error output (STDERR), if available
  • fromCommandFailure() - Static factory method for creating command failure exceptions
  • timeout() - Static factory method for creating timeout exceptions
  • processStartFailed() - 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

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Run quality checks (composer quality)
  4. Commit your changes (git commit -m 'Add amazing feature')
  5. Push to the branch (git push origin feature/amazing-feature)
  6. 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.