├── .gitignore
├── LICENSE
├── README.md
├── bin
├── .gitignore
├── drupe
└── pecan
├── composer.json
└── src
├── Console.php
├── Console
├── ConsoleInterface.php
├── Input.php
└── Output
│ ├── ConsoleOutput.php
│ ├── ConsoleOutputInterface.php
│ ├── PecanOutput.php
│ ├── StreamOutput.php
│ └── StreamOutputInterface.php
├── Drupe.php
├── Readline.php
└── Shell.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | composer.lock
3 | /vendor
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Michael Crumm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pecan
2 |
3 | Event-driven, non-blocking shell for [ReactPHP](http://reactphp.org).
4 |
5 | Pecan (`/pɪˈkɑːn/`) provides a non-blocking alternative to the shell provided in the [Symfony Console](https://github.com/symfony/console) component. Additionally, Pecan includes a basic, framework-agnostic shell component called `Drupe` that can be used as the basis for building custom shells.
6 |
7 | ## Shells
8 |
9 | ### Drupe
10 |
11 | `Pecan\Drupe` is a standalone component for building event-driven console components. I like to think of it as Pecan without the Shell.
12 |
13 | Pass an `EventLoopInterface` to `start()` listening for input.
14 |
15 | ```
16 | $loop = \React\EventLoop\Factory::create();
17 | $shell = new \Pecan\Drupe();
18 |
19 | // $shell->start() returns $loop to allow this chaining.
20 | $shell->start($loop)->run();
21 | ```
22 |
23 | ### Drupe Events
24 |
25 | - `running` - The shell is running.
26 | - `data` - Data was received from `STDIN`.
27 | - `error` - Indicates an I/O problem.
28 | - `close` - The shell was closed.
29 |
30 | ### Shell
31 |
32 | `Pecan\Shell` extends `Drupe` to provide an event-driven wrapper for a standard `Symfony\Component\Console\Application`. It can be used as a drop-in replacement for `Symfony\Component\Console\Shell`.
33 |
34 | >**Note**: To maintain a Symfony Console-like workflow, calling `$shell->run()` on `Pecan\Shell` starts the EventLoop, so make sure it gets called last.
35 |
36 | ### Shell Events
37 |
38 | `Shell` emits the same events as `Drupe`.
39 |
40 | ## Readline
41 |
42 | `Pecan\Readline` provides an interface for reading line-by-line input from `STDIN`. This component is heavily inspired by, and strives for parity with the [NodeJS Readline Component](http://nodejs.org/api/readline.html).
43 |
44 | ### Readline Events
45 |
46 | - `line` - A line has been read from `STDIN`.
47 | - `pause` - Reading from the stream has been paused.
48 | - `resume` - Reading from the stream has resumed.
49 | - `error` - The input stream encountered an error.
50 | - `close` - Then input stream was closed.
51 |
52 | ## Console
53 |
54 | `Pecan\Console\Console` provides a standard interface for working with `STDOUT` and `STDERR`. It is inspired heavily by the [NodeJS Console Component](http://nodejs.org/api/console.html) and takes some functionality from the [Symfony Console Component](http://symfony.com)
55 |
56 | ### Output
57 |
58 | The Output classes extend the base Console Output. `StreamOutput` wraps a single stream resource, while `ConsoleOutput` contains both the `STDOUT` and `STDERR` streams.
59 |
60 | - `StreamOutputInterface`
61 | - `ConsoleOutputInterface`
62 | - `PecanOutput`
63 |
64 | ## Using Pecan
65 |
66 | ### Using Drupe as a Standalone Shell
67 |
68 | ```php
69 | use Pecan\Drupe;
70 |
71 | $loop = \React\EventLoop\Factory::create();
72 | $shell = new Drupe();
73 |
74 | // Example one-time callback to write the initial prompt.
75 | // This resumes reading from STDIN and kicks off the shell.
76 | $shell->once('running', function (Drupe $shell) {
77 | $shell->setPrompt('drupe> ')->prompt();
78 | });
79 |
80 | // Example callback for the data event.
81 | // By convention, any call to write() will be followed by a call to prompt()
82 | // once the data has been written to the output stream.
83 | $shell->on('data', function ($line, Drupe $shell) {
84 |
85 | $command = (!$line && strlen($line) == 0) ? false : rtrim($line);
86 |
87 | if ('exit' === $command || false === $command) {
88 | $shell->close();
89 | } else {
90 | $shell->writeln(sprintf(PHP_EOL.'// in: %s', $line));
91 | }
92 |
93 | });
94 |
95 | // Example callback for the close event.
96 | $shell->on('close', function ($code, Drupe $shell) {
97 | $shell->writeln([
98 | '// Goodbye.',
99 | sprintf('// Shell exits with code %d', $code),
100 | ]);
101 | });
102 |
103 | $shell->start($loop)->run();
104 | ```
105 |
106 | ### Using Shell with Symfony Console Applications
107 |
108 | Here is a shell that echoes back any input it receives, and then exits.
109 |
110 | ```php
111 | // Pecan\Shell wraps a standard Console Application.
112 | use Symfony\Component\Console\Application;
113 | use Pecan\Shell;
114 |
115 | $shell = new Shell(new Application('pecan'));
116 |
117 | $shell->on('data', function($line, Shell $shell) {
118 | $shell->write($line)->then(function($shell) {
119 | $shell->close();
120 | });
121 | });
122 |
123 | $shell->run();
124 | ```
125 |
126 | ### Injecting An `EventLoopInterface` into `Pecan\Shell`
127 |
128 | Unless you pass `\Pecan\Shell` an object implementing `EventLoopInterface` as its second constructor method, the `Shell` will get one from the EventLoop Factory. Keep this in mind if you want to integrate Pecan into an existing ReactPHP project.
129 |
130 | ```php
131 | use Symfony\Component\Console\Application;
132 | use Pecan\Shell;
133 |
134 | $loop = \React\EventLoop\Factory::create();
135 |
136 | // Do other things requiring $loop...
137 |
138 | $shell = new Shell(new Application('pecan'), $loop);
139 |
140 | // We must still let the shell run the EventLoop.
141 | $shell->run();
142 | ```
143 |
144 | ### Example `exit` callback
145 |
146 | ```php
147 | // Example callback for the exit event.
148 | $shell->on('exit', function($code, \Pecan\Shell $shell) {
149 | $shell->emit('output', [
150 | [
151 | 'Goodbye.',
152 | sprintf('// Shell exits with code %d', $code)
153 | ],
154 | true
155 | ]);
156 | });
157 | ```
158 |
--------------------------------------------------------------------------------
/bin/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !drupe
3 | !pecan
4 | !.gitignore
--------------------------------------------------------------------------------
/bin/drupe:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | once('running', function (Drupe $shell) {
28 | $shell->setPrompt('drupe> ')->prompt();
29 | });
30 |
31 | // Example callback for the data event.
32 | $shell->on('data', function ($line, Drupe $shell) {
33 |
34 | $command = (!$line && strlen($line) == 0) ? false : rtrim($line);
35 |
36 | if ('exit' === $command || false === $command) {
37 | $shell->close();
38 | } else {
39 | $shell->console()->log([ sprintf(PHP_EOL.'// in: %s', $line), true ]);
40 | $shell->prompt();
41 | }
42 |
43 | });
44 |
45 | // Example callback for the close event.
46 | $shell->on('close', function ($code, Drupe $shell) {
47 | $shell->console()->log([
48 | [
49 | '// Goodbye.',
50 | sprintf('// Shell exits with code %d', $code)
51 | ],
52 | true
53 | ]);
54 | });
55 |
56 | $shell->start($loop)->run();
57 |
--------------------------------------------------------------------------------
/bin/pecan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | on('data', function($line, Shell $shell) {
31 | $shell->console()->log(sprintf("\n// in: %s\n", $line));
32 | //return $shell->close();
33 | });
34 |
35 | // Example callback for the close event.
36 | $shell->on('close', function($code, Shell $shell) {
37 | $shell->console()->log([
38 | [
39 | 'Goodbye.',
40 | sprintf('// Shell exits with code %d', $code)
41 | ],
42 | true
43 | ]);
44 | });
45 |
46 | $shell->run();
47 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mcrumm/pecan",
3 | "description": "An event-driven, non-blocking shell for ReactPHP based on the Symfony Console Shell.",
4 | "keywords": [ "shell", "console", "react" ],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Michael Crumm",
9 | "email": "mike@crumm.net"
10 | }
11 | ],
12 | "require": {
13 | "php": ">= 5.4",
14 | "react/promise": "~2.0",
15 | "symfony/console": "~2.4",
16 | "react/child-process": "0.4.*"
17 |
18 | },
19 | "require-dev": {
20 | "phpspec/phpspec": "2.0.*@dev"
21 | },
22 | "autoload": {
23 | "psr-4": {
24 | "Pecan\\": "src"
25 | }
26 | },
27 | "config": {
28 | "bin-dir": "bin"
29 | },
30 | "minimum-stability": "stable",
31 | "extra": {
32 | "branch-alias": {
33 | "dev-develop": "2.0-dev"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Console.php:
--------------------------------------------------------------------------------
1 | output = $output;
26 | }
27 |
28 | /**
29 | * @param ConsoleOutputInterface $output
30 | * @return $this
31 | */
32 | public function setOutput(ConsoleOutputInterface $output)
33 | {
34 | $this->output = $output;
35 | return $this;
36 | }
37 |
38 | /**
39 | * @return ConsoleOutputInterface|null
40 | */
41 | public function getOutput()
42 | {
43 | return $this->output;
44 | }
45 |
46 | /**
47 | * Formats a message.
48 | *
49 | * @param $message
50 | * @return string
51 | * @throws \LogicException When called before Output is set.
52 | */
53 | public function format($message)
54 | {
55 | if (!$this->output) {
56 | throw new \LogicException('A ConsoleOutputInterface must be set before calling format().');
57 | }
58 |
59 | return $this->output->getFormatter()->format($message);
60 | }
61 |
62 |
63 | /**
64 | * Writes log line(s) to STDOUT.
65 | *
66 | * @return $this
67 | */
68 | public function log()
69 | {
70 | if (!$this->output) { return $this; }
71 |
72 | foreach (func_get_args() as $line) {
73 |
74 | if (is_array($line)) {
75 | call_user_func_array([ $this->output, 'write' ], $line);
76 | continue;
77 | }
78 |
79 | $this->output->write($line);
80 | };
81 |
82 | return $this;
83 | }
84 |
85 | /**
86 | * @see log
87 | * @return $this
88 | */
89 | public function info()
90 | {
91 | if (!$this->output) { return $this; }
92 |
93 | call_user_func_array([ $this, 'log' ], func_get_args());
94 |
95 | return $this;
96 | }
97 |
98 | /**
99 | * Writes log line(s) to STDERR.
100 | *
101 | * @return $this
102 | */
103 | public function error()
104 | {
105 | if (!$this->output) { return $this; }
106 |
107 | foreach (func_get_args() as $line) {
108 |
109 | if (is_array($line)) {
110 | call_user_func_array([ $this->output, 'write' ], $line);
111 | continue;
112 | }
113 |
114 | $this->output->getErrorOutput()->write($line);
115 | };
116 |
117 | return $this;
118 | }
119 |
120 | /**
121 | * @see error
122 | * @return $this
123 | */
124 | public function warn()
125 | {
126 | return $this->_notImplemented(__METHOD__);
127 | }
128 |
129 | public function dir($object)
130 | {
131 | return $this->_notImplemented(__METHOD__);
132 | }
133 |
134 | public function time($label)
135 | {
136 | return $this->_notImplemented(__METHOD__);
137 | }
138 |
139 | public function timeEnd($label)
140 | {
141 | return $this->_notImplemented(__METHOD__);
142 | }
143 |
144 | public function trace($label)
145 | {
146 | return $this->_notImplemented(__METHOD__);
147 | }
148 |
149 | public function assert($expression, $messages = [])
150 | {
151 | $this->_notImplemented(__METHOD__);
152 | return false;
153 | }
154 |
155 | private function _notImplemented($method)
156 | {
157 | trigger_error($method . ' is not implemented.', E_USER_NOTICE);
158 | return $this;
159 | }
160 |
161 | }
162 |
--------------------------------------------------------------------------------
/src/Console/ConsoleInterface.php:
--------------------------------------------------------------------------------
1 | stream, 0);
23 |
24 | $this->pause();
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/Console/Output/ConsoleOutput.php:
--------------------------------------------------------------------------------
1 | stderr = $error;
28 | }
29 |
30 | /**
31 | * {@inheritDoc}
32 | */
33 | public function getErrorOutput()
34 | {
35 | return $this->stderr;
36 | }
37 |
38 | /**
39 | * {@inheritDoc}
40 | */
41 | public function emit($event, array $arguments = [])
42 | {
43 | parent::emit($event, $arguments);
44 | $this->stderr->emit($event, $arguments);
45 | }
46 |
47 | /**
48 | * {@inheritDoc}
49 | */
50 | public function on($event, callable $listener)
51 | {
52 | parent::on($event, $listener);
53 | $this->stderr->on($event, $listener);
54 | }
55 |
56 | /**
57 | * Helper method to proxy event listeners to the wrapped stream.
58 | *
59 | * @param $event
60 | * @param callable $listener
61 | */
62 | public function once($event, callable $listener)
63 | {
64 | parent::once($event, $listener);
65 | $this->stderr->once($event, $listener);
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/Console/Output/ConsoleOutputInterface.php:
--------------------------------------------------------------------------------
1 | initializePipes();
30 |
31 | parent::__construct(new Stream($this->pipes[0], $loop), $verbosity, $decorated, $formatter);
32 |
33 | $this->setErrorOutput(new StreamOutput(new Stream($this->pipes[1], $loop), $verbosity, $decorated, $formatter));
34 | }
35 |
36 | /**
37 | * {@inheritDoc}
38 | */
39 | public function setDecorated($decorated)
40 | {
41 | parent::setDecorated($decorated);
42 | $this->stderr->setDecorated($decorated);
43 | }
44 |
45 | /**
46 | * {@inheritDoc}
47 | */
48 | public function setFormatter(OutputFormatterInterface $formatter)
49 | {
50 | parent::setFormatter($formatter);
51 | $this->stderr->setFormatter($formatter);
52 | }
53 |
54 | /**
55 | * {@inheritDoc}
56 | */
57 | public function setVerbosity($level)
58 | {
59 | parent::setVerbosity($level);
60 | $this->stderr->setVerbosity($level);
61 | }
62 |
63 | /**
64 | * Initialize the STDOUT and STDERR pipes.
65 | */
66 | protected function initializePipes()
67 | {
68 | $this->pipes = [
69 | fopen($this->hasStdoutSupport() ? 'php://stdout' : 'php://output', 'w'),
70 | fopen('php://stderr', 'w'),
71 | ];
72 |
73 | foreach ($this->pipes as $pipe) {
74 | stream_set_blocking($pipe, 0);
75 | }
76 | }
77 |
78 | /**
79 | * Returns true if current environment supports writing console output to
80 | * STDOUT.
81 | *
82 | * IBM iSeries (OS400) exhibits character-encoding issues when writing to
83 | * STDOUT and doesn't properly convert ASCII to EBCDIC, resulting in garbage
84 | * output.
85 | *
86 | * @return boolean
87 | */
88 | protected function hasStdoutSupport()
89 | {
90 | return ('OS400' != php_uname('s'));
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Console/Output/StreamOutput.php:
--------------------------------------------------------------------------------
1 | setStream($stream);
31 |
32 | if (null === $decorated) {
33 | $decorated = $this->hasColorSupport();
34 | }
35 |
36 | parent::__construct($verbosity, $decorated, $formatter);
37 | }
38 |
39 | /**
40 | * @param Stream $stream
41 | * @return $this
42 | */
43 | public function setStream(Stream $stream)
44 | {
45 | $this->stream = $stream;
46 |
47 | $this->stream->on('error', function($error) {
48 | $this->emit('error', [ $error, $this ]);
49 | });
50 |
51 | return $this;
52 | }
53 |
54 | /**
55 | * @param null $data
56 | */
57 | public function end($data = null)
58 | {
59 | $this->stream->end($data);
60 | }
61 |
62 | /**
63 | * Helper method to proxy events to the wrapped stream.
64 | *
65 | * @param $event
66 | * @param array $arguments
67 | */
68 | public function emit($event, array $arguments = [])
69 | {
70 | $this->stream->emit($event, $arguments);
71 | }
72 |
73 | /**
74 | * Helper method to proxy event listeners to the wrapped stream.
75 | *
76 | * @param $event
77 | * @param callable $listener
78 | */
79 | public function on($event, callable $listener)
80 | {
81 | $this->stream->on($event, $listener);
82 | }
83 |
84 | /**
85 | * Helper method to proxy event listeners to the wrapped stream.
86 | *
87 | * @param $event
88 | * @param callable $listener
89 | */
90 | public function once($event, callable $listener)
91 | {
92 | $this->stream->once($event, $listener);
93 | }
94 |
95 | /**
96 | * Writes a message to the output.
97 | *
98 | * @param string $message A message to write to the output
99 | * @param boolean $newline Whether to add a newline or not
100 | */
101 | protected function doWrite($message, $newline)
102 | {
103 | $this->stream->write($newline ? $message . PHP_EOL : $message);
104 | }
105 |
106 | /**
107 | * Returns true if the stream supports colorization.
108 | *
109 | * Colorization is disabled if not supported by the stream:
110 | *
111 | * - Windows without Ansicon and ConEmu
112 | * - non tty consoles
113 | *
114 | * @return Boolean true if the stream supports colorization, false otherwise
115 | */
116 | protected function hasColorSupport()
117 | {
118 | // @codeCoverageIgnoreStart
119 | if (DIRECTORY_SEPARATOR == '\\') {
120 | return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
121 | }
122 |
123 | return function_exists('posix_isatty') && @posix_isatty($this->stream->stream);
124 | // @codeCoverageIgnoreEnd
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/src/Console/Output/StreamOutputInterface.php:
--------------------------------------------------------------------------------
1 | readline = new Readline($history);
45 |
46 | $this->once('running', function() {
47 | $this->console = $this->readline->console();
48 | $this->readline->setPrompt('drupe> ');
49 | });
50 | }
51 |
52 | /**
53 | * Starts the shell.
54 | *
55 | * @param LoopInterface $loop
56 | * @param float $interval
57 | * @return LoopInterface Returns the provided loop to allow for fluent calls to "run()"
58 | * @throws \LogicException if the shell is already running.
59 | */
60 | public function start(LoopInterface $loop, $interval = 0.001)
61 | {
62 | if ($this->running) {
63 | throw new \LogicException('The shell is already running.');
64 | }
65 |
66 | $this->running = true;
67 |
68 | $this->readline->on('error', function ($error, $object) {
69 | $this->emit('error', [ $error, $object, $this ]);
70 | });
71 |
72 | $this->readline->on('line', function ($command) {
73 | $this->readline->addHistory($command);
74 | $this->emit('data', [ $command, $this ]);
75 | });
76 |
77 | $this->readline->on('close', function() {
78 | $this->close();
79 | });
80 |
81 | $this->on('close', function ($exitCode = 0) {
82 | if ($this->running) {
83 | $this->exitCode = $exitCode;
84 | $this->running = false;
85 | }
86 | });
87 |
88 | $loop->addPeriodicTimer($interval, [ $this, 'checkRunning' ]);
89 |
90 | $this->readline->start($loop);
91 |
92 | $this->emit('running', [ $this ]);
93 |
94 | return $loop;
95 | }
96 |
97 | /**
98 | * @return Console\ConsoleInterface|null
99 | */
100 | public function console()
101 | {
102 | return $this->readline->console();
103 | }
104 |
105 | /**
106 | * Closes the shell.
107 | *
108 | * @param int $exitExitCode
109 | * @return $this
110 | */
111 | public function close($exitExitCode = 0)
112 | {
113 | if ($this->running) {
114 | $this->emit('close', [ $exitExitCode, $this ]);
115 | }
116 |
117 | return $this;
118 | }
119 |
120 | /**
121 | * Sets the prompt on the terminal, if running.
122 | *
123 | * @param string $prompt
124 | * @return $this
125 | */
126 | public function setPrompt($prompt)
127 | {
128 | if ($this->running) {
129 | $this->readline->setPrompt($prompt);
130 | }
131 |
132 | return $this;
133 | }
134 |
135 | /**
136 | * @see Readline::prompt()
137 | */
138 | public function prompt()
139 | {
140 | if ($this->running) {
141 | $this->readline->prompt();
142 | }
143 | return $this;
144 | }
145 |
146 | /**
147 | * Checks whether or not the shell is still running.
148 | *
149 | * @param Timer $timer
150 | */
151 | public function checkRunning(Timer $timer)
152 | {
153 | if (!$this->running) {
154 | $timer->cancel();
155 | // @codeCoverageIgnoreStart
156 | exit($this->exitCode);
157 | // @codeCoverageIgnoreEnd
158 | }
159 | }
160 |
161 | }
162 |
--------------------------------------------------------------------------------
/src/Readline.php:
--------------------------------------------------------------------------------
1 | history = $history;
54 | $this->hasReadline = Readline::isFullySupported();
55 |
56 | if (!$completer && $this->hasReadline) { $completer = function () { return []; }; }
57 |
58 | $this->setCompleter($completer);
59 |
60 | $this->setPrompt('> ');
61 | }
62 |
63 | /**
64 | * @param LoopInterface $loop
65 | * @param float $interval
66 | * @throws \LogicException When called while already running.
67 | */
68 | public function start(LoopInterface $loop, $interval = 0.001)
69 | {
70 | if ($this->running) {
71 | throw new \LogicException('Readline is already running.');
72 | }
73 |
74 | $this->running = true;
75 | $this->input = new Input($loop);
76 | $this->output = new PecanOutput($loop);
77 | $this->console = new Console($this->output);
78 |
79 | if (!$this->terminal) {
80 | $this->terminal = $this->output->isDecorated();
81 | }
82 |
83 | if ($this->hasReadline) {
84 | $this->readHistory();
85 | }
86 |
87 | // Setup I/O Error Emitters
88 | $errorEmitter = $this->getErrorEmitter();
89 | $this->output->on('error', $errorEmitter);
90 | $this->input->on('error', $errorEmitter);
91 | $this->input->on('end', function () { $this->close(); });
92 |
93 | if ($this->hasReadline) {
94 | $loop->addPeriodicTimer($interval, [ $this, 'readlineHandler' ]);
95 | } else {
96 | $this->input->on('data', [ $this, 'lineHandler' ]);
97 | $this->input->resume();
98 | $this->paused = false;
99 | }
100 |
101 | $this->emit('running', [ $this ]);
102 | }
103 |
104 | /**
105 | * Reads a line from the history file, if available.
106 | *
107 | */
108 | public function readHistory()
109 | {
110 | if ($this->hasReadline) {
111 | readline_read_history($this->history);
112 | }
113 | }
114 |
115 | /**
116 | * Adds a line to the readline history.
117 | *
118 | */
119 | public function addHistory($line)
120 | {
121 | if ($this->hasReadline) {
122 | readline_add_history($line);
123 | readline_write_history($this->history);
124 | }
125 | }
126 |
127 | /**
128 | * Sets the auto-complete callback
129 | *
130 | * @param callable $completer
131 | * @return $this
132 | * @throws \LogicException When PHP is not compiled with readline support.
133 | */
134 | public function setCompleter(callable $completer)
135 | {
136 | if (!Readline::isFullySupported()) {
137 | throw new \LogicException(sprintf('%s requires readline support to use the completer.', __CLASS__));
138 | }
139 |
140 | $this->completer = $completer;
141 | return $this;
142 | }
143 |
144 | /**
145 | * Sets the terminal prompt.
146 | *
147 | * @param string $prompt
148 | * @return $this
149 | */
150 | public function setPrompt($prompt)
151 | {
152 | $this->prompt = $prompt;
153 | return $this;
154 | }
155 |
156 | /**
157 | * Renders the prompt.
158 | *
159 | * @return string
160 | */
161 | public function getPrompt()
162 | {
163 | // using the formatter here is required when using readline
164 | return $this->console->format($this->prompt);
165 | }
166 |
167 | /**
168 | * @return Console\ConsoleInterface
169 | */
170 | public function console()
171 | {
172 | if ($this->running) {
173 | return $this->console;
174 | }
175 | }
176 |
177 | /**
178 | * Writes the prompt to the output.
179 | *
180 | * @return $this
181 | */
182 | public function prompt()
183 | {
184 | if ($this->hasReadline) {
185 | readline_callback_handler_install($this->getPrompt(), [ $this, 'lineHandler' ]);
186 | } else {
187 | if ($this->paused) { $this->resume(); }
188 | $this->console->log($this->getPrompt());
189 | }
190 |
191 | return $this;
192 | }
193 |
194 | /**
195 | * Prompts for input.
196 | *
197 | * @param string $query
198 | * @param callable $callback
199 | * @return $this
200 | */
201 | public function question($query, callable $callback)
202 | {
203 | if (!is_callable($callback)) { return $this; }
204 |
205 | if ($this->questionCallback) {
206 | $this->prompt();
207 | } else {
208 | $this->oldPrompt = $this->prompt;
209 | $this->setPrompt($query);
210 | $this->questionCallback = $callback;
211 | $this->prompt();
212 | }
213 |
214 | return $this;
215 | }
216 |
217 | /**
218 | * Pauses the input stream.
219 | * @return $this
220 | */
221 | public function pause()
222 | {
223 | if ($this->paused) { return $this; }
224 | $this->input->pause();
225 | $this->paused = true;
226 | $this->emit('pause');
227 | return $this;
228 | }
229 |
230 | /**
231 | * Resumes the input stream.
232 | * @return $this
233 | */
234 | public function resume()
235 | {
236 | if (!$this->paused) { return $this; }
237 | $this->input->resume();
238 | $this->paused = false;
239 | $this->emit('resume');
240 | return $this;
241 | }
242 |
243 | /**
244 | * Closes the streams.
245 | * @return $this
246 | */
247 | public function close()
248 | {
249 | if ($this->closed) { return; }
250 | $this->pause();
251 | $this->closed = true;
252 | $this->emit('close');
253 | }
254 |
255 | /**
256 | * @param $line
257 | */
258 | public function lineHandler($line)
259 | {
260 | if ($this->questionCallback) {
261 | $cb = $this->questionCallback;
262 | $this->questionCallback = null;
263 | $this->setPrompt($this->oldPrompt);
264 | $cb($line);
265 | } else {
266 | $this->emit('line', [ $line ]);
267 | }
268 |
269 | if ($this->hasReadline) {
270 | //readline_callback_handler_remove();
271 | }
272 | }
273 |
274 | public function readlineHandler(Timer $timer)
275 | {
276 | $w = NULL;
277 | $e = NULL;
278 | $r = [ $this->input->stream ];
279 | $n = stream_select($r, $w, $e, 0, 20000);
280 | if ($n && in_array($this->input->stream, $r)) {
281 | readline_callback_read_char();
282 | }
283 | }
284 |
285 | /**
286 | * Returns whether or not readline is available to PHP.
287 | *
288 | * @return boolean
289 | */
290 | static public function isFullySupported()
291 | {
292 | return function_exists('readline');
293 | }
294 |
295 | /**
296 | * @return \Closure
297 | */
298 | protected function getErrorEmitter()
299 | {
300 | return function ($error, $input) {
301 | $this->emit('error', [ $error, $input ]);
302 | };
303 | }
304 |
305 | }
306 |
--------------------------------------------------------------------------------
/src/Shell.php:
--------------------------------------------------------------------------------
1 | setAutoExit(false);
35 | $application->setCatchExceptions(true);
36 |
37 | $this->application = $application;
38 |
39 | $this->on('error', function ($error) {
40 | $this->console->error($error);
41 | $exitCode = $error instanceof \Exception && $error->getCode() != 0 ? $error->getCode() : 1;
42 | $this->close($exitCode);
43 | });
44 |
45 | $this->once('running', function(Drupe $shell) {
46 | $this->console = $this->readline->console();
47 | $this->console->getOutput()->writeln($this->getHeader());
48 | $shell->setPrompt($this->getPrompt())->prompt();
49 | });
50 |
51 | $this->loop = $loop ?: Factory::create();
52 |
53 | parent::__construct(getenv('HOME').'/.history_'.$application->getName());
54 | }
55 |
56 | /**
57 | * @param float $interval
58 | * @throws \LogicException
59 | */
60 | public function run($interval = 0.001)
61 | {
62 | if ($this->running) {
63 | throw new \LogicException('The shell is already running.');
64 | }
65 |
66 | $this->running = true;
67 |
68 | parent::start($this->loop, $interval);
69 |
70 | $inputHandler = function ($command) {
71 |
72 | $command = (!$command && strlen($command) == 0) ? false : rtrim($command);
73 |
74 | if ('exit' === $command || false === $command) {
75 | $this->close();
76 | return;
77 | } else {
78 | $this->emit('data', [ $command, $this ]);
79 | }
80 |
81 | $ret = $this->application->run(new StringInput($command), $this->console->getOutput());
82 |
83 | if (0 !== $ret) {
84 | $this->console->error([ sprintf('The command terminated with an error status (%s)', $ret), true ]);
85 | } else {
86 | $this->readline->prompt();
87 | }
88 | };
89 |
90 | // Remove parent listener to override emit for 'data'
91 | $this->readline->removeAllListeners('line');
92 | $this->readline->on('line', $inputHandler);
93 |
94 | $this->loop->run();
95 | }
96 |
97 | /**
98 | * Exits the shell.
99 | *
100 | * @param integer $exitCode
101 | * @return $this;
102 | */
103 | public function close($exitCode = 0)
104 | {
105 | if (!$this->running) { return $this; }
106 |
107 | return parent::close($exitCode);
108 | }
109 |
110 | /**
111 | * @return Application
112 | */
113 | public function getApplication()
114 | {
115 | return $this->application;
116 | }
117 |
118 | /**
119 | * @return Console\Output\ConsoleOutputInterface
120 | */
121 | public function getOutput()
122 | {
123 | return $this->console->getOutput();
124 | }
125 |
126 | /**
127 | * Returns the shell header.
128 | *
129 | * @return string The header string
130 | */
131 | protected function getHeader()
132 | {
133 | return <<{$this->application->getName()} shell ({$this->application->getVersion()}).
136 |
137 | At the prompt, type help for some help,
138 | or list to get a list of available commands.
139 |
140 | To exit the shell, type exit or ^D.
141 |
142 | EOF;
143 | }
144 |
145 | /**
146 | * Renders a prompt.
147 | *
148 | * @return string The prompt
149 | */
150 | protected function getPrompt()
151 | {
152 | return $this->application->getName() . ' > ';
153 | }
154 |
155 | }
156 |
--------------------------------------------------------------------------------