TYPO3  7.6
QuestionHelper.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11 
12 namespace Symfony\Component\Console\Helper;
13 
19 
25 class QuestionHelper extends Helper
26 {
27  private $inputStream;
28  private static $shell;
29  private static $stty;
30 
42  public function ask(InputInterface $input, OutputInterface $output, Question $question)
43  {
44  if (!$input->isInteractive()) {
45  return $question->getDefault();
46  }
47 
48  if (!$question->getValidator()) {
49  return $this->doAsk($output, $question);
50  }
51 
52  $that = $this;
53 
54  $interviewer = function () use ($output, $question, $that) {
55  return $that->doAsk($output, $question);
56  };
57 
58  return $this->validateAttempts($interviewer, $output, $question);
59  }
60 
70  public function setInputStream($stream)
71  {
72  if (!is_resource($stream)) {
73  throw new \InvalidArgumentException('Input stream must be a valid resource.');
74  }
75 
76  $this->inputStream = $stream;
77  }
78 
84  public function getInputStream()
85  {
86  return $this->inputStream;
87  }
88 
92  public function getName()
93  {
94  return 'question';
95  }
96 
110  public function doAsk(OutputInterface $output, Question $question)
111  {
112  $this->writePrompt($output, $question);
113 
114  $inputStream = $this->inputStream ?: STDIN;
115  $autocomplete = $question->getAutocompleterValues();
116 
117  if (null === $autocomplete || !$this->hasSttyAvailable()) {
118  $ret = false;
119  if ($question->isHidden()) {
120  try {
121  $ret = trim($this->getHiddenResponse($output, $inputStream));
122  } catch (\RuntimeException $e) {
123  if (!$question->isHiddenFallback()) {
124  throw $e;
125  }
126  }
127  }
128 
129  if (false === $ret) {
130  $ret = fgets($inputStream, 4096);
131  if (false === $ret) {
132  throw new \RuntimeException('Aborted');
133  }
134  $ret = trim($ret);
135  }
136  } else {
137  $ret = trim($this->autocomplete($output, $question, $inputStream));
138  }
139 
140  $ret = strlen($ret) > 0 ? $ret : $question->getDefault();
141 
142  if ($normalizer = $question->getNormalizer()) {
143  return $normalizer($ret);
144  }
145 
146  return $ret;
147  }
148 
155  protected function writePrompt(OutputInterface $output, Question $question)
156  {
157  $message = $question->getQuestion();
158 
159  if ($question instanceof ChoiceQuestion) {
160  $width = max(array_map('strlen', array_keys($question->getChoices())));
161 
162  $messages = (array) $question->getQuestion();
163  foreach ($question->getChoices() as $key => $value) {
164  $messages[] = sprintf(" [<info>%-${width}s</info>] %s", $key, $value);
165  }
166 
167  $output->writeln($messages);
168 
169  $message = $question->getPrompt();
170  }
171 
172  $output->write($message);
173  }
174 
181  protected function writeError(OutputInterface $output, \Exception $error)
182  {
183  if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
184  $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
185  } else {
186  $message = '<error>'.$error->getMessage().'</error>';
187  }
188 
189  $output->writeln($message);
190  }
191 
200  private function autocomplete(OutputInterface $output, Question $question, $inputStream)
201  {
202  $autocomplete = $question->getAutocompleterValues();
203  $ret = '';
204 
205  $i = 0;
206  $ofs = -1;
207  $matches = $autocomplete;
208  $numMatches = count($matches);
209 
210  $sttyMode = shell_exec('stty -g');
211 
212  // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
213  shell_exec('stty -icanon -echo');
214 
215  // Add highlighted text style
216  $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
217 
218  // Read a keypress
219  while (!feof($inputStream)) {
220  $c = fread($inputStream, 1);
221 
222  // Backspace Character
223  if ("\177" === $c) {
224  if (0 === $numMatches && 0 !== $i) {
225  --$i;
226  // Move cursor backwards
227  $output->write("\033[1D");
228  }
229 
230  if ($i === 0) {
231  $ofs = -1;
232  $matches = $autocomplete;
233  $numMatches = count($matches);
234  } else {
235  $numMatches = 0;
236  }
237 
238  // Pop the last character off the end of our string
239  $ret = substr($ret, 0, $i);
240  } elseif ("\033" === $c) {
241  // Did we read an escape sequence?
242  $c .= fread($inputStream, 2);
243 
244  // A = Up Arrow. B = Down Arrow
245  if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
246  if ('A' === $c[2] && -1 === $ofs) {
247  $ofs = 0;
248  }
249 
250  if (0 === $numMatches) {
251  continue;
252  }
253 
254  $ofs += ('A' === $c[2]) ? -1 : 1;
255  $ofs = ($numMatches + $ofs) % $numMatches;
256  }
257  } elseif (ord($c) < 32) {
258  if ("\t" === $c || "\n" === $c) {
259  if ($numMatches > 0 && -1 !== $ofs) {
260  $ret = $matches[$ofs];
261  // Echo out remaining chars for current match
262  $output->write(substr($ret, $i));
263  $i = strlen($ret);
264  }
265 
266  if ("\n" === $c) {
267  $output->write($c);
268  break;
269  }
270 
271  $numMatches = 0;
272  }
273 
274  continue;
275  } else {
276  $output->write($c);
277  $ret .= $c;
278  ++$i;
279 
280  $numMatches = 0;
281  $ofs = 0;
282 
283  foreach ($autocomplete as $value) {
284  // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
285  if (0 === strpos($value, $ret) && $i !== strlen($value)) {
286  $matches[$numMatches++] = $value;
287  }
288  }
289  }
290 
291  // Erase characters from cursor to end of line
292  $output->write("\033[K");
293 
294  if ($numMatches > 0 && -1 !== $ofs) {
295  // Save cursor position
296  $output->write("\0337");
297  // Write highlighted text
298  $output->write('<hl>'.substr($matches[$ofs], $i).'</hl>');
299  // Restore cursor position
300  $output->write("\0338");
301  }
302  }
303 
304  // Reset stty so it behaves normally again
305  shell_exec(sprintf('stty %s', $sttyMode));
306 
307  return $ret;
308  }
309 
319  private function getHiddenResponse(OutputInterface $output, $inputStream)
320  {
321  if ('\\' === DIRECTORY_SEPARATOR) {
322  $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
323 
324  // handle code running from a phar
325  if ('phar:' === substr(__FILE__, 0, 5)) {
326  $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
327  copy($exe, $tmpExe);
328  $exe = $tmpExe;
329  }
330 
331  $value = rtrim(shell_exec($exe));
332  $output->writeln('');
333 
334  if (isset($tmpExe)) {
335  unlink($tmpExe);
336  }
337 
338  return $value;
339  }
340 
341  if ($this->hasSttyAvailable()) {
342  $sttyMode = shell_exec('stty -g');
343 
344  shell_exec('stty -echo');
345  $value = fgets($inputStream, 4096);
346  shell_exec(sprintf('stty %s', $sttyMode));
347 
348  if (false === $value) {
349  throw new \RuntimeException('Aborted');
350  }
351 
352  $value = trim($value);
353  $output->writeln('');
354 
355  return $value;
356  }
357 
358  if (false !== $shell = $this->getShell()) {
359  $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
360  $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
361  $value = rtrim(shell_exec($command));
362  $output->writeln('');
363 
364  return $value;
365  }
366 
367  throw new \RuntimeException('Unable to hide the response.');
368  }
369 
381  private function validateAttempts($interviewer, OutputInterface $output, Question $question)
382  {
383  $error = null;
384  $attempts = $question->getMaxAttempts();
385  while (null === $attempts || $attempts--) {
386  if (null !== $error) {
387  $this->writeError($output, $error);
388  }
389 
390  try {
391  return call_user_func($question->getValidator(), $interviewer());
392  } catch (\Exception $error) {
393  }
394  }
395 
396  throw $error;
397  }
398 
404  private function getShell()
405  {
406  if (null !== self::$shell) {
407  return self::$shell;
408  }
409 
410  self::$shell = false;
411 
412  if (file_exists('/usr/bin/env')) {
413  // handle other OSs with bash/zsh/ksh/csh if available to hide the answer
414  $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
415  foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) {
416  if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
417  self::$shell = $sh;
418  break;
419  }
420  }
421  }
422 
423  return self::$shell;
424  }
425 
431  private function hasSttyAvailable()
432  {
433  if (null !== self::$stty) {
434  return self::$stty;
435  }
436 
437  exec('stty 2>&1', $output, $exitcode);
438 
439  return self::$stty = $exitcode === 0;
440  }
441 }