4. Commands

In this section, we go through an actual command registration and leave command options and execution for later in a documentation. You can find more detailed info in Command Registration.spring-doc.cn

4.1. Registration

There are two different ways to define a command: through an annotation model and through a programmatic model. In the annotation model, you define your methods in a class and annotate the class and the methods with specific annotations. In the programmatic model, you use a more low level approach, defining command registrations (either as beans or by dynamically registering with a command catalog).spring-doc.cn

4.1.1. Annotation Model

When you use the standard API, methods on beans are turned into executable commands, provided that:spring-doc.cn

  • The bean class bears the @ShellComponent annotation. (This is used to restrict the set of beans that are considered.)spring-doc.cn

  • The method bears the @ShellMethod annotation.spring-doc.cn

The @ShellComponent is a stereotype annotation that is itself meta-annotated with @Component. As a result, you can use it in addition to the filtering mechanism to declare beans (for example, by using @ComponentScan).spring-doc.cn

You can customize the name of the created bean by using the value attribute of the annotation.spring-doc.cn

@ShellComponent
static class MyCommands {

	@ShellMethod
	public void mycommand() {
	}
}

The only required attribute of the @ShellMethod annotation is its value attribute, which should have a short, one-sentence, description of what the command does. This lets your users get consistent help about your commands without having to leave the shell (see Help).spring-doc.cn

The description of your command should be short — no more than one or two sentences. For better consistency, it should start with a capital letter and end with a period.

By default, you need not specify the key for your command (that is, the word(s) that should be used to invoke it in the shell). The name of the method is used as the command key, turning camelCase names into dashed, gnu-style, names (for example, sayHello() becomes say-hello).spring-doc.cn

You can, however, explicitly set the command key, by using the key attribute of the annotation:spring-doc.cn

@ShellMethod(value = "Add numbers.", key = "sum")
public int add(int a, int b) {
	return a + b;
}
The key attribute accepts multiple values. If you set multiple keys for a single method, the command is registered with those different aliases.
The command key can contain pretty much any character, including spaces. When coming up with names though, keep in mind that consistency is often appreciated by users. That is, you should avoid mixing dashed-names with spaced names and other inconsistencies.

4.1.2. Programmatic Model

In the programmatic model, CommandRegistration is defined as a @Bean, and it is automatically registered:spring-doc.cn

@Bean
CommandRegistration commandRegistration() {
	return CommandRegistration.builder()
		.command("mycommand")
		.build();
}

4.2. Organizing Commands

When your shell starts to provide a lot of functionality, you may end up with a lot of commands, which could be confusing for your users. By typing help, they would see a daunting list of commands, organized in alphabetical order, which may not always be the best way to show the available commands.spring-doc.cn

To alleviate this possible confusion, Spring Shell provides the ability to group commands together, with reasonable defaults. Related commands would then end up in the same group (for example, User Management Commands) and be displayed together in the help screen and other places.spring-doc.cn

By default, commands are grouped according to the class they are implemented in, turning the camelCase class name into separate words (so URLRelatedCommands becomes URL Related Commands). This is a sensible default, as related commands are often already in the class anyway, because they need to use the same collaborating objects.spring-doc.cn

If, however, this behavior does not suit you, you can override the group for a command in the following ways, in order of priority:spring-doc.cn

  1. Specify a group() in the @ShellMethod annotation.spring-doc.cn

  2. Place a @ShellCommandGroup on the class in which the command is defined. This applies the group for all commands defined in that class (unless overridden, as explained earlier).spring-doc.cn

  3. Place a @ShellCommandGroup on the package (through package-info.java) in which the command is defined. This applies to all the commands defined in the package (unless overridden at the method or class level, as explained earlier).spring-doc.cn

The following listing shows an example:spring-doc.cn

public class UserCommands {
    @ShellMethod(value = "This command ends up in the 'User Commands' group")
    public void foo() {}

    @ShellMethod(value = "This command ends up in the 'Other Commands' group",
    	group = "Other Commands")
    public void bar() {}
}

...

@ShellCommandGroup("Other Commands")
public class SomeCommands {
	@ShellMethod(value = "This one is in 'Other Commands'")
	public void wizz() {}

	@ShellMethod(value = "And this one is 'Yet Another Group'",
		group = "Yet Another Group")
	public void last() {}
}

4.3. Dynamic Command Availability

Registered commands do not always make sense, due to the internal state of the application. For example, there may be a download command, but it only works once the user has used connect on a remote server. Now, if the user tries to use the download command, the shell should explain that the command exists but that it is not available at the time. Spring Shell lets you do that, even letting you provide a short explanation of the reason for the command not being available.spring-doc.cn

There are three possible ways for a command to indicate availability. They all use a no-arg method that returns an instance of Availability. Consider the following example:spring-doc.cn

@ShellComponent
public class MyCommands {

    private boolean connected;

    @ShellMethod("Connect to the server.")
    public void connect(String user, String password) {
        [...]
        connected = true;
    }

    @ShellMethod("Download the nuclear codes.")
    public void download() {
        [...]
    }

    public Availability downloadAvailability() {
        return connected
            ? Availability.available()
            : Availability.unavailable("you are not connected");
    }
}

The connect method is used to connect to the server (details omitted), altering the state of the command through the connected boolean when done. The download command as marked as unavailable until the user has connected, thanks to the presence of a method named exactly as the download command method with the Availability suffix in its name. The method returns an instance of Availability, constructed with one of the two factory methods. If the command is not available, an explanation has to be provided. Now, if the user tries to invoke the command while not being connected, here is what happens:spring-doc.cn

shell:>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.

Information about currently unavailable commands is also used in the integrated help. See Help.spring-doc.cn

The reason provided when the command is not available should read nicely if appended after “Because”.spring-doc.cn

You should not start the sentence with a capital or add a final periodspring-doc.cn

If naming the availability method after the name of the command method does not suit you, you can provide an explicit name by using the @ShellMethodAvailability annotation:spring-doc.cn

    @ShellMethod("Download the nuclear codes.")
    @ShellMethodAvailability("availabilityCheck") (1)
    public void download() {
        [...]
    }

    public Availability availabilityCheck() { (1)
        return connected
            ? Availability.available()
            : Availability.unavailable("you are not connected");
    }
1 the names have to match

Finally, it is often the case that several commands in the same class share the same internal state and, thus, should all be available or unavailable as a group. Instead of having to stick the @ShellMethodAvailability on all command methods, Spring Shell lets you flip things around and put the @ShellMethodAvailabilty annotation on the availability method, specifying the names of the commands that it controls:spring-doc.cn

    @ShellMethod("Download the nuclear codes.")
    public void download() {
        [...]
    }

    @ShellMethod("Disconnect from the server.")
    public void disconnect() {
        [...]
    }

    @ShellMethodAvailability({"download", "disconnect"})
    public Availability availabilityCheck() {
        return connected
            ? Availability.available()
            : Availability.unavailable("you are not connected");
    }

The default value for the @ShellMethodAvailability.value() attribute is *. This special wildcard matches all command names. This makes it easy to turn all commands of a single class on or off with a single availability method:spring-doc.cn

@ShellComponent
public class Toggles {
  @ShellMethodAvailability
  public Availability availabilityOnWeekdays() {
    return Calendar.getInstance().get(DAY_OF_WEEK) == SUNDAY
      ? Availability.available()
      : Availability.unavailable("today is not Sunday");
  }

  @ShellMethod
  public void foo() {}

  @ShellMethod
  public void bar() {}
}
Spring Shell does not impose many constraints on how to write commands and how to organize classes. However, it is often good practice to put related commands in the same class, and the availability indicators can benefit from that.

4.4. Exit Code

Many command line applications when applicable return an exit code which running environment can use to differentiate if command has been executed successfully or not. In a spring-shell this mostly relates when a command is run on a non-interactive mode meaning one command is always executed once with an instance of a spring-shell.spring-doc.cn

Default behaviour of an exit codes is as:spring-doc.cn

Every CommandRegistration can define its own mappings between Exception and exit code. Essentially we’re bound to functionality in Spring Boot regarding exit code and simply integrate into that.spring-doc.cn

Assuming there is an exception show below which would be thrown from a command:spring-doc.cn

static class MyException extends RuntimeException {

	private final int code;

	MyException(String msg, int code) {
		super(msg);
		this.code = code;
	}

	public int getCode() {
		return code;
	}
}

It is possible to define a mapping function between Throwable and exit code. You can also just configure a class to exit code which is just a syntactic sugar within configurations.spring-doc.cn

CommandRegistration.builder()
	.withExitCode()
		.map(MyException.class, 3)
		.map(t -> {
			if (t instanceof MyException) {
				return ((MyException) t).getCode();
			}
			return 0;
		})
		.and()
	.build();
Exit codes cannot be customized with annotation based configuration