Skip to content

Commit

Permalink
Clean up command-line overrides for configuration.
Browse files Browse the repository at this point in the history
 - Move responsibility for command line option declarations into
   `Configuration::getInputOptions`.
 - Move responsibility for overriding configuration via command line
   options into `Configuration::fromInput`.
 - Remove an implicit dependency on side effects from command line
   option name collisions with the base Console Application (this made
   `--verbose`, `--no-interaction`, and `--ansi` ... kind of work).

Update available command-line options:

 - `--interactive` and `--no-interactive` for interactive mode input.
 - `--color` and `--no-color` for decorated output.
 - `--verbose`, `--quiet` and `-v`/`-vv`/`-vvv` for output verbosity.

Add option aliases for compatibility with Symfony, Composer, and other
familiar tools:

 - `--ansi` and `--no-ansi` for `--color` and `--no-color`.
 - `--no-interaction` for `--no-interactive`.
 - `-i` and `-a` to force interactive mode input (hey there `php -a`).
  • Loading branch information
bobthecow committed Apr 6, 2020
1 parent 4b99308 commit fc6cb55
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 40 deletions.
195 changes: 194 additions & 1 deletion src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
use Psy\VersionUpdater\IntervalChecker;
use Psy\VersionUpdater\NoopChecker;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
Expand Down Expand Up @@ -148,6 +150,193 @@ public function __construct(array $config = [])
$this->init();
}

/**
* Construct a Configuration object from Symfony Console input.
*
* This is great for adding psysh-compatible command line options to framework- or app-specific
* wrappers.
*
* $input should already be bound to an appropriate InputDefinition (see self::getInputOptions
* if you want to build your own) before calling this method. It's not required, but things work
* a lot better if we do.
*
* @see self::getInputOptions
*
* @throws \InvalidArgumentException
*
* @param InputInterface $input
*
* @return self
*/
public static function fromInput(InputInterface $input)
{
$config = new self(['configFile' => self::getConfigFileFromInput($input)]);

// Handle --color and --no-color (and --ansi and --no-ansi aliases)
if (self::getOptionFromInput($input, ['color', 'ansi'])) {
$config->setColorMode(self::COLOR_MODE_FORCED);
} elseif (self::getOptionFromInput($input, ['no-color', 'no-ansi'])) {
$config->setColorMode(self::COLOR_MODE_DISABLED);
}

// Handle verbosity options
if ($verbosity = self::getVerbosityFromInput($input)) {
$config->setVerbosity($verbosity);
}

// Handle interactive mode
if (self::getOptionFromInput($input, ['interactive', 'interaction'], ['-a', '-i'])) {
$config->setInteractiveMode(self::INTERACTIVE_MODE_FORCED);
} elseif (self::getOptionFromInput($input, ['no-interactive', 'no-interaction'], ['-n'])) {
$config->setInteractiveMode(self::INTERACTIVE_MODE_DISABLED);
}

// Handle --raw-output
// @todo support raw output with interactive input?
if (!$config->getInputInteractive()) {
if (self::getOptionFromInput($input, ['raw-output'], ['-r'])) {
$config->setRawOutput(true);
}
}

return $config;
}

/**
* Get the desired config file from the given input.
*
* @return string|null config file path, or null if none is specified
*/
private static function getConfigFileFromInput(InputInterface $input)
{
// Best case, input is properly bound and validated.
if ($input->hasOption('config')) {
return $input->getOption('config');
}

return $input->getParameterOption('--config', null, true) ?: $input->getParameterOption('-c', null, true);
}

/**
* Get a boolean option from the given input.
*
* This helper allows fallback for unbound and unvalidated input. It's not perfect--for example,
* it can't deal with several short options squished together--but it's better than falling over
* any time someone gives us unbound input.
*
* @return bool true if the option (or an alias) is present
*/
private static function getOptionFromInput(InputInterface $input, array $names, array $otherParams = [])
{
// Best case, input is properly bound and validated.
foreach ($names as $name) {
if ($input->hasOption($name) && $input->getOption($name)) {
return true;
}
}

foreach ($names as $name) {
$otherParams[] = '--' . $name;
}

foreach ($otherParams as $name) {
if ($input->hasParameterOption($name, true)) {
return true;
}
}

return false;
}

/**
* Get the desired verbosity from the given input.
*
* This is a bit more complext than the other options parsers. It handles `--quiet` and
* `--verbose`, along with their short aliases, and fancy things like `-vvv`.
*
* @return string|null configuration constant, or null if no verbosity option is specified
*/
private static function getVerbosityFromInput(InputInterface $input)
{
// --quiet wins!
if (self::getOptionFromInput($input, ['quiet'], ['-q'])) {
return self::VERBOSITY_QUIET;
}

// Best case, input is properly bound and validated.
if ($input->hasOption('verbose')) {
switch ($input->getOption('verbose')) {
case '-1':
return self::VERBOSITY_QUIET;
case '0': // explicitly normal, overrides config file default
return self::VERBOSITY_NORMAL;
case '1':
case null: // `--verbose` and `-v`
return self::VERBOSITY_VERBOSE;
case '2':
case 'v': // `-vv`
return self::VERBOSITY_VERY_VERBOSE;
case '3':
case 'vv': // `-vvv`
return self::VERBOSITY_DEBUG;
default: // implicitly normal, config file default wins
return;
}
}

// quiet and normal have to come before verbose, because it eats everything else.
if ($input->hasParameterOption('--verbose=-1', true) || $input->getParameterOption('--verbose', false, true) === '-1') {
return self::VERBOSITY_QUIET;
}

if ($input->hasParameterOption('--verbose=0', true) || $input->getParameterOption('--verbose', false, true) === '0') {
return self::VERBOSITY_NORMAL;
}

// `-vvv`, `-vv` and `-v` have to come in descending length order, because `hasParameterOption` matches prefixes.
if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || $input->getParameterOption('--verbose', false, true) === '3') {
return self::VERBOSITY_DEBUG;
}

if ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || $input->getParameterOption('--verbose', false, true) === '2') {
return self::VERBOSITY_VERY_VERBOSE;
}

if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true)) {
return self::VERBOSITY_VERBOSE;
}
}

/**
* Get a list of input options expected when initializing Configuration via input.
*
* @see self::fromInput
*
* @return InputOption[]
*/
public static function getInputOptions()
{
return [
new InputOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use an alternate PsySH config file location.'),
new InputOption('cwd', null, InputOption::VALUE_REQUIRED, 'Use an alternate working directory.'),

new InputOption('color', null, InputOption::VALUE_NONE, 'Force colors in output.'),
new InputOption('no-color', null, InputOption::VALUE_NONE, 'Disable colors in output.'),
// --ansi and --no-ansi aliases to match Symfony, Composer, etc.
new InputOption('ansi', null, InputOption::VALUE_NONE, 'Force colors in output.'),
new InputOption('no-ansi', null, InputOption::VALUE_NONE, 'Disable colors in output.'),

new InputOption('quiet', 'q', InputOption::VALUE_NONE, 'Shhhhhh.'),
new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_OPTIONAL, 'Increase the verbosity of messages.', '0'),
new InputOption('interactive', 'i|a', InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'),
new InputOption('no-interactive', 'n', InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'),
// --interaction and --no-interaction aliases for compatibility with Symfony, Composer, etc
new InputOption('interaction', null, InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'),
new InputOption('no-interaction', null, InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'),
new InputOption('raw-output', 'r', InputOption::VALUE_NONE, 'Print var_export-style return values (for non-interactive input)'),
];
}

/**
* Initialize the configuration.
*
Expand Down Expand Up @@ -267,12 +456,16 @@ public function loadConfig(array $options)
* The config file may directly manipulate the configuration, or may return
* an array of options which will be merged with the current configuration.
*
* @throws \InvalidArgumentException if the config file returns a non-array result
* @throws \InvalidArgumentException if the config file does not exist or returns a non-array result
*
* @param string $file
*/
public function loadConfigFile($file)
{
if (!\is_file($file)) {
throw new \InvalidArgumentException(\sprintf('Invalid configuration file specified, %s does not exist', $file));
}

$__psysh_config_file__ = $file;
$load = function ($config) use ($__psysh_config_file__) {
$result = require $__psysh_config_file__;
Expand Down
22 changes: 18 additions & 4 deletions src/Shell.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -302,9 +302,8 @@ public function setOutput(OutputInterface $output)
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
if ($input === null && !isset($_SERVER['argv'])) {
$input = new ArgvInput([]);
}
// We'll just ignore the input passed in, and set up our own!
$input = new ArrayInput([]);

if ($output === null) {
$output = $this->config->getOutput();
Expand Down Expand Up @@ -405,6 +404,21 @@ private function doNonInteractiveRun($rawOutput)
$this->afterRun();
}

/**
* Configures the input and output instances based on the user arguments and options.
*/
protected function configureIO(InputInterface $input, OutputInterface $output)
{
// @todo overrides via environment variables (or should these happen in config? ... probably config)
$input->setInteractive($this->config->getInputInteractive());

if ($this->config->getOutputDecorated() !== null) {
$output->setDecorated($this->config->getOutputDecorated());
}

$output->setVerbosity($this->config->getOutputVerbosity());
}

/**
* Load user-defined includes.
*/
Expand Down
47 changes: 12 additions & 35 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,48 +323,24 @@ function bin()

$input = new ArgvInput();
try {
$input->bind(new InputDefinition([
new InputOption('help', 'h', InputOption::VALUE_NONE),
new InputOption('config', 'c', InputOption::VALUE_REQUIRED),
new InputOption('version', 'V', InputOption::VALUE_NONE),
new InputOption('cwd', null, InputOption::VALUE_REQUIRED),

new InputOption('color', null, InputOption::VALUE_NONE),
new InputOption('no-color', null, InputOption::VALUE_NONE),

new InputOption('quiet', 'q', InputOption::VALUE_NONE),
new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE),
new InputOption('no-interaction', 'n', InputOption::VALUE_NONE),
new InputOption('raw-output', 'r', InputOption::VALUE_NONE),
$input->bind(new InputDefinition(\array_merge(Configuration::getInputOptions(), [
new InputOption('help', 'h', InputOption::VALUE_NONE),
new InputOption('version', 'V', InputOption::VALUE_NONE),

new InputArgument('include', InputArgument::IS_ARRAY),
]));
])));
} catch (\RuntimeException $e) {
$usageException = $e;
}

$config = [];

// Handle --config
if ($configFile = $input->getOption('config')) {
$config['configFile'] = $configFile;
}

// Handle --color and --no-color
if ($input->getOption('color') && $input->getOption('no-color')) {
$usageException = new \RuntimeException('Using both "--color" and "--no-color" options is invalid');
} elseif ($input->getOption('color')) {
$config['colorMode'] = Configuration::COLOR_MODE_FORCED;
} elseif ($input->getOption('no-color')) {
$config['colorMode'] = Configuration::COLOR_MODE_DISABLED;
}

// Handle --raw-output
if ($input->getOption('raw-output')) {
$config['rawOutput'] = true;
try {
$config = Configuration::fromInput($input);
} catch (\InvalidArgumentException $e) {
$config = new Configuration();
$usageException = $e;
}

$shell = new Shell(new Configuration($config));
$shell = new Shell($config);

// Handle --help
if ($usageException !== null || $input->getOption('help')) {
Expand All @@ -387,7 +363,8 @@ function bin()
-V, --version Display the PsySH version.
--color Force colors in output.
--no-color Disable colors in output.
-n, --no-interaction Run PsySH without interaction. Requires input from stdin.
-i, --interactive Force PsySH to run in interactive mode.
-n, --no-interactive Run PsySH without interactive input. Requires input from stdin.
-r, --raw-output Print var_export-style return values (for non-interactive input)
-q, --quiet Shhhhhh.
-v|vv|vvv, --verbose Increase the verbosity of messages.
Expand Down
Loading

0 comments on commit fc6cb55

Please sign in to comment.