Spring Shell - Quick Reference

SkillAI & models

Spring Shell for building interactive CLI applications. Covers @ShellComponent, @ShellMethod, input validation, tables, and command groups.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Spring Shell - Quick Reference skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/backend-frameworks/spring-shell/SKILL.md and read by ahel’s review.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: spring-shell for comprehensive documentation.

Dependencies

<dependency>
    <groupId>org.springframework.shell</groupId>
    <artifactId>spring-shell-starter</artifactId>
</dependency>

Configuration

application.yml

spring:
  shell:
    interactive:
      enabled: true
    noninteractive:
      enabled: true
    history:
      enabled: true
      name: .myapp_history
    command:
      version:
        enabled: true

Basic Commands

Simple Command

@ShellComponent
public class GreetingCommands {

    @ShellMethod(value = "Say hello", key = "hello")
    public String hello(
            @ShellOption(defaultValue = "World") String name) {
        return "Hello, " + name + "!";
    }

    @ShellMethod("Add two numbers")
    public int add(int a, int b) {
        return a + b;
    }

    @ShellMethod("Show current date")
    public String date() {
        return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}

Command Groups

@ShellComponent
@ShellCommandGroup("User Management")
public class UserCommands {

    @ShellMethod("List all users")
    public Table listUsers() {
        List<User> users = userService.findAll();
        return buildUserTable(users);
    }

    @ShellMethod("Create a new user")
    public String createUser(
            @ShellOption(help = "Username") String username,
            @ShellOption(help = "Email address") String email,
            @ShellOption(help = "User role", defaultValue = "USER") String role) {

        User user = userService.create(username, email, role);
        return "Created user: " + user.getId();
    }

    @ShellMethod("Delete a user")
    public String deleteUser(@ShellOption(help = "User ID") Long id) {
        userService.delete(id);
        return "User deleted successfully";
    }
}

Shell Options

Option Annotations

@ShellComponent
public class AdvancedCommands {

    @ShellMethod("Process file with options")
    public String process(
            // Required option
            @ShellOption(help = "Input file path") String input,

            // Optional with default
            @ShellOption(defaultValue = "output.txt", help = "Output file") String output,

            // Boolean flag
            @ShellOption(defaultValue = "false", help = "Verbose mode") boolean verbose,

            // Array/List option
            @ShellOption(help = "Tags to apply") String[] tags,

            // Arity (number of values)
            @ShellOption(arity = 2, help = "Coordinates x y") int[] coords) {

        // Implementation
        return "Processing " + input;
    }

    // Named options with aliases
    @ShellMethod("Export data")
    public String export(
            @ShellOption(value = {"-f", "--format"}, defaultValue = "json") String format,
            @ShellOption(value = {"-o", "--output"}) String output,
            @ShellOption(value = {"-c", "--compress"}, defaultValue = "false") boolean compress) {

        return String.format("Exporting to %s in %s format (compressed: %s)",
            output, format, compress);
    }
}

Availability and Validation

Command Availability

@ShellComponent
public class AdminCommands {

    private boolean authenticated = false;

    @ShellMethod("Login to the system")
    public String login(String username, String password) {
        if (authService.authenticate(username, password)) {
            authenticated = true;
            return "Login successful";
        }
        return "Login failed";
    }

    @ShellMethod("Perform admin action")
    @ShellMethodAvailability("isAuthenticated")
    public String adminAction() {
        return "Admin action performed";
    }

    public Availability isAuthenticated() {
        return authenticated
            ? Availability.available()
            : Availability.unavailable("You must login first");
    }

    // Apply to multiple methods
    @ShellMethodAvailability({"adminAction", "deleteAll", "resetSystem"})
    public Availability requiresAdmin() {
        return currentUser.isAdmin()
            ? Availability.available()
            : Availability.unavailable("Admin privileges required");
    }
}

Input Validation

@ShellComponent
public class ValidatedCommands {

    @ShellMethod("Create user with validation")
    public String createUser(
            @ShellOption @Size(min = 3, max = 20) String username,
            @ShellOption @Email String email,
            @ShellOption @Min(18) @Max(120) int age) {

        return "User created: " + username;
    }
}

// Custom validator
@Component
public class FileExistsValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return String.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        String path = (String) target;
        if (!Files.exists(Path.of(path))) {
            errors.reject("file.notfound", "File does not exist: " + path);
        }
    }
}

Output Formatting

Tables

@ShellComponent
public class TableCommands {

    @ShellMethod("Show users in table format")
    public Table showUsers() {
        List<User> users = userService.findAll();

        TableModel model = new BeanListTableModel<>(users,
            new LinkedHashMap<>() {{
                put("id", "ID");
                put("username", "Username");
                put("email", "Email");
                put("role", "Role");
                put("createdAt", "Created");
            }});

        TableBuilder tableBuilder = new TableBuilder(model);
        tableBuilder.addFullBorder(BorderStyle.fancy_light);
        return tableBuilder.build();
    }

    // Custom table
    @ShellMethod("Show system info")
    public Table systemInfo() {
        String[][] data = {
            {"OS", System.getProperty("os.name")},
            {"Java", System.getProperty("java.version")},
            {"Memory", Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB"},
            {"Processors", String.valueOf(Runtime.getRuntime().availableProcessors())}
        };

        TableModel model = new ArrayTableModel(data);
        TableBuilder builder = new TableBuilder(model);
        builder.addHeaderBorder(BorderStyle.fancy_double);
        return builder.build();
    }
}

Colored Output

@ShellComponent
public class ColoredCommands {

    @ShellMethod("Show status with colors")
    public AttributedString status() {
        AttributedStringBuilder builder = new AttributedStringBuilder();

        builder.append("Status: ");
        builder.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
        builder.append("RUNNING");
        builder.style(AttributedStyle.DEFAULT);
        builder.append("\n");

        builder.append("Errors: ");
        builder.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold());
        builder.append("3");
        builder.style(AttributedStyle.DEFAULT);

        return builder.toAttributedString();
    }
}

Interactive Input

User Prompts

@ShellComponent
public class InteractiveCommands {

    private final LineReader lineReader;

    @ShellMethod("Interactive user creation")
    public String createUserInteractive() {
        String username = lineReader.readLine("Enter username: ");
        String email = lineReader.readLine("Enter email: ");
        String password = lineReader.readLine("Enter password: ", '*');

        String confirm = lineReader.readLine("Create user? (y/n): ");
        if ("y".equalsIgnoreCase(confirm)) {
            userService.create(username, email, password);
            return "User created successfully";
        }
        return "Cancelled";
    }
}

Progress Indicators

@ShellComponent
public class ProgressCommands {

    private final TerminalUI terminalUI;

    @ShellMethod("Import data with progress")
    public void importData(String file) throws Exception {
        List<String> lines = Files.readAllLines(Path.of(file));
        int total = lines.size();

        try (ProgressView progress = terminalUI.progressView()
                .spinner(Spinner.SIMPLE)
                .text("Importing data...")
                .max(total)
                .start()) {

            for (int i = 0; i < total; i++) {
                processLine(lines.get(i));
                progress.setValue(i + 1);
                progress.setText("Processing " + (i + 1) + "/" + total);
            }
        }
    }
}

Non-Interactive Mode

// Run single command
// java -jar myapp.jar hello --name John

// Run script
// java -jar myapp.jar @commands.txt

@SpringBootApplication
public class CliApplication implements CommandLineRunner {

    @Autowired
    private Shell shell;

    @Override
    public void run(String... args) throws Exception {
        if (args.length > 0 && args[0].equals("--batch")) {
            // Run in non-interactive mode
            shell.evaluate(() -> "process-batch --file data.csv");
            shell.evaluate(() -> "export --format json --output result.json");
            System.exit(0);
        }
        // Otherwise, start interactive shell
    }
}

Custom Prompt

@Component
public class CustomPromptProvider implements PromptProvider {

    private String currentContext = "default";

    @Override
    public AttributedString getPrompt() {
        return new AttributedString(
            String.format("myapp:%s> ", currentContext),
            AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN)
        );
    }

    public void setContext(String context) {
        this.currentContext = context;
    }
}

Exception Handling

@Component
public class CustomExceptionHandler implements CommandExceptionResolver {

    @Override
    public CommandHandlingResult resolve(Exception ex) {
        if (ex instanceof IllegalArgumentException) {
            return CommandHandlingResult.of(
                "Invalid argument: " + ex.getMessage() + "\n",
                1  // Exit code
            );
        }
        if (ex instanceof ResourceNotFoundException) {
            return CommandHandlingResult.of(
                "Error: " + ex.getMessage() + "\n",
                2
            );
        }
        // Let default handler process
        return null;
    }
}

Testing

@SpringBootTest
class CommandsTest {

    @Autowired
    private Shell shell;

    @Test
    void shouldSayHello() {
        Object result = shell.evaluate(() -> "hello --name John");
        assertThat(result).isEqualTo("Hello, John!");
    }

    @Test
    void shouldAddNumbers() {
        Object result = shell.evaluate(() -> "add 5 3");
        assertThat(result).isEqualTo(8);
    }

    @Test
    void shouldShowUnavailableForAdminCommands() {
        Object result = shell.evaluate(() -> "admin-action");
        assertThat(result.toString()).contains("must login first");
    }
}

Best Practices

DoDon't
Group related commandsCreate flat command structure
Provide --help for all optionsLeave options undocumented
Use tables for structured dataDump raw data
Implement availability checksLet commands fail cryptically
Support both interactive and batchForce one mode only

Production Checklist

  • Commands grouped logically
  • All options documented (help)
  • Input validation implemented
  • Proper error handling
  • Progress indicators for long ops
  • Non-interactive mode supported
  • Exit codes defined
  • History enabled/configured
  • Security checks (authentication)
  • Tab completion working

When NOT to Use This Skill

  • Simple scripts - Use regular main method
  • Batch processing - Use spring-batch skill
  • Web APIs - Use spring-rest skill
  • GUI applications - Consider JavaFX

Anti-Patterns

Anti-PatternProblemSolution
Flat command structureHard to navigateGroup related commands
Undocumented optionsPoor UXAdd help to all options
No availability checksCryptic failuresImplement @ShellMethodAvailability
Raw data outputUnreadableUse tables for structured data
Interactive-only modeNot scriptableSupport batch mode

Quick Troubleshooting

ProblemDiagnosticFix
Command not foundCheck annotationAdd @ShellComponent
Option not recognizedCheck namingVerify @ShellOption config
Validation not workingCheck annotationsAdd Bean Validation
Command unavailableCheck availabilityVerify availability method
Tab completion missingCheck setupConfigure completion bean

Reference Documentation

Signals

GitHub stars
33
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
spring-shell
Source
github.com/claude-dev-suite/claude-dev-suite