1
0
Fork 0

Merge pull request #10320 from GromNaN/command-completion

Add completion to commands options and arguments
pull/10815/head
Jordi Boggiano 2022-06-01 21:59:39 +02:00 committed by GitHub
commit ef06702e45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 725 additions and 125 deletions

View File

@ -13,6 +13,12 @@ php composer.phar dump
``` ```
calls `composer dump-autoload`. calls `composer dump-autoload`.
## Bash Completions
To install bash completions you can run `composer completion bash > completion.bash` (put the file
in /etc/bash_completion.d/composer to make it load automatically in new terminals) and then
`source completion.bash` to enable it in the current terminal session.
## Global Options ## Global Options
The following options are available with every command: The following options are available with every command:

View File

@ -248,6 +248,10 @@ class Cache
return false; return false;
} }
if (Platform::isInputCompletionProcess()) {
return false;
}
return !random_int(0, 50); return !random_int(0, 50);
} }

View File

@ -27,9 +27,9 @@ use Composer\Util\Filesystem;
use Composer\Util\Loop; use Composer\Util\Loop;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -39,6 +39,10 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class ArchiveCommand extends BaseCommand class ArchiveCommand extends BaseCommand
{ {
use CompletionTrait;
private const FORMATS = ['tar', 'tar.gz', 'tar.bz2', 'zip'];
/** /**
* @return void * @return void
*/ */
@ -48,9 +52,9 @@ class ArchiveCommand extends BaseCommand
->setName('archive') ->setName('archive')
->setDescription('Creates an archive of this composer package.') ->setDescription('Creates an archive of this composer package.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project'), new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project', null, $this->suggestAvailablePackage()),
new InputArgument('version', InputArgument::OPTIONAL, 'A version constraint to find the package to archive'), new InputArgument('version', InputArgument::OPTIONAL, 'A version constraint to find the package to archive'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)', null, self::FORMATS),
new InputOption('dir', null, InputOption::VALUE_REQUIRED, 'Write the archive to this directory'), new InputOption('dir', null, InputOption::VALUE_REQUIRED, 'Write the archive to this directory'),
new InputOption('file', null, InputOption::VALUE_REQUIRED, 'Write the archive with the given file name.' new InputOption('file', null, InputOption::VALUE_REQUIRED, 'Write the archive with the given file name.'
.' Note that the format will be appended.'), .' Note that the format will be appended.'),

View File

@ -15,6 +15,8 @@ namespace Composer\Command;
use Composer\Composer; use Composer\Composer;
use Composer\Config; use Composer\Config;
use Composer\Console\Application; use Composer\Console\Application;
use Composer\Console\Input\InputArgument;
use Composer\Console\Input\InputOption;
use Composer\Factory; use Composer\Factory;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface;
@ -24,6 +26,8 @@ use Composer\Plugin\PreCommandRunEvent;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -182,6 +186,32 @@ abstract class BaseCommand extends Command
$this->io = $io; $this->io = $io;
} }
/**
* @inheritdoc
*
* Backport suggested values definition from symfony/console 6.1+
*
* TODO drop when PHP 8.1 / symfony 6.1+ can be required
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$definition = $this->getDefinition();
$name = (string) $input->getCompletionName();
if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType()
&& $definition->hasOption($name)
&& ($option = $definition->getOption($name)) instanceof InputOption
) {
$option->complete($input, $suggestions);
} elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
&& $definition->hasArgument($name)
&& ($argument = $definition->getArgument($name)) instanceof InputArgument
) {
$argument->complete($input, $suggestions);
} else {
parent::complete($input, $suggestions);
}
}
/** /**
* @inheritDoc * @inheritDoc
* *

View File

@ -0,0 +1,197 @@
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Command;
use Composer\Composer;
use Composer\Package\BasePackage;
use Composer\Package\PackageInterface;
use Composer\Pcre\Preg;
use Composer\Repository\CompositeRepository;
use Composer\Repository\InstalledRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\RootPackageRepository;
use Symfony\Component\Console\Completion\CompletionInput;
/**
* Adds completion to arguments and options.
*
* @internal
*/
trait CompletionTrait
{
/**
* @see BaseCommand::requireComposer()
*/
abstract public function requireComposer(bool $disablePlugins = null, bool $disableScripts = null): Composer;
/**
* Suggestion values for "prefer-install" option
*
* @return string[]
*/
private function suggestPreferInstall(): array
{
return ['dist', 'source', 'auto'];
}
/**
* Suggest package names from installed.
*/
private function suggestInstalledPackage(bool $includePlatformPackages = false): \Closure
{
return function (CompletionInput $input) use ($includePlatformPackages): array {
$composer = $this->requireComposer();
$installedRepos = [new RootPackageRepository(clone $composer->getPackage())];
$locker = $composer->getLocker();
if ($locker->isLocked()) {
$installedRepos[] = $locker->getLockedRepository(true);
} else {
$installedRepos[] = $composer->getRepositoryManager()->getLocalRepository();
}
$platformHint = [];
if ($includePlatformPackages) {
if ($locker->isLocked()) {
$platformRepo = new PlatformRepository(array(), $locker->getPlatformOverrides());
} else {
$platformRepo = new PlatformRepository(array(), $composer->getConfig()->get('platform'));
}
if ($input->getCompletionValue() === '') {
// to reduce noise, when no text is yet entered we list only two entries for ext- and lib- prefixes
$hintsToFind = ['ext-' => 0, 'lib-' => 0, 'php' => 99, 'composer' => 99];
foreach ($platformRepo->getPackages() as $pkg) {
foreach ($hintsToFind as $hintPrefix => $hintCount) {
if (str_starts_with($pkg->getName(), $hintPrefix)) {
if ($hintCount === 0 || $hintCount >= 99) {
$platformHint[] = $pkg->getName();
$hintsToFind[$hintPrefix]++;
} elseif ($hintCount === 1) {
unset($hintsToFind[$hintPrefix]);
$platformHint[] = substr($pkg->getName(), 0, max(strlen($pkg->getName()) - 3, strlen($hintPrefix) + 1)).'...';
}
continue 2;
}
}
}
} else {
$installedRepos[] = $platformRepo;
}
}
$installedRepo = new InstalledRepository($installedRepos);
return array_merge(
array_map(function (PackageInterface $package) {
return $package->getName();
}, $installedRepo->getPackages()),
$platformHint
);
};
}
/**
* Suggest package names available on all configured repositories.
*/
private function suggestAvailablePackage(int $max = 99): \Closure
{
return function (CompletionInput $input) use ($max): array {
if ($max < 1) {
return [];
}
$composer = $this->requireComposer();
$repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories());
$results = [];
$showVendors = false;
if (!str_contains($input->getCompletionValue(), '/')) {
$results = $repos->search('^' . preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_VENDOR);
$showVendors = true;
}
// if we get a single vendor, we expand it into its contents already
if (\count($results) <= 1) {
$results = $repos->search('^'.preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME);
$showVendors = false;
}
$results = array_column($results, 'name');
if ($showVendors) {
$results = array_map(function (string $name): string {
return $name.'/';
}, $results);
// sort shorter results first to avoid auto-expanding the completion to a longer string than needed
usort($results, function (string $a, string $b) {
$lenA = \strlen($a);
$lenB = \strlen($b);
if ($lenA === $lenB) {
return $a <=> $b;
}
return $lenA - $lenB;
});
$pinned = [];
// ensure if the input is an exact match that it is always in the result set
$completionInput = $input->getCompletionValue().'/';
if (false !== ($exactIndex = array_search($completionInput, $results, true))) {
$pinned[] = $completionInput;
array_splice($results, $exactIndex, 1);
}
return array_merge($pinned, array_slice($results, 0, $max - \count($pinned)));
}
return array_slice($results, 0, $max);
};
}
/**
* Suggest package names available on all configured repositories or
* platform packages from the ones available on the currently-running PHP
*/
private function suggestAvailablePackageInclPlatform(): \Closure
{
return function (CompletionInput $input): array {
if (Preg::isMatch('{^(ext|lib|php)(-|$)|^com}', $input->getCompletionValue())) {
$matches = $this->suggestPlatformPackage()($input);
} else {
$matches = [];
}
return array_merge($matches, $this->suggestAvailablePackage(99 - \count($matches))($input));
};
}
/**
* Suggest platform packages from the ones available on the currently-running PHP
*/
private function suggestPlatformPackage(): \Closure
{
return function (CompletionInput $input): array {
$repos = new PlatformRepository([], $this->requireComposer()->getConfig()->get('platform'));
$pattern = BasePackage::packageNameToRegexp($input->getCompletionValue().'*');
return array_filter(array_map(function (PackageInterface $package) {
return $package->getName();
}, $repos->getPackages()), function (string $name) use ($pattern): bool {
return Preg::isMatch($pattern, $name);
});
};
}
}

View File

@ -33,9 +33,9 @@ use Composer\Repository\InstalledArrayRepository;
use Composer\Repository\RepositorySet; use Composer\Repository\RepositorySet;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
@ -55,6 +55,8 @@ use Composer\Package\Version\VersionParser;
*/ */
class CreateProjectCommand extends BaseCommand class CreateProjectCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @var SuggestedPackagesReporter * @var SuggestedPackagesReporter
*/ */
@ -69,13 +71,13 @@ class CreateProjectCommand extends BaseCommand
->setName('create-project') ->setName('create-project')
->setDescription('Creates new project from a package into given directory.') ->setDescription('Creates new project from a package into given directory.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('package', InputArgument::OPTIONAL, 'Package name to be installed'), new InputArgument('package', InputArgument::OPTIONAL, 'Package name to be installed', null, $this->suggestAvailablePackage()),
new InputArgument('directory', InputArgument::OPTIONAL, 'Directory where the files should be created'), new InputArgument('directory', InputArgument::OPTIONAL, 'Directory where the files should be created'),
new InputArgument('version', InputArgument::OPTIONAL, 'Version, will default to latest'), new InputArgument('version', InputArgument::OPTIONAL, 'Version, will default to latest'),
new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum-stability allowed (unless a version is specified).'), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum-stability allowed (unless a version is specified).'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'),
new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()),
new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories to look the package up, either by URL or using JSON arrays'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories to look the package up, either by URL or using JSON arrays'),
new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'DEPRECATED: Use --repository instead.'), new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'DEPRECATED: Use --repository instead.'),
new InputOption('add-repository', null, InputOption::VALUE_NONE, 'Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.'), new InputOption('add-repository', null, InputOption::VALUE_NONE, 'Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.'),
@ -157,7 +159,7 @@ EOT
$preferSource, $preferSource,
$preferDist, $preferDist,
!$input->getOption('no-dev'), !$input->getOption('no-dev'),
$input->getOption('repository') ?: $input->getOption('repository-url'), \count($input->getOption('repository')) > 0 ? $input->getOption('repository') : $input->getOption('repository-url'),
$input->getOption('no-plugins'), $input->getOption('no-plugins'),
$input->getOption('no-scripts'), $input->getOption('no-scripts'),
$input->getOption('no-progress'), $input->getOption('no-progress'),
@ -195,7 +197,7 @@ EOT
$repositories = (array) $repositories; $repositories = (array) $repositories;
} }
$platformRequirementFilter = $platformRequirementFilter ?: PlatformRequirementFilterFactory::ignoreNothing(); $platformRequirementFilter = $platformRequirementFilter ?? PlatformRequirementFilterFactory::ignoreNothing();
// we need to manually load the configuration to pass the auth credentials to the io interface! // we need to manually load the configuration to pass the auth credentials to the io interface!
$io->loadConfiguration($config); $io->loadConfiguration($config);
@ -422,7 +424,7 @@ EOT
} }
} }
$platformOverrides = $config->get('platform') ?: array(); $platformOverrides = $config->get('platform');
$platformRepo = new PlatformRepository(array(), $platformOverrides); $platformRepo = new PlatformRepository(array(), $platformOverrides);
// find the latest version if there are multiple // find the latest version if there are multiple

View File

@ -14,14 +14,16 @@ namespace Composer\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
/** /**
* @author Niels Keurentjes <niels.keurentjes@omines.com> * @author Niels Keurentjes <niels.keurentjes@omines.com>
*/ */
class DependsCommand extends BaseDependencyCommand class DependsCommand extends BaseDependencyCommand
{ {
use CompletionTrait;
/** /**
* Configure command metadata. * Configure command metadata.
* *
@ -34,7 +36,7 @@ class DependsCommand extends BaseDependencyCommand
->setAliases(array('why')) ->setAliases(array('why'))
->setDescription('Shows which packages cause the given package to be installed.') ->setDescription('Shows which packages cause the given package to be installed.')
->setDefinition(array( ->setDefinition(array(
new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect'), new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestInstalledPackage(true)),
new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'),
new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'),
)) ))

View File

@ -13,9 +13,9 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
/** /**
* @author Davey Shafik <me@daveyshafik.com> * @author Davey Shafik <me@daveyshafik.com>
@ -32,7 +32,9 @@ class ExecCommand extends BaseCommand
->setDescription('Executes a vendored binary/script.') ->setDescription('Executes a vendored binary/script.')
->setDefinition(array( ->setDefinition(array(
new InputOption('list', 'l', InputOption::VALUE_NONE), new InputOption('list', 'l', InputOption::VALUE_NONE),
new InputArgument('binary', InputArgument::OPTIONAL, 'The binary to run, e.g. phpunit'), new InputArgument('binary', InputArgument::OPTIONAL, 'The binary to run, e.g. phpunit', null, function () {
return $this->getBinaries(false);
}),
new InputArgument( new InputArgument(
'args', 'args',
InputArgument::IS_ARRAY | InputArgument::OPTIONAL, InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
@ -52,14 +54,11 @@ EOT
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$composer = $this->requireComposer(); $composer = $this->requireComposer();
$binDir = $composer->getConfig()->get('bin-dir');
if ($input->getOption('list') || null === $input->getArgument('binary')) { if ($input->getOption('list') || null === $input->getArgument('binary')) {
$bins = glob($binDir . '/*'); $bins = $this->getBinaries(true);
$bins = array_merge($bins, array_map(function ($e) { if ([] === $bins) {
return "$e (local)"; $binDir = $composer->getConfig()->get('bin-dir');
}, $composer->getPackage()->getBinaries()));
if (!$bins) {
throw new \RuntimeException("No binaries found in composer.json or in bin-dir ($binDir)"); throw new \RuntimeException("No binaries found in composer.json or in bin-dir ($binDir)");
} }
@ -70,13 +69,6 @@ EOT
); );
foreach ($bins as $bin) { foreach ($bins as $bin) {
// skip .bat copies
if (isset($previousBin) && $bin === $previousBin.'.bat') {
continue;
}
$previousBin = $bin;
$bin = basename($bin);
$this->getIO()->write( $this->getIO()->write(
<<<EOT <<<EOT
<info>- $bin</info> <info>- $bin</info>
@ -105,4 +97,34 @@ EOT
return $dispatcher->dispatchScript('__exec_command', true, $input->getArgument('args')); return $dispatcher->dispatchScript('__exec_command', true, $input->getArgument('args'));
} }
/**
* @param bool $forDisplay
* @return string[]
*/
private function getBinaries(bool $forDisplay): array
{
$composer = $this->requireComposer();
$binDir = $composer->getConfig()->get('bin-dir');
$bins = glob($binDir . '/*');
$localBins = $composer->getPackage()->getBinaries();
if ($forDisplay) {
$localBins = array_map(function ($e) {
return "$e (local)";
}, $localBins);
}
$binaries = [];
foreach (array_merge($bins, $localBins) as $bin) {
// skip .bat copies
if (isset($previousBin) && $bin === $previousBin.'.bat') {
continue;
}
$previousBin = $bin;
$binaries[] = basename($bin);
}
return $binaries;
}
} }

View File

@ -21,7 +21,7 @@ use Composer\Repository\CompositeRepository;
use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Semver\Constraint\MatchAllConstraint;
use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -38,7 +38,7 @@ class FundCommand extends BaseCommand
$this->setName('fund') $this->setName('fund')
->setDescription('Discover how to help fund the maintenance of your dependencies.') ->setDescription('Discover how to help fund the maintenance of your dependencies.')
->setDefinition(array( ->setDefinition(array(
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['text', 'json']),
)) ))
; ;
} }

View File

@ -16,8 +16,11 @@ use Composer\Factory;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -26,6 +29,28 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class GlobalCommand extends BaseCommand class GlobalCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$application = $this->getApplication();
if ($input->mustSuggestArgumentValuesFor('command-name')) {
$suggestions->suggestValues(array_filter(array_map(function (Command $command) {
return $command->isHidden() ? null : $command->getName();
}, $application->all())));
return;
}
if ($application->has($commandName = $input->getArgument('command-name'))) {
$input = $this->prepareSubcommandInput($input, true);
$input = CompletionInput::fromString($input->__toString(), 2);
$command = $application->find($commandName);
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
$command->complete($input, $suggestions);
}
}
/** /**
* @return void * @return void
*/ */
@ -87,6 +112,17 @@ EOT
return parent::run($input, $output); return parent::run($input, $output);
} }
$input = $this->prepareSubcommandInput($input);
return $this->getApplication()->run($input, $output);
}
private function prepareSubcommandInput(InputInterface $input, bool $quiet = false): StringInput
{
if (!method_exists($input, '__toString')) {
throw new \LogicException('Expected an Input instance that is stringable, got '.get_class($input));
}
// The COMPOSER env var should not apply to the global execution scope // The COMPOSER env var should not apply to the global execution scope
if (Platform::getEnv('COMPOSER')) { if (Platform::getEnv('COMPOSER')) {
Platform::clearEnv('COMPOSER'); Platform::clearEnv('COMPOSER');
@ -109,13 +145,15 @@ EOT
} catch (\Exception $e) { } catch (\Exception $e) {
throw new \RuntimeException('Could not switch to home directory "'.$home.'"', 0, $e); throw new \RuntimeException('Could not switch to home directory "'.$home.'"', 0, $e);
} }
if (!$quiet) {
$this->getIO()->writeError('<info>Changed current directory to '.$home.'</info>'); $this->getIO()->writeError('<info>Changed current directory to '.$home.'</info>');
}
// create new input without "global" command prefix // create new input without "global" command prefix
$input = new StringInput(Preg::replace('{\bg(?:l(?:o(?:b(?:a(?:l)?)?)?)?)?\b}', '', $input->__toString(), 1)); $input = new StringInput(Preg::replace('{\bg(?:l(?:o(?:b(?:a(?:l)?)?)?)?)?\b}', '', $input->__toString(), 1));
$this->getApplication()->resetComposer(); $this->getApplication()->resetComposer();
return $this->getApplication()->run($input, $output); return $input;
} }
/** /**

View File

@ -18,8 +18,8 @@ use Composer\Repository\RootPackageRepository;
use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryFactory;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -28,6 +28,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class HomeCommand extends BaseCommand class HomeCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @inheritDoc * @inheritDoc
* *
@ -40,7 +42,7 @@ class HomeCommand extends BaseCommand
->setAliases(array('home')) ->setAliases(array('home'))
->setDescription('Opens the package\'s repository URL or homepage in your browser.') ->setDescription('Opens the package\'s repository URL or homepage in your browser.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY, 'Package(s) to browse to.'), new InputArgument('packages', InputArgument::IS_ARRAY, 'Package(s) to browse to.', null, $this->suggestInstalledPackage()),
new InputOption('homepage', 'H', InputOption::VALUE_NONE, 'Open the homepage instead of the repository URL.'), new InputOption('homepage', 'H', InputOption::VALUE_NONE, 'Open the homepage instead of the repository URL.'),
new InputOption('show', 's', InputOption::VALUE_NONE, 'Only show the homepage or repository URL.'), new InputOption('show', 's', InputOption::VALUE_NONE, 'Only show the homepage or repository URL.'),
)) ))

View File

@ -25,7 +25,7 @@ use Composer\Util\Filesystem;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
@ -37,6 +37,7 @@ use Symfony\Component\Console\Helper\FormatterHelper;
*/ */
class InitCommand extends BaseCommand class InitCommand extends BaseCommand
{ {
use CompletionTrait;
use PackageDiscoveryTrait; use PackageDiscoveryTrait;
/** @var array<string, string> */ /** @var array<string, string> */
@ -58,8 +59,8 @@ class InitCommand extends BaseCommand
new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'),
new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'),
new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'),
new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()),
new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()),
new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'),
new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'),
new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'),

View File

@ -17,8 +17,8 @@ use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -29,6 +29,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class InstallCommand extends BaseCommand class InstallCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @return void * @return void
*/ */
@ -41,7 +43,7 @@ class InstallCommand extends BaseCommand
->setDefinition(array( ->setDefinition(array(
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'),
new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'),
new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'),

View File

@ -12,6 +12,7 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\Console\Input\InputOption;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackageInterface;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
@ -22,7 +23,6 @@ use Composer\Util\PackageInfo;
use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
@ -40,7 +40,7 @@ class LicensesCommand extends BaseCommand
->setName('licenses') ->setName('licenses')
->setDescription('Shows information about licenses of dependencies.') ->setDescription('Shows information about licenses of dependencies.')
->setDefinition(array( ->setDefinition(array(
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text, json or summary', 'text'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text, json or summary', 'text', ['text', 'json', 'summary']),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'),
)) ))
->setHelp( ->setHelp(

View File

@ -13,9 +13,9 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -23,6 +23,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class OutdatedCommand extends BaseCommand class OutdatedCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @return void * @return void
*/ */
@ -32,7 +34,7 @@ class OutdatedCommand extends BaseCommand
->setName('outdated') ->setName('outdated')
->setDescription('Shows a list of installed packages that have updates available, including their latest version.') ->setDescription('Shows a list of installed packages that have updates available, including their latest version.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.'), new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestInstalledPackage()),
new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show only packages that are outdated (this is the default, but present here for compat with `show`'), new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show only packages that are outdated (this is the default, but present here for compat with `show`'),
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all installed packages with their latest versions'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all installed packages with their latest versions'),
new InputOption('locked', null, InputOption::VALUE_NONE, 'Shows updates for packages from the lock file, regardless of what is currently in vendor dir'), new InputOption('locked', null, InputOption::VALUE_NONE, 'Shows updates for packages from the lock file, regardless of what is currently in vendor dir'),
@ -40,8 +42,8 @@ class OutdatedCommand extends BaseCommand
new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'),
new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'),
new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --outdated option.'), new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --outdated option.'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']),
new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.'), new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage()),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'),
new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'),
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'),

View File

@ -99,7 +99,7 @@ trait PackageDiscoveryTrait
foreach ($requires as $requirement) { foreach ($requires as $requirement) {
if (!isset($requirement['version'])) { if (!isset($requirement['version'])) {
// determine the best version automatically // determine the best version automatically
list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, null, null, $fixed); list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $fixed);
$requirement['version'] = $version; $requirement['version'] = $version;
// replace package name from packagist.org // replace package name from packagist.org
@ -268,7 +268,7 @@ trait PackageDiscoveryTrait
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return array{string, string} name version * @return array{string, string} name version
*/ */
private function findBestVersionAndNameForPackage(InputInterface $input, string $name, ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', ?string $requiredVersion = null, ?string $minimumStability = null, bool $fixed = false): array private function findBestVersionAndNameForPackage(InputInterface $input, string $name, ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', bool $fixed = false): array
{ {
// handle ignore-platform-reqs flag if present // handle ignore-platform-reqs flag if present
if ($input->hasOption('ignore-platform-reqs') && $input->hasOption('ignore-platform-req')) { if ($input->hasOption('ignore-platform-reqs') && $input->hasOption('ignore-platform-req')) {
@ -278,17 +278,17 @@ trait PackageDiscoveryTrait
} }
// find the latest version allowed in this repo set // find the latest version allowed in this repo set
$repoSet = $this->getRepositorySet($input, $minimumStability); $repoSet = $this->getRepositorySet($input);
$versionSelector = new VersionSelector($repoSet, $platformRepo); $versionSelector = new VersionSelector($repoSet, $platformRepo);
$effectiveMinimumStability = $minimumStability ?? $this->getMinimumStability($input); $effectiveMinimumStability = $this->getMinimumStability($input);
$package = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter); $package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter);
if (false === $package) { if (false === $package) {
// platform packages can not be found in the pool in versions other than the local platform's has // platform packages can not be found in the pool in versions other than the local platform's has
// so if platform reqs are ignored we just take the user's word for it // so if platform reqs are ignored we just take the user's word for it
if ($platformRequirementFilter->isIgnored($name)) { if ($platformRequirementFilter->isIgnored($name)) {
return array($name, $requiredVersion ?: '*'); return array($name, '*');
} }
// Check if it is a virtual package provided by others // Check if it is a virtual package provided by others
@ -308,17 +308,16 @@ trait PackageDiscoveryTrait
} }
// Check whether the package requirements were the problem // Check whether the package requirements were the problem
if (!($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter) && false !== ($candidate = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, PlatformRequirementFilterFactory::ignoreAll()))) { if (!($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter) && false !== ($candidate = $versionSelector->findBestCandidate($name, null, $preferredStability, PlatformRequirementFilterFactory::ignoreAll()))) {
throw new \InvalidArgumentException(sprintf( throw new \InvalidArgumentException(sprintf(
'Package %s%s has requirements incompatible with your PHP version, PHP extensions and Composer version' . $this->getPlatformExceptionDetails($candidate, $platformRepo), 'Package %s has requirements incompatible with your PHP version, PHP extensions and Composer version' . $this->getPlatformExceptionDetails($candidate, $platformRepo),
$name, $name
is_string($requiredVersion) ? ' at version '.$requiredVersion : ''
)); ));
} }
// Check whether the minimum stability was the problem but the package exists // Check whether the minimum stability was the problem but the package exists
if (false !== ($package = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) { if (false !== ($package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) {
// we must first verify if a valid package would be found in a lower priority repository // we must first verify if a valid package would be found in a lower priority repository
if (false !== ($allReposPackage = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_SHADOWED_REPOSITORIES))) { if (false !== ($allReposPackage = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_SHADOWED_REPOSITORIES))) {
throw new \InvalidArgumentException( throw new \InvalidArgumentException(
'Package '.$name.' exists in '.$allReposPackage->getRepository()->getRepoName().' and '.$package->getRepository()->getRepoName().' which has a higher repository priority. The packages from the higher priority repository do not match your minimum-stability and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.' 'Package '.$name.' exists in '.$allReposPackage->getRepository()->getRepoName().' and '.$package->getRepository()->getRepoName().' which has a higher repository priority. The packages from the higher priority repository do not match your minimum-stability and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.'
); );
@ -330,21 +329,6 @@ trait PackageDiscoveryTrait
$effectiveMinimumStability $effectiveMinimumStability
)); ));
} }
// Check whether the required version was the problem
if (is_string($requiredVersion) && false !== ($package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter))) {
// we must first verify if a valid package would be found in a lower priority repository
if (false !== ($allReposPackage = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, PlatformRequirementFilterFactory::ignoreNothing(), RepositorySet::ALLOW_SHADOWED_REPOSITORIES))) {
throw new \InvalidArgumentException(
'Package '.$name.' exists in '.$allReposPackage->getRepository()->getRepoName().' and '.$package->getRepository()->getRepoName().' which has a higher repository priority. The packages from the higher priority repository do not match your constraint and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.'
);
}
throw new \InvalidArgumentException(sprintf(
'Could not find package %s in a version matching "%s" and a stability matching "'.$effectiveMinimumStability.'".',
$name,
$requiredVersion
));
}
// Check whether the PHP version was the problem for all versions // Check whether the PHP version was the problem for all versions
if (!$platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter && false !== ($candidate = $versionSelector->findBestCandidate($name, null, $preferredStability, PlatformRequirementFilterFactory::ignoreAll(), RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) { if (!$platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter && false !== ($candidate = $versionSelector->findBestCandidate($name, null, $preferredStability, PlatformRequirementFilterFactory::ignoreAll(), RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) {
$additional = ''; $additional = '';

View File

@ -14,14 +14,16 @@ namespace Composer\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
/** /**
* @author Niels Keurentjes <niels.keurentjes@omines.com> * @author Niels Keurentjes <niels.keurentjes@omines.com>
*/ */
class ProhibitsCommand extends BaseDependencyCommand class ProhibitsCommand extends BaseDependencyCommand
{ {
use CompletionTrait;
/** /**
* Configure command metadata. * Configure command metadata.
* *
@ -34,7 +36,7 @@ class ProhibitsCommand extends BaseDependencyCommand
->setAliases(array('why-not')) ->setAliases(array('why-not'))
->setDescription('Shows which packages prevent the given package from being installed.') ->setDescription('Shows which packages prevent the given package from being installed.')
->setDefinition(array( ->setDefinition(array(
new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect'), new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestAvailablePackage()),
new InputArgument(self::ARGUMENT_CONSTRAINT, InputArgument::REQUIRED, 'Version constraint, which version you expected to be installed'), new InputArgument(self::ARGUMENT_CONSTRAINT, InputArgument::REQUIRED, 'Version constraint, which version you expected to be installed'),
new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'),
new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'),

View File

@ -23,8 +23,8 @@ use Composer\Plugin\PluginEvents;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -32,6 +32,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class ReinstallCommand extends BaseCommand class ReinstallCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @return void * @return void
*/ */
@ -43,7 +45,7 @@ class ReinstallCommand extends BaseCommand
->setDefinition(array( ->setDefinition(array(
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'),
new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()),
new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'),
@ -52,7 +54,7 @@ class ReinstallCommand extends BaseCommand
new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'),
new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'),
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'),
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.', null, $this->suggestInstalledPackage()),
)) ))
->setHelp( ->setHelp(
<<<EOT <<<EOT

View File

@ -21,8 +21,8 @@ use Composer\Plugin\PluginEvents;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Factory; use Composer\Factory;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
@ -32,6 +32,8 @@ use Composer\Package\BasePackage;
*/ */
class RemoveCommand extends BaseCommand class RemoveCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @return void * @return void
*/ */
@ -41,7 +43,7 @@ class RemoveCommand extends BaseCommand
->setName('remove') ->setName('remove')
->setDescription('Removes a package from the require or require-dev.') ->setDescription('Removes a package from the require or require-dev.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.', null, $this->suggestInstalledPackage(true)),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),

View File

@ -15,8 +15,8 @@ namespace Composer\Command;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Composer\Factory; use Composer\Factory;
use Composer\Installer; use Composer\Installer;
@ -39,6 +39,7 @@ use Composer\Util\Silencer;
*/ */
class RequireCommand extends BaseCommand class RequireCommand extends BaseCommand
{ {
use CompletionTrait;
use PackageDiscoveryTrait; use PackageDiscoveryTrait;
/** @var bool */ /** @var bool */
@ -67,12 +68,12 @@ class RequireCommand extends BaseCommand
->setName('require') ->setName('require')
->setDescription('Adds required packages to your composer.json and installs them.') ->setDescription('Adds required packages to your composer.json and installs them.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'),
new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()),
new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'),
new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),

View File

@ -17,8 +17,8 @@ use Composer\Script\ScriptEvents;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -54,7 +54,9 @@ class RunScriptCommand extends BaseCommand
->setAliases(array('run')) ->setAliases(array('run'))
->setDescription('Runs the scripts defined in composer.json.') ->setDescription('Runs the scripts defined in composer.json.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('script', InputArgument::OPTIONAL, 'Script name to run.'), new InputArgument('script', InputArgument::OPTIONAL, 'Script name to run.', null, function () {
return array_keys($this->requireComposer()->getPackage()->getScripts());
}),
new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''),
new InputOption('timeout', null, InputOption::VALUE_REQUIRED, 'Sets script timeout in seconds, or 0 for never.'), new InputOption('timeout', null, InputOption::VALUE_REQUIRED, 'Sets script timeout in seconds, or 0 for never.'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'),

View File

@ -13,8 +13,8 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**

View File

@ -16,8 +16,8 @@ use Composer\Factory;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
@ -42,7 +42,7 @@ class SearchCommand extends BaseCommand
new InputOption('only-name', 'N', InputOption::VALUE_NONE, 'Search only in package names'), new InputOption('only-name', 'N', InputOption::VALUE_NONE, 'Search only in package names'),
new InputOption('only-vendor', 'O', InputOption::VALUE_NONE, 'Search only for vendor / organization names, returns only "vendor" as result'), new InputOption('only-vendor', 'O', InputOption::VALUE_NONE, 'Search only for vendor / organization names, returns only "vendor" as result'),
new InputOption('type', 't', InputOption::VALUE_REQUIRED, 'Search for a specific package type'), new InputOption('type', 't', InputOption::VALUE_REQUIRED, 'Search for a specific package type'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']),
new InputArgument('tokens', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'tokens to search for'), new InputArgument('tokens', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'tokens to search for'),
)) ))
->setHelp( ->setHelp(

View File

@ -24,8 +24,8 @@ use Composer\IO\IOInterface;
use Composer\Downloader\FilesystemException; use Composer\Downloader\FilesystemException;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;

View File

@ -20,7 +20,6 @@ use Composer\Package\BasePackage;
use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackageInterface;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\Package;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Package\Version\VersionSelector; use Composer\Package\Version\VersionSelector;
@ -41,11 +40,12 @@ use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Semver; use Composer\Semver\Semver;
use Composer\Spdx\SpdxLicenses; use Composer\Spdx\SpdxLicenses;
use Composer\Util\PackageInfo; use Composer\Util\PackageInfo;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -56,6 +56,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class ShowCommand extends BaseCommand class ShowCommand extends BaseCommand
{ {
use CompletionTrait;
/** @var VersionParser */ /** @var VersionParser */
protected $versionParser; protected $versionParser;
/** @var string[] */ /** @var string[] */
@ -74,7 +76,7 @@ class ShowCommand extends BaseCommand
->setAliases(array('info')) ->setAliases(array('info'))
->setDescription('Shows information about packages.') ->setDescription('Shows information about packages.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.'), new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestPackageBasedOnMode()),
new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'), new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'),
new InputOption('all', null, InputOption::VALUE_NONE, 'List all packages'), new InputOption('all', null, InputOption::VALUE_NONE, 'List all packages'),
new InputOption('locked', null, InputOption::VALUE_NONE, 'List all locked packages'), new InputOption('locked', null, InputOption::VALUE_NONE, 'List all locked packages'),
@ -87,12 +89,12 @@ class ShowCommand extends BaseCommand
new InputOption('tree', 't', InputOption::VALUE_NONE, 'List the dependencies as a tree'), new InputOption('tree', 't', InputOption::VALUE_NONE, 'List the dependencies as a tree'),
new InputOption('latest', 'l', InputOption::VALUE_NONE, 'Show the latest version'), new InputOption('latest', 'l', InputOption::VALUE_NONE, 'Show the latest version'),
new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show the latest version but only for packages that are outdated'), new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show the latest version but only for packages that are outdated'),
new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.'), new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage()),
new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'),
new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --outdated option.'), new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --outdated option.'),
new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'), new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'),
new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'),
new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'),
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'),
@ -108,6 +110,21 @@ EOT
; ;
} }
protected function suggestPackageBasedOnMode(): \Closure
{
return function (CompletionInput $input) {
if ($input->getOption('available') || $input->getOption('all')) {
return $this->suggestAvailablePackageInclPlatform()($input);
}
if ($input->getOption('platform')) {
return $this->suggestPlatformPackage()($input);
}
return $this->suggestInstalledPackage()($input);
};
}
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$this->versionParser = new VersionParser; $this->versionParser = new VersionParser;
@ -170,7 +187,7 @@ EOT
// init repos // init repos
$platformOverrides = array(); $platformOverrides = array();
if ($composer) { if ($composer) {
$platformOverrides = $composer->getConfig()->get('platform') ?: array(); $platformOverrides = $composer->getConfig()->get('platform');
} }
$platformRepo = new PlatformRepository(array(), $platformOverrides); $platformRepo = new PlatformRepository(array(), $platformOverrides);
$lockedRepo = null; $lockedRepo = null;

View File

@ -13,7 +13,7 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Composer\Downloader\ChangeReportInterface; use Composer\Downloader\ChangeReportInterface;
use Composer\Downloader\DvcsDownloaderInterface; use Composer\Downloader\DvcsDownloaderInterface;

View File

@ -16,13 +16,15 @@ use Composer\Repository\PlatformRepository;
use Composer\Repository\RootPackageRepository; use Composer\Repository\RootPackageRepository;
use Composer\Repository\InstalledRepository; use Composer\Repository\InstalledRepository;
use Composer\Installer\SuggestedPackagesReporter; use Composer\Installer\SuggestedPackagesReporter;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class SuggestsCommand extends BaseCommand class SuggestsCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @return void * @return void
*/ */
@ -37,7 +39,7 @@ class SuggestsCommand extends BaseCommand
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'),
new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'), new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'),
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.', null, $this->suggestInstalledPackage()),
)) ))
->setHelp( ->setHelp(
<<<EOT <<<EOT
@ -63,7 +65,7 @@ EOT
$installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides()); $installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides());
$installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev')); $installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev'));
} else { } else {
$installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform'));
$installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository();
} }

View File

@ -27,8 +27,8 @@ use Composer\Semver\Constraint\MultiConstraint;
use Composer\Package\Link; use Composer\Package\Link;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Composer\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\Question;
@ -38,6 +38,8 @@ use Symfony\Component\Console\Question\Question;
*/ */
class UpdateCommand extends BaseCommand class UpdateCommand extends BaseCommand
{ {
use CompletionTrait;
/** /**
* @return void * @return void
*/ */
@ -48,11 +50,11 @@ class UpdateCommand extends BaseCommand
->setAliases(array('u', 'upgrade')) ->setAliases(array('u', 'upgrade'))
->setDescription('Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file.') ->setDescription('Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.', null, $this->suggestInstalledPackage()),
new InputOption('with', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0'), new InputOption('with', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'),
new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),

View File

@ -467,7 +467,7 @@ class Application extends BaseApplication
if (null === $this->composer) { if (null === $this->composer) {
try { try {
$this->composer = Factory::create($this->io, null, $disablePlugins, $disableScripts); $this->composer = Factory::create(Platform::isInputCompletionProcess() ? new NullIO() : $this->io, null, $disablePlugins, $disableScripts);
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
if ($required) { if ($required) {
$this->io->writeError($e->getMessage()); $this->io->writeError($e->getMessage());

View File

@ -0,0 +1,69 @@
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Console\Input;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputArgument as BaseInputArgument;
/**
* Backport suggested values definition from symfony/console 6.1+
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*
* @internal
*
* TODO drop when PHP 8.1 / symfony 6.1+ can be required
*/
class InputArgument extends BaseInputArgument
{
/**
* @var string[]|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion>
*/
private $suggestedValues;
/**
* @param string $name The argument name
* @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
* @param string $description A description text
* @param string|bool|int|float|string[]|null $default The default value (for self::OPTIONAL mode only)
* @param string[]|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @throws InvalidArgumentException When argument mode is not valid
*/
public function __construct(string $name, int $mode = null, string $description = '', $default = null, $suggestedValues = [])
{
parent::__construct($name, $mode, $description, $default);
$this->suggestedValues = $suggestedValues;
}
/**
* Adds suggestions to $suggestions for the current completion input.
*
* @see Command::complete()
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$values = $this->suggestedValues;
if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore-line
throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values)));
}
if ([] !== $values) {
$suggestions->suggestValues($values);
}
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Console\Input;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputOption as BaseInputOption;
/**
* Backport suggested values definition from symfony/console 6.1+
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*
* @internal
*
* TODO drop when PHP 8.1 / symfony 6.1+ can be required
*/
class InputOption extends BaseInputOption
{
/**
* @var string[]|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion>
*/
private $suggestedValues;
/**
* @param string|string[]|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the VALUE_* constants
* @param string|bool|int|float|string[]|null $default The default value (must be null for self::VALUE_NONE)
* @param string[]|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completionnull for self::VALUE_NONE)
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null, $suggestedValues = [])
{
parent::__construct($name, $shortcut, $mode, $description, $default);
$this->suggestedValues = $suggestedValues;
if ([] !== $suggestedValues && !$this->acceptValue()) {
throw new LogicException('Cannot set suggested values if the option does not accept a value.');
}
}
/**
* Adds suggestions to $suggestions for the current completion input.
*
* @see Command::complete()
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$values = $this->suggestedValues;
if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore-line
throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values)));
}
if ([] !== $values) {
$suggestions->suggestValues($values);
}
}
}

View File

@ -309,6 +309,7 @@ class Factory
throw new \InvalidArgumentException($message.PHP_EOL.$instructions); throw new \InvalidArgumentException($message.PHP_EOL.$instructions);
} }
if (!Platform::isInputCompletionProcess()) {
try { try {
$file->validateSchema(JsonFile::LAX_SCHEMA); $file->validateSchema(JsonFile::LAX_SCHEMA);
} catch (JsonValidationException $e) { } catch (JsonValidationException $e) {
@ -316,6 +317,7 @@ class Factory
$message = $e->getMessage() . ':' . PHP_EOL . $errors; $message = $e->getMessage() . ':' . PHP_EOL . $errors;
throw new JsonValidationException($message); throw new JsonValidationException($message);
} }
}
$localConfig = $file->read(); $localConfig = $file->read();
$localConfigSource = $file->getPath(); $localConfigSource = $file->getPath();
@ -702,6 +704,10 @@ class Factory
*/ */
private static function validateJsonSchema(?IOInterface $io, $fileOrData, int $schema = JsonFile::LAX_SCHEMA, ?string $source = null): void private static function validateJsonSchema(?IOInterface $io, $fileOrData, int $schema = JsonFile::LAX_SCHEMA, ?string $source = null): void
{ {
if (Platform::isInputCompletionProcess()) {
return;
}
try { try {
if ($fileOrData instanceof JsonFile) { if ($fileOrData instanceof JsonFile) {
$fileOrData->validateSchema($schema); $fileOrData->validateSchema($schema);

View File

@ -19,6 +19,7 @@ use Composer\IO\NullIO;
use Composer\Semver\VersionParser as SemverVersionParser; use Composer\Semver\VersionParser as SemverVersionParser;
use Composer\Util\Git as GitUtil; use Composer\Util\Git as GitUtil;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Svn as SvnUtil; use Composer\Util\Svn as SvnUtil;
use React\Promise\CancellablePromiseInterface; use React\Promise\CancellablePromiseInterface;
@ -74,6 +75,12 @@ class VersionGuesser
return null; return null;
} }
// bypass version guessing in bash completions as it takes time to create
// new processes and the root version is usually not that important
if (Platform::isInputCompletionProcess()) {
return null;
}
$versionData = $this->guessGitVersion($packageConfig, $path); $versionData = $this->guessGitVersion($packageConfig, $path);
if (null !== $versionData && null !== $versionData['version']) { if (null !== $versionData && null !== $versionData['version']) {
return $this->postprocess($versionData); return $this->postprocess($versionData);

View File

@ -228,6 +228,14 @@ class Platform
return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
} }
/**
* @return bool Whether the current command is for bash completion
*/
public static function isInputCompletionProcess(): bool
{
return '_complete' === ($_SERVER['argv'][1] ?? null);
}
/** /**
* @return void * @return void
*/ */

View File

@ -0,0 +1,112 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test;
use Composer\Console\Application;
use Symfony\Component\Console\Tester\CommandCompletionTester;
/**
* Validate autocompletion for all commands.
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class CompletionFunctionalTest extends TestCase
{
/**
* @return iterable<array<string|string[]|null>>
*/
public function getCommandSuggestions(): iterable
{
$randomVendor = 'a/';
$installedPackages = ['composer/semver', 'psr/log'];
$preferInstall = ['dist', 'source', 'auto'];
yield ['archive ', [$randomVendor]];
yield ['archive symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']];
yield ['archive --format ', ['tar', 'zip']];
yield ['create-project ', [$randomVendor]];
yield ['create-project symfony/skeleton --prefer-install ', $preferInstall];
yield ['depends ', $installedPackages];
yield ['why ', $installedPackages];
yield ['exec ', ['composer', 'jsonlint', 'phpstan', 'phpstan.phar', 'simple-phpunit', 'validate-json']];
yield ['browse ', $installedPackages];
yield ['home -H ', $installedPackages];
yield ['init --require ', [$randomVendor]];
yield ['init --require-dev foo/bar --require-dev ', [$randomVendor]];
yield ['install --prefer-install ', $preferInstall];
yield ['install ', null];
yield ['outdated ', $installedPackages];
yield ['prohibits ', [$randomVendor]];
yield ['why-not symfony/http-ker', ['symfony/http-kernel']];
yield ['reinstall --prefer-install ', $preferInstall];
yield ['reinstall ', $installedPackages];
yield ['remove ', $installedPackages];
yield ['require --prefer-install ', $preferInstall];
yield ['require ', [$randomVendor]];
yield ['require --dev symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']];
yield ['run-script ', ['compile', 'test', 'phpstan']];
yield ['run-script test ', null];
yield ['search --format ', ['text', 'json']];
yield ['show --format ', ['text', 'json']];
yield ['info ', $installedPackages];
yield ['suggests ', $installedPackages];
yield ['update --prefer-install ', $preferInstall];
yield ['update ', $installedPackages];
}
/**
* @dataProvider getCommandSuggestions
*
* @param string $input The command that is typed
* @param string[]|null $expectedSuggestions Sample expected suggestions. Null if nothing is expected.
*/
public function testComplete(string $input, ?array $expectedSuggestions): void
{
$input = explode(' ', $input);
$commandName = array_shift($input);
$command = $this->getApplication()->get($commandName);
$tester = new CommandCompletionTester($command);
$suggestions = $tester->complete($input);
if (null === $expectedSuggestions) {
$this->assertEmpty($suggestions);
return;
}
$diff = array_diff($expectedSuggestions, $suggestions);
$this->assertEmpty($diff, sprintf('Suggestions must contain "%s". Got "%s".', implode('", "', $diff), implode('", "', $suggestions)));
}
private function getApplication(): Application
{
return new Application();
}
}