* Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\DefaultPolicy; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Package\PackageInterface; use Composer\Util\Platform; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\ArrayRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\ComposerRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryFactory; use Composer\Spdx\SpdxLicenses; /** * @author Robert Schönthal * @author Jordi Boggiano * @author Jérémy Romey */ class ShowCommand extends BaseCommand { protected $versionParser; protected $colors; protected function configure() { $this ->setName('show') ->setAliases(array('info')) ->setDescription('Show information about packages') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'Package 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('installed', 'i', InputOption::VALUE_NONE, 'List installed packages only (enabled by default, only present for BC).'), new InputOption('platform', 'p', InputOption::VALUE_NONE, 'List platform packages only'), new InputOption('available', 'a', InputOption::VALUE_NONE, 'List available packages only'), new InputOption('self', 's', InputOption::VALUE_NONE, 'Show the root package information'), new InputOption('name-only', 'N', InputOption::VALUE_NONE, 'List package names only'), new InputOption('path', 'P', InputOption::VALUE_NONE, 'Show package paths'), new InputOption('tree', 't', InputOption::VALUE_NONE, 'List the dependencies as a tree'), )) ->setHelp(<<versionParser = new VersionParser; if ($input->getOption('tree')) { $this->initStyles($output); } $composer = $this->getComposer(false); $io = $this->getIO(); if ($input->getOption('installed')) { $io->writeError('You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.'); } if ($input->getOption('tree') && ($input->getOption('all') || $input->getOption('available'))) { $io->writeError('The --tree (-t) option is not usable in combination with --all or --available (-a)'); return; } // init repos $platformOverrides = array(); if ($composer) { $platformOverrides = $composer->getConfig()->get('platform') ?: array(); } $platformRepo = new PlatformRepository(array(), $platformOverrides); if ($input->getOption('self')) { $package = $this->getComposer()->getPackage(); $repos = $installedRepo = new ArrayRepository(array($package)); } elseif ($input->getOption('platform')) { $repos = $installedRepo = $platformRepo; } elseif ($input->getOption('available')) { $installedRepo = $platformRepo; if ($composer) { $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); } else { $defaultRepos = RepositoryFactory::defaultRepos($io); $repos = new CompositeRepository($defaultRepos); $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); } } elseif ($input->getOption('all') && $composer) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $installedRepo = new CompositeRepository(array($localRepo, $platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); } elseif ($input->getOption('all')) { $defaultRepos = RepositoryFactory::defaultRepos($io); $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); $installedRepo = $platformRepo; $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } else { $repos = $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); } if ($composer) { $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'show', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); } // show single package or single version if ($input->getArgument('package') || !empty($package)) { if (empty($package)) { list($package, $versions) = $this->getPackage($installedRepo, $repos, $input->getArgument('package'), $input->getArgument('version')); if (!$package) { throw new \InvalidArgumentException('Package '.$input->getArgument('package').' not found'); } } else { $versions = array($package->getPrettyVersion() => $package->getVersion()); } if ($input->getOption('tree')) { $this->displayPackageTree($package, $installedRepo, $repos); } else { $this->printMeta($package, $versions, $installedRepo); $this->printLinks($package, 'requires'); $this->printLinks($package, 'devRequires', 'requires (dev)'); if ($package->getSuggests()) { $io->write("\nsuggests"); foreach ($package->getSuggests() as $suggested => $reason) { $io->write($suggested . ' ' . $reason . ''); } } $this->printLinks($package, 'provides'); $this->printLinks($package, 'conflicts'); $this->printLinks($package, 'replaces'); } return; } // show tree view if requested if ($input->getOption('tree')) { $rootPackage = $this->getComposer()->getPackage(); $rootRequires = array_map( 'strtolower', array_keys(array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires())) ); foreach ($installedRepo->getPackages() as $package) { if (in_array($package->getName(), $rootRequires, true)) { $this->displayPackageTree($package, $installedRepo, $repos); } } return 0; } if ($repos instanceof CompositeRepository) { $repos = $repos->getRepositories(); } elseif (!is_array($repos)) { $repos = array($repos); } // list packages $packages = array(); foreach ($repos as $repo) { if ($repo === $platformRepo) { $type = 'platform:'; } elseif ( $repo === $installedRepo || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true)) ) { $type = 'installed:'; } else { $type = 'available:'; } if ($repo instanceof ComposerRepository && $repo->hasProviders()) { foreach ($repo->getProviderNames() as $name) { $packages[$type][$name] = $name; } } else { foreach ($repo->getPackages() as $package) { if (!isset($packages[$type][$package->getName()]) || !is_object($packages[$type][$package->getName()]) || version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<') ) { $packages[$type][$package->getName()] = $package; } } } } $showAllTypes = $input->getOption('all'); $indent = $showAllTypes ? ' ' : ''; foreach (array('platform:' => true, 'available:' => false, 'installed:' => true) as $type => $showVersion) { if (isset($packages[$type])) { if ($showAllTypes) { $io->write($type); } ksort($packages[$type]); $nameLength = $versionLength = 0; foreach ($packages[$type] as $package) { if (is_object($package)) { $nameLength = max($nameLength, strlen($package->getPrettyName())); $versionLength = max($versionLength, strlen($package->getFullPrettyVersion())); } else { $nameLength = max($nameLength, $package); } } list($width) = $this->getApplication()->getTerminalDimensions(); if (null === $width) { // In case the width is not detected, we're probably running the command // outside of a real terminal, use space without a limit $width = PHP_INT_MAX; } if (Platform::isWindows()) { $width--; } if ($input->getOption('path') && null === $composer) { $io->writeError('No composer.json found in the current directory, disabling "path" option'); $input->setOption('path', false); } $writePath = !$input->getOption('name-only') && $input->getOption('path'); $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion && ($nameLength + $versionLength + 3 <= $width); $writeDescription = !$input->getOption('name-only') && !$input->getOption('path') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width); foreach ($packages[$type] as $package) { if (is_object($package)) { $io->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); if ($writeVersion) { $io->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false); } if ($writeDescription) { $description = strtok($package->getDescription(), "\r\n"); $remaining = $width - $nameLength - $versionLength - 4; if (strlen($description) > $remaining) { $description = substr($description, 0, $remaining - 3) . '...'; } $io->write(' ' . $description, false); } if ($writePath) { $path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n"); $io->write(' ' . $path, false); } } else { $io->write($indent . $package, false); } $io->write(''); } if ($showAllTypes) { $io->write(''); } } } } /** * finds a package by name and version if provided * * @param RepositoryInterface $installedRepo * @param RepositoryInterface $repos * @param string $name * @param string $version * @throws \InvalidArgumentException * @return array array(CompletePackageInterface, array of versions) */ protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null) { $name = strtolower($name); $constraint = null; if (is_string($version)) { $constraint = $this->versionParser->parseConstraints($version); } $policy = new DefaultPolicy(); $pool = new Pool('dev'); $pool->addRepository($repos); $matchedPackage = null; $versions = array(); $matches = $pool->whatProvides($name, $constraint); foreach ($matches as $index => $package) { // skip providers/replacers if ($package->getName() !== $name) { unset($matches[$index]); continue; } // select an exact match if it is in the installed repo and no specific version was required if (null === $version && $installedRepo->hasPackage($package)) { $matchedPackage = $package; } $versions[$package->getPrettyVersion()] = $package->getVersion(); $matches[$index] = $package->getId(); } // select preferred package according to policy rules if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) { $matchedPackage = $pool->literalToPackage($preferred[0]); } return array($matchedPackage, $versions); } /** * prints package meta data */ protected function printMeta(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo) { $io = $this->getIO(); $io->write('name : ' . $package->getPrettyName()); $io->write('descrip. : ' . $package->getDescription()); $io->write('keywords : ' . join(', ', $package->getKeywords() ?: array())); $this->printVersions($package, $versions, $installedRepo); $io->write('type : ' . $package->getType()); $this->printLicenses($package); $io->write('source : ' . sprintf('[%s] %s %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference())); $io->write('dist : ' . sprintf('[%s] %s %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference())); $io->write('names : ' . implode(', ', $package->getNames())); if ($package->isAbandoned()) { $replacement = ($package->getReplacementPackage() !== null) ? ' The author suggests using the ' . $package->getReplacementPackage(). ' package instead.' : null; $io->writeError( sprintf('Attention: This package is abandoned and no longer maintained.%s', $replacement) ); } if ($package->getSupport()) { $io->write("\nsupport"); foreach ($package->getSupport() as $type => $value) { $io->write('' . $type . ' : '.$value); } } if ($package->getAutoload()) { $io->write("\nautoload"); foreach ($package->getAutoload() as $type => $autoloads) { $io->write('' . $type . ''); if ($type === 'psr-0') { foreach ($autoloads as $name => $path) { $io->write(($name ?: '*') . ' => ' . (is_array($path) ? implode(', ', $path) : ($path ?: '.'))); } } elseif ($type === 'psr-4') { foreach ($autoloads as $name => $path) { $io->write(($name ?: '*') . ' => ' . (is_array($path) ? implode(', ', $path) : ($path ?: '.'))); } } elseif ($type === 'classmap') { $io->write(implode(', ', $autoloads)); } } if ($package->getIncludePaths()) { $io->write('include-path'); $io->write(implode(', ', $package->getIncludePaths())); } } } /** * prints all available versions of this package and highlights the installed one if any */ protected function printVersions(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo) { uasort($versions, 'version_compare'); $versions = array_keys(array_reverse($versions)); // highlight installed version if ($installedRepo->hasPackage($package)) { $installedVersion = $package->getPrettyVersion(); $key = array_search($installedVersion, $versions); if (false !== $key) { $versions[$key] = '* ' . $installedVersion . ''; } } $versions = implode(', ', $versions); $this->getIO()->write('versions : ' . $versions); } /** * print link objects * * @param CompletePackageInterface $package * @param string $linkType * @param string $title */ protected function printLinks(CompletePackageInterface $package, $linkType, $title = null) { $title = $title ?: $linkType; $io = $this->getIO(); if ($links = $package->{'get'.ucfirst($linkType)}()) { $io->write("\n" . $title . ""); foreach ($links as $link) { $io->write($link->getTarget() . ' ' . $link->getPrettyConstraint() . ''); } } } /** * Prints the licenses of a package with metadata * * @param CompletePackageInterface $package */ protected function printLicenses(CompletePackageInterface $package) { $spdxLicenses = new SpdxLicenses(); $licenses = $package->getLicense(); $io = $this->getIO(); foreach ($licenses as $licenseId) { $license = $spdxLicenses->getLicenseByIdentifier($licenseId); // keys: 0 fullname, 1 osi, 2 url if (!$license) { $out = $licenseId; } else { // is license OSI approved? if ($license[1] === true) { $out = sprintf('%s (%s) (OSI approved) %s', $license[0], $licenseId, $license[2]); } else { $out = sprintf('%s (%s) %s', $license[0], $licenseId, $license[2]); } } $io->write('license : ' . $out); } } /** * Init styles for tree * * @param OutputInterface $output */ protected function initStyles(OutputInterface $output) { $this->colors = array( 'green', 'yellow', 'cyan', 'magenta', 'blue', ); foreach ($this->colors as $color) { $style = new OutputFormatterStyle($color); $output->getFormatter()->setStyle($color, $style); } } /** * Display the tree * * @param PackageInterface|string $package * @param RepositoryInterface $installedRepo * @param RepositoryInterface $distantRepos */ protected function displayPackageTree(PackageInterface $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos) { $io = $this->getIO(); $io->write(sprintf('%s', $package->getPrettyName()), false); $io->write(' ' . $package->getPrettyVersion(), false); $io->write(' ' . strtok($package->getDescription(), "\r\n")); if (is_object($package)) { $requires = $package->getRequires(); $treeBar = '├'; $j = 0; $total = count($requires); foreach ($requires as $requireName => $require) { $j++; if ($j == 0) { $this->writeTreeLine($treeBar); } if ($j == $total) { $treeBar = '└'; } $level = 1; $color = $this->colors[$level]; $info = sprintf('%s──<%s>%s %s', $treeBar, $color, $requireName, $color, $require->getPrettyConstraint()); $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); $packagesInTree = array($package->getName(), $requireName); $this->displayTree($requireName, $require, $installedRepo, $distantRepos, $packagesInTree, $treeBar, $level + 1); } } } /** * Display a package tree * * @param string $name * @param PackageInterface|string $package * @param RepositoryInterface $installedRepo * @param RepositoryInterface $distantRepos * @param array $packagesInTree * @param string $previousTreeBar * @param int $level */ protected function displayTree($name, $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos, array $packagesInTree, $previousTreeBar = '├', $level = 1) { $previousTreeBar = str_replace('├', '│', $previousTreeBar); list($package, $versions) = $this->getPackage($installedRepo, $distantRepos, $name, $package->getPrettyConstraint() === 'self.version' ? $package->getConstraint() : $package->getPrettyConstraint()); if (is_object($package)) { $requires = $package->getRequires(); $treeBar = $previousTreeBar . ' ├'; $i = 0; $total = count($requires); foreach ($requires as $requireName => $require) { $currentTree = $packagesInTree; $i++; if ($i == $total) { $treeBar = $previousTreeBar . ' └'; } $colorIdent = $level % count($this->colors); $color = $this->colors[$colorIdent]; $circularWarn = in_array($requireName, $currentTree) ? '(circular dependency aborted here)' : ''; $info = rtrim(sprintf('%s──<%s>%s %s %s', $treeBar, $color, $requireName, $color, $require->getPrettyConstraint(), $circularWarn)); $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); if (!in_array($requireName, $currentTree)) { $currentTree[] = $requireName; $this->displayTree($requireName, $require, $installedRepo, $distantRepos, $currentTree, $treeBar, $level + 1); } } } } private function writeTreeLine($line) { $io = $this->getIO(); if (!$io->isDecorated()) { $line = str_replace(array('└', '├', '──', '│'), array('`-', '|-', '-', '|'), $line); } $io->write($line); } }