Pull to refresh

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

Reading time6 min
Views5.4K
По ходу написания небольшого проекта для себя на 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, просьба — поделитесь.
Tags:
Hubs:
+5
Comments0

Articles