Unlocking the Power of Spring Shell: A Guide for Beginners

Diving into the world of Java development, we often encounter the need to interact with applications directly through the command line. Enter Spring Shell, a project within the larger Spring Framework ecosystem designed to simplify the creation of interactive command-line applications. This beginner-friendly guide aims to introduce you to Spring Shell, helping you unlock its potential to build powerful CLI tools with ease. Whether you’re new to programming or looking to expand your Java toolkit, this journey will set the foundation for your command-line adventures.

What’s Spring Shell?

Spring Shell is a powerful library from the broader Spring Framework universe, tailored specifically for developing interactive command-line applications. Just as Spring Boot simplifies web application development, Spring Shell streamlines the creation of command-line interfaces (CLI), enabling developers to craft complex commands, manage dependencies, and handle input/output with minimal fuss.

Imagine having the ability to interact with your application through a text interface, executing tasks, querying data, or configuring features with simple commands. That’s the essence of Spring Shell. It leverages the robustness of the Spring Framework, including dependency injection and auto-configuration, to make CLI development as straightforward as building a web app.

With Spring Shell, you’re not just writing scripts; you’re building a rich, interactive user interface that runs in the terminal. This is particularly useful for applications that need to run automated tasks, scripts, or management commands in environments where a graphical user interface (GUI) may not be available or necessary.

Spring Shell excels in scenarios where complex inputs are required, supporting dynamic commands, parameter autocompletion, and even the grouping of commands into logical sets. It’s an ideal choice for both developers looking to add CLI capabilities to their Spring applications and for those aiming to build standalone command-line tools.

Key Concepts and Annotations

When venturing into the realm of Spring Shell, understanding its core concepts and the annotations that bring your command-line application to life is crucial. These elements are the building blocks that enable you to define how your application behaves in the command line environment.

@CommandScan

  • Purpose: Automatically discovers and registers command classes and methods throughout your application.
  • Usage: Placed on the main application class, it instructs Spring Shell to scan the application’s packages for classes and methods annotated with @Command, registering them as commands that users can execute.

@Command

  • Purpose: Marks a method or class as containing executable commands. When applied to a method, it designates that method as a command. When used on a class, it serves as a namespace or grouping for related commands within that class.
  • Usage: This is where you define the actions your CLI can perform. Each command can be customized with a name, help text, and other attributes to guide users.

@Option

  • Purpose: Specifies method parameters as command options, allowing users to pass arguments to commands.
  • Usage: Each @Option-annotated parameter in a command method becomes a user-specifiable option, making your commands versatile and interactive.

@ShellComponent

  • Purpose: Identifies a class as a Spring Shell component, making it a candidate for auto-detection when using component scanning.
  • Usage: Classes annotated with @ShellComponent are automatically picked up by Spring Shell if @CommandScan is present on the application class. This simplifies the organization of command methods.

Interactive Commands

Spring Shell shines with its support for interactive commands. Through prompts and dynamic input handling, you can create a user-friendly CLI experience. Methods can prompt users for input if required options are not provided, or dynamically adjust available options based on previous inputs. This interactivity is achieved through careful design of your command methods and leveraging Spring Shell’s utilities for user input.

Command Grouping

  • Purpose: Organizes related commands under a common namespace or heading, improving the usability and navigability of your CLI application.
  • Usage: By structuring commands within classes and using class-level @Command annotations, you can create intuitive groupings that make it easier for users to find and use related commands.

Building a Simple CLI with Spring Shell Using Modern Annotations

Creating a CLI application with the new annotation-based approach in Spring Shell involves a straightforward yet powerful method of defining commands. This process enables you to design and implement an interactive command-line interface efficiently. Here’s how to get started with this enhanced model.

Step 1: Initialize Your Spring Boot Application

Begin by setting up a Spring Boot project. Utilize tools like Spring Initializr to include Spring Shell dependencies alongside Spring Web for a comprehensive setup.

Step 2: Activate Command Scanning

Enable your application to auto-detect CLI commands by annotating the main class with @CommandScan. This crucial step prepares your application for recognizing commands you’ll define.

1
2
3
4
5
6
7
@CommandScan
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}

Step 3: Define Commands with @Command

Craft your first command by annotating methods within a class designated for command definitions. The @Command annotation marks these methods as executable commands. Organize your commands in a meaningful way within your project structure.

1
2
3
4
5
6
7
public class GreetingCommands {

@Command(description = "Greet someone")
public String greet(@Option(names = {"--name", "-n"}, description = "Name of the person to greet") String name) {
return String.format("Hello, %s!", name);
}
}
  • @Command: Now directly used on methods to define a command. The description attribute allows for detailing what the command does.
  • @Option: Defines the parameters that can be passed to your command, making it flexible and interactive.

Step 4: Run and Interact with Your CLI

To run and interact with your CLI application using the greet command, after launching your application in the terminal, you would execute a command like this:

1
shell:> greet --name "Spring Shell User"

In response, your CLI should output a personalized greeting, demonstrating the command’s functionality:

1
Hello, Spring Shell User!

This simple interaction showcases how users can execute commands and pass options to them, providing a straightforward example of the application’s capabilities.

Explaining the Example Program

The example program introduces a ProjectGenerator class that leverages Spring Shell to create an interactive CLI application. Key annotations include:

  • @Command: Marks the class for command discovery by Spring Shell, with a specific method generator defined to execute as a CLI command.

The generator method uses @Option annotations to define various parameters like buildTool, groupId, and others, allowing users to input these values when running the command. It showcases interactive prompts for collecting user input directly from the CLI, employing helper methods such as promptForBuildTool and promptForInput to facilitate user choices or text input.

Interactive Implementation Explained

Interactive features in Spring Shell are highlighted through methods like selectFromOptions and promptForInput, which guide users through inputting necessary details. These methods leverage Spring Shell components like SingleItemSelector for option selection and StringInput for text inputs, demonstrating an engaging user experience by requesting additional information as needed.

This program exemplifies the flexibility and user-friendly nature of Spring Shell for building CLI tools that require detailed user input, showing how developers can implement complex input logic in a structured and interactive way.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@Slf4j
@Command
@RequiredArgsConstructor
public class ProjectGenerator extends AbstractShellComponent {

private final GeneratorMapper generatorMapper;
private final ProjectService projectService;

@Command(command = { "generator" })
public void generator(
@Option(longNames = "buildTool") BuildToolEnum buildToolEnum,
@Option(longNames = "groupId") String groupId,
@Option(longNames = "artifactId") String artifactId,
@Option(longNames = "name") String name,
@Option(longNames = "description") String description,
@Option(longNames = "packageName") String packageName,
@Option(longNames = "jvmVersion") String jvmVersion,
@Option(longNames = "openapiFilePath") String openapiFilePath,
@Option(longNames = "dbUrl") String dbUrl,
@Option(longNames = "dbUsername") String dbUsername,
@Option(longNames = "dbPassword") String dbPassword,
@Option(longNames = "runtime") RuntimeEnum runtimeEnum
) {
if (ObjectUtils.isEmpty(buildToolEnum)) {
String buildToolStr = this.promptForBuildTool();
buildToolEnum = BuildToolEnum.valueOf(buildToolStr.toUpperCase());
}

groupId = StringUtils.hasText(groupId)
? groupId
: promptForInput("Please enter group id", "com.example");
artifactId = StringUtils.hasText(artifactId)
? artifactId
: promptForInput("Please enter artifact id", "demo");
name = StringUtils.hasText(name)
? name
: promptForInput("Please enter project name", "demo");
description = StringUtils.hasText(description)
? description
: promptForInput(
"Please enter project description",
"Demo project for Spring Boot"
);
packageName = StringUtils.hasText(packageName)
? packageName
: promptForInput("Please enter package name", "com.example.demo");
jvmVersion = StringUtils.hasText(jvmVersion)
? jvmVersion
: promptForJvmVersion();
openapiFilePath = StringUtils.hasText(openapiFilePath)
? openapiFilePath
: promptForInput(
"Please enter OpenAPI file path",
"/path/to/openapi.yaml"
);
dbUrl = StringUtils.hasText(dbUrl)
? dbUrl
: promptForInput(
"Please enter database URL",
"jdbc:postgresql://localhost:5432/mydatabase"
);
dbUsername = StringUtils.hasText(dbUsername)
? dbUsername
: promptForInput("Please enter database username", "myuser");
dbPassword = StringUtils.hasText(dbPassword)
? dbPassword
: promptForSecretInput("Please enter database password", "secret");
if (ObjectUtils.isEmpty(runtimeEnum)) {
String runtimeStr = promptForRuntime();
runtimeEnum = RuntimeEnum.valueOf(runtimeStr.toUpperCase());
}

CreateProjectCommand createProjectCommand = null;
Path projectTempPath = null;
try {
Path userDirectoryPath = Paths.get("");
projectTempPath = userDirectoryPath.resolve(
Path.of(name + "-" + new Random().nextInt(1000))
);

createProjectCommand = generatorMapper.toCreateProjectCommand(
projectTempPath,
buildToolEnum,
groupId,
artifactId,
name,
description,
packageName,
jvmVersion,
openapiFilePath,
dbUrl,
dbUsername,
dbPassword,
runtimeEnum
);
projectService.create(createProjectCommand);
} catch (Exception e) {
log.error("", e);
}
}

private String promptForBuildTool() {
return selectFromOptions(
"Please choose a build tool",
List.of("Gradle", "Maven"),
"Gradle"
);
}

private String promptForRuntime() {
return selectFromOptions(
"Please choose a runtime environment",
List.of("CloudRun", "Kubernetes"),
"CloudRun"
);
}

private String promptForJvmVersion() {
return selectFromOptions(
"Please choose a Java version",
List.of("17"),
"17"
);
}

private String selectFromOptions(
String prompt,
List<String> options,
String defaultValue
) {
List<SelectorItem<String>> items = options
.stream()
.map(option -> SelectorItem.of(option, option.toUpperCase()))
.collect(Collectors.toList());

SingleItemSelector<String, SelectorItem<String>> selector =
new SingleItemSelector<>(getTerminal(), items, prompt, null);
configureComponent(selector);
SingleItemSelectorContext<String, SelectorItem<String>> context =
selector.run(SingleItemSelectorContext.empty());

return context
.getResultItem()
.map(SelectorItem::getItem)
.orElse(defaultValue);
}

private String promptForInput(String prompt, String defaultValue) {
StringInput input = new StringInput(getTerminal(), prompt, defaultValue);
configureComponent(input);
StringInputContext context = input.run(StringInputContext.empty());
return context.getResultValue();
}

private String promptForSecretInput(String prompt, String defaultValue) {
StringInput input = new StringInput(getTerminal(), prompt, defaultValue);
input.setMaskCharacter('*');
configureComponent(input);
StringInputContext context = input.run(StringInputContext.empty());
return context.getResultValue();
}

private void configureComponent(
SingleItemSelector<String, SelectorItem<String>> component
) {
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
}

private void configureComponent(StringInput component) {
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
}
}

Conclusion

Exploring Spring Shell through the ProjectGenerator example offers a glimpse into building interactive CLI applications with Java. By harnessing annotations like @Command and @Option, coupled with interactive prompts, developers can create sophisticated, user-friendly command-line interfaces. This guide underscores Spring Shell’s capability to elevate CLI tool development, making it accessible even for beginners in the Java ecosystem.

Further Reading

To deepen your understanding of Spring Shell and explore more advanced features, the following resources are invaluable:

cover