Интерактивная консоль с автодополнением на 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. Там уже всё сделано как надо и не придётся вымучивать свой велосипед.
    Поделиться публикацией

    Комментарии 3

      +6
      Стандартный шелл пыха умеет такое:
      //>$ cat test.php
      <?php
      
      function myGrevus()
      {
          echo 'test' . PHP_EOL;
      }
      
      function myGrevusTest2()
      {
          echo 'test 2' . PHP_EOL;
      }
      

      >$ php -d auto_prepend_file=$PWD/test.php -a
      Interactive shell
      
      php > myGrevus
      myGrevus       myGrevusTest2  
      php > myGrevus();
      test
      

      Просто на заметку.
        +1
        Спасибо! Даже не думал о таком варианте.
        0
        Очень понравилось наличие автодополнения в консоли Symfony2.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое