Skip to content

Single + Default Command #1067

Open
Open

Description

@danvega

What if my application is only going to have a single command? I would love the ability to define a configuration property that says I only have a single command and here is what it is. When I run the program I am dropped into the shell and whatever I type is now the argument to the single default command.

This came up when building a Spring Shell + Spring AI application that interfaces with Open AI's GPT-4. I have included the rep for reference below.

https://github.com/danvega/sbdocs

Activity

  1. changed the title [-]Default Command[/-] [+]Single + Default Command[/+] on May 13, 2024
  2. helloshisheng commented on May 13, 2024

    @helloshisheng
  3. added a commit that references this issue on May 22, 2024
    cd7add3
  4. jvalkeal commented on May 23, 2024

    @jvalkeal
    Contributor

    Ups, linked and closed wrong issue

  5. jvalkeal commented on May 24, 2024

    @jvalkeal
    Contributor

    Little clarification when I discussed with Dan. This is about interactive shell and having that one command there without having a need to define that command.

  6. zhoumiv commented on Jul 16, 2025

    @zhoumiv

    Hi~ What's the progress on this issue? I'm eager to find out how to specify a default command in interactive mode, just like the single-command in non-interactive mode.

  7. zhoumiv commented on Feb 11, 2026

    @zhoumiv

    @danvega After some exploration, I've found a way to implement a "default command" — by customizing org.springframework.shell.Shell and overriding its evaluate method. The original evaluate method:

    Image

    So, what we need to do is modify the behavior of evaluate to use a default command for every input (except special commands):

    I've defined the default command as chat:

    @Command(command = "chat", description = "Chat with the assistant")
    public void chat(@Option(longNames = "query", shortNames = 'q', description = "The message to send", required = true, arity = ONE_OR_MORE)
                     String[] words) {
        String userQuery = String.join(" ", words);
    
        try {
            streamChat(userQuery);
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
    }

    Customize Shell to change the behavior of evaluate:

    /**
     * @see org.springframework.shell.Shell
     * @see org.springframework.shell.boot.SpringShellAutoConfiguration
     */
    @Primary
    @Component
    public class MyShell extends Shell {
    
        // This is just an example; you may obtain the current system’s command set to replace it.
        private static final Set<String> INTERNAL_COMMANDS = Set.of("help", "exit", "quit");
    
        public MyShell(ResultHandlerService resultHandlerService,
                       CommandCatalog commandRegistry,
                       Terminal terminal,
                       ShellContext shellContext,
                       ExitCodeMappings exitCodeMappings) {
            super(resultHandlerService, commandRegistry, terminal, shellContext, exitCodeMappings);
        }
    
        @Override
        protected Object evaluate(Input input) {
            List<String> words = input.words();
    
            if (words.isEmpty() || words.getFirst().isEmpty()) {
                return super.evaluate(input);
            }
    
            // If the first word is an internal command (like `help`, `exit`), execute the original logic directly
            if (INTERNAL_COMMANDS.contains(words.getFirst())) {
                return super.evaluate(input);
            }
    
            // Use the `chat` command to process the input text
            String rawText = input.rawText();
            return super.evaluate(() -> "chat " + rawText);
        }
    }

    Running example as shown below. You can see that if the input does not match INTERNAL_COMMANDS, it will default to using the chat command to process the input:
    Image

    Now there's another issue: because there's no matching command, the "highlighting feature" will treat this as an erroneous input and display it in red, which looks odd. Fortunately, we can also customize the "highlighting feature":

    /**
     * @see org.springframework.shell.boot.LineReaderAutoConfiguration#lineReader()
     */
    public class MyHighlighter implements Highlighter {
    
        // This is just an example; you may obtain the current system’s command set to replace it.
        private static final Set<String> INTERNAL_COMMANDS = Set.of("help", "exit", "quit");
    
        private final CommandCatalog commandRegistry;
    
        public MyHighlighter(CommandCatalog commandRegistry) {
            this.commandRegistry = commandRegistry;
        }
    
        @Override
        public AttributedString highlight(LineReader reader, String buffer) {
            String trimmedBuffer = buffer.trim();
    
            if (trimmedBuffer.isEmpty()) {
                return new AttributedString(buffer);
            }
    
            String[] words = trimmedBuffer.split(" ", 2);
    
            // If the first word of the input is not an internal command (like `help`, `exit`), then it's using the `chat` command
            if (!INTERNAL_COMMANDS.contains(words[0])) {
                // You can choose a style you like; here I'm using foreground color CYAN
                return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN));
            }
    
            // Copy from org.springframework.shell.boot.LineReaderAutoConfiguration#lineReader()
            int len = 0;
            String best = null;
            // Find the longest matching command
            for (String command : commandRegistry.getRegistrations().keySet()) {
                if (buffer.startsWith(command) && command.length() > len) {
                    len = command.length();
                    best = command;
                }
            }
    
            if (best != null) {
                return new AttributedStringBuilder(buffer.length())
                        .append(best, AttributedStyle.BOLD)
                        .append(buffer.substring(len))
                        .toAttributedString();
            } else {
                return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
            }
        }
    }

    Customize JLine's LineReader to use MyHighlighter for the "highlighting feature":

    /**
     * @see org.springframework.shell.boot.LineReaderAutoConfiguration;
     */
    @Configuration
    public class LineReaderConfig {
    
        private final LineReader lineReader;
    
        private final CommandCatalog commandRegistry;
    
        public LineReaderConfig(LineReader lineReader, CommandCatalog commandRegistry) {
            this.lineReader = lineReader;
            this.commandRegistry = commandRegistry;
        }
    
        @PostConstruct
        public void customHighlighter() {
            if (lineReader instanceof LineReaderImpl lineReaderImpl) {
                var highlighter = new MyHighlighter(commandRegistry);
                lineReaderImpl.setHighlighter(highlighter);
            }
        }
    }

    Final result:

    Image

    My Java version: 25.0.2
    My Spring Shell version: 3.4.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status/need-triageTeam needs to triage and take a first look

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions