Интерактивная консоль с автодополнением на PHP
В этой маленькой статье я покажу, как использовать в своём PHP-скрипте консоль с автодополнением по нажатию Tab. Из подобных статей на хабре нашёл только статью от CKOPOBAPKuH, и у неё несколько другое направление, хотя суть — та же.На самом деле, никакой магии тут нет, из сложностей — сформулировать для себя, как должна работать ваша консоль. Поэтому минимум слов, минимум кода, только самое необходимое.
Есть вопрос: можно ли (и если можно, то как) сделать свою консоль с командами и подсказками на PHP.Есть ответ: можно, но соответствующее расширение (readline) для PHP доступно только на Linux, увы.
Итак, приступим.
План действий такой: — готовим метод, который будет обрабатывать входящие данные по нажатию Tab и возвращать список команд для автодополнения.— готовим список этих самых команд для дополнения— организуем бесконечный цикл программы, выход — по команде 'exit'
Вроде больше ничего нам не потребуется.
Чтобы было немного интереснее, сделаем так, чтобы консоль понимала, что сейчас нужно подставлять. Сделаем два «уровня» подстановки: при вводе первого слова в консоли, будем предлагать действия, а при вводе второго слова — существительные. Если в консоли больше слов, то по нажатию Tab не меняем строку.
Для нашего примера потребуются функции: — readline_completion_function — Регистрирует нашу собственную функцию обработки входящей строки— readline — Считываем строку— readline_info — с её помощью узнаем подробную информацию о строке в консоли, по нажатию Tab
На самом деле работы совсем немного, поэтому сразу к делу. Вот код небольшого класса, отвечающего за словарь и обработку команд:
Dictionary.php class Dictionary { const EXIT_COMMAND = 'exit'; protected $mainDictionary = [ 'list', 'load', 'get', 'go', 'put', 'parse', 'paint', 'delete', 'download', self: EXIT_COMMAND ];
protected $subDictionary = [ 'level', 'library', 'document', 'dragon', 'daemon', 'data', 'port', 'password', 'paragraph' ];
private $promptLine = '> '; public function initCommandCompletion () { // if readline lib accessible — use it for command completions if (function_exists ('readline_completion_function')) { readline_completion_function ( function ($currWord, $stringPosition, $cursorInLine) { $fullLine = readline_info ()['line_buffer']; if (count (explode (' ', $fullLine)) > 2) { return []; } // if not first word — return list allowed commands if (strrpos ($fullLine, ' ') !== false && (strrpos ($fullLine, $currWord) === false || strrpos ($fullLine, ' ') < strrpos($fullLine, $currWord)) ) { return $this->subDictionary; } return $this→mainDictionary; } ); } }
public function readCommand () { if (function_exists ('readline')) { $command = readline ($this→promptLine); } else { fputs (STDOUT, $this→promptLine); $command = fgets (STDIN); } return $command; }
public function executeCommand ($command) { $param = ''; if (strpos ($command, ' ') !== false) { list ($command, $param) = explode (' ', $command, 2); }
// NEED TO CHECK EXISTS COMMAND if (!$this→isCommandExists ($command)) { fputs (STDOUT, «Hey! I don’t know what are you talking about!\n»); return false; } // AND NOW CHECK FOR COMMAND AND RUN IT $message = «You try to run command '{$command}'»; if (! empty ($param)) { $message .= » and with param '{$param}'.»; } fputs (STDOUT, $message.»\n»); return true; }
private function isCommandExists ($command) { return in_array ($command, array_merge ($this→mainDictionary, $this→subDictionary)); }
} Для наших целей всё самое нужное и интересное — в методе initCommandCompletion (). А больше… А больше ничего интересного и нет. Анонимная функция, которую мы используем при вызове, принимает первым параметром последнее слово из консоли, а для получения полной строки, потребуется использовать readline_info (). Ну, а дальше — проверяем, какое по порядку слово сейчас вводится, и возвращаем один из словарей для автоподстановки.
И для получения эффекта — используем этот класс. Создадим index.php со следующим содержимым:
index.php require_once __DIR__ . '/Dictionary.php';
$app = new Dictionary (); $app→initCommandCompletion ();
// START LOOP. 'exit' command will stop execution while (true) { $command = $app→readCommand (); $command = trim ($command); if ($command == Dictionary: EXIT_COMMAND) { break; } $app→executeCommand ($command); } exit; Никакой магии, всё предельно просто:
Для первого слова — используется один словарь:
$ php index.php > l[Tab] list load >l Для второго слова — другой:
$ php index.php > li[Tab] > list l[Tab] level library >list l Ну вот и всё.
В расширении ещё доступны методы для работы с историей команд, таким образом можно сделать совсем вертолёт.
Как вы будете это использовать — дело ваше.Я в виде эксперимента, после того как разобрался с консолью, сделал набросок текстовой игры с парой комнат и предметами в них, чтобы игрок ходил и подбирал предметы или выбрасывал их из инвентаря. Соответственно — набор команд, и для второго слова в команде показывается название предметов в комнате и в инвентаре.
Пилить — было интересно. На первой волне энтузиазма, так сказать. :)
Исходники, если вдруг кому любопытно, тут.
P.S. если соберётесь делать что-нибудь более серьёзное в таком духе, посмотрите на компонент Console для Symfony2. Там уже всё сделано как надо и не придётся вымучивать свой велосипед.