Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Yaml\Command; Chris@0: Chris@0: use Symfony\Component\Console\Command\Command; Chris@16: use Symfony\Component\Console\Exception\InvalidArgumentException; Chris@16: use Symfony\Component\Console\Exception\RuntimeException; Chris@0: use Symfony\Component\Console\Input\InputInterface; Chris@0: use Symfony\Component\Console\Input\InputOption; Chris@0: use Symfony\Component\Console\Output\OutputInterface; Chris@0: use Symfony\Component\Console\Style\SymfonyStyle; Chris@0: use Symfony\Component\Yaml\Exception\ParseException; Chris@0: use Symfony\Component\Yaml\Parser; Chris@14: use Symfony\Component\Yaml\Yaml; Chris@0: Chris@0: /** Chris@0: * Validates YAML files syntax and outputs encountered errors. Chris@0: * Chris@0: * @author Grégoire Pineau Chris@0: * @author Robin Chalas Chris@0: */ Chris@0: class LintCommand extends Command Chris@0: { Chris@14: protected static $defaultName = 'lint:yaml'; Chris@14: Chris@0: private $parser; Chris@0: private $format; Chris@0: private $displayCorrectFiles; Chris@0: private $directoryIteratorProvider; Chris@0: private $isReadableProvider; Chris@0: Chris@0: public function __construct($name = null, $directoryIteratorProvider = null, $isReadableProvider = null) Chris@0: { Chris@0: parent::__construct($name); Chris@0: Chris@0: $this->directoryIteratorProvider = $directoryIteratorProvider; Chris@0: $this->isReadableProvider = $isReadableProvider; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function configure() Chris@0: { Chris@0: $this Chris@0: ->setDescription('Lints a file and outputs encountered errors') Chris@0: ->addArgument('filename', null, 'A file or a directory or STDIN') Chris@0: ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') Chris@14: ->addOption('parse-tags', null, InputOption::VALUE_NONE, 'Parse custom tags') Chris@0: ->setHelp(<<%command.name% command lints a YAML file and outputs to STDOUT Chris@0: the first encountered syntax error. Chris@0: Chris@0: You can validates YAML contents passed from STDIN: Chris@0: Chris@0: cat filename | php %command.full_name% Chris@0: Chris@0: You can also validate the syntax of a file: Chris@0: Chris@0: php %command.full_name% filename Chris@0: Chris@0: Or of a whole directory: Chris@0: Chris@0: php %command.full_name% dirname Chris@0: php %command.full_name% dirname --format=json Chris@0: Chris@0: EOF Chris@0: ) Chris@0: ; Chris@0: } Chris@0: Chris@0: protected function execute(InputInterface $input, OutputInterface $output) Chris@0: { Chris@0: $io = new SymfonyStyle($input, $output); Chris@0: $filename = $input->getArgument('filename'); Chris@0: $this->format = $input->getOption('format'); Chris@0: $this->displayCorrectFiles = $output->isVerbose(); Chris@14: $flags = $input->getOption('parse-tags') ? Yaml::PARSE_CUSTOM_TAGS : 0; Chris@0: Chris@0: if (!$filename) { Chris@0: if (!$stdin = $this->getStdin()) { Chris@16: throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); Chris@0: } Chris@0: Chris@17: return $this->display($io, [$this->validate($stdin, $flags)]); Chris@0: } Chris@0: Chris@0: if (!$this->isReadable($filename)) { Chris@16: throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); Chris@0: } Chris@0: Chris@17: $filesInfo = []; Chris@0: foreach ($this->getFiles($filename) as $file) { Chris@14: $filesInfo[] = $this->validate(file_get_contents($file), $flags, $file); Chris@0: } Chris@0: Chris@0: return $this->display($io, $filesInfo); Chris@0: } Chris@0: Chris@14: private function validate($content, $flags, $file = null) Chris@0: { Chris@14: $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) { Chris@14: if (E_USER_DEPRECATED === $level) { Chris@14: throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1); Chris@14: } Chris@14: Chris@14: return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false; Chris@14: }); Chris@14: Chris@0: try { Chris@14: $this->getParser()->parse($content, Yaml::PARSE_CONSTANT | $flags); Chris@0: } catch (ParseException $e) { Chris@17: return ['file' => $file, 'line' => $e->getParsedLine(), 'valid' => false, 'message' => $e->getMessage()]; Chris@14: } finally { Chris@14: restore_error_handler(); Chris@0: } Chris@0: Chris@17: return ['file' => $file, 'valid' => true]; Chris@0: } Chris@0: Chris@0: private function display(SymfonyStyle $io, array $files) Chris@0: { Chris@0: switch ($this->format) { Chris@0: case 'txt': Chris@0: return $this->displayTxt($io, $files); Chris@0: case 'json': Chris@0: return $this->displayJson($io, $files); Chris@0: default: Chris@16: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format)); Chris@0: } Chris@0: } Chris@0: Chris@0: private function displayTxt(SymfonyStyle $io, array $filesInfo) Chris@0: { Chris@17: $countFiles = \count($filesInfo); Chris@0: $erroredFiles = 0; Chris@0: Chris@0: foreach ($filesInfo as $info) { Chris@0: if ($info['valid'] && $this->displayCorrectFiles) { Chris@0: $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); Chris@0: } elseif (!$info['valid']) { Chris@0: ++$erroredFiles; Chris@0: $io->text(' ERROR '.($info['file'] ? sprintf(' in %s', $info['file']) : '')); Chris@0: $io->text(sprintf(' >> %s', $info['message'])); Chris@0: } Chris@0: } Chris@0: Chris@14: if (0 === $erroredFiles) { Chris@0: $io->success(sprintf('All %d YAML files contain valid syntax.', $countFiles)); Chris@0: } else { Chris@0: $io->warning(sprintf('%d YAML files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); Chris@0: } Chris@0: Chris@0: return min($erroredFiles, 1); Chris@0: } Chris@0: Chris@0: private function displayJson(SymfonyStyle $io, array $filesInfo) Chris@0: { Chris@0: $errors = 0; Chris@0: Chris@0: array_walk($filesInfo, function (&$v) use (&$errors) { Chris@0: $v['file'] = (string) $v['file']; Chris@0: if (!$v['valid']) { Chris@0: ++$errors; Chris@0: } Chris@0: }); Chris@0: Chris@0: $io->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); Chris@0: Chris@0: return min($errors, 1); Chris@0: } Chris@0: Chris@0: private function getFiles($fileOrDirectory) Chris@0: { Chris@0: if (is_file($fileOrDirectory)) { Chris@0: yield new \SplFileInfo($fileOrDirectory); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { Chris@17: if (!\in_array($file->getExtension(), ['yml', 'yaml'])) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: yield $file; Chris@0: } Chris@0: } Chris@0: Chris@0: private function getStdin() Chris@0: { Chris@0: if (0 !== ftell(STDIN)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $inputs = ''; Chris@0: while (!feof(STDIN)) { Chris@0: $inputs .= fread(STDIN, 1024); Chris@0: } Chris@0: Chris@0: return $inputs; Chris@0: } Chris@0: Chris@0: private function getParser() Chris@0: { Chris@0: if (!$this->parser) { Chris@0: $this->parser = new Parser(); Chris@0: } Chris@0: Chris@0: return $this->parser; Chris@0: } Chris@0: Chris@0: private function getDirectoryIterator($directory) Chris@0: { Chris@0: $default = function ($directory) { Chris@0: return new \RecursiveIteratorIterator( Chris@0: new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), Chris@0: \RecursiveIteratorIterator::LEAVES_ONLY Chris@0: ); Chris@0: }; Chris@0: Chris@0: if (null !== $this->directoryIteratorProvider) { Chris@17: return \call_user_func($this->directoryIteratorProvider, $directory, $default); Chris@0: } Chris@0: Chris@0: return $default($directory); Chris@0: } Chris@0: Chris@0: private function isReadable($fileOrDirectory) Chris@0: { Chris@0: $default = function ($fileOrDirectory) { Chris@0: return is_readable($fileOrDirectory); Chris@0: }; Chris@0: Chris@0: if (null !== $this->isReadableProvider) { Chris@17: return \call_user_func($this->isReadableProvider, $fileOrDirectory, $default); Chris@0: } Chris@0: Chris@0: return $default($fileOrDirectory); Chris@0: } Chris@0: }