├── tests
├── requirable.php
├── requirable2.php
├── autoload.php
└── lib
│ ├── Boris
│ ├── ExportInspectorTest.php
│ ├── DumpInspectorTest.php
│ └── ConfigTest.php
│ └── autoloadTest.php
├── .gitignore
├── .travis.yml
├── tests.xml
├── box.json
├── lib
├── Boris
│ ├── ExportInspector.php
│ ├── DumpInspector.php
│ ├── Inspector.php
│ ├── Config.php
│ ├── CLIOptionsHandler.php
│ ├── ReadlineClient.php
│ ├── Boris.php
│ ├── ColoredInspector.php
│ ├── ShallowParser.php
│ └── EvalWorker.php
└── autoload.php
├── bin
└── boris
├── composer.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
└── release.php
/tests/requirable.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tests/requirable2.php:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | *.swp
3 | boris.phar
4 | .token
5 |
6 | # Testing #
7 | ###########
8 | /reports/
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 7.0
4 | - 5.6
5 | - 5.5
6 | - 5.4
7 | - 5.3
8 | - hhvm
9 | script: phpunit --bootstrap tests/autoload.php -c tests.xml
10 |
--------------------------------------------------------------------------------
/tests.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/box.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": ["LICENSE"],
3 | "finder": [{
4 | "name": "*.php",
5 | "exclude": [
6 | "tests"
7 | ],
8 | "in": "lib"
9 | }],
10 | "git-version": "git_tag",
11 | "main": "bin/boris",
12 | "output": "boris.phar",
13 | "stub": true
14 | }
15 |
--------------------------------------------------------------------------------
/lib/Boris/ExportInspector.php:
--------------------------------------------------------------------------------
1 | apply($boris);
14 |
15 | $options = new \Boris\CLIOptionsHandler();
16 | $options->handle($boris);
17 |
18 | $boris->start();
19 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "d11wtq/boris",
3 | "description": "A tiny, but robust REPL (Read-Evaluate-Print-Loop) for PHP.",
4 | "license": "MIT",
5 | "require": {
6 | "php": ">=5.3.0",
7 | "ext-readline": "*",
8 | "ext-pcntl": "*",
9 | "ext-posix": "*"
10 | },
11 | "require-dev": {
12 | "phpunit/phpunit": "4.6.*"
13 | },
14 | "autoload": {
15 | "psr-0": {
16 | "Boris": "lib"
17 | }
18 | },
19 | "bin": ["bin/boris"]
20 | }
21 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | ## Rules
4 |
5 | There are a few basic ground-rules for contributors:
6 |
7 | 1. **No `--force` pushes** or modifying the Git history in any way.
8 | 2. **External API changes and significant modifications** should be subject to a **pull request** to solicit feedback from other contributors.
9 | 3. Use a non-`master` branch for ongoing work.
10 | 4. Adhere to existing code style as much as possible.
11 |
12 | ## Releases
13 |
14 | Declaring formal releases remains the prerogative of the project maintainer.
15 |
--------------------------------------------------------------------------------
/tests/autoload.php:
--------------------------------------------------------------------------------
1 | inspect($variable);
25 | $this->assertEquals(" → '$variable'", $test);
26 |
27 | // random integer
28 | $variable = rand();
29 | $test = $ExportInspector->inspect($variable);
30 | $this->assertEquals(" → $variable", $test);
31 |
32 | // random array
33 | $k = rand();
34 | $v = rand();
35 | $variable = array($k=>$v);
36 | $test = $ExportInspector->inspect($variable);
37 | $this->assertContains("$k", $test);
38 | $this->assertContains("$v", $test);
39 |
40 | // object
41 | $test = $ExportInspector->inspect($ExportInspector);
42 | $this->assertContains(" → Boris\ExportInspector", $test);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/lib/Boris/DumpInspectorTest.php:
--------------------------------------------------------------------------------
1 | inspect($variable);
25 | $this->assertEquals(' → string('.strlen($variable).') "' . $variable . '"', $test);
26 |
27 | // random integer
28 | $variable = rand();
29 | $test = $DumpInspector->inspect($variable);
30 | $this->assertEquals(" → int($variable)", $test);
31 |
32 | // random float
33 | $variable = (float) rand();
34 | $test = $DumpInspector->inspect($variable);
35 | $this->assertEquals(" → float($variable)", $test);
36 |
37 | // random array
38 | $k = rand();
39 | $v = rand();
40 | $variable = array($k=>$v);
41 | $test = $DumpInspector->inspect($variable);
42 | $this->assertContains("$k", $test);
43 | $this->assertContains("$v", $test);
44 |
45 | // object
46 | $test = $DumpInspector->inspect($DumpInspector);
47 | $this->assertContains(" → object(Boris\DumpInspector)", $test);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/lib/autoloadTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('Boris\\'.$class, get_class($test));
34 | }
35 |
36 | // Classes whose constructors require a socket parameter
37 | $classes = array(
38 | 'EvalWorker',
39 | 'ReadlineClient'
40 | );
41 | $pipes = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
42 | $socket = $pipes[1];
43 | foreach ($classes as $class) {
44 | $namespaced_class = '\\Boris\\'.$class;
45 | $test = new $namespaced_class($socket);
46 | $this->assertEquals('Boris\\'.$class, get_class($test));
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/Boris/Config.php:
--------------------------------------------------------------------------------
1 | _cascade = (bool) $cascade;
28 | $this->_searchPaths = is_array($searchPaths) ? $searchPaths : null;
29 |
30 | if (is_null($this->_searchPaths)) {
31 | $this->_searchPaths = array();
32 |
33 | if ($userHome = getenv('HOME')) {
34 | $this->_searchPaths[] = "{$userHome}/.borisrc";
35 | }
36 |
37 | $this->_searchPaths[] = getcwd() . '/.borisrc';
38 | }
39 | }
40 |
41 | /**
42 | * Searches for configuration files in the available
43 | * search paths, and applies them.
44 | *
45 | * Returns true if any configuration files were found.
46 | *
47 | * @param Boris\Boris $boris
48 | * @return bool
49 | */
50 | public function apply(Boris $boris)
51 | {
52 | $applied = false;
53 |
54 | foreach ($this->_searchPaths as $path) {
55 | if (is_readable($path)) {
56 | $this->_loadInIsolation($path, $boris);
57 |
58 | $applied = true;
59 | $this->_files[] = $path;
60 |
61 | if (!$this->_cascade) {
62 | break;
63 | }
64 | }
65 | }
66 |
67 | return $applied;
68 | }
69 |
70 | /**
71 | * Returns an array of files that were loaded
72 | * for this Config
73 | *
74 | * @return array
75 | */
76 | public function loadedFiles()
77 | {
78 | return $this->_files;
79 | }
80 |
81 | // -- Private Methods
82 |
83 | private function _loadInIsolation($path, $boris)
84 | {
85 | require $path;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/lib/Boris/CLIOptionsHandler.php:
--------------------------------------------------------------------------------
1 | $value) {
24 | switch ($option) {
25 | /*
26 | * Sets files to load at startup, may be used multiple times,
27 | * i.e: boris -r test.php,foo/bar.php -r ba/foo.php --require hey.php
28 | */
29 | case 'r':
30 | case 'require':
31 | $this->_handleRequire($boris, $value);
32 | break;
33 |
34 | /*
35 | * Show Usage info
36 | */
37 | case 'h':
38 | case 'help':
39 | $this->_handleUsageInfo();
40 | break;
41 |
42 | /*
43 | * Show version
44 | */
45 | case 'v':
46 | case 'version':
47 | $this->_handleVersion();
48 | break;
49 | }
50 | }
51 | }
52 |
53 | // -- Private Methods
54 |
55 | private function _handleRequire($boris, $paths)
56 | {
57 | $require = array_reduce((array) $paths, function($acc, $v)
58 | {
59 | return array_merge($acc, explode(',', $v));
60 | }, array());
61 |
62 | $boris->onStart(function($worker, $scope) use ($require)
63 | {
64 | foreach ($require as $path) {
65 | require $path;
66 | }
67 |
68 | $worker->setLocal(get_defined_vars());
69 | });
70 | }
71 |
72 | private function _handleUsageInfo()
73 | {
74 | echo << **Announcement:** I'm looking to add one or two additional collaborators with
10 | > commit access. If you are actively involved in open source and have a GitHub
11 | > profile for review, ping me on Twitter (@d11wtq) to express your interest.
12 | > Experienced developers with active GitHub projects only.
13 |
14 | 
15 |
16 | Python has one. Ruby has one. Clojure has one. Now PHP has one, too. Boris is
17 | PHP's missing REPL (read-eval-print loop), allowing developers to experiment
18 | with PHP code in the terminal in an interactive manner. If you make a mistake,
19 | it doesn't matter, Boris will report the error and stand to attention for
20 | further input.
21 |
22 | Everything you enter into Boris is evaluated and the result inspected so you
23 | can understand what is happening. State is maintained between inputs, allowing
24 | you to gradually build up a solution to a problem.
25 |
26 | > __Note:__ The PCNTL function which is required to run Boris is not available on Windows platforms.
27 |
28 | ## Why?
29 |
30 | I'm in the process of transitioning away from PHP to Ruby. I have come to find
31 | PHP's lack of a real REPL to be frustrating and was not able to find an existing
32 | implementation that was complete. Boris weighs in at a few hundred lines of
33 | fairly straightforward code.
34 |
35 |
36 | ## Usage
37 |
38 | Check out our wonderful [wiki] for usage instructions.
39 |
40 |
41 | ## Contributing
42 |
43 | We're committed to a loosely-coupled architecture for Boris and would love to get your contributions.
44 |
45 | Before jumping in, check out our **[Contributing] [contributing]** page on the wiki!
46 |
47 | ## Contributing
48 |
49 | We're using [PHPUnit](https://phpunit.de/) for testing. To run all the tests,
50 |
51 | phpunit --bootstrap tests/autoload.php -c tests.xml
52 |
53 | ## Core Team
54 |
55 | This module was originally developed by [Chris Corbyn](https://github.com/d11wtq), and is now maintained by [Tejas Manohar](https://github.com/tejasmanohar), [Dennis Hotson](https://github.com/dhotson), and [other wonderful contributors](https://github.com/borisrepl/boris/graphs/contributors).
56 |
57 | ## Copyright & Licensing
58 |
59 | See the [LICENSE] file for details.
60 |
61 | [LICENSE]: https://github.com/borisrepl/boris/blob/master/LICENSE
62 | [wiki]: https://github.com/borisrepl/boris/wiki
63 | [contributing]: https://github.com/borisrepl/boris/blob/master/CONTRIBUTING.md
64 | [Chris Corbyn]: https://github.com/borisrepl
65 |
--------------------------------------------------------------------------------
/lib/Boris/ReadlineClient.php:
--------------------------------------------------------------------------------
1 | _socket = $socket;
25 | }
26 |
27 | /**
28 | * Start the client with an prompt and readline history path.
29 | *
30 | * This method never returns.
31 | *
32 | * @param string $prompt
33 | * @param string $historyFile
34 | */
35 | public function start($prompt, $historyFile)
36 | {
37 | readline_read_history($historyFile);
38 |
39 | declare (ticks = 1);
40 | pcntl_signal(SIGCHLD, SIG_IGN);
41 | pcntl_signal(SIGINT, array(
42 | $this,
43 | 'clear'
44 | ), true);
45 |
46 | // wait for the worker to finish executing hooks
47 | if (fread($this->_socket, 1) != EvalWorker::READY) {
48 | throw new \RuntimeException('EvalWorker failed to start');
49 | }
50 |
51 | $parser = new ShallowParser();
52 | $buf = '';
53 | $lineno = 1;
54 |
55 | for (;;) {
56 | $this->_clear = false;
57 | $line = readline(sprintf('[%d] %s', $lineno, ($buf == '' ? $prompt : str_pad('*> ', strlen($prompt), ' ', STR_PAD_LEFT))));
58 |
59 | if ($this->_clear) {
60 | $buf = '';
61 | continue;
62 | }
63 |
64 | if (false === $line) {
65 | $buf = 'exit(0);'; // ctrl-d acts like exit
66 | }
67 |
68 | if (strlen($line) > 0) {
69 | readline_add_history($line);
70 | }
71 |
72 | $buf .= sprintf("%s\n", $line);
73 |
74 | if ($statements = $parser->statements($buf)) {
75 | ++$lineno;
76 |
77 | $buf = '';
78 | foreach ($statements as $stmt) {
79 | if (false === $written = fwrite($this->_socket, $stmt)) {
80 | throw new \RuntimeException('Socket error: failed to write data');
81 | }
82 |
83 | if ($written > 0) {
84 | $status = fread($this->_socket, 1);
85 | if ($status == EvalWorker::EXITED) {
86 | readline_write_history($historyFile);
87 | echo "\n";
88 | exit(0);
89 | } elseif ($status == EvalWorker::FAILED) {
90 | break;
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
98 | /**
99 | * Clear the input buffer.
100 | */
101 | public function clear()
102 | {
103 | // FIXME: I'd love to have this send \r to readline so it puts the user on a blank line
104 | $this->_clear = true;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/release.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 |
6 | *
7 | * Copyright © 2013-2014 Chris Corbyn.
8 | */
9 |
10 | /* Generate releases in Github */
11 |
12 | namespace Boris;
13 |
14 | require __DIR__ . '/lib/autoload.php';
15 |
16 | $args = getopt('hv:', array(
17 | 'help',
18 | 'version:'
19 | ));
20 |
21 | if (count($args) != 1) {
22 | help();
23 | exit(1);
24 | }
25 |
26 | foreach ($args as $opt => $value) {
27 | switch ($opt) {
28 | case 'v':
29 | case 'version':
30 | version($value);
31 | exit(0);
32 |
33 | case 'h':
34 | case 'help':
35 | help();
36 | exit(0);
37 |
38 | default:
39 | unknown($opt);
40 | exit(1);
41 | }
42 | }
43 |
44 | function help()
45 | {
46 | echo <<assertContains('[_searchPaths:Boris\Config:private] => Array', $class);
30 | $this->assertContains('[_cascade:Boris\Config:private]', $class);
31 | $this->assertContains('[_files:Boris\Config:private] => Array', $class);
32 |
33 | // check _searchPaths are initialized properly
34 | $pwd = getcwd() . '/.borisrc';
35 | $this->assertContains('] => ' . $pwd, $class);
36 | if (getenv('HOME')) {
37 | $home = getenv('HOME').'/.borisrc';
38 | $this->assertContains('[0] => ' . $home, $class);
39 | }
40 | }
41 |
42 | /**
43 | * Tests the constructor with parameters.
44 | */
45 | public function test_constructor_params () {
46 | // test of good parameters
47 | $path1 = '/path/to/awesome';
48 | $path2 = '/dev/random';
49 | $Config = new Config(array($path1, $path2), true);
50 |
51 | // since all the class variables are private...
52 | ob_start();
53 | print_r($Config);
54 | $class = ob_get_contents();
55 | ob_end_clean();
56 |
57 | // check class vars exist
58 | $this->assertContains('[_searchPaths:Boris\Config:private] => Array', $class);
59 | $this->assertContains('[0] => ' . $path1, $class);
60 | $this->assertContains('[1] => ' . $path2, $class);
61 | $this->assertContains('[_cascade:Boris\Config:private] => 1', $class);
62 | $this->assertContains('[_files:Boris\Config:private] => Array', $class);
63 |
64 | // test of bad params
65 | $Config = new Config('not an array', 27);
66 |
67 | // since all the class variables are private...
68 | ob_start();
69 | print_r($Config);
70 | $class = ob_get_contents();
71 | ob_end_clean();
72 |
73 | // check class vars exist
74 | $this->assertContains('[_searchPaths:Boris\Config:private] => Array', $class);
75 | $this->assertContains('[_cascade:Boris\Config:private]', $class);
76 | $this->assertContains('[_files:Boris\Config:private] => Array', $class);
77 |
78 | // check _searchPaths are initialized properly
79 | $pwd = getcwd() . '/.borisrc';
80 | $this->assertContains('] => ' . $pwd, $class);
81 | if (getenv('HOME')) {
82 | $home = getenv('HOME').'/.borisrc';
83 | $this->assertContains('[0] => ' . $home, $class);
84 | }
85 | }
86 |
87 | /**
88 | * Tests the apply method.
89 | */
90 | public function test_apply () {
91 | //setup
92 | $test_file_to_apply = getcwd() . '/tests/requirable.php';
93 | $test_file_to_apply2 = getcwd() . '/tests/requirable2.php';
94 | $files = array($test_file_to_apply, $test_file_to_apply2);
95 |
96 | // without cascade
97 | $Config = new Config($files);
98 | $Config->apply();
99 | $this->assertEquals(getenv('REQUIRED_MESSAGE'), 'You successfully required the file!');
100 |
101 | // with cascade
102 | $Config = new Config($files, true);
103 | $Config->apply();
104 | $this->assertEquals(getenv('REQUIRED_MESSAGE'), 'You successfully required the 2nd file!');
105 | }
106 |
107 | /**
108 | * Tests the loadedFiles method.
109 | */
110 | public function test_loadedFiles () {
111 | $test_file_to_apply = getcwd() . '/tests/requirable.php';
112 | $test_file_to_apply2 = getcwd() . '/tests/requirable2.php';
113 | $test_file_to_apply3 ='/this/path/is/fake/and/will/fail';
114 | $files = array($test_file_to_apply, $test_file_to_apply2, $test_file_to_apply3);
115 | $Config = new Config($files, true);
116 | $Config->apply();
117 | $files = $Config->loadedFiles();
118 | $this->assertEquals(2, count($files));
119 | $this->assertEquals($test_file_to_apply, $files[0]);
120 | $this->assertEquals($test_file_to_apply2, $files[1]);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/Boris/Boris.php:
--------------------------------------------------------------------------------
1 | ', $historyFile = null)
26 | {
27 | $this->setPrompt($prompt);
28 | $this->_historyFile = $historyFile ? $historyFile : sprintf('%s/.boris_history', getenv('HOME'));
29 | $this->_inspector = new ColoredInspector();
30 | }
31 |
32 | /**
33 | * Add a new hook to run in the context of the REPL when it starts.
34 | *
35 | * @param mixed $hook
36 | *
37 | * The hook is either a string of PHP code to eval(), or a Closure accepting
38 | * the EvalWorker object as its first argument and the array of defined
39 | * local variables in the second argument.
40 | *
41 | * If the hook is a callback and needs to set any local variables in the
42 | * REPL's scope, it should invoke $worker->setLocal($var_name, $value) to
43 | * do so.
44 | *
45 | * Hooks are guaranteed to run in the order they were added and the state
46 | * set by each hook is available to the next hook (either through global
47 | * resources, such as classes and interfaces, or through the 2nd parameter
48 | * of the callback, if any local variables were set.
49 | *
50 | * @example Contrived example where one hook sets the date and another
51 | * prints it in the REPL.
52 | *
53 | * $boris->onStart(function($worker, $vars){
54 | * $worker->setLocal('date', date('Y-m-d'));
55 | * });
56 | *
57 | * $boris->onStart('echo "The date is $date\n";');
58 | */
59 | public function onStart($hook)
60 | {
61 | $this->_startHooks[] = $hook;
62 | }
63 |
64 | /**
65 | * Add a new hook to run in the context of the REPL when a fatal error occurs.
66 | *
67 | * @param mixed $hook
68 | *
69 | * The hook is either a string of PHP code to eval(), or a Closure accepting
70 | * the EvalWorker object as its first argument and the array of defined
71 | * local variables in the second argument.
72 | *
73 | * If the hook is a callback and needs to set any local variables in the
74 | * REPL's scope, it should invoke $worker->setLocal($var_name, $value) to
75 | * do so.
76 | *
77 | * Hooks are guaranteed to run in the order they were added and the state
78 | * set by each hook is available to the next hook (either through global
79 | * resources, such as classes and interfaces, or through the 2nd parameter
80 | * of the callback, if any local variables were set.
81 | *
82 | * @example An example if your project requires some database connection cleanup:
83 | *
84 | * $boris->onFailure(function($worker, $vars){
85 | * DB::reset();
86 | * });
87 | */
88 | public function onFailure($hook)
89 | {
90 | $this->_failureHooks[] = $hook;
91 | }
92 |
93 | /**
94 | * Set a local variable, or many local variables.
95 | *
96 | * @example Setting a single variable
97 | * $boris->setLocal('user', $bob);
98 | *
99 | * @example Setting many variables at once
100 | * $boris->setLocal(array('user' => $bob, 'appContext' => $appContext));
101 | *
102 | * This method can safely be invoked repeatedly.
103 | *
104 | * @param array|string $local
105 | * @param mixed $value, optional
106 | */
107 | public function setLocal($local, $value = null)
108 | {
109 | if (!is_array($local)) {
110 | $local = array(
111 | $local => $value
112 | );
113 | }
114 |
115 | $this->_exports = array_merge($this->_exports, $local);
116 | }
117 |
118 | /**
119 | * Sets the Boris prompt text
120 | *
121 | * @param string $prompt
122 | */
123 | public function setPrompt($prompt)
124 | {
125 | $this->_prompt = $prompt;
126 | }
127 |
128 | /**
129 | * Set an Inspector object for Boris to output return values with.
130 | *
131 | * @param object $inspector any object the responds to inspect($v)
132 | */
133 | public function setInspector($inspector)
134 | {
135 | $this->_inspector = $inspector;
136 | }
137 |
138 | /**
139 | * Start the REPL (display the readline prompt).
140 | *
141 | * This method never returns.
142 | */
143 | public function start()
144 | {
145 | declare (ticks = 1);
146 | pcntl_signal(SIGINT, SIG_IGN, true);
147 |
148 | if (!$pipes = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP)) {
149 | throw new \RuntimeException('Failed to create socket pair');
150 | }
151 |
152 | $pid = pcntl_fork();
153 |
154 | if ($pid > 0) {
155 | if (function_exists('setproctitle')) {
156 | setproctitle('boris (master)');
157 | }
158 |
159 | fclose($pipes[0]);
160 | $client = new ReadlineClient($pipes[1]);
161 | $client->start($this->_prompt, $this->_historyFile);
162 | } elseif ($pid < 0) {
163 | throw new \RuntimeException('Failed to fork child process');
164 | } else {
165 | if (function_exists('setproctitle')) {
166 | setproctitle('boris (worker)');
167 | }
168 |
169 | fclose($pipes[1]);
170 | $worker = new EvalWorker($pipes[0]);
171 | $worker->setLocal($this->_exports);
172 | $worker->setStartHooks($this->_startHooks);
173 | $worker->setFailureHooks($this->_failureHooks);
174 | $worker->setInspector($this->_inspector);
175 | $worker->start();
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/lib/Boris/ColoredInspector.php:
--------------------------------------------------------------------------------
1 |
5 | * @author Chris Corbyn
6 | *
7 | * Copyright © 2013-2014 Rob Morris.
8 | */
9 |
10 | namespace Boris;
11 |
12 | /**
13 | * Identifies data types in data structures and syntax highlights them.
14 | */
15 | class ColoredInspector implements Inspector
16 | {
17 | static $TERM_COLORS = array('black' => "\033[0;30m", 'white' => "\033[1;37m", 'none' => "\033[1;30m", 'dark_grey' => "\033[1;30m", 'light_grey' => "\033[0;37m", 'dark_red' => "\033[0;31m", 'light_red' => "\033[1;31m", 'dark_green' => "\033[0;32m", 'light_green' => "\033[1;32m", 'dark_yellow' => "\033[0;33m", 'light_yellow' => "\033[1;33m", 'dark_blue' => "\033[0;34m", 'light_blue' => "\033[1;34m", 'dark_purple' => "\033[0;35m", 'light_purple' => "\033[1;35m", 'dark_cyan' => "\033[0;36m", 'light_cyan' => "\033[1;36m");
18 |
19 | private $_fallback;
20 | private $_colorMap = array();
21 |
22 | /**
23 | * Initialize a new ColoredInspector, using $colorMap.
24 | *
25 | * The colors should be an associative array with the keys:
26 | *
27 | * - 'integer'
28 | * - 'float'
29 | * - 'keyword'
30 | * - 'string'
31 | * - 'boolean'
32 | * - 'default'
33 | *
34 | * And the values, one of the following colors:
35 | *
36 | * - 'none'
37 | * - 'black'
38 | * - 'white'
39 | * - 'dark_grey'
40 | * - 'light_grey'
41 | * - 'dark_red'
42 | * - 'light_red'
43 | * - 'dark_green'
44 | * - 'light_green'
45 | * - 'dark_yellow'
46 | * - 'light_yellow'
47 | * - 'dark_blue'
48 | * - 'light_blue'
49 | * - 'dark_purple'
50 | * - 'light_purple'
51 | * - 'dark_cyan'
52 | * - 'light_cyan'
53 | *
54 | * An empty $colorMap array effectively means 'none' for all types.
55 | *
56 | * @param array $colorMap
57 | */
58 | public function __construct($colorMap = null)
59 | {
60 | $this->_fallback = new DumpInspector();
61 |
62 | if (isset($colorMap)) {
63 | $this->_colorMap = $colorMap;
64 | } else {
65 | $this->_colorMap = $this->_defaultColorMap();
66 | }
67 | }
68 |
69 | public function inspect($variable)
70 | {
71 | return preg_replace('/^/m', $this->_colorize('comment', '// '), $this->_dump($variable));
72 | }
73 |
74 | /**
75 | * Returns an associative array of an object's properties.
76 | *
77 | * This method is public so that subclasses may override it.
78 | *
79 | * @param object $value
80 | * @return array
81 | * */
82 | public function objectVars($value)
83 | {
84 | return get_object_vars($value);
85 | }
86 |
87 | // -- Private Methods
88 |
89 | public function _dump($value)
90 | {
91 | $tests = array(
92 | 'is_null' => '_dumpNull',
93 | 'is_string' => '_dumpString',
94 | 'is_bool' => '_dumpBoolean',
95 | 'is_integer' => '_dumpInteger',
96 | 'is_float' => '_dumpFloat',
97 | 'is_array' => '_dumpArray',
98 | 'is_object' => '_dumpObject'
99 | );
100 |
101 | foreach ($tests as $predicate => $outputMethod) {
102 | if (call_user_func($predicate, $value))
103 | return call_user_func(array(
104 | $this,
105 | $outputMethod
106 | ), $value);
107 | }
108 |
109 | return $this->_fallback->inspect($value);
110 | }
111 |
112 | private function _dumpNull($value)
113 | {
114 | return $this->_colorize('keyword', 'NULL');
115 | }
116 |
117 | private function _dumpString($value)
118 | {
119 | return $this->_colorize('string', var_export($value, true));
120 | }
121 |
122 | private function _dumpBoolean($value)
123 | {
124 | return $this->_colorize('bool', var_export($value, true));
125 | }
126 |
127 | private function _dumpInteger($value)
128 | {
129 | return $this->_colorize('integer', var_export($value, true));
130 | }
131 |
132 | private function _dumpFloat($value)
133 | {
134 | return $this->_colorize('float', var_export($value, true));
135 | }
136 |
137 | private function _dumpArray($value)
138 | {
139 | return $this->_dumpStructure('array', $value);
140 | }
141 |
142 | private function _dumpObject($value)
143 | {
144 | return $this->_dumpStructure(sprintf('object(%s)', get_class($value)), $this->objectVars($value));
145 | }
146 |
147 | private function _dumpStructure($type, $value)
148 | {
149 | return $this->_astToString($this->_buildAst($type, $value));
150 | }
151 |
152 | public function _buildAst($type, $value, $seen = array())
153 | {
154 | // FIXME: Improve this AST so it doesn't require access to dump() or colorize()
155 | if ($this->_isSeen($value, $seen)) {
156 | return $this->_colorize('default', '*** RECURSION ***');
157 | } else {
158 | $nextSeen = array_merge($seen, array(
159 | $value
160 | ));
161 | }
162 |
163 | if (is_object($value)) {
164 | $vars = $this->objectVars($value);
165 | } else {
166 | $vars = $value;
167 | }
168 |
169 | $self = $this;
170 |
171 | return array(
172 | 'name' => $this->_colorize('keyword', $type),
173 | 'children' => empty($vars) ? array() : array_combine(array_map(array(
174 | $this,
175 | '_dump'
176 | ), array_keys($vars)), array_map(function($v) use ($self, $nextSeen)
177 | {
178 | if (is_object($v)) {
179 | return $self->_buildAst(sprintf('object(%s)', get_class($v)), $v, $nextSeen);
180 | } elseif (is_array($v)) {
181 | return $self->_buildAst('array', $v, $nextSeen);
182 | } else {
183 | return $self->_dump($v);
184 | }
185 | }, array_values($vars)))
186 | );
187 | }
188 |
189 | public function _astToString($node, $indent = 0)
190 | {
191 | $children = $node['children'];
192 | $self = $this;
193 |
194 | return implode("\n", array(
195 | sprintf('%s(', $node['name']),
196 | implode(",\n", array_map(function($k) use ($self, $children, $indent)
197 | {
198 | if (is_array($children[$k])) {
199 | return sprintf('%s%s => %s', str_repeat(' ', ($indent + 1) * 2), $k, $self->_astToString($children[$k], $indent + 1));
200 | } else {
201 | return sprintf('%s%s => %s', str_repeat(' ', ($indent + 1) * 2), $k, $children[$k]);
202 | }
203 | }, array_keys($children))),
204 | sprintf('%s)', str_repeat(' ', $indent * 2))
205 | ));
206 | }
207 |
208 | private function _defaultColorMap()
209 | {
210 | return array(
211 | 'integer' => 'light_green',
212 | 'float' => 'light_yellow',
213 | 'string' => 'light_red',
214 | 'bool' => 'light_purple',
215 | 'keyword' => 'light_cyan',
216 | 'comment' => 'dark_grey',
217 | 'default' => 'none'
218 | );
219 | }
220 |
221 | private function _colorize($type, $value)
222 | {
223 | if (!empty($this->_colorMap[$type])) {
224 | $colorName = $this->_colorMap[$type];
225 | } else {
226 | $colorName = $this->_colorMap['default'];
227 | }
228 |
229 | return sprintf("%s%s\033[0m", static::$TERM_COLORS[$colorName], $value);
230 | }
231 |
232 | private function _isSeen($value, $seen)
233 | {
234 | foreach ($seen as $v) {
235 | if ($v === $value)
236 | return true;
237 | }
238 |
239 | return false;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/lib/Boris/ShallowParser.php:
--------------------------------------------------------------------------------
1 | ')', '{' => '}', '[' => ']', '"' => '"', "'" => "'", '//' => "\n", '#' => "\n", '/*' => '*/', '<<<' => '_heredoc_special_case_');
11 |
12 | private $_initials;
13 |
14 | public function __construct()
15 | {
16 | $this->_initials = '/^(' . implode('|', array_map(array(
17 | $this,
18 | 'quote'
19 | ), array_keys($this->_pairs))) . ')/';
20 | }
21 |
22 | /**
23 | * Break the $buffer into chunks, with one for each highest-level construct possible.
24 | *
25 | * If the buffer is incomplete, returns an empty array.
26 | *
27 | * @param string $buffer
28 | *
29 | * @return array
30 | */
31 | public function statements($buffer)
32 | {
33 | $result = $this->_createResult($buffer);
34 |
35 | while (strlen($result->buffer) > 0) {
36 | $this->_resetResult($result);
37 |
38 | if ($result->state == '<<<') {
39 | if (!$this->_initializeHeredoc($result)) {
40 | continue;
41 | }
42 | }
43 |
44 | $rules = array(
45 | '_scanUse',
46 | '_scanEscapedChar',
47 | '_scanRegion',
48 | '_scanStateEntrant',
49 | '_scanWsp',
50 | '_scanChar'
51 | );
52 |
53 | foreach ($rules as $method) {
54 | if ($this->$method($result)) {
55 | break;
56 | }
57 | }
58 |
59 | if ($result->stop) {
60 | break;
61 | }
62 | }
63 |
64 | if (!empty($result->statements) && trim($result->stmt) === '' && strlen($result->buffer) == 0) {
65 | $this->_combineStatements($result);
66 | $this->_prepareForDebug($result);
67 | return $result->statements;
68 | }
69 | }
70 |
71 | public function quote($token)
72 | {
73 | return preg_quote($token, '/');
74 | }
75 |
76 | // -- Private Methods
77 |
78 | private function _createResult($buffer)
79 | {
80 | $result = new \stdClass();
81 | $result->buffer = $buffer;
82 | $result->stmt = '';
83 | $result->state = null;
84 | $result->states = array();
85 | $result->statements = array();
86 | $result->stop = false;
87 |
88 | return $result;
89 | }
90 |
91 | private function _resetResult($result)
92 | {
93 | $result->stop = false;
94 | $result->state = end($result->states);
95 | $result->terminator = $result->state ? '/^(.*?' . preg_quote($this->_pairs[$result->state], '/') . ')/s' : null;
96 | }
97 |
98 | private function _combineStatements($result)
99 | {
100 | $combined = array();
101 |
102 | foreach ($result->statements as $scope) {
103 | if (trim($scope) == ';' || substr(trim($scope), -1) != ';') {
104 | $combined[] = ((string) array_pop($combined)) . $scope;
105 | } else {
106 | $combined[] = $scope;
107 | }
108 | }
109 |
110 | $result->statements = $combined;
111 | }
112 |
113 | private function _prepareForDebug($result)
114 | {
115 | $result->statements[] = $this->_prepareDebugStmt(array_pop($result->statements));
116 | }
117 |
118 | private function _initializeHeredoc($result)
119 | {
120 | if (preg_match('/^([\'"]?)([a-z_][a-z0-9_]*)\\1/i', $result->buffer, $match)) {
121 | $docId = $match[2];
122 | $result->stmt .= $match[0];
123 | $result->buffer = substr($result->buffer, strlen($match[0]));
124 |
125 | $result->terminator = '/^(.*?\n' . $docId . ');?\n/s';
126 |
127 | return true;
128 | } else {
129 | return false;
130 | }
131 | }
132 |
133 | private function _scanWsp($result)
134 | {
135 | if (preg_match('/^\s+/', $result->buffer, $match)) {
136 | if (!empty($result->statements) && $result->stmt === '') {
137 | $result->statements[] = array_pop($result->statements) . $match[0];
138 | } else {
139 | $result->stmt .= $match[0];
140 | }
141 | $result->buffer = substr($result->buffer, strlen($match[0]));
142 |
143 | return true;
144 | } else {
145 | return false;
146 | }
147 | }
148 |
149 | private function _scanEscapedChar($result)
150 | {
151 | if (($result->state == '"' || $result->state == "'") && preg_match('/^[^' . $result->state . ']*?\\\\./s', $result->buffer, $match)) {
152 |
153 | $result->stmt .= $match[0];
154 | $result->buffer = substr($result->buffer, strlen($match[0]));
155 |
156 | return true;
157 | } else {
158 | return false;
159 | }
160 | }
161 |
162 | private function _scanRegion($result)
163 | {
164 | if (in_array($result->state, array(
165 | '"',
166 | "'",
167 | '<<<',
168 | '//',
169 | '#',
170 | '/*'
171 | ))) {
172 | if (preg_match($result->terminator, $result->buffer, $match)) {
173 | $result->stmt .= $match[1];
174 | $result->buffer = substr($result->buffer, strlen($match[1]));
175 | array_pop($result->states);
176 | } else {
177 | $result->stop = true;
178 | }
179 |
180 | return true;
181 | } else {
182 | return false;
183 | }
184 | }
185 |
186 | private function _scanStateEntrant($result)
187 | {
188 | if (preg_match($this->_initials, $result->buffer, $match)) {
189 | $result->stmt .= $match[0];
190 | $result->buffer = substr($result->buffer, strlen($match[0]));
191 | $result->states[] = $match[0];
192 |
193 | return true;
194 | } else {
195 | return false;
196 | }
197 | }
198 |
199 | private function _scanChar($result)
200 | {
201 | $chr = substr($result->buffer, 0, 1);
202 | $result->stmt .= $chr;
203 | $result->buffer = substr($result->buffer, 1);
204 | if ($result->state && $chr == $this->_pairs[$result->state]) {
205 | array_pop($result->states);
206 | }
207 |
208 | if (empty($result->states) && ($chr == ';' || $chr == '}')) {
209 | if (!$this->_isLambda($result->stmt) || $chr == ';') {
210 | $result->statements[] = $result->stmt;
211 | $result->stmt = '';
212 | }
213 | }
214 |
215 | return true;
216 | }
217 |
218 | private function _scanUse($result)
219 | {
220 | if (preg_match("/^use (.+?);/", $result->buffer, $use)) {
221 | $result->buffer = substr($result->buffer, strlen($use[0]));
222 | if (strpos($use[0], ' as ') !== false) {
223 | list($class, $alias) = explode(' as ', $use[1]);
224 | } else {
225 | $class = $use[1];
226 | $alias = substr($use[1], strrpos($use[1], '\\') + 1);
227 | }
228 | $result->statements[] = sprintf("class_alias('%s', '%s');", $class, $alias);
229 | return true;
230 | } else {
231 | return false;
232 | }
233 | }
234 |
235 | private function _isLambda($input)
236 | {
237 | return preg_match('/^([^=]*?=\s*)?function\s*\([^\)]*\)\s*(use\s*\([^\)]*\)\s*)?\s*\{.*\}\s*;?$/is', trim($input));
238 | }
239 |
240 | private function _isReturnable($input)
241 | {
242 | $input = trim($input);
243 | if (substr($input, -1) == ';' && substr($input, 0, 1) != '{') {
244 | return $this->_isLambda($input) || !preg_match('/^(' . 'echo|print|exit|die|goto|global|include|include_once|require|require_once|list|' . 'return|do|for|foreach|while|if|function|namespace|class|interface|abstract|switch|' . 'declare|throw|try|unset' . ')\b/i', $input);
245 | } else {
246 | return false;
247 | }
248 | }
249 |
250 | private function _prepareDebugStmt($input)
251 | {
252 | if ($this->_isReturnable($input) && !preg_match('/^\s*return/i', $input)) {
253 | $input = sprintf('return %s', $input);
254 | }
255 |
256 | return $input;
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/lib/Boris/EvalWorker.php:
--------------------------------------------------------------------------------
1 | _socket = $socket;
34 | $this->_inspector = new DumpInspector();
35 | stream_set_blocking($socket, 0);
36 | }
37 |
38 | /**
39 | * Set local variables to be placed in the workers's scope.
40 | *
41 | * @param array|string $local
42 | * @param mixed $value, if $local is a string
43 | */
44 | public function setLocal($local, $value = null)
45 | {
46 | if (!is_array($local)) {
47 | $local = array(
48 | $local => $value
49 | );
50 | }
51 |
52 | $this->_exports = array_merge($this->_exports, $local);
53 | }
54 |
55 | /**
56 | * Set hooks to run inside the worker before it starts looping.
57 | *
58 | * @param array $hooks
59 | */
60 | public function setStartHooks($hooks)
61 | {
62 | $this->_startHooks = $hooks;
63 | }
64 |
65 | /**
66 | * Set hooks to run inside the worker after a fatal error is caught.
67 | *
68 | * @param array $hooks
69 | */
70 | public function setFailureHooks($hooks)
71 | {
72 | $this->_failureHooks = $hooks;
73 | }
74 |
75 | /**
76 | * Set an Inspector object for Boris to output return values with.
77 | *
78 | * @param object $inspector any object the responds to inspect($v)
79 | */
80 | public function setInspector($inspector)
81 | {
82 | $this->_inspector = $inspector;
83 | }
84 |
85 | /**
86 | * Start the worker.
87 | *
88 | * This method never returns.
89 | */
90 | public function start()
91 | {
92 | $__scope = $this->_runHooks($this->_startHooks);
93 | extract($__scope);
94 |
95 | $this->_write($this->_socket, self::READY);
96 |
97 | /* Note the naming of the local variables due to shared scope with the user here */
98 | for (;;) {
99 | declare (ticks = 1);
100 | // don't exit on ctrl-c
101 | pcntl_signal(SIGINT, SIG_IGN, true);
102 |
103 | $this->_cancelled = false;
104 |
105 | $__input = $this->_transform($this->_read($this->_socket));
106 |
107 | if ($__input === null) {
108 | continue;
109 | }
110 |
111 | $__response = self::DONE;
112 |
113 | $this->_ppid = posix_getpid();
114 | $this->_pid = pcntl_fork();
115 |
116 | if ($this->_pid < 0) {
117 | throw new \RuntimeException('Failed to fork child labourer');
118 | } elseif ($this->_pid > 0) {
119 | // kill the child on ctrl-c
120 | pcntl_signal(SIGINT, array(
121 | $this,
122 | 'cancelOperation'
123 | ), true);
124 | pcntl_waitpid($this->_pid, $__status);
125 |
126 | if (!$this->_cancelled && $__status != (self::ABNORMAL_EXIT << 8)) {
127 | $__response = self::EXITED;
128 | } else {
129 | $this->_runHooks($this->_failureHooks);
130 | $__response = self::FAILED;
131 | }
132 | } else {
133 | // if the user has installed a custom exception handler, install a new
134 | // one which calls it and then (if the custom handler didn't already exit)
135 | // exits with the correct status.
136 | // If not, leave the exception handler unset; we'll display
137 | // an uncaught exception error and carry on.
138 | $__oldexh = set_exception_handler(array(
139 | $this,
140 | 'delegateExceptionHandler'
141 | ));
142 | if ($__oldexh && !$this->_userExceptionHandler) {
143 | $this->_userExceptionHandler = $__oldexh; // save the old handler (once)
144 | } else {
145 | restore_exception_handler();
146 | }
147 |
148 | // undo ctrl-c signal handling ready for user code execution
149 | pcntl_signal(SIGINT, SIG_DFL, true);
150 | $__pid = posix_getpid();
151 |
152 | $__result = eval($__input);
153 |
154 | if (posix_getpid() != $__pid) {
155 | // whatever the user entered caused a forked child
156 | // (totally valid, but we don't want that child to loop and wait for input)
157 | exit(0);
158 | }
159 |
160 | if (preg_match('/\s*return\b/i', $__input)) {
161 | fwrite(STDOUT, sprintf("%s\n", $this->_inspector->inspect($__result)));
162 | }
163 | $this->_expungeOldWorker();
164 | }
165 |
166 | $this->_write($this->_socket, $__response);
167 |
168 | if ($__response == self::EXITED) {
169 | exit(0);
170 | }
171 | }
172 | }
173 |
174 | /**
175 | * While a child process is running, terminate it immediately.
176 | */
177 | public function cancelOperation()
178 | {
179 | printf("Cancelling...\n");
180 | $this->_cancelled = true;
181 | posix_kill($this->_pid, SIGKILL);
182 | pcntl_signal_dispatch();
183 | }
184 |
185 | /**
186 | * Call the user-defined exception handler, then exit correctly.
187 | */
188 | public function delegateExceptionHandler($ex)
189 | {
190 | call_user_func($this->_userExceptionHandler, $ex);
191 | exit(self::ABNORMAL_EXIT);
192 | }
193 |
194 | // -- Private Methods
195 |
196 | private function _runHooks($hooks)
197 | {
198 | extract($this->_exports);
199 |
200 | foreach ($hooks as $__hook) {
201 | if (is_string($__hook)) {
202 | eval($__hook);
203 | } elseif (is_callable($__hook)) {
204 | call_user_func($__hook, $this, get_defined_vars());
205 | } else {
206 | throw new \RuntimeException(sprintf('Hooks must be closures or strings of PHP code. Got [%s].', gettype($__hook)));
207 | }
208 |
209 | // hooks may set locals
210 | extract($this->_exports);
211 | }
212 |
213 | return get_defined_vars();
214 | }
215 |
216 | private function _expungeOldWorker()
217 | {
218 | posix_kill($this->_ppid, SIGTERM);
219 | pcntl_signal_dispatch();
220 | }
221 |
222 | private function _write($socket, $data)
223 | {
224 | if (!fwrite($socket, $data)) {
225 | throw new \RuntimeException('Socket error: failed to write data');
226 | }
227 | }
228 |
229 | private function _read($socket)
230 | {
231 | $read = array(
232 | $socket
233 | );
234 | $except = array(
235 | $socket
236 | );
237 |
238 | if ($this->_select($read, $except) > 0) {
239 | if ($read) {
240 | return stream_get_contents($read[0]);
241 | } else if ($except) {
242 | throw new \UnexpectedValueException("Socket error: closed");
243 | }
244 | }
245 | }
246 |
247 | private function _select(&$read, &$except)
248 | {
249 | $write = null;
250 | set_error_handler(function()
251 | {
252 | return true;
253 | }, E_WARNING);
254 | $result = stream_select($read, $write, $except, 10);
255 | restore_error_handler();
256 | return $result;
257 | }
258 |
259 | private function _transform($input)
260 | {
261 | if ($input === null) {
262 | return null;
263 | }
264 |
265 | $transforms = array(
266 | 'exit' => 'exit(0)'
267 | );
268 |
269 | foreach ($transforms as $from => $to) {
270 | $input = preg_replace('/^\s*' . preg_quote($from, '/') . '\s*;?\s*$/', $to . ';', $input);
271 | }
272 |
273 | return $input;
274 | }
275 | }
276 |
--------------------------------------------------------------------------------