From 9cbe7e75f902a2a4d76647561c185d3880931106 Mon Sep 17 00:00:00 2001 From: dana Date: Sat, 2 Jun 2018 01:47:11 -0500 Subject: [PATCH] Significantly rework application: 0.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Get rid of Symfony Console application/command/input components These were causing me serious problems — not least of all was the fact that the Console argument parser chokes on command lines lke `twigc -j - foo.twig`. I am not happy with the way i've structured this, but i just needed to get it *done*, so here we are. If someone has any advice on to how to make this nicer (maybe break it up into different classes, &c.), i would be appreciative - Add GetOpt.php for argument parsing and usage-help printing The usage help is really ugly, tbh. But whatever - Support multiple input data sources at once - Add `-E` short option for `--env` - Display an error when `-E` is used without the appropriate `variables_order` configuration in place - Add shell (`sh`) auto-escape method - Simplify/eliminate a lot of code (handling of version numbers, validation, &c.) - Change a lot of white space and formatting stuff (RIP `blame`) --- bin/compile | 16 +- bin/twigc | 9 +- src/Twigc/Application.php | 546 +++++++++++++++++++++++++---- src/Twigc/ComposerHelper.php | 88 +++++ src/Twigc/DefaultCommand.php | 438 ------------------------ src/Twigc/PharCompiler.php | 645 +++++++++++++++-------------------- src/Twigc/Twigc.php | 88 ----- src/bootstrap.php | 75 ++-- 8 files changed, 891 insertions(+), 1014 deletions(-) mode change 100644 => 100755 bin/compile mode change 100644 => 100755 bin/twigc create mode 100644 src/Twigc/ComposerHelper.php delete mode 100644 src/Twigc/DefaultCommand.php delete mode 100644 src/Twigc/Twigc.php diff --git a/bin/compile b/bin/compile old mode 100644 new mode 100755 index 6d4ec34..a91d5ea --- a/bin/compile +++ b/bin/compile @@ -4,21 +4,23 @@ /** * This file is part of twigc. * - * @author dana geier + * @author dana * @license MIT */ require_once __DIR__ . '/../src/bootstrap.php'; +use Symfony\Component\Console\Output\ConsoleOutput; +use Dana\Twigc\PharCompiler; + $verbose = false; $verboseArgs = ['v', 'vv', 'vvv', 'verbose', 'debug']; foreach ( $argv as $arg ) { - if ( in_array(ltrim($arg, '-'), $verboseArgs, true) ) { - $verbose = true; - break; - } + if ( in_array(ltrim($arg, '-'), $verboseArgs, true) ) { + $verbose = true; + break; + } } -(new \Dana\Twigc\PharCompiler($verbose))->compile(); - +(new PharCompiler(new ConsoleOutput(), $verbose))->compile('twigc.phar'); diff --git a/bin/twigc b/bin/twigc old mode 100644 new mode 100755 index 13e4af9..b785a16 --- a/bin/twigc +++ b/bin/twigc @@ -4,11 +4,16 @@ /** * This file is part of twigc. * - * @author dana geier + * @author dana * @license MIT */ require_once __DIR__ . '/../src/bootstrap.php'; -(new \Dana\Twigc\Application())->run(); +use Symfony\Component\Console\Output\ConsoleOutput; +use Dana\Twigc\Application; +$output = new ConsoleOutput(); +$app = new Application(); + +exit($app->run($output)); diff --git a/src/Twigc/Application.php b/src/Twigc/Application.php index ae1d898..dc5e025 100644 --- a/src/Twigc/Application.php +++ b/src/Twigc/Application.php @@ -3,97 +3,495 @@ /** * This file is part of twigc. * - * @author dana geier + * @author dana * @license MIT */ namespace Dana\Twigc; -use Symfony\Component\Console\Application as BaseApplication; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +use GetOpt\{Argument,GetOpt,Operand,Option}; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Output\{ConsoleOutputInterface,OutputInterface}; +use Twig\Environment; +use Twig\Loader\{ArrayLoader,FilesystemLoader}; + +use Dana\Twigc\ComposerHelper; /** - * twigc application container. + * This class represents the entire `twigc` application. * - * This class overrides a bunch of the default Console behaviour to make the - * application work like a more traditional UNIX CLI tool. + * To be completely honest, this feels really shitty to me, and i don't like it. + * But after eliminating the standard Symfony\Console structure (due to + * Console's woefully inadequate argument handling, amongst other things), i + * find myself unsure of the best way to structure this, especially given how + * simple the application is, and just kind of want to be done with it. I guess + * this works for now, but i would welcome any improvements. */ -class Application extends BaseApplication { - /** - * {@inheritdoc} - */ - public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { - parent::__construct('twigc', \Dana\Twigc\Twigc::VERSION_NUMBER); - } +class Application { + const NAME = 'twigc'; + const VERSION = '0.3.0'; + const BUILD_DATE = '%BUILD_DATE%'; // Replaced during build - /** - * {@inheritdoc} - * - * In a normal Console application, this method handles the --version and - * --help options. In our application, the default command handles all of - * that. - */ - public function doRun(InputInterface $input, OutputInterface $output) { - $name = $this->getCommandName($input); + protected $name; + protected $version; - if ( ! $name ) { - $name = $this->defaultCommand; - $input = new ArrayInput(['command' => $this->defaultCommand]); - } + /** + * Construct the object. + * + * @param string|null $name + * (optional) The name of the application, to be used in error messages and + * the like. + * + * @param string|null $version + * (optional) The version number of the application, to be used in the + * `--version` output. + * + * @return self + */ + public function __construct(string $name = null, string $version = null) { + $this->name = $name ?? static::NAME; + $this->version = $version ?? static::VERSION; + } - $command = $this->find($name); + /** + * Run the application. + * + * This is mostly a wrapper around doRun() to handle error printing. + * + * @param OutputInterface $output + * The output to write to. + * + * @param array|null $argv + * (optional) Command-line arguments to the application (with the 0th member + * as the application name). + * + * @return int + */ + public function run(OutputInterface $output, array $argv = null): int { + if ( $output instanceof ConsoleOutputInterface ) { + $error = $output->getErrorOutput(); + } else { + $error = $output; + } - $this->runningCommand = $command; - $exitCode = $this->doRunCommand($command, $input, $output); - $this->runningCommand = null; + try { + return $this->doRun($output, $argv); + } catch ( \Exception $e ) { + $error->writeln(sprintf( + '%s: %s', $this->name, + rtrim($e->getMessage(), "\r\n")) + ); + return 1; + } + } - return $exitCode; - } + /** + * Run the application (for real). + * + * @param OutputInterface $output + * The output to write to. + * + * @param array|null $argv + * (optional) Command-line arguments to the application (with the 0th member + * as the application name). + * + * @return int + */ + public function doRun(OutputInterface $output, array $argv = null): int { + $argv = $argv ?? $_SERVER['argv']; + $getopt = $this->getGetOpt(); - /** - * {@inheritdoc} - */ - public function getDefinition() { - $definition = parent::getDefinition(); - $definition->setArguments(); - return $definition; - } + $getopt->process(array_slice($argv, 1)); - /** - * {@inheritdoc} - * - * Since we're a one-command application, we always use the name of the - * default command. - */ - protected function getCommandName(InputInterface $input) { - return $this->getDefaultCommands()[0]->getName(); - } + if ( $getopt->getOption('help') ) { + $this->doHelp($output, $getopt); + return 0; + } + if ( $getopt->getOption('version') ) { + $this->doVersion($output); + return 0; + } + if ( $getopt->getOption('credits') ) { + $this->doCredits($output); + return 0; + } - /** - * {@inheritdoc} - * - * Since we're a one-command application, we always use the definition of - * the default command. This means that none of the built-in Console options - * like --help and --ansi are automatically defined — the default command - * must handle all of that. - */ - protected function getDefaultInputDefinition() { - return $this->getDefaultCommands()[0]->getDefinition(); - } + $inputData = []; + $template = $getopt->getOperand('template'); + $dirs = $getopt->getOption('dir'); + $temp = false; - /** - * {@inheritdoc} - * - * Since we're a one-command application, we always return just the default - * command. - */ - protected function getDefaultCommands() { - return [new \Dana\Twigc\DefaultCommand()]; - } + // If we're receiving data on standard input, and we didn't get a template, + // assume `-` — we'll make sure this doesn't conflict with `-j` below + if ( ! posix_isatty(\STDIN) ) { + $template = $template ?? '-'; + } + // Add the template's parent directory if we're not using standard input + if ( ($template ?? '-') !== '-' ) { + $dirs = array_merge([dirname($template)], $dirs); + } + + if ( $template === null ) { + $this->doHelp($output, $getopt, 'No template specified'); + return 1; + } + + // Input data via environment + if ( $getopt->getOption('env') ) { + if ( empty($_ENV) && strpos(ini_get('variables_order'), 'E') === false ) { + throw new \RuntimeException( + "INI setting 'variables_order' must include 'E' to use option 'env'" + ); + } + $inputData = array_merge($inputData, $_ENV); + } + + // Input data via query string + foreach ( $getopt->getOption('query') as $query ) { + $query = ltrim($query, '?'); + $parsed = []; + + parse_str($query, $parsed); + + $inputData = array_merge($inputData, $parsed); + } + + // Input data via JSON + foreach ( $getopt->getOption('json') as $json ) { + // JSON supplied via standard input + if ( $json === '-' ) { + if ( $template === '-' ) { + throw new \InvalidArgumentException( + 'Can not read both template and JSON input from stdin' + ); + } + + $json = file_get_contents('php://stdin'); + + // JSON supplied via file + } elseif ( (ltrim($json)[0] ?? '') !== '{' ) { + if ( ! file_exists($json) || is_dir($json) ) { + throw new \InvalidArgumentException( + "Missing or invalid JSON file: ${json}" + ); + } + $json = file_get_contents($json); + } + + // This check is here to prevent errors if the input is just empty + if ( trim($json) !== '' ) { + $json = json_decode($json, true); + } + + if ( ! is_array($json) ) { + throw new \InvalidArgumentException( + 'JSON input must be a dictionary' + ); + } + + $inputData = array_merge($inputData, $json); + } + + // Input data via key=value pair + foreach ( $getopt->getOption('pair') as $pair ) { + $kv = explode('=', $pair, 2); + + if ( count($kv) !== 2 ) { + throw new \InvalidArgumentException( + "Illegal key=value pair: ${pair}" + ); + } + + $inputData[$kv[0]] = $kv[1]; + } + + // Template supplied via file path + if ( $template !== '-' ) { + $loader = new FilesystemLoader($dirs); + // Template supplied via standard input + } else { + // If we've been supplied one or more search directories, we'll need to + // write the template out to a temp directory so we can use the file- + // system loader + if ( $dirs ) { + $temp = true; + $template = implode('/', [ + sys_get_temp_dir(), + implode('.', ['', $this->name, getmypid(), md5(time())]), + $template, + ]); + + mkdir(dirname($template)); + file_put_contents($template, file_get_contents('php://stdin'), \LOCK_EX); + + $dirs = array_merge([dirname($template)], $dirs); + $loader = new FilesystemLoader($dirs); + + // Otherwise, we can just use the array loader, which is a little faster + // and cleaner + } else { + $loader = new ArrayLoader([ + $template => file_get_contents('php://stdin'), + ]); + } + } + + // Render + try { + $twig = new Environment($loader, [ + 'cache' => $getopt->getOption('cache') ?? false, + 'debug' => false, + 'strict_variables' => (bool) $getopt->getOption('strict'), + 'autoescape' => $this->getEscaper( + $getopt->getOption('escape'), + $template + ), + ]); + + $twig->getExtension('Twig_Extension_Core')->setEscaper( + 'json', + function($twigEnv, $string, $charset) { + return json_encode( + $string, + \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE + ); + } + ); + $twig->getExtension('Twig_Extension_Core')->setEscaper( + 'sh', + function($twigEnv, $string, $charset) { + return '"' . addcslashes($string, '$`\\"') . '"'; + } + ); + + $output->writeln( + rtrim($twig->render(basename($template), $inputData), "\r\n") + ); + + // Clean up + } finally { + if ( $temp ) { + unlink($template); + rmdir(dirname($template)); + } + } + + return 0; + } + + /** + * Display the application's usage help. + * + * @param OutputInterface $output + * The output to write to. + * + * @param GetOpt $getopt + * The GetOpt instance from which to derive the usage help. + * + * @param string|null $message + * (optional) An additional message to print above the usage help. This is + * intended primarily for error messages. + * + * @return int + */ + public function doHelp( + OutputInterface $output, + GetOpt $getopt, + string $message = null + ): int { + if ( $message !== null && $message !== '' ) { + $output->writeln("{$this->name}: " . rtrim($message, "\r\n") . "\n"); + } + $output->writeln(rtrim($getopt->getHelpText(), "\r\n")); + return 0; + } + + /** + * Display the application's version information. + * + * @param OutputInterface $output The output to write to. + * + * @return int + */ + public function doVersion(OutputInterface $output): int { + $version = sprintf('%s version %s', $this->name, $this->version); + + if ( strpos(static::BUILD_DATE, '%') === false ) { + $version .= sprintf(' (built %s)', static::BUILD_DATE); + } + + $output->writeln($version); + return 0; + } + + /** + * Display the application's dependency information. + * + * @param OutputInterface $output The output to write to. + * + * @return int + */ + public function doCredits(OutputInterface $output): int { + $packages = (new ComposerHelper())->getPackages(); + + $table = new Table($output); + $table->setStyle('compact'); + $table->getStyle()->setVerticalBorderChar(''); + $table->getStyle()->setCellRowContentFormat('%s '); + $table->setHeaders(['name', 'version', 'licence']); + + foreach ( $packages as $package ) { + $table->addRow([ + $package->name, + ltrim($package->version, 'v'), + implode(', ', $package->license) ?: '?', + ]); + } + $table->render(); + + return 0; + } + + /** + * Get an instance of GetOpt configured for this application. + * + * @return GetOpt + */ + public function getGetOpt(): GetOpt { + $getopt = new GetOpt(null, [ + GetOpt::SETTING_SCRIPT_NAME => $this->name, + GetOpt::SETTING_STRICT_OPERANDS => true, + ]); + $getopt->addOptions([ + Option::create('h', 'help', GetOpt::NO_ARGUMENT) + ->setDescription('Display this usage help and exit') + , + Option::create('V', 'version', GetOpt::NO_ARGUMENT) + ->setDescription('Display version information and exit') + , + Option::create(null, 'credits', GetOpt::NO_ARGUMENT) + ->setDescription('Display dependency information and exit') + , + Option::create(null, 'cache', GetOpt::REQUIRED_ARGUMENT) + ->setDescription('Enable caching to specified directory') + ->setArgumentName('dir') + ->setValidation('is_dir') + , + Option::create('d', 'dir', GetOpt::MULTIPLE_ARGUMENT) + ->setDescription('Add specified search directory to loader') + ->setArgumentName('dir') + ->setValidation('is_dir') + , + Option::create('e', 'escape', GetOpt::REQUIRED_ARGUMENT) + ->setArgumentName('strategy') + ->setDescription('Specify default auto-escaping strategy') + , + Option::create('E', 'env', GetOpt::NO_ARGUMENT) + ->setDescription('Derive input data from environment') + , + Option::create('j', 'json', GetOpt::MULTIPLE_ARGUMENT) + ->setArgumentName('dict/file') + ->setDescription('Derive input data from specified JSON file or dictionary string') + , + Option::create('p', 'pair', GetOpt::MULTIPLE_ARGUMENT) + ->setArgumentName('input') + ->setDescription('Derive input data from specified key=value pair') + , + Option::create(null, 'query', GetOpt::MULTIPLE_ARGUMENT) + ->setArgumentName('input') + ->setDescription('Derive input data from specified URL query string') + , + Option::create('s', 'strict', GetOpt::NO_ARGUMENT) + ->setDescription('Throw exception when undefined variable is referenced') + , + ]); + $getopt->addOperands([ + Operand::create('template', Operand::OPTIONAL), + ]); + + return $getopt; + } + + /** + * Get the correct Twig escape method given the provided options. + * + * @param string|null $escape + * The user-provided escape option, or null if not provided. + * + * @param string|null $template + * The name/path of the template file, or null if not provided. This is only + * used when $escape is null or 'auto'. + * + * @return string|false + */ + public function getEscaper($escape, string $template = null) { + $escape = $escape === null ? $escape : strtolower($escape); + $template = $template ?? ''; + + if ( $escape === null || $escape === 'auto' ) { + if ( + substr($template, -5) === '.twig' + && + strpos(substr($template, 0, -5), '.') + ) { + $ext = pathinfo(substr($template, 0, -5), \PATHINFO_EXTENSION); + } else { + $ext = pathinfo($template, \PATHINFO_EXTENSION); + } + + switch ( strtolower($ext) ) { + case 'htm': + case 'html': + case 'phtml': + case 'thtml': + case 'xhtml': + case 'template': + case 'tmpl': + case 'tpl': + return 'html'; + case 'css': + case 'scss': + return 'css'; + case 'js': + return 'js'; + case 'json': + return 'json'; + case 'bash': + case 'ksh': + case 'sh': + case 'zsh': + return 'sh'; + } + + return false; + } + + // Otherwise, try to parse the supplied method + switch ( $escape ) { + case 'f': + case 'n': + case 'none': + case 'never': + $escape = 'false'; + break; + case 't': + case 'y': + case 'always': + $escape = 'true'; + break; + } + + $bool = filter_var( + $escape, + \FILTER_VALIDATE_BOOLEAN, + \FILTER_NULL_ON_FAILURE + ); + + if ( $bool !== null ) { + $escape = $bool ? 'html' : false; + } + + return $escape; + } } - diff --git a/src/Twigc/ComposerHelper.php b/src/Twigc/ComposerHelper.php new file mode 100644 index 0000000..5bce15b --- /dev/null +++ b/src/Twigc/ComposerHelper.php @@ -0,0 +1,88 @@ + + * @license MIT + */ + +namespace Dana\Twigc; + +/** + * Helper class with various functions for interacting with Composer's lock + * file. + */ +class ComposerHelper { + /** + * Get an array of the installed non-dev packages listed in a Composer lock + * file. + * + * @param string|null (optional) The path to the lock file to parse. + * + * @return object[] + */ + public function getPackages(string $lockFile = null) { + $packages = $this->parseLockFile($lockFile)['packages']; + return $this->massagePackages($packages); + } + + /** + * Get an array of the installed dev packages listed in a Composer lock file. + * + * @param string|null (optional) The path to the lock file to parse. + * + * @return object[] + */ + public function getDevPackages(string $lockFile = null) { + $packages = $this->parseLockFile($lockFile)['packages-dev']; + return $this->massagePackages($packages); + } + + /** + * Get an array of data representing a Composer lock file. + * + * @param string $path + * (optional) The path to the lock file to parse. The default is the lock + * file associated with the current project. + * + * @return array + * + * @throws \RuntimeException if composer.lock doesn't exist + * @throws \RuntimeException if composer.lock can't be decoded + */ + public function parseLockFile(string $lockFile = null): array { + $lockFile = $lockFile ?? __DIR__ . '/../../composer.lock'; + + if ( ! file_exists($lockFile) ) { + throw new \RuntimeException('Missing ' . basename($lockFile)); + } + + $lock = json_decode(file_get_contents($lockFile), true); + + if ( empty($lock) || ! isset($lock['packages']) ) { + throw new \RuntimeException('Error decoding ' . basename($lockFile)); + } + + return $lock; + } + + /** + * Sort and object-ify an array of package data. + * + * @param array $packages Package data from composer.lock. + * + * @return object[] + */ + public function massagePackages(array $packages): array { + usort($packages, function ($a, $b) { + return strcasecmp($a['name'], $b['name']); + }); + + foreach ( $packages as &$package ) { + $package = (object) $package; + } + + return $packages; + } +} diff --git a/src/Twigc/DefaultCommand.php b/src/Twigc/DefaultCommand.php deleted file mode 100644 index cfe3bb3..0000000 --- a/src/Twigc/DefaultCommand.php +++ /dev/null @@ -1,438 +0,0 @@ - - * @license MIT - */ - -namespace Dana\Twigc; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\DescriptorHelper; -use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Default twigc command. - */ -class DefaultCommand extends Command { - /** - * {@inheritdoc} - */ - protected function configure() { - $this - ->setName('twigc') - ->setDescription('Compile a Twig template') - ->addArgument( - 'template', - InputArgument::OPTIONAL, - 'Twig template file to render (use `-` for STDIN)' - ) - ->addOption( - 'help', - 'h', - InputOption::VALUE_NONE, - 'Display this usage help' - ) - ->addOption( - 'version', - 'V', - InputOption::VALUE_NONE, - 'Display version information' - ) - ->addOption( - 'credits', - null, - InputOption::VALUE_NONE, - 'Display dependency credits (including Twig version)' - ) - ->addOption( - 'cache', - null, - InputOption::VALUE_REQUIRED, - 'Enable caching to specified directory' - ) - ->addOption( - 'dir', - 'd', - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Add search directory to loader' - ) - ->addOption( - 'env', - null, - InputOption::VALUE_NONE, - 'Treat environment variables as input data' - ) - ->addOption( - 'escape', - 'e', - InputOption::VALUE_REQUIRED, - 'Set autoescape environment option' - ) - ->addOption( - 'json', - 'j', - InputOption::VALUE_REQUIRED, - 'Pass variables as JSON (dictionary string or file path)' - ) - ->addOption( - 'pair', - 'p', - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Pass variable as key=value pair' - ) - ->addOption( - 'query', - null, - InputOption::VALUE_REQUIRED, - 'Pass variables as URL query string' - ) - ->addOption( - 'strict', - 's', - InputOption::VALUE_NONE, - 'Enable strict_variables environment option' - ) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) { - switch ( true ) { - // Display usage help - case $input->getOption('help'): - return $this->doHelp($input, $output); - // Display version information - case $input->getOption('version'): - return $this->doVersion($input, $output); - // Display package credits - case $input->getOption('credits'): - return $this->doCredits($input, $output); - } - // Render Twig template - return $this->doRender($input, $output); - } - - /** - * {@inheritdoc} - * - * Overriding this prevents TextDescriptor from displaying the Help section. - */ - public function getProcessedHelp() { - return ''; - } - - /** - * Displays usage help. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ - public function doHelp(InputInterface $input, OutputInterface $output) { - (new DescriptorHelper())->describe($output, $this); - return 0; - } - - /** - * Displays version information. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ - public function doVersion(InputInterface $input, OutputInterface $output) { - $nameFmt = 'twigc'; - $versionFmt = '%s (%s @ %s)'; - - $output->writeln(sprintf( - "${nameFmt} version ${versionFmt}", - \Dana\Twigc\Twigc::VERSION_NUMBER, - \Dana\Twigc\Twigc::VERSION_COMMIT, - \Dana\Twigc\Twigc::VERSION_DATE - )); - - return 0; - } - - /** - * Displays package credits. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ - public function doCredits(InputInterface $input, OutputInterface $output) { - $installed = \Dana\Twigc\Twigc::getComposerPackages(); - - $table = new Table($output); - $table->setStyle('compact'); - $table->getStyle()->setVerticalBorderChar(''); - $table->getStyle()->setCellRowContentFormat('%s '); - $table->setHeaders(['name', 'version', 'licence']); - - foreach ( $installed as $package ) { - $table->addRow([ - $package->name, - ltrim($package->version, 'v'), - implode(', ', $package->license) ?: '?', - ]); - } - $table->render(); - } - - /** - * Renders a Twig template. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ - public function doRender(InputInterface $input, OutputInterface $output) { - $inputData = []; - $template = $input->getArgument('template'); - $template = $template === null ? '-' : $template; - $cache = $input->getOption('cache'); - $cache = $cache === null ? false : $cache; - $dirs = $template === '-' ? [] : [dirname($template)]; - $dirs = array_merge($dirs, $input->getOption('dir')); - $temp = false; - $strict = (bool) $input->getOption('strict'); - $escape = $input->getOption('escape'); - $inputs = [ - 'json' => (int) ($input->getOption('json') !== null), - 'pair' => (int) (! empty($input->getOption('pair'))), - 'query' => (int) ($input->getOption('query') !== null), - ]; - - // If we're reading from STDIN, but STDIN is a TTY, print help and die - if ( $template === '-' && posix_isatty(\STDIN) ) { - $this->doHelp($input, $output); - return 1; - } - - // Validate search directories - foreach ( $dirs as $dir ) { - if ( ! is_dir($dir) ) { - throw new \InvalidArgumentException( - "Illegal search directory: ${dir}" - ); - } - } - - // If no escape option was supplied, try to auto-detect - // (we could do this with Twig's 'filename' method, but i have some - // control over this) - if ( $escape === null ) { - if ( substr($template, -5) === '.twig' ) { - $ext = pathinfo(substr($template, -5), \PATHINFO_EXTENSION); - } else { - $ext = pathinfo($template, \PATHINFO_EXTENSION); - } - - switch ( strtolower($ext) ) { - case 'template': - case 'tmpl': - case 'tpl': - case 'htm': - case 'html': - case 'phtml': - case 'thtml': - case 'xhtml': - $escape = 'html'; - break; - case 'css': - case 'scss': - $escape = 'css'; - break; - case 'js': - $escape = 'js'; - break; - case 'json': - $escape = 'json'; - break; - default: - $escape = false; - break; - } - - // Otherwise, try to parse the supplied method - } else { - // Normalise some boolean values - $escape = strtolower($escape); - $escape = $escape === 'f' ? 'false' : $escape; - $escape = $escape === 'n' ? 'false' : $escape; - $escape = $escape === 'none' ? 'false' : $escape; - $escape = $escape === 'never' ? 'false' : $escape; - $escape = $escape === 't' ? 'true' : $escape; - $escape = $escape === 'y' ? 'true' : $escape; - $escape = $escape === 'always' ? 'true' : $escape; - - $bool = filter_var($escape, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE); - - if ( $bool !== null ) { - $escape = $bool ? 'html' : false; - } - } - - // Because Console doesn't allow us to see the order of options supplied - // at the command line, there's no good way to sort out the precedence - // amongst the different input methods... so let's just say we can only - // use one of them at a time - if ( array_sum($inputs) > 1 ) { - throw new \InvalidArgumentException( - '-j, -p, and --query options are mutually exclusive' - ); - } - - // Input data supplied via query string - if ( ($query = $input->getOption('query')) !== null ) { - if ( $query && $query[0] === '?' ) { - $query = substr($query, 1); - } - parse_str($query, $inputData); - - // Input data supplied via JSON - } elseif ( ($json = $input->getOption('json')) !== null ) { - $json = trim($json); - - // JSON supplied via STDIN - if ( $json === '-' ) { - if ( $template === '-' ) { - throw new \InvalidArgumentException( - 'Can not read both template and JSON input from STDIN' - ); - } - if ( posix_isatty(\STDIN) ) { - throw new \InvalidArgumentException( - 'Expected JSON input on STDIN' - ); - } - $json = file_get_contents('php://stdin'); - - // JSON supplied via file - } elseif ( $json && $json[0] !== '{' ) { - if ( ! file_exists($json) || is_dir($json) ) { - throw new \InvalidArgumentException( - "Missing or illegal JSON file name: ${json}" - ); - } - $json = file_get_contents($json); - } - - // This check is here to prevent errors if the input is just empty - if ( trim($json) !== '' ) { - $inputData = json_decode($json, true); - } - - if ( ! is_array($inputData) ) { - throw new \InvalidArgumentException( - 'JSON input must be a dictionary' - ); - } - - // Input data supplied via key=value pair - } elseif ( count($input->getOption('pair')) ) { - foreach ( $input->getOption('pair') as $pair ) { - $kv = explode('=', $pair, 2); - - if ( count($kv) !== 2 ) { - throw new \InvalidArgumentException( - "Illegal key=value pair: ${pair}" - ); - } - - $inputData[$kv[0]] = $kv[1]; - } - } - - if ( $input->getOption('env') ) { - $inputData = array_merge($_ENV, $inputData); - } - - // Validate key names now - foreach ( $inputData as $key => $value ) { - if ( ! preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#', $key) ) { - throw new \InvalidArgumentException( - "Illegal variable name: ${key}" - ); - } - } - - // Template supplied via STDIN - if ( $template === '-' ) { - // If we've been supplied one or more search directories, we'll need - // to write the template out to a temp directory so we can use the - // file-system loader - if ( $dirs ) { - $temp = true; - $template = implode('/', [ - sys_get_temp_dir(), - implode('.', ['twigc', getmypid(), md5(time())]), - '-', - ]); - - mkdir(dirname($template)); - file_put_contents($template, file_get_contents('php://stdin'), LOCK_EX); - - $dirs = array_merge([dirname($template)], $dirs); - - $loader = new \Twig_Loader_Filesystem($dirs); - - // Otherwise, we can just use the array loader, which is a little - // faster and cleaner - } else { - $loader = new \Twig_Loader_Array([ - $template => file_get_contents('php://stdin'), - ]); - } - - // Template supplied via file path - } else { - $loader = new \Twig_Loader_Filesystem($dirs); - } - - try { - $twig = new \Twig_Environment($loader, [ - 'cache' => $cache, - 'debug' => false, - 'strict_variables' => $strict, - 'autoescape' => $escape, - ]); - - $twig->getExtension('core')->setEscaper( - 'json', - function($twigEnv, $string, $charset) { - return json_encode( - $string, - \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE - ); - } - ); - - $output->writeln( - rtrim($twig->render(basename($template), $inputData), "\r\n") - ); - } finally { - if ( $temp ) { - unlink($template); - rmdir(dirname($template)); - } - } - - return 0; - } -} - diff --git a/src/Twigc/PharCompiler.php b/src/Twigc/PharCompiler.php index 681fbc7..8a36570 100644 --- a/src/Twigc/PharCompiler.php +++ b/src/Twigc/PharCompiler.php @@ -3,18 +3,19 @@ /** * This file is part of twigc. * - * @author dana geier + * @author dana * @license MIT */ namespace Dana\Twigc; -use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; +use Dana\Twigc\Application; + /** - * Compiles twigc into an executable phar file. + * Compile twigc to an executable phar. * * This clas is heavily inspired by Composer's Compiler: * @@ -39,407 +40,321 @@ use Symfony\Component\Finder\Finder; * THE SOFTWARE. */ class PharCompiler { - protected $output; - protected $baseDir; - protected $finderSort; - protected $versionNumber; - protected $versionCommit; - protected $versionDate; + protected $output; + protected $baseDir; + protected $finderSort; - /** - * Object constructor. - * - * @param (bool) $verbose (optional) Whether to display verbose output. - * - * @return self - */ - public function __construct($verbose = false) { - $this->output = new ConsoleOutput(); - $this->baseDir = realpath(\Dana\Twigc\Twigc::BASE_DIR); - $this->finderSort = function ($a, $b) { - return strcmp( - strtr($a->getRealPath(), '\\', '/'), - strtr($b->getRealPath(), '\\', '/') - ); - }; + /** + * Object constructor. + * + * @param OutputInterface $output The output to write to. + * @param bool $verbose (optional) Whether to display verbose output. + * + * @return self + */ + public function __construct(OutputInterface $output, bool $verbose = false) { + $this->output = $output; + $this->baseDir = realpath(__DIR__ . '/../..'); + $this->finderSort = function ($a, $b) { + return strcmp( + strtr($a->getRealPath(), '\\', '/'), + strtr($b->getRealPath(), '\\', '/') + ); + }; - if ( $verbose ) { - $this->output->setVerbosity(ConsoleOutput::VERBOSITY_VERBOSE); - } - } + if ( $verbose ) { + $this->output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + } + } - /** - * Compiles the project into an executable phar file. - * - * @param string $pharFile - * (optional) The path (absolute or relative to the CWD) to write the - * resulting phar file to. - * - * @return void - */ - public function compile($pharFile = 'twigc.phar') { - $this->output->writeln('Compiling phar...'); + /** + * Compiles the project into an executable phar file. + * + * @param string $phar + * (optional) The path (absolute or relative to the CWD) to write the + * resulting phar file to. + * + * @return void + */ + public function compile(string $phar) { + $this->output->writeln('Compiling phar...'); - $this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE); - $this->extractVersionInformation(); + $this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE); - if ( file_exists($pharFile) ) { - unlink($pharFile); - } + if ( file_exists($phar) ) { + unlink($phar); + } - $phar = new \Phar($pharFile, 0, 'twigc.phar'); - $phar->setSignatureAlgorithm(\Phar::SHA1); - $phar->startBuffering(); + $obj = new \Phar($phar, 0, basename($phar)); + $obj->setSignatureAlgorithm(\Phar::SHA1); + $obj->startBuffering(); - $this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE); - $this->output->writeln('Adding src files...'); - $this->addSrc($phar); + $this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE); + $this->output->writeln('Adding src files...'); + $this->addSrc($obj); - $this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE); - $this->output->writeln('Adding vendor files...'); - $this->addVendor($phar); + $this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE); + $this->output->writeln('Adding vendor files...'); + $this->addVendor($obj); - $this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE); - $this->output->writeln('Adding root files...'); - $this->addRoot($phar); + $this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE); + $this->output->writeln('Adding root files...'); + $this->addRoot($obj); - $this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE); - $this->output->writeln('Adding bin files...'); - $this->addBin($phar); + $this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE); + $this->output->writeln('Adding bin files...'); + $this->addBin($obj); - $phar->setStub($this->getStub()); - $phar->stopBuffering(); + $obj->setStub($this->getStub()); + $obj->stopBuffering(); + unset($obj); - unset($phar); + chmod($phar, 0755); - chmod($pharFile, 0755); + $this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE); + $this->output->writeln("Compiled to ${phar}."); + } - $this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE); - $this->output->writeln("Compiled to ${pharFile}."); + /** + * Add a file to a phar archive. + * + * @param \Phar $phar + * The phar file to add to. + * + * @param \SplFileInfo|string $file + * The file to add, or its path. + * + * @param bool|null $strip + * (optional) Whether to strip extraneous white space from the file in + * order to reduce its size. The default is to auto-detect based on file + * extension. + * + * @return void + */ + protected function addFile(\Phar $phar, $file, bool $strip = null) { + if ( is_string($file) ) { + $file = new \SplFileInfo($file); + } - /* - // Re-sign the phar with reproducible time stamps and signature - $util = new Timestamps($pharFile); - $util->updateTimestamps($this->versionDate); - $util->save($pharFile, \Phar::SHA1); - */ - } + // Strip the absolute base directory off the front of the path + $prefix = $this->baseDir . DIRECTORY_SEPARATOR; + $path = strtr( + str_replace($prefix, '', $file->getRealPath()), + '\\', + '/' + ); - /** - * Extracts the application version information from the git repository and - * sets the associated object properties. - * - * @return void - * - * @throws \RuntimeException if `git describe` fails - * @throws \RuntimeException if `git log` fails - * @throws \RuntimeException if `git log` fails (2) - */ - protected function extractVersionInformation() { - $workDir = escapeshellarg(__DIR__); + $this->output->writeln( + "Adding file: ${path}", + OutputInterface::VERBOSITY_VERBOSE + ); - // Get version number - $output = []; - exec("cd ${workDir} && git describe --tags --match='v*.*.*' --dirty='!'", $output, $ret); + $content = file_get_contents($file); - if ( $ret !== 0 || empty($output) ) { - $output = ['0.0.0']; - } + // Strip interpreter directives + if ( strpos($path, 'bin/') === 0 ) { + $content = preg_replace('%^#!/usr/bin/env php\s*%', '', $content); - $tokens = explode('-', trim($output[0])); + // Replace build-date place-holder + } elseif ( $path === 'src/Twigc/Application.php' ) { + $date = new \DateTime('now', new \DateTimeZone('UTC')); - $this->versionNumber = rtrim(ltrim($tokens[0], 'v'), '!'); + $content = str_replace( + '%BUILD_DATE%', + $date->format('D Y-m-d H:i:s T'), + $content + ); + } - // If we're ahead of a tag, add the number of commits - if ( count($tokens) > 1 ) { - $this->versionNumber .= '-plus' . rtrim($tokens[1], '!'); - } + if ( $strip === null ) { + $strip = in_array($file->getExtension(), ['json', 'lock', 'php'], true); + } - // If the index is dirty, add that - if ( rtrim(implode('-', $tokens), '!') !== implode('-', $tokens) ) { - $this->versionNumber .= '-dirty'; - } + if ( $strip ) { + $content = $this->stripWhiteSpace($content); + } - // Get version last commit hash - $output = []; - exec("cd ${workDir} && git log -1 --pretty='%H' HEAD", $output, $ret); + $phar->addFromString($path, $content); + } - if ( $ret !== 0 || empty($output) ) { - throw new \RuntimeException( - 'An error occurred whilst running `git log`' - ); - } + /** + * Remove extraneous white space from a string whilst preserving PHP line + * numbers. + * + * @param string $source + * The PHP or JSON string to strip white space from. + * + * @param string $type + * (optional) The type of file the string represents. Available options + * are 'php' and 'json'. The default is 'php'. + * + * @return string + */ + protected function stripWhiteSpace( + string $source, + string $type = 'php' + ): string { + $output = ''; - $this->versionCommit = trim($output[0]); + if ( $type === 'json' ) { + $output = json_encode(json_decode($json, true)); - // Get version last commit date - $output = []; - exec("cd ${workDir} && git log -1 --pretty='%ci' HEAD", $output, $ret); + return $output === null ? $source : $output . "\n"; + } - if ( $ret !== 0 || empty($output) ) { - throw new \RuntimeException( - 'An error occurred whilst running `git log`' - ); - } + if ( ! function_exists('token_get_all') ) { + return $source; + } - $this->versionDate = new \DateTime(trim($output[0])); - $this->versionDate->setTimezone(new \DateTimeZone('UTC')); + foreach ( token_get_all($source) as $token ) { + // Arbitrary text, return as-is + if ( is_string($token) ) { + $output .= $token; + // Replace comments by empty lines + } elseif ( in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT]) ) { + $output .= str_repeat("\n", substr_count($token[1], "\n")); + // Collapse and normalise white-space + } elseif ($token[0] === \T_WHITESPACE) { + // Collapse consecutive spaces + $space = preg_replace('/[ \t]+/', ' ', $token[1]); + // Normalise new-lines to \n + $space = preg_replace('/(?:\r\n|\r|\n)/', "\n", $space); + // Trim leading spaces + $space = preg_replace('/\n[ ]+/', "\n", $space); + $output .= $space; + // Anything else, return as-is + } else { + $output .= $token[1]; + } + } - if ( $this->output->isVerbose() ) { - $this->output->writeln( - 'Got version number: ' . $this->versionNumber - ); - $this->output->writeln( - 'Got version commit: ' . $this->versionCommit - ); - $this->output->writeln( - 'Got version date: ' . $this->versionDate->getTimestamp() - ); - } - } + return $output; + } - /** - * Adds a file to a phar. - * - * @param \Phar $phar - * The phar file to add to. - * - * @param \SplFileInfo|string $file - * The file to add, or its path. - * - * @param null|bool $strip - * (optional) Whether to strip extraneous white space from the file in - * order to reduce its size. The default is to auto-detect based on file - * extension. - * - * @return void - */ - protected function addFile($phar, $file, $strip = null) { - if ( is_string($file) ) { - $file = new \SplFileInfo($file); - } + /** + * Add files from src directory to a phar. + * + * @param \Phar $phar The phar file to add to. + * + * @return void + */ + protected function addSrc(\Phar $phar) { + $finder = new Finder(); + $finder + ->files() + ->in($this->baseDir . '/src') + ->ignoreDotFiles(true) + ->ignoreVCS(true) + ->name('*.php') + ->notName('PharCompiler.php') + ->sort($this->finderSort) + ; - // Strip the absolute base directory off the front of the path - $prefix = $this->baseDir . DIRECTORY_SEPARATOR; - $path = strtr( - str_replace($prefix, '', $file->getRealPath()), - '\\', - '/' - ); + foreach ( $finder as $file ) { + $this->addFile($phar, $file); + } + } - $this->output->writeln("Adding file: ${path}", ConsoleOutput::VERBOSITY_VERBOSE); + /** + * Adds files from vendor directory to a phar. + * + * @param \Phar $phar The phar file to add to. + * + * @return void + */ + protected function addVendor($phar) { + $devPaths = (new ComposerHelper())->getDevPackages(); + $devPaths = array_map(function ($x) { + return $x->name . '/'; + }, $devPaths); - $content = file_get_contents($file); + $finder = new Finder(); + $finder + ->files() + ->in($this->baseDir . '/vendor') + ->ignoreDotFiles(true) + ->ignoreVCS(true) + ; - // Strip interpreter directives - if ( strpos($path, 'bin/') === 0 ) { - $content = preg_replace('%^#!/usr/bin/env php\s*%', '', $content); + // Exclude files from dev packages + foreach ( $devPaths as $path ) { + $finder->notPath($path); + } - // Replace version place-holders - } elseif ( $path === 'src/Twigc/Twigc.php' ) { - $content = str_replace( - [ - '%version_number%', - '%version_commit%', - '%version_date%', - ], - [ - $this->versionNumber, - $this->versionCommit, - $this->versionDate->format('Y-m-d H:i:s'), - ], - $content - ); - } + $finder + ->exclude('bin') + ->exclude('doc') + ->exclude('docs') + ->exclude('test') + ->exclude('tests') + ->notPath('/^[^\/]+\/[^\/]+\/Tests?\//') + ->notName('*.c') + ->notName('*.h') + ->notName('*.m4') + ->notName('*.w32') + ->notName('*.xml.dist') + ->notName('build.xml') + ->notName('composer.json') + ->notName('composer.lock') + ->notName('travis-ci.xml') + ->notName('phpunit.xml') + ->notName('ChangeLog*') + ->notName('CHANGE*') + ->notName('*CONDUCT*') + ->notName('CONTRIBUT*') + ->notName('README*') + ->sort($this->finderSort) + ; - if ( $strip === null ) { - $strip = in_array($file->getExtension(), ['json', 'lock', 'php'], true); - } + foreach ( $finder as $file ) { + $this->addFile($phar, $file); + } + } - if ( $strip ) { - $content = $this->stripWhiteSpace($content); - } + /** + * Adds files from project root directory to a phar. + * + * @param \Phar $phar The phar file to add to. + * + * @return void + */ + protected function addRoot(\Phar $phar) { + $this->addFile($phar, $this->baseDir . '/composer.json'); + $this->addFile($phar, $this->baseDir . '/composer.lock'); + } - $phar->addFromString($path, $content); - } + /** + * Adds files from bin directory to a phar. + * + * @param \Phar $phar The phar file to add to. + * + * @return void + */ + protected function addBin(\Phar $phar) { + $this->addFile($phar, $this->baseDir . '/bin/twigc'); + } - /** - * Removes extraneous white space from a string whilst preserving PHP line - * numbers. - * - * @param string $source - * The PHP or JSON string to strip white space from. - * - * @param string $type - * (optional) The type of file the string represents. Available options - * are 'php' and 'json'. The default is 'php'. - * - * @return string - */ - protected function stripWhiteSpace($source, $type = 'php') { - $output = ''; + /** + * Returns the phar stub. + * + * @return string + */ + protected function getStub() { + $stub = " + #!/usr/bin/env php + + * @license MIT + */ - return $output === null ? $source : $output . "\n"; - } - - if ( ! function_exists('token_get_all') ) { - return $source; - } - - foreach ( token_get_all($source) as $token ) { - // Arbitrary text, return as-is - if ( is_string($token) ) { - $output .= $token; - // Replace comments by empty lines - } elseif ( in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT]) ) { - $output .= str_repeat("\n", substr_count($token[1], "\n")); - // Collapse and normalise white-space - } elseif (T_WHITESPACE === $token[0]) { - // Collapse consecutive spaces - $space = preg_replace('#[ \t]+#', ' ', $token[1]); - // Normalise new-lines to \n - $space = preg_replace('#(?:\r\n|\r|\n)#', "\n", $space); - // Trim leading spaces - $space = preg_replace('#\n[ ]+#', "\n", $space); - $output .= $space; - // Anything else, return as-is - } else { - $output .= $token[1]; - } - } - - return $output; - } - - /** - * Adds files from src directory to a phar. - * - * @param \Phar $phar The phar file to add to. - * - * @return void - */ - protected function addSrc($phar) { - $finder = new Finder(); - $finder - ->files() - ->in($this->baseDir . '/src') - ->ignoreDotFiles(true) - ->ignoreVCS(true) - ->name('*.php') - ->notName('PharCompiler.php') - ->sort($this->finderSort) - ; - - foreach ( $finder as $file ) { - $this->addFile($phar, $file); - } - } - - /** - * Adds files from vendor directory to a phar. - * - * @param \Phar $phar The phar file to add to. - * - * @return void - */ - protected function addVendor($phar) { - $devPaths = \Dana\Twigc\Twigc::getComposerDevPackages(); - $devPaths = array_map(function ($x) { - return $x->name . '/'; - }, $devPaths); - - $finder = new Finder(); - $finder - ->files() - ->in($this->baseDir . '/vendor') - ->ignoreDotFiles(true) - ->ignoreVCS(true) - ; - - // Exclude files from dev packages - foreach ( $devPaths as $path ) { - $finder->notPath($path); - } - - $finder - ->exclude('bin') - ->exclude('doc') - ->exclude('docs') - ->exclude('test') - ->exclude('tests') - ->notPath('/^[^\/]+\/[^\/]+\/Tests?\//') - ->notName('*.c') - ->notName('*.h') - ->notName('*.m4') - ->notName('*.w32') - ->notName('*.xml.dist') - ->notName('build.xml') - ->notName('composer.json') - ->notName('composer.lock') - ->notName('travis-ci.xml') - ->notName('phpunit.xml') - ->notName('ChangeLog*') - ->notName('CHANGE*') - ->notName('*CONDUCT*') - ->notName('CONTRIBUT*') - ->notName('README*') - ->sort($this->finderSort) - ; - - foreach ( $finder as $file ) { - $this->addFile($phar, $file); - } - } - - /** - * Adds files from project root directory to a phar. - * - * @param \Phar $phar The phar file to add to. - * - * @return void - */ - protected function addRoot($phar) { - $this->addFile($phar, $this->baseDir . '/composer.json'); - $this->addFile($phar, $this->baseDir . '/composer.lock'); - } - - /** - * Adds files from bin directory to a phar. - * - * @param \Phar $phar The phar file to add to. - * - * @return void - */ - protected function addBin($phar) { - $this->addFile($phar, $this->baseDir . '/bin/twigc'); - } - - /** - * Returns the phar stub. - * - * @return string - */ - protected function getStub() { - $stub = " - #!/usr/bin/env php - - * @license MIT - */ - - \Phar::mapPhar('twigc.phar'); - require 'phar://twigc.phar/bin/twigc'; - __HALT_COMPILER(); - "; - - return str_replace("\t", '', trim($stub)) . "\n"; - } + \Phar::mapPhar('twigc.phar'); + require 'phar://twigc.phar/bin/twigc'; + __HALT_COMPILER(); + "; + return preg_replace('/^\s+/m', '', trim($stub)) . "\n"; + } } - diff --git a/src/Twigc/Twigc.php b/src/Twigc/Twigc.php deleted file mode 100644 index 09e6fc1..0000000 --- a/src/Twigc/Twigc.php +++ /dev/null @@ -1,88 +0,0 @@ - - * @license MIT - */ - -namespace Dana\Twigc; - -/** - * Holds various project-specific constants and methods. - */ -class Twigc { - const BASE_DIR = __DIR__ . '/../..'; - const VERSION_NUMBER = '%version_number%'; - const VERSION_COMMIT = '%version_commit%'; - const VERSION_DATE = '%version_date%'; - - /** - * Returns an array of data representing the project's Composer lock file. - * - * @return array - * - * @throws \RuntimeException if composer.lock doesn't exist - * @throws \RuntimeException if composer.lock can't be decoded - */ - private static function parseComposerLock() { - $lockFile = static::BASE_DIR . '/composer.lock'; - - if ( ! file_exists($lockFile) ) { - throw new \RuntimeException('Missing ' . basename($lockFile)); - } - - $installed = json_decode(file_get_contents($lockFile), true); - - if ( empty($installed) || ! isset($installed['packages']) ) { - throw new \RuntimeException('Error decoding ' . basename($lockFile)); - } - - return $installed; - } - - /** - * Sorts and object-ifies an array of package data. - * - * @param array $packages Package data from composer.lock. - * - * @return array - */ - private static function massagePackages(array $packages) { - usort($packages, function ($a, $b) { - return strcasecmp($a['name'], $b['name']); - }); - - foreach ( $packages as &$package ) { - $package = (object) $package; - } - - return $packages; - } - - /** - * Returns an array of installed non-dev Composer packages based on the - * project's Composer lock file. - * - * @return object[] An array of objects representing Composer packages. - */ - public static function getComposerPackages() { - $packages = static::parseComposerLock()['packages']; - - return static::massagePackages($packages); - } - - /** - * Returns an array of installed dev Composer packages based on the - * project's lock file. - * - * @return object[] An array of objects representing Composer packages. - */ - public static function getComposerDevPackages() { - $packages = static::parseComposerLock()['packages-dev']; - - return static::massagePackages($packages); - } -} - diff --git a/src/bootstrap.php b/src/bootstrap.php index da8f7d1..f872de4 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -3,62 +3,57 @@ /** * This file is part of twigc. * - * @author dana geier + * @author dana * @license MIT */ /** - * Helper function for printing an error message. + * Print an error message. * - * Uses fprintf() to print to STDERR if available; uses echo otherwise. + * Uses fprintf() to print to stderr if available; uses echo otherwise. * * @param string $string - * (optional) The message to print. Will be passed through rtrim(); if the - * result is an empty string, only an empty line will be printed; otherwise, - * the text 'twigc: ' will be appended to the beginning. + * (optional) The message to print. Is passed through rtrim(); if the result + * is an empty string, only an empty line is printed; otherwise, the text + * 'twigc: ' is appended to the beginning. * * @return void */ function twigc_puts_error($string = '') { - $string = rtrim($string); - $string = $string === '' ? '' : "twigc: ${string}"; + $string = rtrim($string); + $string = $string === '' ? '' : "twigc: ${string}"; - // \STDERR only exists when we're using the CLI SAPI - if ( defined('\\STDERR') ) { - fprintf(\STDERR, "%s\n", $string); - } else { - echo $string, "\n"; - } + if ( defined('\\STDERR') ) { + fprintf(\STDERR, "%s\n", $string); + } else { + echo $string, "\n"; + } } -// Find Composer's auto-loader -$autoloaders = [ - // Phar/repo path - __DIR__ . '/../vendor/autoload.php', - // Composer path - __DIR__ . '/../../../autoload.php', -]; - -foreach ( $autoloaders as $autoloader ) { - if ( file_exists($autoloader) ) { - define('TWIGC_AUTOLOADER', $autoloader); - break; - } -} - -unset($autoloaders, $autoloader); - -// Disallow running from non-CLI SAPIs if ( \PHP_SAPI !== 'cli' ) { - twigc_puts_error("This tool must be invoked via PHP's CLI SAPI."); - exit(1); + twigc_puts_error("This tool must be invoked via PHP's CLI SAPI."); + exit(1); } -// Give a meaningful error if we don't have the auto-loader -if ( ! defined('TWIGC_AUTOLOADER') ) { - twigc_puts_error('Auto-loader is missing — try running `composer install`.'); - exit(1); +(function () { + $paths = [ + // Phar/repo path + __DIR__ . '/../vendor/autoload.php', + // Composer path + __DIR__ . '/../../../autoload.php', + ]; + + foreach ( $paths as $path ) { + if ( file_exists($path) ) { + define('TWIGC_AUTOLOADER', $path); + return; + } + } +})(); + +if ( ! defined('\\TWIGC_AUTOLOADER') ) { + twigc_puts_error('Auto-loader is missing — try running `composer install`.'); + exit(1); } -require_once TWIGC_AUTOLOADER; - +require_once \TWIGC_AUTOLOADER;