Skip to content

Commit

Permalink
Migrate to symfony/console
Browse files Browse the repository at this point in the history
Symfony console has a lot of nice things -- like color-coding outputs,
prompting for inputs, and generating more detailed help screens for
subcommands.  The CLI parsing is ideal for use with shebangs, so we have to
do some acrobatics to make it fit.
  • Loading branch information
totten committed Dec 2, 2019
1 parent 3707288 commit a16eef2
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 110 deletions.
25 changes: 1 addition & 24 deletions bin/pogo
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,4 @@ if (!$found) {
die("Failed to find autoloader");
}

function pogo_main(\Pogo\PogoInput $input) {
$actions = [
'run' => new \Pogo\Command\RunCommand(),
'get' => new \Pogo\Command\DownloadCommand(),
'up' => new \Pogo\Command\UpdateCommand(),
'parse' => new \Pogo\Command\ParseCommand(),
'help' => new \Pogo\Command\HelpCommand(),
];

if (empty($input->action) && !empty($input->script)) {
$exit = $actions['run']->run($input);
exit($exit === NULL ? 0 : $exit);
}
elseif (isset($actions[$input->action])) {
$exit = $actions[$input->action]->run($input);
exit($exit === NULL ? 0 : $exit);
}
else {
$actions['help']->run($input);
return 1;
}
}

pogo_main(\Pogo\PogoInput::create($argv));
exit(\Pogo\Application::main($argv));
46 changes: 46 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
namespace Pogo;

use Pogo\Command\DownloadCommand;
use Pogo\Command\HelpCommand;
use Pogo\Command\ParseCommand;
use Pogo\Command\RunCommand;
use Pogo\Command\UpdateCommand;
use Symfony\Component\Console\Input\ArgvInput;

class Application extends \Symfony\Component\Console\Application {

/**
* Primary entry point for execution of the standalone command.
*
* @param array $args
*/
public static function main($args) {
$version = '@package_version@';

// FIXME: this handles "-D=foo script.php" but not "-D foo script.php"
$pogoInput = PogoInput::create($args);
$input = new ArgvInput($pogoInput->encode());

$application = new Application('pogo', ($version{0} === '@') ? '(local version)' : $version);
$application->setAutoExit(FALSE);
$application->setCatchExceptions(TRUE);
return $application->run($input);
}

/**
* Gets the default commands that should always be available.
*
* @return \Symfony\Component\Console\Command\Command[] An array of default Command instances
*/
protected function getDefaultCommands() {
return [
new HelpCommand(),
new ParseCommand(),
new DownloadCommand(),
new RunCommand(),
new UpdateCommand(),
];
}

}
30 changes: 30 additions & 0 deletions src/Command/BaseCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Pogo\Command;

use Symfony\Component\Console\Command\Command;

class BaseCommand extends Command {

private $synopsis = [];

/**
* Returns the synopsis for the command.
*
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
*
* @return string The synopsis
*/
public function getSynopsis($short = FALSE) {
$key = $short ? 'short' : 'long';

if (!isset($this->synopsis[$key])) {
global $argv;
$prog = basename($argv[0]);
$this->synopsis[$key] = trim(sprintf('%s --%s %s', $prog, $this->getName(), $this->getDefinition()->getSynopsis($short)));
}

return $this->synopsis[$key];
}

}
27 changes: 20 additions & 7 deletions src/Command/DownloadCommand.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
<?php
namespace Pogo\Command;

use Pogo\PogoInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class DownloadCommand {
class DownloadCommand extends BaseCommand {

use DownloadCommandTrait;

public function run(PogoInput $input) {
if (empty($input->script)) {
protected function configure() {
$this
->setName('get')
->setDescription('Get dependencies for a PHP script')
->addArgument('script', InputArgument::REQUIRED, 'PHP script')
->addOption('dl', 'D', InputOption::VALUE_REQUIRED, 'Dependency download directory')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force download of any dependencies');
}

protected function execute(InputInterface $input, OutputInterface $output) {
$script = $input->getArgument('script');
if (empty($script)) {
throw new \Exception("[get] Missing required file name");
}

// TODO: realpath($target) but using getenv(PWD) or `pwd` to preserve symlink structure

if (!file_exists($input->script)) {
throw new \Exception("[get] Non-existent file: {$input->script}");
if (!file_exists($script)) {
throw new \Exception("[get] Non-existent file: {$script}");
}

$this->initProject($input, $input->script);
$this->initProject($input, $script);

return 0;
}
Expand Down
14 changes: 7 additions & 7 deletions src/Command/DownloadCommandTrait.php
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<?php
namespace Pogo\Command;

use Pogo\PogoInput;
use Pogo\PogoProject;
use Symfony\Component\Console\Input\InputInterface;

trait DownloadCommandTrait {

/**
* @param \Pogo\PogoInput $input
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param string $target
* @return \Pogo\PogoProject
*/
public function initProject(PogoInput $input, $target) {
public function initProject(InputInterface $input, $target) {
$scriptMetadata = \Pogo\ScriptMetadata::parse($target);
$path = $this->pickBaseDir($input, $scriptMetadata);
$project = new PogoProject($scriptMetadata, $path);

$project->buildHelpers();
if ($input->getOption(['force', 'f'])
if ($input->getOption('force')
|| in_array($project->getStatus(), ['empty', 'stale'])
) {
$project->buildComposer();
Expand All @@ -27,12 +27,12 @@ public function initProject(PogoInput $input, $target) {
}

/**
* @param \Pogo\PogoInput $input
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Pogo\ScriptMetadata $scriptMetadata
* @return string
*/
public function pickBaseDir(PogoInput $input, $scriptMetadata) {
$result = $input->getOption('D');
public function pickBaseDir(InputInterface $input, $scriptMetadata) {
$result = $input->getOption('dl');
if ($result) {
return $result;
}
Expand Down
79 changes: 43 additions & 36 deletions src/Command/HelpCommand.php
Original file line number Diff line number Diff line change
@@ -1,47 +1,54 @@
<?php
namespace Pogo\Command;

use Pogo\PogoInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class HelpCommand {
class HelpCommand extends \Symfony\Component\Console\Command\HelpCommand {

public function run(PogoInput $input) {
$cmd = basename($input->interpreter);
protected function execute(InputInterface $input, OutputInterface $output) {
if (!empty($input->getArgument('command')) && $input->getArgument('command') !== 'help') {
return parent::execute($input, $output);
}

global $argv;
$cmd = basename($argv[0]);
$version = '@package_version@';
$name = ($version{0} === '@') ? 'pogo (local version)' : 'pogo @package_version@';

echo "$name\n";
echo "Usage: $cmd [--<action>] [action-options] <script-file> [--] [script-options]\n";
echo "\n";
echo "Example: Run a script\n";
echo " $cmd my-script.php\n";
echo "\n";
echo "Example: Download dependencies for a script to a specific location\n";
echo " $cmd --get -D=/tmp/deps my-script.php\n";
echo "\n";
echo "Example: Update dependencies in an existing project directory\n";
echo " cd <out-dir>\n";
echo " $cmd --up\n";
echo "\n";
// echo "Example: Remove any expired code from the common base folder\n";
// echo " $cmd --clean\n";
// echo "\n";
echo "Actions:\n";
echo " --get Download dependencies, but do not execute.\n";
echo " --run Run the script. Download anything necessary. (*default*)\n";
echo " --parse Extract any pragmas or metadata from the script.\n";
echo " --up Update dependencies (in current directory).\n";
echo " --help Show help screen.\n";
echo "\n";
echo "Action-Options:\n";
echo " -f Force; recreate project, even if it appears current\n";
echo " -D=<out> Output dependencies in this directory\n";
echo "\n";
echo "Environment:\n";
echo " POGO_BASE Default location for output folders\n";
echo " To store in-situ as a dot folder, use POGO_BASE=.\n";
echo " If omitted, defaults to ~/.cache/pogo or /tmp/pogo\n";
echo "\n";
$output->writeln("$name");
$output->writeln("<comment>Usage:</comment>");
$output->writeln(" $cmd [<action>] [action-options] <script-file> [--] [script-options]");
$output->writeln("");
$output->writeln("<comment>Example: Run a script</comment>");
$output->writeln(" $cmd my-script.php");
$output->writeln("");
$output->writeln("<comment>Example: Download dependencies for a script to a specific location</comment>");
$output->writeln(" $cmd --get -D=/tmp/deps my-script.php");
$output->writeln("");
$output->writeln("<comment>Example: Update dependencies in an existing project directory</comment>");
$output->writeln(" cd <out-dir>");
$output->writeln(" $cmd --up");
$output->writeln("");
// $output->writeln("Example: Remove any expired code from the common base folder");
// $output->writeln(" $cmd --clean");
// $output->writeln("");
$output->writeln("<comment>Actions:</comment>");
$output->writeln(" <info>--get</info> Download dependencies, but do not execute.");
$output->writeln(" <info>--run</info> Run the script. Download anything necessary. (<comment>default</comment>)");
$output->writeln(" <info>--parse</info> Extract any pragmas or metadata from the script.");
$output->writeln(" <info>--up</info> Update dependencies (in current directory).");
$output->writeln(" <info>--help</info> Show help screen.");
$output->writeln("");
$output->writeln("<comment>Action-Options:</comment>");
$output->writeln(" <info>-f</info> Force; recreate project, even if it appears current");
$output->writeln(" <info>-D=DIR</info> Output dependencies in this directory");
$output->writeln("");
$output->writeln("<comment>Environment:</comment>");
$output->writeln(" <info>POGO_BASE</info> Default location for output folders");
$output->writeln(" To store in-situ as a dot folder, use POGO_BASE=.");
$output->writeln(" If omitted, defaults to ~/.cache/pogo or /tmp/pogo");
$output->writeln("");
return 0;
}

Expand Down
30 changes: 20 additions & 10 deletions src/Command/ParseCommand.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
<?php
namespace Pogo\Command;

use Pogo\PogoInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ParseCommand {
class ParseCommand extends BaseCommand {

use DownloadCommandTrait;
protected function configure() {
$this
->setName('parse')
->setDescription('Extract any pragmas or metadata from the script')
->addArgument('script', InputArgument::REQUIRED, 'PHP script');
}

protected function execute(InputInterface $input, OutputInterface $output) {
$script = $input->getArgument('script');

public function run(PogoInput $input) {
if (empty($input->script)) {
if (empty($script)) {
throw new \Exception("[parse] Missing required file name");
}

if (!file_exists($input->script)) {
throw new \Exception("[parse] Non-existent file: {$input->script}");
if (!file_exists($script)) {
throw new \Exception("[parse] Non-existent file: {$script}");
}

$scriptMetadata = \Pogo\ScriptMetadata::parse($input->script);
$scriptMetadata = \Pogo\ScriptMetadata::parse($script);
$scriptMetadata = (array) $scriptMetadata;
echo json_encode($scriptMetadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
echo "\n";

$output->writeln(json_encode($scriptMetadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
OutputInterface::OUTPUT_RAW);

return 0;
}
Expand Down
30 changes: 23 additions & 7 deletions src/Command/RunCommand.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
<?php
namespace Pogo\Command;

use Pogo\PogoInput;
use Pogo\Runner\EvalRunner;
use Pogo\Runner\FileRunner;
use Pogo\Runner\DashBRunner;
use Pogo\Runner\DataRunner;
use Pogo\Runner\IncludeRunner;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class RunCommand {
class RunCommand extends BaseCommand {

use DownloadCommandTrait;

public function run(PogoInput $input) {
if (empty($input->script)) {
protected function configure() {
$this
->setName('run')
->setDescription('Execute a PHP script')
->addArgument('script', InputArgument::REQUIRED, 'PHP script')
->addArgument('script-args', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Arguments to pass through ')
->addOption('dl', 'D', InputOption::VALUE_REQUIRED, 'Dependency download directory')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force download of any dependencies')
->addOption('run-mode', NULL, InputOption::VALUE_REQUIRED, 'How to launch PHP subscripts (ex: include, eval)');
}

protected function execute(InputInterface $input, OutputInterface $output) {
$target = $input->getArgument('script');
if (empty($target)) {
throw new \Exception("[run] Missing required file name");
}

// TODO: realpath($target) but using getenv(PWD) or `pwd` to preserve symlink structure
$target = $input->script;
if (!file_exists($target)) {
throw new \Exception("[run] Non-existent file: $target");
}
Expand All @@ -38,13 +52,15 @@ public function run(PogoInput $input) {
'file' => new FileRunner(),
'include' => new IncludeRunner(),
];
$runMode = $input->getOption('run-mode', $project->scriptMetadata->runner['with']);
$runMode = $input->getOption('run-mode');
$runMode = empty($runMode) ? $project->scriptMetadata->runner['with'] : $runMode;
$runMode = ($runMode === 'auto') ? $this->pickRunner($target) : $runMode;
if (!isset($runners[$runMode])) {
throw new \Exception("Invalid run mode: $runMode");
}

return $runners[$runMode]->run($autoloader, $project->scriptMetadata, $input->scriptArgs);
$scriptArgs = $input->getArgument('script-args');
return $runners[$runMode]->run($autoloader, $project->scriptMetadata, $scriptArgs);
}
else {
fwrite(STDERR, "[run] Script not found ($target)");
Expand Down
Loading

0 comments on commit a16eef2

Please sign in to comment.