--- /dev/null
+<?php
+
+/*
+ * This file is part of Psy Shell.
+ *
+ * (c) 2012-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Psy\Input;
+
+use Psy\Exception\RuntimeException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+
+/**
+ * Parse, validate and match --grep, --insensitive and --invert command options.
+ */
+class FilterOptions
+{
+ private $filter = false;
+ private $pattern;
+ private $insensitive;
+ private $invert;
+
+ /**
+ * Get input option definitions for filtering.
+ *
+ * @return InputOption[]
+ */
+ public static function getOptions()
+ {
+ return array(
+ new InputOption('grep', 'G', InputOption::VALUE_REQUIRED, 'Limit to items matching the given pattern (string or regex).'),
+ new InputOption('insensitive', 'i', InputOption::VALUE_NONE, 'Case-insensitive search (requires --grep).'),
+ new InputOption('invert', 'v', InputOption::VALUE_NONE, 'Inverted search (requires --grep).'),
+ );
+ }
+
+ /**
+ * Bind input and prepare filter.
+ *
+ * @param InputInterface $input
+ */
+ public function bind(InputInterface $input)
+ {
+ $this->validateInput($input);
+
+ if (!$pattern = $input->getOption('grep')) {
+ $this->filter = false;
+
+ return;
+ }
+
+ if (!$this->stringIsRegex($pattern)) {
+ $pattern = '/' . preg_quote($pattern, '/') . '/';
+ }
+
+ if ($insensitive = $input->getOption('insensitive')) {
+ $pattern .= 'i';
+ }
+
+ $this->validateRegex($pattern);
+
+ $this->filter = true;
+ $this->pattern = $pattern;
+ $this->insensitive = $insensitive;
+ $this->invert = $input->getOption('invert');
+ }
+
+ /**
+ * Check whether the bound input has filter options.
+ *
+ * @return bool
+ */
+ public function hasFilter()
+ {
+ return $this->filter;
+ }
+
+ /**
+ * Check whether a string matches the current filter options.
+ *
+ * @param string $string
+ * @param array $matches
+ *
+ * @return bool
+ */
+ public function match($string, array &$matches = null)
+ {
+ return $this->filter === false || (preg_match($this->pattern, $string, $matches) xor $this->invert);
+ }
+
+ /**
+ * Validate that grep, invert and insensitive input options are consistent.
+ *
+ * @param InputInterface $input
+ *
+ * @return bool
+ */
+ private function validateInput(InputInterface $input)
+ {
+ if (!$input->getOption('grep')) {
+ foreach (array('invert', 'insensitive') as $option) {
+ if ($input->getOption($option)) {
+ throw new RuntimeException('--' . $option . ' does not make sense without --grep');
+ }
+ }
+ }
+ }
+
+ /**
+ * Check whether a string appears to be a regular expression.
+ *
+ * @param string $string
+ *
+ * @return bool
+ */
+ private function stringIsRegex($string)
+ {
+ return substr($string, 0, 1) === '/' && substr($string, -1) === '/' && strlen($string) >= 3;
+ }
+
+ /**
+ * Validate that $pattern is a valid regular expression.
+ *
+ * @param string $pattern
+ *
+ * @return bool
+ */
+ private function validateRegex($pattern)
+ {
+ set_error_handler(array('Psy\Exception\ErrorException', 'throwException'));
+ try {
+ preg_match($pattern, '');
+ } catch (ErrorException $e) {
+ throw new RuntimeException(str_replace('preg_match(): ', 'Invalid regular expression: ', $e->getRawMessage()));
+ }
+ restore_error_handler();
+ }
+}