目录
Spring Shell是什么
Spring Shell是Spring生态中的一员,用于开发命令行应用程序,官网:https://projects.spring.io/spring-shell/ 。
Spring Shell构建在JLine之上,集成Bean Validation API实现命令参数校验。
从2.0版本开始,Spring Shell还可以非常方便地与Spring Boot进行集成,直接使用Spring Boot提供的一些非常实用的功能(如:打包可执行jar文件)。
入门实践
使用Spring Shell非常简单,直接添加对应的依赖配置即可,而为了使用Spring Boot提供的便利性,通常都是与Spring Boot集成使用。
基础配置
集成Spring Boot本质上就是先新建一个Spring Boot工程,然后添加Spring Shell依赖即可,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>chench.org.extra</groupId>
<artifactId>test-springshell</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-springshell</name>
<description>Test Spring Shell</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 在Spring Boot项目中添加Spring Shell依赖 -->
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包可执行文件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
添加完上述配置之后,一个基于Spring Boot的使用Spring Shell开发命令行应用程序的基础开发框架已经搭建完毕,打包运行:
$ mvn clean package -Dmaven.test.skip=true
$ java -jar test-springshell-0.0.1-SNAPSHOT.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.6.RELEASE)
2019-06-21 11:23:54.966 INFO 11286 --- [ main] c.o.e.t.TestSpringshellApplication : Starting TestSpringshellApplication v0.0.1-SNAPSHOT on chench9-pc with PID 11286 (/home/chench9/sun/workspace/test-springshell/target/test-springshell-0.0.1-SNAPSHOT.jar started by chench9 in /home/chench9)
2019-06-21 11:23:54.970 INFO 11286 --- [ main] c.o.e.t.TestSpringshellApplication : No active profile set, falling back to default profiles: default
2019-06-21 11:23:56.457 INFO 11286 --- [ main] c.o.e.t.TestSpringshellApplication : Started TestSpringshellApplication in 2.26 seconds (JVM running for 2.771)
shell:>
显然,使用Spring Shell开发的命令行应用程序与其他普通应用不同,启动之后停留在命令交互界面,等待用户输入。
目前还没有编写任何与业务相关的代码,输入help
命令看看。
shell:>help
AVAILABLE COMMANDS
Built-In Commands
clear: Clear the shell screen.
exit, quit: Exit the shell.
help: Display help about available commands.
script: Read and execute commands from a file.
stacktrace: Display the full stacktrace of the last error.
shell:>
可以看到,Spring Shell已经内置了一些常用的命令,如:help
命令显示帮助信息,clear
命令清空命令行界面,exit
退出应用。
在交互界面输出exit
命令退出应用程序。
实际上,Spring Shell默认就集成了Spring Boot。
如下,我们在pom.xml文件只添加Spring Shell依赖配置(不明确配置依赖Spring Boot):
<dependencies>
<!-- Spring Shell -->
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>
项目依赖关系如下图所示:
简单示例
按照国际惯例,通过编写一个简单的“Hello,World!”程序来介绍Spring Shell的相关概念。
@ShellComponent
public class HelloWorld {
@ShellMethod("Say hello")
public void hello(String name) {
System.out.println("hello, " + name + "!");
}
}
如上所示,HellWorld
是一个非常简单的Java类,在Spring Shell应用中Java类需要使用注解@ShellComponent
来修饰,类中的方法使用注解@ShellMethod
表示为一个具体的命令。
打包运行,输入help
命令之后将会看到,默认情况下在Java类中定义的方法名就是在交互界面中可以使用的命令名称。
shell:>help
AVAILABLE COMMANDS
Built-In Commands
clear: Clear the shell screen.
exit, quit: Exit the shell.
help: Display help about available commands.
script: Read and execute commands from a file.
stacktrace: Display the full stacktrace of the last error.
Hello World # 命令所属组名
hello: Say hello # 具体的命令
shell:>hello World
hello, World!
shell:>
至此,一个简单的基于Spring Shell的命令行交互应用就完成了,下面对Spring Shell中的相关组件进行详细介绍。
注解@ShellMethod
默认情况下,使用注解@ShellMethod
修饰的Java方法名称就是具体的交互命令名称,如上述示例。
追溯注解@ShellMethod
源码:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ShellMethod {
String INHERITED = "";
String[] key() default {}; // 设置命令名称
String value() default ""; // 设置命名描述
String prefix() default "--"; // 设置命令参数前缀,默认为“--”
String group() default ""; // 设置命令分组
}
还可以使用注解@ShellMethod
的属性key设置命令名称(注意:可以为一个命令设置多个名称)。
@ShellComponent
public class Calculator {
// 为一个命令指定多个名称
@ShellMethod(value = "Add numbers.", key = {"sum", "addition"})
public void add(int a, int b) {
int sum = a + b;
System.out.println(String.format("%d + %d = %d", a, b, sum));
}
}
shell:>help
AVAILABLE COMMANDS
Built-In Commands
clear: Clear the shell screen.
exit, quit: Exit the shell.
help: Display help about available commands.
script: Read and execute commands from a file.
stacktrace: Display the full stacktrace of the last error.
Calculator
addition, sum: Add numbers.
shell:>addition 1 2
1 + 2 = 3
shell:>sum 1 2
1 + 2 = 3
shell:>sum --a 1 --b 2 # 使用带命令参数前缀的方式
1 + 2 = 3
shell:>
显然,使用注解@ShellMethod
的key属性可以为方法指定多个命令名称,而且,此时方法名不再是可用的命令了。
除了可以自定义命令名称,还可以自定义命令参数前缀(默认为“--”)和命令分组(默认为命令对应Java方法所在的类名称)。
// 1.使用属性value定义命令描述
// 2.使用属性key定义命令名称
// 3.使用属性prefix定义参数前缀
// 4.使用属性group定义命令分组
@ShellMethod(value = "Add numbers.", key = {"sum", "addition"}, prefix = "-", group = "Cal")
public void add(int a, int b) {
int sum = a + b;
System.out.println(String.format("%d + %d = %d", a, b, sum));
}
如下为自定义了注解@ShellMethod
各个属性之后的结果:
shell:>help
AVAILABLE COMMANDS
Built-In Commands
clear: Clear the shell screen.
exit, quit: Exit the shell.
help: Display help about available commands.
script: Read and execute commands from a file.
stacktrace: Display the full stacktrace of the last error.
Cal
addition, sum: Add numbers.
shell:>sum --a 1 --b 2
Too many arguments: the following could not be mapped to parameters: '--b 2'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>sum -a 1 -b 2
1 + 2 = 3
shell:>
显然,命令分组为自定义的“Cal”,命令参数前缀为自定义的“-”(此时将不能再使用默认的参数前缀“--”)。
注解@ShellOption
注解@ShellMethod
应用在Java方法上对命令进行定制,还可以使用注解@ShellOption
对命令参数进行定制。
自定义参数名称
@ShellMethod("Echo params")
public void echo(int a, int b, @ShellOption("--third") int c) {
System.out.println(String.format("a=%d, b=%d, c=%d", a, b, c));
}
如上所示,使用注解@ShellOption
为第三个参数指定名称为“third”。
shell:>echo 1 2 3
a=1, b=2, c=3
shell:>echo --a 1 --b 2 --c 3 # 显然,当明确指定了参数名称之后,必须使用指定的名称
Too many arguments: the following could not be mapped to parameters: '3'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>echo --a 1 --b 2 --third 3
a=1, b=2, c=3
shell:>
使用注解@ShellOption
还可以为命令参数指定多个名称:
@ShellMethod("Echo command help")
public void myhelp(@ShellOption({"-C", "--command"}) String cmd) {
System.out.println(cmd);
}
shell:>myhelp action
action
shell:>myhelp -C action
action
shell:>myhelp --command action
action
设置参数默认值
还可以使用注解@ShellOption
通过属性“defaultValue”为参数指定默认值。
@ShellMethod("Say hello")
public void hello(@ShellOption(defaultValue = "World") String name) {
System.out.println("hello, " + name + "!");
}
shell:>hello # 显然,当参数值为空时使用默认值
hello, World!
shell:>hello zhangsan
hello, zhangsan!
为一个参数传递多个值
通常,一个命令参数只对应一个值,如果希望为一个参数传递多个值(对应Java中的数组或集合),可以使用注解@ShellOption
的属性arity指定参数值的个数。
// 参数为一个数组
@ShellMethod("Add by array")
public void addByArray(@ShellOption(arity = 3) int[] numbers) {
int sum = 0;
for(int number : numbers) {
sum += number;
}
System.out.println(String.format("sum=%d", sum));
}
shell:>add-by-array 1 2 3
sum=6
shell:>add-by-array --numbers 1 2 3
sum=6
shell:>add-by-array --numbers 1 2 3 4 # 传递的参数个数超过arity属性值时报错
Too many arguments: the following could not be mapped to parameters: '4'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
// 参数为集合
@ShellMethod("Add by list")
public void addByList(@ShellOption(arity = 3) List<Integer> numbers) {
int s = 0;
for(int number : numbers) {
s += number;
}
System.out.println(String.format("s=%d", s));
}
shell:>add-by-list 1 2 3
s=6
shell:>add-by-list --numbers 1 2 3
s=6
shell:>add-by-list --numbers 1 2 3 4 # 传递的参数个数超过arity属性值时报错
Too many arguments: the following could not be mapped to parameters: '4'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
注意: 传递的参数个数不能大于@ShellOption
属性arity设置的值。
对布尔参数的特殊处理
// 参数为Boolean类型
@ShellMethod("Shutdown action")
public void shutdown(boolean shutdown) {
System.out.println(String.format("shutdown=%s", shutdown));
}
shell:>shutdown
shutdown=false
shell:>shutdown --shutdown
shutdown=true
shell:>shutdown --shutdown true
Too many arguments: the following could not be mapped to parameters: 'true'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
从上述示例可以知道,对于布尔类型的参数,默认值为false,当明确传递参数名时,值为true。
注意: 对于布尔参数值处理比较特别,无需像普通参数一样传递参数值,否则报错。
带空格的参数处理
Spring Shell使用空格来分割参数,当需要传递带空格的参数时,需要将参数使用引号(单引号或者双引号)引起来。
// 带空格的参数需要使用引号引起来
@ShellMethod("Echo.")
public void echo(String what) {
System.out.println(what);
}
shell:>echo "Hello,World!"
Hello,World!
shell:>echo 'Hello,World!'
Hello,World!
shell:>echo "Hello,\"World!\""
Hello,"World!"
shell:>echo '\"Hello,World!\"'
"Hello,World!"
shell:>echo Hello World # 当参数值中包含空格时,需要使用引号引起来,否则报错
Too many arguments: the following could not be mapped to parameters: 'World'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
参数校验
Spring Shell集成了Bean Validation API,可用来实现参数校验。可支持参数校验的类型很多,如:是否为空,长度,最大值,最小值等等。
实现参数校验也是通过注解实现的,常用的参数校验注解有:@Size
(校验参数长度),@Max
(校验参数最大值),@Min
(校验参数最小值),@Pattern
(支持自定义正则表达式校验规则)。
// 使用@Size注解校验参数长度
@ShellMethod("Change password")
public void changePwd(@Size(min = 6, max = 30) String pwd) {
System.out.println(pwd);
}
shell:>change-pwd 123 # 当参数长度小于最小值6时报错
The following constraints were not met:
--pwd string : size must be between 6 and 30 (You passed '123')
shell:>change-pwd 1234567890123456789012345678901 # 当参数长度大于最大值30时报错
The following constraints were not met:
--pwd string : size must be between 6 and 30 (You passed '1234567890123456789012345678901')
shell:>change-pwd 1234567890 # 参数在指定范围是成功
1234567890
Spring Shell支持的参数注解如下图所示:
动态命令可用性
如果存在这样一种场景:命令A是否可以执行需要依赖命令B的执行结果,换言之,当命令B的执行结果不满足条件时不允许执行命令A。
Spring Shell针对这个需求也做了支持,翻译为:动态命令可用性(Dynamic Command Availability),具体实现有2种方式。
这个概念理解起来有些生硬,简而言之:命令必须满足特定条件时才能被执行,也就说命令必须满足特定条件才可用。因为这个“特定条件”是在动态变化的,所以叫做“动态命令可用性”。
为单一命令提供动态可用性
为单一命令提供动态可用性支持通过控制方法命名来实现。
@ShellComponent
public class Downloader {
private boolean connected = false;
@ShellMethod("Connect server")
public void connect() {
connected = true;
}
@ShellMethod("Download file")
public void download() {
System.out.println("Downloaded.");
}
// 为命令download提供可用行支持
public Availability downloadAvailability() {
return connected ? Availability.available():Availability.unavailable("you are not connected");
}
}
shell:>download # download命令依赖connect命令的执行结果,因此在执行connect命令成功之前直接调用download命令时报错
Command 'download' exists but is not currently available because you are not connected
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>connect
shell:>download # 在执行命令connect成功之后再执行download命令时成功
Downloaded.
显然,在这种方式下,必须为需要实现动态可用性的命令提供一个对应名称的方法(方法名必须是:“命令名 + Availability”,如:downloadAvailability),且方法的返回值必须为org.springframework.shell.Availability
对象。
该方式的缺点也很明显,如果需要实现动态可用性的命令比较多,必须定义同等数量的可用性方法,比较繁琐。
为多个命令提供动态可用性
如果需要为多个命令提供动态可用性支持,使用注解@ShellMethodAvailability
才是比较明智的。
而注解@ShellMethodAvailability
的使用方式又有2种:
1.在命令方法上使用@ShellMethodAvailability
指定提供动态可用性支持的方法名
private boolean connected = false;
@ShellMethod("Connect server")
public void connect() {
connected = true;
}
@ShellMethod("Download")
@ShellMethodAvailability({"connectCheck"})
public void download() {
System.out.println("Downloaded.");
}
@ShellMethod("Upload")
@ShellMethodAvailability({"connectCheck"})
public void upload() {
System.out.println("Uploaded.");
}
public Availability connectCheck() {
return connected ? Availability.available():Availability.unavailable("you are not connected");
}
如上所示,在命令方法download()
和upload()
通过注解@ShellMethodAvailability
指定提供命令动态性实现的方法名:connectCheck,这样就可以很方便地实现使用一个方法为多个命令提供动态可用性支持。
2.直接在提供命令动态可用性支持的方法上使用注解@ShellMethodAvailability
指定命令方法名
另外一种实现用一个方法为多个命令提供动态可用性实现的方式是:直接在命令动态可用性方法上使用注解@ShellMethodAvailability
指定对应的命令方法名。
@ShellMethod("Download")
public void download() {
System.out.println("Downloaded.");
}
@ShellMethod("Upload")
public void upload() {
System.out.println("Uploaded.");
}
// 直接在提供命令动态可用性的方法上通过注解`@ShellMethodAvailability`指定命令方法名
@ShellMethodAvailability({"download", "upload"})
public Availability connectCheck() {
return connected ? Availability.available():Availability.unavailable("you are not connected");
}
命令动态可用性小结
1.使用了动态命令可用性的命令会在交互界面中显示一个星号提示,明确提示该命令的执行需要依赖指定状态(通常是其他命令的执行结果)。
Downloader
connect: Connect server
* download: Download # download和upload命令的执行都需要依赖指定状态
* upload: Upload
# 说明标注星号的命令不可用,可以通过help命令查看帮助信息
Commands marked with (*) are currently unavailable.
Type `help <command>` to learn more.
shell:>help download # 通过help命令查看指定命令的帮助信息
NAME
download - Download
SYNOPSYS
download
CURRENTLY UNAVAILABLE
This command is currently not available because you are not connected.
2.不论如何,提供动态命令可用性的方法返回值必须是org.springframework.shell.Availability
类型对象。
命令分组
Spring Shell管理命令分组有3种实现方式,分别是:默认以类名为组名,使用注解@ShellMethod
的group属性指定组名,使用注解@ShellCommandGroup
指定组名。
默认命令分组规则
命令所在的组为其对应方法所在的Java类名称按驼峰法则分隔的名称(如:“HelloWord”为类名,则其中的命令组名为“Hello Word”),这是默认的命令组管理方式。
Hello World # 默认的命令组管理方式
hello: Say hello
使用@ShellMethod注解的group属性指定分组
通过注解@ShellMethod
的group属性指定命令所属的组名
@ShellComponent
public class Cmd1 {
@ShellMethod(value = "Cmd1 action1", group = "CMD")
public void action11() {
System.out.println("cmd1 action1");
}
@ShellMethod(value = "Cmd1 action2", group = "CMD")
public void action12() {
System.out.println("cmd1 action2");
}
}
@ShellComponent
public class Cmd2 {
@ShellMethod(value = "Cmd2 action1", group = "CMD")
public void action21() {
System.out.println("cmd2 action1");
}
@ShellMethod(value = "Cmd2 action2", group = "CMD")
public void action22() {
System.out.println("cmd2 action2");
}
}
shell:>help
AVAILABLE COMMANDS
CMD
action11: Cmd1 action1
action12: Cmd1 action2
action21: Cmd2 action1
action22: Cmd2 action2
显然,使用注解@ShellMethod
的group属性可以将不同类的不同命令指定到同一个命令组下。
使用@ShellCommandGroup注解指定分组
在使用注解@ShellCommandGroup
指定命令分组时有2种方法:
方法一: 在类上使用注解@ShellCommandGroup
指定组名,则该类下的所有命令都属于该组
@ShellComponent
@ShellCommandGroup("CMD")
public class Cmd1 {
@ShellMethod(value = "Cmd1 action1")
public void action11() {
System.out.println("cmd1 action1");
}
@ShellMethod(value = "Cmd1 action2")
public void action12() {
System.out.println("cmd1 action2");
}
}
@ShellComponent
@ShellCommandGroup("CMD")
public class Cmd2 {
@ShellMethod(value = "Cmd2 action1")
public void action21() {
System.out.println("cmd2 action1");
}
@ShellMethod(value = "Cmd2 action2")
public void action22() {
System.out.println("cmd2 action2");
}
}
# 使用注解@ShellCommandGroup将多个类中的命令指定到一个组下
shell:>help
AVAILABLE COMMANDS
CMD
action11: Cmd1 action1
action12: Cmd1 action2
action21: Cmd2 action1
action22: Cmd2 action2
方法二: 在package-info.java
中使用注解@ShellCommandGroup
指定整个包下的所有类中的命令为一个组。
如下图所示,Cmd1.java和Cmd2.java都在包chench.org.extra.testspringshell.group
下,package-info.java
为对应的包描述类。
Cmd1.java:
package chench.org.extra.testspringshell.group;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class Cmd1 {
@ShellMethod(value = "Cmd1 action1")
public void action11() {
System.out.println("cmd1 action1");
}
@ShellMethod(value = "Cmd1 action2")
public void action12() {
System.out.println("cmd1 action2");
}
}
Cmd2.java
package chench.org.extra.testspringshell.group;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class Cmd2 {
@ShellMethod(value = "Cmd2 action1")
public void action21() {
System.out.println("cmd2 action1");
}
@ShellMethod(value = "Cmd2 action2")
public void action22() {
System.out.println("cmd2 action2");
}
}
package-info.java:
// 在Java包描述类中通过注解`@ShellCommandGroup`为该包下的所有类中的命令指定统一组名
@ShellCommandGroup("CMD")
package chench.org.extra.testspringshell.group;
import org.springframework.shell.standard.ShellCommandGroup;
shell:>help
AVAILABLE COMMANDS
CMD
action11: Cmd1 action1
action12: Cmd1 action2
action21: Cmd2 action1
action22: Cmd2 action2
注意: 通过注解@ShellCommandGroup
指定的命令分组可以被注解@ShellMethod
的group属性指定的组名覆盖。
内置命令
Spring Shell提供了5个内置命令:
shell:>help
AVAILABLE COMMANDS
Built-In Commands
clear: Clear the shell screen. # 清空命令行界面
exit, quit: Exit the shell. # 退出应用
help: Display help about available commands. # 显示帮助信息
script: Read and execute commands from a file. # 从文件中读取并执行批量命令
stacktrace: Display the full stacktrace of the last error. # 报错时读取异常堆栈信息
写在最后
Spring Shell大大简化了使用Java开发基于命令行交互应用的步骤,只需要简单配置,再使用相关注解就可以开发一个命令行应用了。
同时,Spring Shell还内置了一些有用的命令,如:help
,clear
,stacktrace
,exit
等。
另外,Spring Shell还支持实用TAB键补全命令,非常方便。
最后,需要特别注意: Spring Shell不允许出现同名的命令(虽然命令对应的同名方法虽然在不同的Java类中被允许,不会出现编译错误,但是运行时将报错,从而无法正确启动应用程序)。即:下面的情形是不允许的。
@ShellComponent
public class Cmd1 {
@ShellMethod(value = "Cmd1 action1")
public void action1() {
System.out.println("cmd1 action1");
}
@ShellMethod(value = "Cmd1 action2")
public void action2() {
System.out.println("cmd1 action2");
}
}
@ShellComponent
public class Cmd2 {
@ShellMethod(value = "Cmd2 action1")
public void action1() {
System.out.println("cmd2 action1");
}
@ShellMethod(value = "Cmd2 action2")
public void action2() {
System.out.println("cmd2 action2");
}
}
除非使用注解@ShellMethod
的key属性不同的命令指定为不同的名称,如下所示:
// 使用注解`@ShellMethod`的key属性不同的命令指定为不同的名称
@ShellComponent
public class Cmd1 {
@ShellMethod(value = "Cmd1 action1", key = {"cmd11"})
public void action1() {
System.out.println("cmd1 action1");
}
@ShellMethod(value = "Cmd1 action2", key = {"cmd12"})
public void action2() {
System.out.println("cmd1 action2");
}
}
@ShellComponent
public class Cmd2 {
@ShellMethod(value = "Cmd2 action1", key = {"cmd21"})
public void action1() {
System.out.println("cmd2 action1");
}
@ShellMethod(value = "Cmd2 action2", key = {"cmd22"})
public void action2() {
System.out.println("cmd2 action2");
}
}
shell:>help
AVAILABLE COMMANDS
CMD
cmd11: Cmd1 action1
cmd12: Cmd1 action2
cmd21: Cmd2 action1
cmd22: Cmd2 action2