Interactive shell library with pluggable transports and streaming support. Build MySQL-like CLI interfaces for your PHP applications with built-in command parsing, history, formatting, and real-time message streaming capabilities.
- Visual Architecture Guide - ASCII diagrams, command flows, and troubleshooting
- Standard Shell (REPL): Traditional request/response interactive shell with readline support
- Streaming Shell: Bidirectional streaming for real-time message processing (with Swoole or polling fallback)
- Pluggable Transports: HTTP, Unix Socket, or implement your own via
TransportInterface - Multiple Output Formats: Table (ASCII), JSON, CSV, Vertical (MySQL
\Gstyle) - Advanced Command Parsing: Quote handling, escape sequences, options (
--format=json), and\Gterminator - Shell Features: Command history with persistence, configurable aliases, multi-line input
- Client-Side Filtering: Real-time message filtering in streaming mode
- Built-in Commands: Help, status, history, aliases, screen clearing
- PHP 8.1 or higher
- Symfony Console ^6.0|^7.0
- Guzzle HTTP ^7.0
ext-swoole: Required for StreamingShell with coroutine-based I/O (recommended for production streaming)ext-readline: Recommended for better input handling and history navigation
composer require nashgao/interactive-shell<?php
use NashGao\InteractiveShell\Shell;
use NashGao\InteractiveShell\Transport\HttpTransport;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
// Create HTTP transport pointing to your backend
$transport = new HttpTransport(
serverUrl: 'http://localhost:8080',
timeout: 30.0,
executePath: '/runtime/command/execute',
pingPath: '/ping'
);
// Create shell with custom prompt and aliases
$shell = new Shell(
transport: $transport,
prompt: 'myapp> ',
defaultAliases: [
'ls' => 'list',
'q' => 'quit',
]
);
// Run the interactive shell
$input = new ArgvInput();
$output = new ConsoleOutput();
$exitCode = $shell->run($input, $output);
exit($exitCode);Example Session:
Connected to http://localhost:8080
Interactive Shell (type "help" for commands, "exit" to quit)
myapp> users list --format=table
+----+----------+------------------+
| ID | Username | Email |
+----+----------+------------------+
| 1 | admin | admin@example.com|
| 2 | john | john@example.com |
+----+----------+------------------+
Query OK, 2 rows returned
myapp> users list --format=json
[
{"id": 1, "username": "admin", "email": "admin@example.com"},
{"id": 2, "username": "john", "email": "john@example.com"}
]
myapp> exit
Goodbye!
<?php
use NashGao\InteractiveShell\StreamingShell;
use NashGao\InteractiveShell\Transport\UnixSocketTransport;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
// Create streaming transport (Unix socket example)
$transport = new UnixSocketTransport(
socketPath: '/var/run/mqtt-debug.sock',
timeout: 30.0
);
// Create streaming shell
$shell = new StreamingShell(
transport: $transport,
prompt: 'stream> ',
defaultAliases: []
);
// Run the streaming shell
$input = new ArgvInput();
$output = new ConsoleOutput();
$exitCode = $shell->run($input, $output);
exit($exitCode);Example Streaming Session:
Connected to /var/run/mqtt-debug.sock
Streaming Shell (Swoole mode)
Commands: filter <pattern>, pause, resume, clear, exit
[2024-01-15 14:32:01] MQTT/sensor/temperature: {"value": 22.5, "unit": "C"}
[2024-01-15 14:32:05] MQTT/sensor/humidity: {"value": 65, "unit": "%"}
stream> filter sensor/temperature
Filter set: topic contains "sensor/temperature"
[2024-01-15 14:32:10] MQTT/sensor/temperature: {"value": 23.1, "unit": "C"}
stream> pause
Streaming paused
stream> stats
Messages received: 127
Filter: topic contains "sensor/temperature"
Paused: Yes
stream> resume
Streaming resumed
stream> exit
Session ended. Total messages: 127
The shell supports four output formats that can be selected via --format option or \G terminator:
// Command: users list --format=table
// Or simply: users listOutput:
+----+----------+------------------+--------+
| ID | Username | Email | Active |
+----+----------+------------------+--------+
| 1 | admin | admin@example.com| Yes |
| 2 | john | john@example.com | Yes |
| 3 | jane | jane@example.com | No |
+----+----------+------------------+--------+
Query OK, 3 rows returned
// Command: users list --format=jsonOutput:
[
{
"id": 1,
"username": "admin",
"email": "admin@example.com",
"active": true
},
{
"id": 2,
"username": "john",
"email": "john@example.com",
"active": true
},
{
"id": 3,
"username": "jane",
"email": "jane@example.com",
"active": false
}
]// Command: users list --format=csvOutput:
ID,Username,Email,Active
1,admin,admin@example.com,Yes
2,john,john@example.com,Yes
3,jane,jane@example.com,No
// Command: users show 1\G
// Or: users show 1 --format=verticalOutput:
*************************** 1. row ***************************
ID: 1
Username: admin
Email: admin@example.com
Active: Yes
1 row in set
These commands work offline without server connection:
| Command | Description | Example |
|---|---|---|
help |
Display help message with available commands | help |
exit, quit |
Exit the shell and save session | exit |
status |
Show connection status and session metrics | status |
clear |
Clear the terminal screen | clear |
history |
Display command history | history |
alias [name=cmd] |
Show all aliases or set a new alias | alias ls=list |
unalias <name> |
Remove an alias | unalias ls |
Status Output Example:
myapp> status
Shell Status:
Session started: 2024-01-15 14:30:15
Session duration: 00:05:23
Commands executed: 12
Server: http://localhost:8080
Connected: Yes
Additional commands available in StreamingShell:
| Command | Description | Example |
|---|---|---|
filter <pattern> |
Set message filter expression | filter sensor/temperature |
filter show |
Show current filter | filter show |
filter clear |
Clear filter (show all messages) | filter clear |
pause |
Pause message streaming | pause |
resume |
Resume message streaming | resume |
stats |
Show streaming statistics | stats |
Filter Patterns:
stream> filter sensor/temperature # Topic contains "sensor/temperature"
stream> filter {"value": 22} # Message contains JSON pattern
stream> filter clear # Clear filter
use NashGao\InteractiveShell\Shell;
use NashGao\InteractiveShell\Formatter\OutputFormat;
$shell = new Shell(
transport: $transport,
prompt: 'myapp> ', // Custom prompt string
defaultAliases: [ // Pre-configured aliases
'ls' => 'list',
'q' => 'quit',
'h' => 'help',
]
);
// Set default output format
$shell->setOutputFormat(OutputFormat::Json);
// Change prompt dynamically
$shell->setPrompt('admin@myapp> ');use NashGao\InteractiveShell\Transport\HttpTransport;
use GuzzleHttp\Client;
$transport = new HttpTransport(
serverUrl: 'http://localhost:8080',
timeout: 30.0, // Request timeout in seconds
executePath: '/runtime/command/execute', // Command execution endpoint
pingPath: '/ping', // Health check endpoint
httpClient: new Client([ // Optional custom Guzzle client
'headers' => [
'Authorization' => 'Bearer token123',
],
])
);Expected Server Response Format:
{
"success": true,
"data": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"}
],
"message": "Query OK, 2 rows returned",
"metadata": {
"execution_time": 0.125
}
}use NashGao\InteractiveShell\Transport\UnixSocketTransport;
$transport = new UnixSocketTransport(
socketPath: '/var/run/myapp.sock',
timeout: 30.0, // Socket timeout
bufferSize: 8192 // Read buffer size
);Implement the TransportInterface to create custom transport backends:
<?php
namespace App\Shell\Transport;
use NashGao\InteractiveShell\Command\CommandResult;
use NashGao\InteractiveShell\Parser\ParsedCommand;
use NashGao\InteractiveShell\Transport\TransportInterface;
final class RedisTransport implements TransportInterface
{
private \Redis $redis;
private bool $connected = false;
public function __construct(
private readonly string $host = '127.0.0.1',
private readonly int $port = 6379,
private readonly float $timeout = 5.0,
) {
$this->redis = new \Redis();
}
public function connect(): void
{
$this->connected = $this->redis->connect(
$this->host,
$this->port,
$this->timeout
);
if (!$this->connected) {
throw new \RuntimeException("Cannot connect to Redis at {$this->host}:{$this->port}");
}
}
public function disconnect(): void
{
if ($this->connected) {
$this->redis->close();
$this->connected = false;
}
}
public function isConnected(): bool
{
return $this->connected && $this->redis->ping() !== false;
}
public function send(ParsedCommand $command): CommandResult
{
try {
// Execute Redis command
$result = $this->redis->rawCommand(
$command->command,
...$command->arguments
);
return CommandResult::success(
data: $result,
message: 'OK'
);
} catch (\RedisException $e) {
return CommandResult::failure(
error: $e->getMessage()
);
}
}
public function ping(): bool
{
try {
return $this->redis->ping() !== false;
} catch (\RedisException) {
return false;
}
}
public function getInfo(): array
{
try {
$info = $this->redis->info();
return is_array($info) ? $info : [];
} catch (\RedisException) {
return [];
}
}
public function getEndpoint(): string
{
return "{$this->host}:{$this->port}";
}
}Usage:
$transport = new RedisTransport(host: 'localhost', port: 6379);
$shell = new Shell($transport, prompt: 'redis> ');
$shell->run($input, $output);Create custom command handlers for your application by implementing CommandHandlerInterface:
<?php
namespace App\Shell\Handler;
use NashGao\InteractiveShell\Command\CommandResult;
use NashGao\InteractiveShell\Parser\ParsedCommand;
use NashGao\InteractiveShell\Server\ContextInterface;
use NashGao\InteractiveShell\Server\Handler\CommandHandlerInterface;
final class DatabaseHandler implements CommandHandlerInterface
{
public function getCommand(): string
{
return 'db:status';
}
public function handle(ParsedCommand $command, ContextInterface $context): CommandResult
{
// Access services via context
$db = $context->get('database');
return CommandResult::success(
data: [
'connected' => $db->isConnected(),
'driver' => $db->getDriver(),
'queries' => $db->getQueryCount(),
],
message: 'Database status retrieved'
);
}
public function getDescription(): string
{
return 'Show database connection status';
}
public function getUsage(): array
{
return ['db:status', 'db:status --verbose'];
}
}Use CommandRegistry to register your handlers:
use NashGao\InteractiveShell\Server\Handler\CommandRegistry;
$registry = new CommandRegistry();
// Register individual handlers
$registry->register(new DatabaseHandler());
$registry->register(new QueueHandler());
$registry->register(new CacheHandler());
// Set a fallback handler for unknown commands
$registry->setFallbackHandler(new UnknownCommandHandler());The library provides a ConfigProvider for Hyperf auto-discovery:
# Publish the configuration file
php bin/hyperf.php vendor:publish nashgao/interactive-shellEdit config/autoload/interactive_shell.php:
<?php
return [
// Enable/disable shell server
'enabled' => (bool) env('SHELL_ENABLED', true),
// Unix socket path
'socket_path' => env('SHELL_SOCKET_PATH', '/var/run/hyperf-shell.sock'),
// Socket file permissions
'socket_permissions' => 0660,
// Register custom handlers
'handlers' => [
App\Shell\Handler\DatabaseHandler::class,
App\Shell\Handler\QueueHandler::class,
],
];Once the Hyperf server is running, connect using a client:
use NashGao\InteractiveShell\Shell;
use NashGao\InteractiveShell\Transport\UnixSocketTransport;
$transport = new UnixSocketTransport(
socketPath: '/var/run/hyperf-shell.sock',
timeout: 30.0
);
$shell = new Shell($transport, prompt: 'hyperf> ');
$shell->run($input, $output);When running in Hyperf, these additional commands are available:
| Command | Description |
|---|---|
config <key> |
Get configuration values using dot notation |
routes |
List all registered HTTP routes |
container <abstract> |
Inspect container bindings |
command <name> |
Execute Hyperf console commands |
| Class | Description |
|---|---|
Shell |
Standard interactive shell with request/response model |
StreamingShell |
Bidirectional streaming shell for real-time messages |
ShellParser |
Command parser with quote handling and options |
OutputFormatter |
Formats command results in table/JSON/CSV/vertical |
MessageFormatter |
Formats streaming messages with timestamps |
HistoryManager |
Manages command history with persistence |
AliasManager |
Manages command aliases |
ShellState |
Tracks session state and metrics |
| Class | Description |
|---|---|
HttpTransport |
HTTP/HTTPS transport using Guzzle |
UnixSocketTransport |
Unix domain socket transport (with streaming support) |
TransportInterface |
Interface for custom transport implementations |
StreamingTransportInterface |
Extended interface for streaming transports |
| Class | Description |
|---|---|
ParsedCommand |
Parsed command with arguments and options |
CommandResult |
Command execution result with data and metadata |
Message |
Streaming message with topic and payload |
FilterExpression |
Message filter for streaming mode |
OutputFormat |
Enum for output format types |
// Shell execution
$shell->run(InputInterface $input, OutputInterface $output): int
$shell->executeCommand(string $command, OutputInterface $output): int
// Shell control
$shell->isRunning(): bool
$shell->stop(): void
// Configuration
$shell->setPrompt(string $prompt): void
$shell->setOutputFormat(OutputFormat $format): void
// Access components
$shell->getTransport(): TransportInterface
$shell->getAliases(): AliasManager
$shell->getHistory(): HistoryManager// StreamingShell-specific
$shell->setFilter(FilterExpression $filter): void
$shell->getMessageCount(): int
$shell->getOutputFormatter(): OutputFormatterUse \ at the end of a line to continue input on the next line:
myapp> SELECT * \
-> FROM users \
-> WHERE active = 1;
- Up/Down arrows: Navigate through command history
- Ctrl+R: Reverse search through history (with readline)
- History persistence: Commands saved between sessions
myapp> alias ll=list --format=table --verbose
Alias set: ll = list --format=table --verbose
myapp> ll users
# Expands to: list --format=table --verbose users
The shell automatically saves:
- Command history
- Session metrics (start time, command count, etc.)
- Connection state
| Mode | Description | Performance | Requirements |
|---|---|---|---|
| Swoole | Coroutine-based async I/O | Excellent (true concurrency) | ext-swoole |
| Polling | Stream select polling | Good (periodic checking) | None |
Recommendation: Install Swoole extension for production streaming workloads:
pecl install swoole| Format | Speed | Use Case |
|---|---|---|
| JSON | Fastest | Machine-readable, API integration |
| CSV | Fast | Data export, spreadsheet import |
| Table | Moderate | Human-readable terminal output |
| Vertical | Moderate | Detailed single-record inspection |
| Suite | Directory | Purpose |
|---|---|---|
| Unit | tests/Unit/ |
Component isolation tests |
| Integration | tests/Integration/ |
Cross-component interactions |
| Specification | tests/Specification/ |
Consumer-perspective behavior specs |
| E2E | tests/E2E/ |
Full system scenarios (requires Swoole) |
# Run all tests
composer test
# Run with coverage report
composer test:coverage
# Run specific suite
./vendor/bin/phpunit --testsuite=Unit
./vendor/bin/phpunit --testsuite=Integration
./vendor/bin/phpunit --testsuite=Specification
# Run single test file
./vendor/bin/phpunit tests/Unit/Parser/ShellParserTest.php
# Static analysis
composer phpstan- Parser (
tests/Unit/Parser/): Command parsing, quote handling, escape sequences - Handlers (
tests/Unit/Server/Handler/): Built-in command handlers - Transport (
tests/Integration/Transport/): HTTP and socket communication - Formatters (
tests/Unit/Formatter/): Table, JSON, CSV, Vertical output - Shell (
tests/Integration/): Full shell lifecycle and command flow
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for new functionality
- Ensure all tests pass (
composer test) - Ensure PHPStan passes at level max (
composer phpstan) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow PSR-12 coding style
- Use strict types (
declare(strict_types=1)) - Add type hints for all parameters and return types
- Write PHPDoc blocks for public methods
- Keep methods focused and concise
This library is licensed under the MIT License. See the LICENSE file for details.
Built with ❤️ by Nash Gao