В этой маленькой статье я покажу, как использовать в своём PHP-скрипте консоль с автодополнением по нажатию Tab. Из подобных статей на хабре нашёл только статью от CKOPOBAPKuH, и у неё несколько другое направление, хотя суть — та же.
На самом деле, никакой магии тут нет, из сложностей — сформулировать для себя, как должна работать ваша консоль. Поэтому минимум слов, минимум кода, только самое необходимое.
Есть вопрос: можно ли (и если можно, то как) сделать свою консоль с командами и подсказками на PHP.
Есть ответ: можно, но соответствующее расширение (readline) для PHP доступно только на Linux, увы.
Итак, приступим.
План действий такой:
— готовим метод, который будет обрабатывать входящие данные по нажатию Tab и возвращать список команд для автодополнения.
— готовим список этих самых команд для дополнения
— организуем бесконечный цикл программы, выход — по команде 'exit'
Вроде больше ничего нам не потребуется.
Чтобы было немного интереснее, сделаем так, чтобы консоль понимала, что сейчас нужно подставлять. Сделаем два «уровня» подстановки: при вводе первого слова в консоли, будем предлагать действия, а при вводе второго слова — существительные. Если в консоли больше слов, то по нажатию Tab не меняем строку.
Для нашего примера потребуются функции:
— readline_completion_function — Регистрирует нашу собственную функцию обработки входящей строки
— readline — Считываем строку
— readline_info — с её помощью узнаем подробную информацию о строке в консоли, по нажатию Tab
На самом деле работы совсем немного, поэтому сразу к делу. Вот код небольшого класса, отвечающего за словарь и обработку команд:
Для наших целей всё самое нужное и интересное — в методе initCommandCompletion(). А больше… А больше ничего интересного и нет. Анонимная функция, которую мы используем при вызове, принимает первым параметром последнее слово из консоли, а для получения полной строки, потребуется использовать readline_info(). Ну а дальше — проверяем, какое по порядку слово сейчас вводится, и возвращаем один из словарей для автоподстановки.
И для получения эффекта — используем этот класс. Создадим index.php со следующим содержимым:
Никакой магии, всё предельно просто:
Для первого слова — используется один словарь:
Для второго слова — другой:
Ну вот и всё.
В расширении ещё доступны методы для работы с историей команд, таким образом можно сделать совсем вертолёт.
Как вы будете это использовать — дело ваше.
Я в виде эксперимента, после того как разобрался с консолью, сделал набросок текстовой игры с парой комнат и предметами в них, чтобы игрок ходил и подбирал предметы или выбрасывал их из инвентаря. Соответственно — набор команд, и для второго слова в команде показывается название предметов в комнате и в инвентаре.
Пилить — было интересно. На первой волне энтузиазма, так сказать. :)
Исходники, если вдруг кому любопытно, тут.
P.S. если соберётесь делать что-нибудь более серьёзное в таком духе, посмотрите на компонент Console для Symfony2. Там уже всё сделано как надо и не придётся вымучивать свой велосипед.
На самом деле, никакой магии тут нет, из сложностей — сформулировать для себя, как должна работать ваша консоль. Поэтому минимум слов, минимум кода, только самое необходимое.
Есть вопрос: можно ли (и если можно, то как) сделать свою консоль с командами и подсказками на 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. Там уже всё сделано как надо и не придётся вымучивать свой велосипед.