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:
14: use Symfony\Component\Console\Output\OutputInterface;
15: use Symfony\Component\Console\Formatter\OutputFormatterStyle;
16:
17: /**
18: * The Dialog class provides helpers to interact with the user.
19: *
20: * @author Fabien Potencier <fabien@symfony.com>
21: */
22: class DialogHelper extends Helper
23: {
24: private $inputStream;
25: private static $shell;
26: private static $stty;
27:
28: /**
29: * Asks the user to select a value.
30: *
31: * @param OutputInterface $output An Output instance
32: * @param string|array $question The question to ask
33: * @param array $choices List of choices to pick from
34: * @param Boolean $default The default answer if the user enters nothing
35: * @param Boolean|integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
36: * @param string $errorMessage Message which will be shown if invalid value from choice list would be picked
37: *
38: * @return integer|string The selected value (the key of the choices array)
39: *
40: * @throws \InvalidArgumentException
41: */
42: public function select(OutputInterface $output, $question, $choices, $default = null, $attempts = false, $errorMessage = 'Value "%s" is invalid')
43: {
44: $width = max(array_map('strlen', array_keys($choices)));
45:
46: $messages = (array) $question;
47: foreach ($choices as $key => $value) {
48: $messages[] = sprintf(" [<info>%-${width}s</info>] %s", $key, $value);
49: }
50:
51: $output->writeln($messages);
52:
53: $result = $this->askAndValidate($output, '> ', function ($picked) use ($choices, $errorMessage) {
54: if (empty($choices[$picked])) {
55: throw new \InvalidArgumentException(sprintf($errorMessage, $picked));
56: }
57:
58: return $picked;
59: }, $attempts, $default);
60:
61: return $result;
62: }
63:
64: /**
65: * Asks a question to the user.
66: *
67: * @param OutputInterface $output An Output instance
68: * @param string|array $question The question to ask
69: * @param string $default The default answer if none is given by the user
70: * @param array $autocomplete List of values to autocomplete
71: *
72: * @return string The user answer
73: *
74: * @throws \RuntimeException If there is no data to read in the input stream
75: */
76: public function ask(OutputInterface $output, $question, $default = null, array $autocomplete = null)
77: {
78: $output->write($question);
79:
80: $inputStream = $this->inputStream ?: STDIN;
81:
82: if (null === $autocomplete || !$this->hasSttyAvailable()) {
83: $ret = fgets($inputStream, 4096);
84: if (false === $ret) {
85: throw new \RuntimeException('Aborted');
86: }
87: $ret = trim($ret);
88: } else {
89: $ret = '';
90:
91: $i = 0;
92: $ofs = -1;
93: $matches = $autocomplete;
94: $numMatches = count($matches);
95:
96: $sttyMode = shell_exec('stty -g');
97:
98: // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
99: shell_exec('stty -icanon -echo');
100:
101: // Add highlighted text style
102: $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
103:
104: // Read a keypress
105: while ($c = fread($inputStream, 1)) {
106: // Backspace Character
107: if ("\177" === $c) {
108: if (0 === $numMatches && 0 !== $i) {
109: $i--;
110: // Move cursor backwards
111: $output->write("\033[1D");
112: }
113:
114: if ($i === 0) {
115: $ofs = -1;
116: $matches = $autocomplete;
117: $numMatches = count($matches);
118: } else {
119: $numMatches = 0;
120: }
121:
122: // Pop the last character off the end of our string
123: $ret = substr($ret, 0, $i);
124: } elseif ("\033" === $c) { // Did we read an escape sequence?
125: $c .= fread($inputStream, 2);
126:
127: // A = Up Arrow. B = Down Arrow
128: if ('A' === $c[2] || 'B' === $c[2]) {
129: if ('A' === $c[2] && -1 === $ofs) {
130: $ofs = 0;
131: }
132:
133: if (0 === $numMatches) {
134: continue;
135: }
136:
137: $ofs += ('A' === $c[2]) ? -1 : 1;
138: $ofs = ($numMatches + $ofs) % $numMatches;
139: }
140: } elseif (ord($c) < 32) {
141: if ("\t" === $c || "\n" === $c) {
142: if ($numMatches > 0 && -1 !== $ofs) {
143: $ret = $matches[$ofs];
144: // Echo out remaining chars for current match
145: $output->write(substr($ret, $i));
146: $i = strlen($ret);
147: }
148:
149: if ("\n" === $c) {
150: $output->write($c);
151: break;
152: }
153:
154: $numMatches = 0;
155: }
156:
157: continue;
158: } else {
159: $output->write($c);
160: $ret .= $c;
161: $i++;
162:
163: $numMatches = 0;
164: $ofs = 0;
165:
166: foreach ($autocomplete as $value) {
167: // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
168: if (0 === strpos($value, $ret) && $i !== strlen($value)) {
169: $matches[$numMatches++] = $value;
170: }
171: }
172: }
173:
174: // Erase characters from cursor to end of line
175: $output->write("\033[K");
176:
177: if ($numMatches > 0 && -1 !== $ofs) {
178: // Save cursor position
179: $output->write("\0337");
180: // Write highlighted text
181: $output->write('<hl>' . substr($matches[$ofs], $i) . '</hl>');
182: // Restore cursor position
183: $output->write("\0338");
184: }
185: }
186:
187: // Reset stty so it behaves normally again
188: shell_exec(sprintf('stty %s', $sttyMode));
189: }
190:
191: return strlen($ret) > 0 ? $ret : $default;
192: }
193:
194: /**
195: * Asks a confirmation to the user.
196: *
197: * The question will be asked until the user answers by nothing, yes, or no.
198: *
199: * @param OutputInterface $output An Output instance
200: * @param string|array $question The question to ask
201: * @param Boolean $default The default answer if the user enters nothing
202: *
203: * @return Boolean true if the user has confirmed, false otherwise
204: */
205: public function askConfirmation(OutputInterface $output, $question, $default = true)
206: {
207: $answer = 'z';
208: while ($answer && !in_array(strtolower($answer[0]), array('y', 'n'))) {
209: $answer = $this->ask($output, $question);
210: }
211:
212: if (false === $default) {
213: return $answer && 'y' == strtolower($answer[0]);
214: }
215:
216: return !$answer || 'y' == strtolower($answer[0]);
217: }
218:
219: /**
220: * Asks a question to the user, the response is hidden
221: *
222: * @param OutputInterface $output An Output instance
223: * @param string|array $question The question
224: * @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
225: *
226: * @return string The answer
227: *
228: * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
229: */
230: public function askHiddenResponse(OutputInterface $output, $question, $fallback = true)
231: {
232: if (defined('PHP_WINDOWS_VERSION_BUILD')) {
233: $exe = __DIR__ . '/../Resources/bin/hiddeninput.exe';
234:
235: // handle code running from a phar
236: if ('phar:' === substr(__FILE__, 0, 5)) {
237: $tmpExe = sys_get_temp_dir() . '/hiddeninput.exe';
238: copy($exe, $tmpExe);
239: $exe = $tmpExe;
240: }
241:
242: $output->write($question);
243: $value = rtrim(shell_exec($exe));
244: $output->writeln('');
245:
246: if (isset($tmpExe)) {
247: unlink($tmpExe);
248: }
249:
250: return $value;
251: }
252:
253: if ($this->hasSttyAvailable()) {
254: $output->write($question);
255:
256: $sttyMode = shell_exec('stty -g');
257:
258: shell_exec('stty -echo');
259: $value = fgets($this->inputStream ?: STDIN, 4096);
260: shell_exec(sprintf('stty %s', $sttyMode));
261:
262: if (false === $value) {
263: throw new \RuntimeException('Aborted');
264: }
265:
266: $value = trim($value);
267: $output->writeln('');
268:
269: return $value;
270: }
271:
272: if (false !== $shell = $this->getShell()) {
273: $output->write($question);
274: $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
275: $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
276: $value = rtrim(shell_exec($command));
277: $output->writeln('');
278:
279: return $value;
280: }
281:
282: if ($fallback) {
283: return $this->ask($output, $question);
284: }
285:
286: throw new \RuntimeException('Unable to hide the response');
287: }
288:
289: /**
290: * Asks for a value and validates the response.
291: *
292: * The validator receives the data to validate. It must return the
293: * validated data when the data is valid and throw an exception
294: * otherwise.
295: *
296: * @param OutputInterface $output An Output instance
297: * @param string|array $question The question to ask
298: * @param callable $validator A PHP callback
299: * @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
300: * @param string $default The default answer if none is given by the user
301: * @param array $autocomplete List of values to autocomplete
302: *
303: * @return mixed
304: *
305: * @throws \Exception When any of the validators return an error
306: */
307: public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null, array $autocomplete = null)
308: {
309: $that = $this;
310:
311: $interviewer = function() use ($output, $question, $default, $autocomplete, $that) {
312: return $that->ask($output, $question, $default, $autocomplete);
313: };
314:
315: return $this->validateAttempts($interviewer, $output, $validator, $attempts);
316: }
317:
318: /**
319: * Asks for a value, hide and validates the response.
320: *
321: * The validator receives the data to validate. It must return the
322: * validated data when the data is valid and throw an exception
323: * otherwise.
324: *
325: * @param OutputInterface $output An Output instance
326: * @param string|array $question The question to ask
327: * @param callable $validator A PHP callback
328: * @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
329: * @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
330: *
331: * @return string The response
332: *
333: * @throws \Exception When any of the validators return an error
334: * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
335: *
336: */
337: public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true)
338: {
339: $that = $this;
340:
341: $interviewer = function() use ($output, $question, $fallback, $that) {
342: return $that->askHiddenResponse($output, $question, $fallback);
343: };
344:
345: return $this->validateAttempts($interviewer, $output, $validator, $attempts);
346: }
347:
348: /**
349: * Sets the input stream to read from when interacting with the user.
350: *
351: * This is mainly useful for testing purpose.
352: *
353: * @param resource $stream The input stream
354: */
355: public function setInputStream($stream)
356: {
357: $this->inputStream = $stream;
358: }
359:
360: /**
361: * Returns the helper's input stream
362: *
363: * @return string
364: */
365: public function getInputStream()
366: {
367: return $this->inputStream;
368: }
369:
370: /**
371: * {@inheritDoc}
372: */
373: public function getName()
374: {
375: return 'dialog';
376: }
377:
378: /**
379: * Return a valid unix shell
380: *
381: * @return string|Boolean The valid shell name, false in case no valid shell is found
382: */
383: private function getShell()
384: {
385: if (null !== self::$shell) {
386: return self::$shell;
387: }
388:
389: self::$shell = false;
390:
391: if (file_exists('/usr/bin/env')) {
392: // handle other OSs with bash/zsh/ksh/csh if available to hide the answer
393: $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
394: foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) {
395: if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
396: self::$shell = $sh;
397: break;
398: }
399: }
400: }
401:
402: return self::$shell;
403: }
404:
405: private function hasSttyAvailable()
406: {
407: if (null !== self::$stty) {
408: return self::$stty;
409: }
410:
411: exec('stty 2>&1', $output, $exitcode);
412:
413: return self::$stty = $exitcode === 0;
414: }
415:
416: /**
417: * Validate an attempt
418: *
419: * @param callable $interviewer A callable that will ask for a question and return the result
420: * @param OutputInterface $output An Output instance
421: * @param callable $validator A PHP callback
422: * @param integer $attempts Max number of times to ask before giving up ; false will ask infinitely
423: *
424: * @return string The validated response
425: *
426: * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
427: */
428: private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts)
429: {
430: $error = null;
431: while (false === $attempts || $attempts--) {
432: if (null !== $error) {
433: $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'));
434: }
435:
436: try {
437: return call_user_func($validator, $interviewer());
438: } catch (\Exception $error) {
439: }
440: }
441:
442: throw $error;
443: }
444: }
445: