Skip to content

Commit

Permalink
Merge pull request #10320 from GromNaN/command-completion
Browse files Browse the repository at this point in the history
Add completion to commands options and arguments
  • Loading branch information
Seldaek committed Jun 1, 2022
2 parents 556450b + 779f3ab commit ef06702
Show file tree
Hide file tree
Showing 35 changed files with 725 additions and 125 deletions.
6 changes: 6 additions & 0 deletions doc/03-cli.md
Expand Up @@ -13,6 +13,12 @@ php composer.phar dump
```
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

The following options are available with every command:
Expand Down
4 changes: 4 additions & 0 deletions src/Composer/Cache.php
Expand Up @@ -248,6 +248,10 @@ public function gcIsNecessary()
return false;
}

if (Platform::isInputCompletionProcess()) {
return false;
}

return !random_int(0, 50);
}

Expand Down
12 changes: 8 additions & 4 deletions src/Composer/Command/ArchiveCommand.php
Expand Up @@ -27,9 +27,9 @@
use Composer\Util\Loop;
use Composer\Util\Platform;
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\InputOption;
use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
Expand All @@ -39,6 +39,10 @@
*/
class ArchiveCommand extends BaseCommand
{
use CompletionTrait;

private const FORMATS = ['tar', 'tar.gz', 'tar.bz2', 'zip'];

/**
* @return void
*/
Expand All @@ -48,9 +52,9 @@ protected function configure(): void
->setName('archive')
->setDescription('Creates an archive of this composer package.')
->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 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('file', null, InputOption::VALUE_REQUIRED, 'Write the archive with the given file name.'
.' Note that the format will be appended.'),
Expand Down
30 changes: 30 additions & 0 deletions src/Composer/Command/BaseCommand.php
Expand Up @@ -15,6 +15,8 @@
use Composer\Composer;
use Composer\Config;
use Composer\Console\Application;
use Composer\Console\Input\InputArgument;
use Composer\Console\Input\InputOption;
use Composer\Factory;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface;
Expand All @@ -24,6 +26,8 @@
use Composer\Package\Version\VersionParser;
use Composer\Plugin\PluginEvents;
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\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -182,6 +186,32 @@ public function setIO(IOInterface $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
*
Expand Down
197 changes: 197 additions & 0 deletions src/Composer/Command/CompletionTrait.php
@@ -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);
});
};
}
}
16 changes: 9 additions & 7 deletions src/Composer/Command/CreateProjectCommand.php
Expand Up @@ -33,9 +33,9 @@
use Composer\Repository\RepositorySet;
use Composer\Script\ScriptEvents;
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\InputOption;
use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile;
Expand All @@ -55,6 +55,8 @@
*/
class CreateProjectCommand extends BaseCommand
{
use CompletionTrait;

/**
* @var SuggestedPackagesReporter
*/
Expand All @@ -69,13 +71,13 @@ protected function configure(): void
->setName('create-project')
->setDescription('Creates new project from a package into given directory.')
->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('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('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-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-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.'),
Expand Down Expand Up @@ -157,7 +159,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$preferSource,
$preferDist,
!$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-scripts'),
$input->getOption('no-progress'),
Expand Down Expand Up @@ -195,7 +197,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $
$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!
$io->loadConfiguration($config);
Expand Down Expand Up @@ -422,7 +424,7 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p
}
}

$platformOverrides = $config->get('platform') ?: array();
$platformOverrides = $config->get('platform');
$platformRepo = new PlatformRepository(array(), $platformOverrides);

// find the latest version if there are multiple
Expand Down
8 changes: 5 additions & 3 deletions src/Composer/Command/DependsCommand.php
Expand Up @@ -14,14 +14,16 @@

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

/**
* @author Niels Keurentjes <niels.keurentjes@omines.com>
*/
class DependsCommand extends BaseDependencyCommand
{
use CompletionTrait;

/**
* Configure command metadata.
*
Expand All @@ -34,7 +36,7 @@ protected function configure(): void
->setAliases(array('why'))
->setDescription('Shows which packages cause the given package to be installed.')
->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_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'),
))
Expand Down

0 comments on commit ef06702

Please sign in to comment.