Аналог cmd из Python для Groovy

от автора

По ходу написания небольшого проекта для себя на Groovy & Grails встала острая необходимость в использовании все различных shell-скриптов. То сервер перезапустить, предварительно передеплоив только что созданный проект, то логи ото всюду собрать, то новые версии конфигов залить и прочее. По началу писал всё на bash-скриптах, но, в силу того, что использую его, помимо своего pet-проекта, крайне редко, каждый раз сталкивался с долгими поисками в интернетах необходимых функций, правил синтаксиса и т.д., что, несомненно, сильно тормозило разработку собственно самого проекта.

И так как с языком программирования Groovy уже хорошо ознакомился, решил писать на нём. Но разводить огромное количество *.groovy файлов очень не хотелось, а хотелось как раз наоборот — иметь один скрипт управления back-end’ом, который уже включал бы в себя все необходимые команды, что бы можно было как во «взрослой» консольной программе иметь возможность и историю команд посмотреть и цепочку последовательно выполняемых команд задать и легко добавить, при необходимости, новые. Это хотелка ещё появилась потому, что вспомнил я про cmd, которым некогда пользовался осваивая Python. Но оказалось что для Groovy такого cmd никто не написал (позже я даже понял почему), что и подтолкнуло меня к очередному велосипедостроению, а именно к созданию небольшого Фреймворка Cli приложений на Groovy.

Целью данного поста является — получить объективную критику и предложения по дополнению моего «Фреймворка», возможно кто то захочет им воспользоваться, благо весь код, а также всю документацию я выложил на свой bitbacket аккаунт.

Основы

Началось всё с одной статьи, где автор описывал как пользоваться Python’овским cmd, собственно я не далеко ушёл в своём начинании и реализовал примерно тот же функционал, где присутствует абстрактный класс Cmd.groovy, наследовавшись от которого, мы получаем среду выполнения своего приложения. Простой пример:

файл cli.groovy:

class MyCli extends Cmd {     boolean do_hello(String[] args) {         if (args.length == 0) {             println('Hello world!');         } else {             args.each {                 println("Hello ${it}!");             }         }         return true;     } }  def cli = new MyCli(); if (this.args.length > 0) {     cli.execute(this.args); } else {     cli.start(); } 

Каждый метод, который мы хотим задействовать в качестве консольной команды должен иметь строгую сигнатуру:

  • Возвращаемый тип — boolean. Это сделано для объединения команд в цепочки, что бы если хоть одна команда завершится некорректно (вернёт false), то цепочка прерывалась;
  • Имя метода должно начинаться с префикса "do_", как и в cmd для Python’а;
  • Аргумент метода должен быть только один и типа "String[]".

В случае, если метод начинается с префикса "do_", но не выполняет одно из выше описанных условий, программа выдаст предупреждение, где будет описано почему метод не вошёл в список команд и как это это исправить.

Создав экземпляр нашего класса запустить его можно двумя способами:

1) Вызвав метод "execute(String[])", передав туда аргументы пользователя. В данном случае объект выполнит все команды, полученные при вызове, и завершит выполнение;

$ groovy -cp . cli.groovy help List of all available commands: (for reference on command print "help <command_name>") 	history	 - prints history of commands 	hello	 - No description 	help	 - prints commands info 	exit	 - for exit from CMD 

2) Запустив цикл интерактивного ввода, вызвав метод "start()". При этом в терминал будет выведено приветствие и «приглашение» ввести команду. Выполнение цикла ввода будет продолжаться до тех пор, пока пользователь не введёт "exit".

$ groovy -cp . cli.groovy Welcome Print "help" for reference cmd:> help  List of all available commands: (for reference on command print "help <command_name>") 	history	 - prints history of commands 	hello	 - No description 	help	 - prints commands info 	exit	 - for exit from CMD  cmd:> exit   Goodbye! $ _ 

Также, к нашему методу можно добавить аннотацию @Description, которая будет содержать краткое (brief) и полное (full) описание метода:

class MyCli extends Cmd {     @Description(         brief='prints greetings',         full='prints greetins for all setted arguments, if arguments list is empty prints default message "Hello world!"'     )     boolean do_hello(String[] args) {         if (args.length == 0) {             println('Hello world!');         } else {             args.each {                 println("Hello ${it}!");             }         }         return true;     } } 

Запустив такую программу, появится возможность запросить подсказку по команде:

cmd:> help hello  Command info ('hello'):  prints greetins for all setted arguments, if arguments list is empty prints default message "Hello world!" 

Цепочки команд

При желании команды можно объединить в цепочку, простым добавлением знака "+" между ними:

cmd:> hello Liza + hello Artem  Hello Liza! Hello Artem! cmd:> _ 

Знак "+" стал использовать, вместо канонического двойного амперсанда из-за того, что при мгновенном запуске программы shell думал, что я пытаюсь вызвать другую команду, а не просто сеттю аргумент:

// так не сработает, shell будет искать программу hello в окружении $ groovy -cp . cli.groovy hello Liza && hello Artem   // а это уже другое дело $ groovy -cp . cli.groovy hello Liza + hello Artem 

Ожидаемо, что при возвращении командой значения false, цепочка оборвётся

$ groovy -cp . cli.groovy hello Liza + popa + hello Artem Hello Liza! ERROR:	No such command 'popa' List of all available commands: (for reference on command print "help <command_name>") 	history	 - prints history of commands 	hello	 - No description 	help	 - prints commands info 	exit	 - for exit from CMD 

«Hello Artem!» такой запуск не выведет.

Локализация

Весь текст вывода распихан по переменным и при желании их значения можно изменить, как например здесь:

class MyCli extends Cmd {     MyCli() {         super();         PROMPT = '$> ';     }      boolean do_hello(String[] args) {         if (args.length == 0) {             println('Hello world!');         } else {             args.each {                 println("Hello ${it}!");             }         }         return true;     } }  def cli = new MyCli(); if (this.args.length > 0) {     cli.execute(this.args); } else {     cli.start(); } 

Запустив такую программу, мы получим изменённый приглашающий к вводу префикс, не стандартный "cmd:> ", а "$> ":

$ groovy -cp . cli.groovy Welcome Print "help" for reference $> _ 

Более подробно о том какие переменные для локализации существуют и какие у них значения по умолчанию можно посмотреть а специальной страничке в вики.

Исполнение команд оболочки

Также имеется возможность использовать класс-хелпер Shell, который имеет всего один статический метод Response execute(String). Передав этому методу класса строку, в ответ мы получим экземпляр класса Response, который имеет два поля:

  1. hasError — флаг, говорящий о успехе выполнения команды в оболочке
  2. out — результат выполнения команды

Например, так могла бы выглядеть команда по отображению пути до текущей директории:

boolean do_pwd(String[] args) {     if (args.length != 0) {         println('ERROR: this method doesn\'t have any arguments');         return false;     }     Response response = Shell.execute('pwd');     if (!response.hasError) {         println(response.out);     	return true;     } else {         println("ERROR: ${response.out}");         return false;     } } 

История ввода команд

Это было самое сложное. На сколько я понял, Java (а соответственно и Groovy) имеют, так сказать, очень натянутые отношения с I/O в терминал, по крайне мере на Linux версиях JVM (на других не пробовал). Например, стандартными средствами невозможно перемещать курсор ввода стрелками на клавиатуре, будет печататься мусор аля "^[[C^[[D^[[A^[[B", примерно тоже самое ждёт Вас при нажатии, например, Tab и прочего.

Есть обходной манёвр, в виде все различных Java библиотек, которые, как я понял, содержат просто код на C\C++ для работы с вводом из терминала и классы-обёртки на Java. Для небольших скриптов тянуть с собой jar’ник файлов, причём зависящего от ОС — это как то чересчур, но в тоже время хотелось иметь функциональность, например просмотра истории команд и изменения списка аргументов в выбранной из истории команде. Для этого я написал велосипед в велосипеде.

Welcome Print "help" for reference cmd:> help  List of all available commands: (for reference on command print "help <command_name>")     help     - prints commands info     exit     - for exit from CMD  cmd:> !!    <- как и в nix'ах, вызываем последнюю команду cmd:> help | _ 

Знак "|" говорит нам о том что мы можем отредактировать список аргументов команды. Мы можем просто нажать Enter и тогда интерпретатор выполнит команду. Можем ввести "q", тогда редактирование будет отменено. А можем сделать так:

cmd:> help | exit          <- добавляем новый аргумент к команде cmd:> help exit | +1=hello <- изменяем первый аргумент на новое значение cmd:> help hello | -1      <- удаляем первый аргумент 

Наигравшись с изменениями аргументов, можно, в конце концов, нажать Enter и интерпретатор выполнит команду, также записав её в историю команд.

На этом я закончу этот бесконечно длинный пост, скажу лишь, что сорцы лежат в свободном доступе в моём репозитории по ссылке.

Очень хотелось бы услышать от Вас объективную критику и, возможно, предложения по дополнению «Фреймворка».

P.S.: Написал я, кстати, при помощи этого «Фреймворка» не только скрипт управления своим сервером, но также и на работе пригодился, для похожих задач.

P.S.S.: Если кто знает как можно решить проблему ввода с консоли на Java под Linux, просьба — поделитесь.

ссылка на оригинал статьи http://habrahabr.ru/post/212457/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *