In the course of writing a small project for myself on Groovy & Grails, there was an urgent need to use all the different shell scripts. Restart the server, first redoing the newly created project, then collect logs from everywhere, then fill in new versions of configs and so on. At first, I wrote everything on bash scripts, but due to the fact that I use it, in addition to my pet-project, it is extremely rare, every time I encountered long searches on the Internet for necessary functions, syntax rules, etc., which undoubtedly , greatly inhibited the development of the project itself.
And since Groovy was already well acquainted with the programming language, I decided to write on it. But I didn’t really like to plant a huge amount of * .groovy files, but I wanted the opposite - to have one back-end management script that would already include all the necessary commands that could be in an “adult” console program the ability and history of commands to look at and a chain of sequentially executed commands to set and easily add, if necessary, new ones. This Wishlist still appeared because I remembered about cmd, which I once used in mastering Python. But it turned out that for Groovy no one wrote such a cmd (later I even understood why), which prompted me to another bicycle building, namely, to create a small
Cli Application Framework on Groovy.
The purpose of this post is to get objective criticism and suggestions for adding to my Framework, maybe someone wants to use it, since all the code and all the documentation I put on my
bitbacket account .
The basics
It all started with one
article , where the author described how to use the Python cmd, in fact, I did not go far in my endeavor and implemented about the same functionality where the abstract class
Cmd.groovy is present , inheriting from which we get the execution environment for our application. A simple example:
')
cli.groovy file:
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(); }
Every method we want to use as a console command must have a strict signature:
- The return type is boolean . This is done to chain the commands together so that if at least one command completes incorrectly (returns false), the chain is interrupted;
- The method name must begin with the prefix " do_ ", as in the cmd for Python;
- The method argument must be only one and of type " String [] ".
If the method starts with the "
do_ " prefix, but does not fulfill one of the above described conditions, the program will issue a warning, where it will be described why the method is not included in the list of commands and how to fix it.
Having created an instance of our class, we can launch it in two ways:
1) Calling the "
execute (String []) " method, passing in the user's arguments. In this case, the object will execute all the commands received during the call and complete the execution;
$ 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) By running an interactive input loop, calling the "
start () " method. At the same time, a greeting and “invitation” to enter the command will be displayed in the terminal. The execution of the input cycle will continue until the user enters "
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! $ _
Also, you can add the
@Description annotation to our method, which will contain a brief (brief) and full (full) description of the method:
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; } }
By running such a program, you will be able to request a hint for the command:
cmd:> help hello Command info ('hello'): prints greetins for all setted arguments, if arguments list is empty prints default message "Hello world!"
Command chains
If desired, teams can be chained by simply adding a "
+ " sign between them:
cmd:> hello Liza + hello Artem Hello Liza! Hello Artem! cmd:> _
The "
+ " sign was used instead of the canonical double ampersand due to the fact that when the program started instantly, the
shell thought that I was trying to call another command, and not just set the argument:
// , shell hello $ groovy -cp . cli.groovy hello Liza && hello Artem // $ groovy -cp . cli.groovy hello Liza + hello Artem
It is expected that when the command returns
false , the chain will break.
$ 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!” Will not display such a launch.
Localization
The entire text of the output is distributed in variables and, if desired, their values ​​can be changed, such as here:
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(); }
By running such a program, we will get the modified inviting prefix, not the standard "
cmd:> ", but "
$> ":
$ groovy -cp . cli.groovy Welcome Print "help" for reference $> _
In more detail about what variables for localization exist and what default values ​​they have can be viewed in a
special page on the wiki .
Execution of shell commands
It is also possible to use the
Shell helper class, which has only one static method
Response execute (String) . By passing a string to this class method, in response we get an instance of the
Response class, which has two fields:
- hasError - a flag that indicates the success of the command in the shell
- out - the result of the command
For example, the command to display the path to the current directory might look like this:
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; } }
Command entry history
It was the most difficult. As far as I understand, Java (and Groovy, respectively) have, so to speak, a very strained relationship with I / O in the terminal, at least on Linux JVM versions (I have not tried others). For example, using standard tools it is impossible to move the input cursor with arrows on the keyboard, garbage will be typed "^ [[C ^ [[D ^ [[A ^ [[B", approximately the same thing awaits you when you press, for example, Tab and others.
There is a workaround, in the form of all the various Java libraries, which, as I understand it, contain simply C / C ++ code for working with input from the terminal and Java wrapper classes. For small scripts, dragging a jar file with you, depending on the OS, is too much, but at the same time I wanted to have functionality, such as viewing the command history and changing the list of arguments in the command selected from the history. For this, I wrote a bicycle in a bicycle.
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 | _
The "
| " sign tells us that we can edit the argument list of the command. We can simply press Enter and then the interpreter will execute the command. We can enter "
q ", then editing will be canceled. And we can do this:
cmd:> help | exit <- cmd:> help exit | +1=hello <- cmd:> help hello | -1 <-
Having played enough with the changes in the arguments, you can finally press Enter and the interpreter will execute the command, also writing it in the command history.
At this point I’ll finish this infinitely long post, I’ll just say that the samples are freely available in my repository
here .
I would very much like to hear from you objective criticism and, perhaps, suggestions for the addition of the Framework.
PS: I wrote, by the way, with the help of this “Framework” not only the script for managing my server, but also at work came in handy for similar tasks.
PSS: If anyone knows how to solve the problem of input from the console to Java under Linux, please - share.