├── 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 | ![Demo](http://dl.dropbox.com/u/508607/BorisDemo-v4.gif "Quick Demo") 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 | --------------------------------------------------------------------------------