Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
root = true

[*.{java,xml,gradle}]
indent_style = tab
indent_size = 4
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.shell.core.command;

import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

import org.springframework.shell.core.commands.AbstractCommand;

/**
* @author Eric Bottard
Expand Down Expand Up @@ -74,4 +76,70 @@ default List<String> getAliases() {
*/
ExitStatus execute(CommandContext commandContext) throws Exception;

/**
* Creates and returns a new instance of a {@code Builder} for defining and
* constructing commands.
* <p>
* The builder allows customization of command properties such as name, description,
* group, help text, aliases, and execution logic.
* @return a new {@code Builder} instance for configuring and creating commands
*/
static Builder builder() {
return new DefaultCommandBuilder();
}

/**
* Builder for creating command.
*/
interface Builder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the Builder should be an interface with a default implementation (creating a custom user-defined builder is not very likely). There are of course good reasons to make a concept configurable/swappable by defining it in an interface/contract, but for builders I am not sure we need that (at least for the most typical average usage). For example, the RetryPolicy in Spring Framework defines a builder in the interface, and it is a static inner class and not an interface + default implementation: https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java.

So to keep things simple, can you please follow the same approach? Thank you upfront.


/**
* Set the name of the command.
* @return this builder
*/
Builder name(String name);

/**
* Set the description of the command.
* @return this builder
*/
Builder description(String description);

/**
* Set the help of the command.
* @return this builder
*/
Builder help(String help);

/**
* Set the group of the command.
* @return this builder
*/
Builder group(String group);

/**
* Set the aliases of the command.
* @return this builder
*/
Builder aliases(String... aliases);

/**
* Set the aliases of the command.
* @return this builder
*/
Builder aliases(List<String> aliases);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have a method accepting aliases as varargs and to keep the API minimalistic, I think there is no need for another one that accepts a list (conversion is always possible on the client side).


/**
* Set command execution logic.
* @return this builder
*/
Builder execute(Consumer<CommandContext> commandExecutor);

/**
* Build the {@link AbstractCommand}.
*/
AbstractCommand build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbstractCommand is a leaky abstraction here (look at the import coming from a different package). The builder should return the interface type (or at least a package private type that we can't break from the outside, but since we don't have that, returning the interface type is the way to go).


}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.springframework.shell.core.command;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import org.jspecify.annotations.Nullable;

import org.springframework.shell.core.commands.AbstractCommand;
import org.springframework.shell.core.commands.adapter.ConsumerCommandAdapter;
import org.springframework.util.Assert;
import static org.springframework.shell.core.command.Command.*;

/**
* Default implementation of {@link Builder}.
*
* @author Piotr Olaszewski
*/
class DefaultCommandBuilder implements Builder {

private @Nullable String name;

private @Nullable String description;

private String group = "";

private String help = "";

private @Nullable List<String> aliases;

private @Nullable Consumer<CommandContext> commandContextConsumer;

@Override
public Builder name(String name) {
this.name = name;
return this;
}

@Override
public Builder description(String description) {
this.description = description;
return this;
}

@Override
public Builder help(String help) {
this.help = help;
return this;
}

@Override
public Builder group(String group) {
this.group = group;
return this;
}

@Override
public Builder aliases(String... aliases) {
this.aliases = Arrays.asList(aliases);
return this;
}

@Override
public Builder aliases(List<String> aliases) {
this.aliases = aliases;
return this;
}

@Override
public Builder execute(Consumer<CommandContext> commandExecutor) {
this.commandContextConsumer = commandExecutor;
return this;
}

@Override
public AbstractCommand build() {
ConsumerCommandAdapter abstractCommand = initCommand();

if (aliases != null) {
abstractCommand.setAliases(aliases);
}

return abstractCommand;
}

private ConsumerCommandAdapter initCommand() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code here is used in a single place, so it could be inlined.

Assert.hasText(name, "'name' must be specified");
Assert.hasText(description, "description");
Assert.notNull(commandContextConsumer, "'commandExecutor' must not be null");

return new ConsumerCommandAdapter(name, description, group, help, commandContextConsumer);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ public ExitStatus execute(CommandContext commandContext) throws Exception {
return doExecute(commandContext);
}

public abstract ExitStatus doExecute(CommandContext commandContext) throws Exception;

private static boolean isHelp(CommandOption option) {
return option.longName().equalsIgnoreCase("help") || option.shortName() == 'h';
}

public abstract ExitStatus doExecute(CommandContext commandContext) throws Exception;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package org.springframework.shell.core.commands.adapter;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
import java.util.List;

import org.jline.terminal.Terminal;
import org.jline.utils.AttributedStringBuilder;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.core.ShellRunner;
import org.springframework.shell.core.command.CommandContext;
import org.springframework.shell.core.command.annotation.*;
import org.springframework.shell.core.command.annotation.Argument;
import org.springframework.shell.core.command.annotation.Arguments;
import org.springframework.shell.core.command.annotation.Command;
import org.springframework.shell.core.command.annotation.EnableCommand;
import org.springframework.shell.core.command.annotation.Option;
import org.springframework.shell.core.commands.AbstractCommand;
import static org.jline.utils.AttributedStyle.*;

@EnableCommand(SpringShellApplication.class)
public class SpringShellApplication {
Expand Down Expand Up @@ -43,6 +50,26 @@ public void sayYo(CommandContext commandContext) {
terminal.writer().println("Yo there! what's up?");
}

@Bean
public AbstractCommand sayGoodMorning() {
return org.springframework.shell.core.command.Command.builder()
.name("good-morning")
.description("Say good morning")
.group("greetings")
.help("A command that greets the user with 'Good morning!'")
.execute(commandContext -> {
Copy link
Contributor

@fmbenhassine fmbenhassine Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using the overloaded method that takes a Function<CommandContext, String> and remove the terminal printing /flushing noise?

That would be a good example to show how to print styled output without dealing with terminal details.

String ansiString = new AttributedStringBuilder().append("Good morning ")
.append("Sir", BOLD.foreground(GREEN))
.append("!")
.toAnsi();

Terminal terminal = commandContext.terminal();
terminal.writer().println(ansiString);
terminal.flush();
})
.build();
}

@Bean
public HelloCommand sayHello() {
return new HelloCommand();
Expand Down