diff --git a/README.md b/README.md index b2cc022..b577efe 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ AWS_PROFILE=myprofile ./mvnw spring-boot:run This project suffers from well-known problems with Intellij's terminal and jline. Most notably, normal input controls don't work, and tab completion is broken: https://youtrack.jetbrains.com/issue/IDEA-183619 -`rm` supports -f / --force, but Spring Shell doesn't seem to be able to handle a boolean flag without args combined with -standalone args, so one has to specify `rm -f true `. +`rm` supports -f / --force, but Spring Shell doesn't seem to be able to handle a boolean flag without an arg except at +the end of the command line. Example: `rm foo --force` works, but `rm --force foo` doesn't. +See: https://github.com/spring-projects/spring-shell/issues/1262 ## TODO diff --git a/pom.xml b/pom.xml index ee67700..82f2b56 100644 --- a/pom.xml +++ b/pom.xml @@ -12,13 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.5.7 + 4.0.2 25 1.18.42 - 2.41.0 + 2.41.19 + 4.0.1 @@ -61,21 +62,37 @@ org.springframework.shell - spring-shell-starter - 3.4.1 + spring-shell-starter-ffm + ${spring-shell.version} org.springframework.boot spring-boot-starter-test + + commons-io + commons-io + 2.21.0 + + + org.springframework.shell + spring-shell-test-autoconfigure + ${spring-shell.version} + test + + + org.awaitility + awaitility + test + org.testcontainers - junit-jupiter + testcontainers-junit-jupiter test org.testcontainers - localstack + testcontainers-localstack test diff --git a/src/main/java/com/internetstaff/parameterstore/ParameterStoreApp.java b/src/main/java/com/internetstaff/parameterstore/ParameterStoreApp.java index b7a5572..8fd9aaf 100644 --- a/src/main/java/com/internetstaff/parameterstore/ParameterStoreApp.java +++ b/src/main/java/com/internetstaff/parameterstore/ParameterStoreApp.java @@ -3,11 +3,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import org.springframework.shell.command.annotation.CommandScan; import software.amazon.awssdk.services.ssm.SsmClient; @SpringBootApplication -@CommandScan class ParameterStoreApp { public static void main(String[] args) { diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cat.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cat.java index f5b1b2c..455c332 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cat.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cat.java @@ -2,17 +2,18 @@ import com.internetstaff.parameterstore.application.port.out.ParameterStore; import lombok.RequiredArgsConstructor; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.shell.core.command.annotation.Argument; +import org.springframework.shell.core.command.annotation.Command; +import org.springframework.stereotype.Component; -@Command +@Component @RequiredArgsConstructor class Cat { private final ParameterStore parameterStore; @Command(description = "Cat Parameter", group = "Parameter Store") public String cat( - @Option(description = "Full path of parameter") String name + @Argument(index = 0, description = "Full path of parameter") String name ) { var result = parameterStore.getParameter(name); diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cd.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cd.java index cb328a4..9c602a2 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cd.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cd.java @@ -2,17 +2,19 @@ import com.internetstaff.parameterstore.application.port.in.SetCurrentDirectoryUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.shell.core.command.annotation.Argument; +import org.springframework.shell.core.command.annotation.Command; +import org.springframework.stereotype.Component; -@Command + +@Component @RequiredArgsConstructor public class Cd { private final SetCurrentDirectoryUseCase setCurrentDirectoryUseCase; @Command(description = "Change directory", group = "Parameter Store") public void cd( - @Option(description = "Path") String path + @Argument(index = 0, description = "Path") String path ) { setCurrentDirectoryUseCase.setCurrentDirectory(path); } diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cp.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cp.java index fc552e4..7f4ba21 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cp.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Cp.java @@ -2,18 +2,19 @@ import com.internetstaff.parameterstore.application.port.out.ParameterStore; import lombok.RequiredArgsConstructor; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.shell.core.command.annotation.Argument; +import org.springframework.shell.core.command.annotation.Command; +import org.springframework.stereotype.Component; -@Command +@Component @RequiredArgsConstructor class Cp { private final ParameterStore parameterStore; @Command(description = "Copy Parameter", group = "Parameter Store") public String cp( - @Option(description = "Full path of source parameter") String source, - @Option(description = "Full path of destination parameter") String destination + @Argument(index = 0, description = "Full path of source parameter") String source, + @Argument(index = 1, description = "Full path of destination parameter") String destination ) { if (parameterStore.copyParameter(source, destination)) { diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Create.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Create.java index 2571779..bea7626 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Create.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Create.java @@ -2,35 +2,40 @@ import com.internetstaff.parameterstore.application.port.out.ParameterStore; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.jline.terminal.Terminal; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; -import org.springframework.shell.standard.AbstractShellComponent; +import org.springframework.shell.core.command.annotation.Argument; +import org.springframework.shell.core.command.annotation.Command; +import org.springframework.shell.core.command.annotation.Option; +import org.springframework.stereotype.Component; import software.amazon.awssdk.services.ssm.model.ParameterType; import java.util.Optional; import java.util.stream.Collectors; +@Component @RequiredArgsConstructor -@Command -class Create extends AbstractShellComponent { +class Create { private final ParameterStore parameterStore; private final Terminal terminal; private final OptionPrompter promptingService; @Command(description = "Create Parameter", group = "Parameter Store") public void create( - @Option String name, - @Option String value, - @Option String type + @Option(description = "Parameter Type") String type, + @Argument(index = 0, description = "Parameter name") String name, + @Argument(index = 1, description = "Parameter value") String value ) { var paramName = Optional.ofNullable(name) + .map(StringUtils::trimToNull) .orElseGet(() -> promptingService.prompt("Parameter name: ")); var paramValue = Optional.ofNullable(value) + .map(StringUtils::trimToNull) .orElseGet(() -> promptingService.prompt("Parameter value: ")); var paramType = Optional.ofNullable(type) + .map(StringUtils::trimToNull) .orElseGet(() -> promptingService.select("Parameter type: ", ParameterType.knownValues().stream() .collect(Collectors.toMap(ParameterType::toString, ParameterType::toString)))); diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Ls.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Ls.java index 30c1c41..4ae5947 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Ls.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Ls.java @@ -1,19 +1,23 @@ package com.internetstaff.parameterstore.adapter.in.shell; +import com.internetstaff.parameterstore.application.port.in.GetCurrentDirectoryUseCase; import com.internetstaff.parameterstore.application.port.out.ParameterStore; import com.internetstaff.parameterstore.application.port.out.ParameterStore.Metadata; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.shell.core.command.annotation.Argument; +import org.springframework.shell.core.command.annotation.Command; +import org.springframework.stereotype.Component; import java.util.Comparator; -@Command +@Component @RequiredArgsConstructor class Ls { private final ParameterStore parameterStore; + private final GetCurrentDirectoryUseCase getCurrentDirectoryUseCase; + // Method to do this in Spring Shell? private int maxLength(java.util.List parameters) { return parameters.stream() .map(Metadata::name) @@ -24,7 +28,7 @@ private int maxLength(java.util.List parameters) { @Command(description = "List parameters", group = "Parameter Store") public String ls( - @Option(defaultValue = "*", description = "Glob pattern") String path + @Argument(index = 0, defaultValue = "*", description = "Glob pattern") String path ) { var parameters = parameterStore.getParameters(path); @@ -33,7 +37,7 @@ public String ls( var result = new StringBuilder(); for (var parameter : parameters) { - result.append(StringUtils.rightPad(parameter.name(), len)) + result.append(StringUtils.rightPad(getCurrentDirectoryUseCase.baseName(parameter.name()), len)) .append(" ") .append(parameter.lastModifiedDate()) .append("\n"); diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Mv.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Mv.java index 3f7c6a1..0e9ba5f 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Mv.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Mv.java @@ -2,18 +2,19 @@ import com.internetstaff.parameterstore.application.port.out.ParameterStore; import lombok.RequiredArgsConstructor; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.shell.core.command.annotation.Argument; +import org.springframework.shell.core.command.annotation.Command; +import org.springframework.stereotype.Component; -@Command +@Component @RequiredArgsConstructor class Mv { private final ParameterStore parameterStore; @Command(description = "Move Parameter", group = "Parameter Store") public String mv( - @Option(description = "Full path of source parameter") String source, - @Option(description = "Full path of destination parameter") String destination + @Argument(index = 0, description = "Full path of source parameter") String source, + @Argument(index = 1, description = "Full path of destination parameter") String destination ) { if (parameterStore.copyParameter(source, destination)) { diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/OptionPrompter.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/OptionPrompter.java index c22ba76..eb1e95f 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/OptionPrompter.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/OptionPrompter.java @@ -3,12 +3,13 @@ import lombok.RequiredArgsConstructor; import org.jline.terminal.Terminal; import org.springframework.core.io.ResourceLoader; -import org.springframework.shell.component.SingleItemSelector; -import org.springframework.shell.component.StringInput; -import org.springframework.shell.component.support.SelectorItem; -import org.springframework.shell.style.TemplateExecutor; +import org.springframework.shell.jline.tui.component.SingleItemSelector; +import org.springframework.shell.jline.tui.component.StringInput; +import org.springframework.shell.jline.tui.component.support.SelectorItem; +import org.springframework.shell.jline.tui.style.TemplateExecutor; import org.springframework.stereotype.Component; +import java.io.IOException; import java.util.Map; @Component @@ -43,4 +44,18 @@ public T select(String name, Map options) { .orElse(null); } + public boolean confirm(String message) { + terminal.writer().print("%s [y/N] ".formatted(message)); + terminal.writer().flush(); + + try { + int ch = terminal.reader().read(); + terminal.writer().println(); + terminal.writer().flush(); + return ch == 'y' || ch == 'Y'; + } catch (IOException e) { + return false; + } + } + } diff --git a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Rm.java b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Rm.java index 559d7db..fc5ab91 100644 --- a/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Rm.java +++ b/src/main/java/com/internetstaff/parameterstore/adapter/in/shell/Rm.java @@ -2,23 +2,32 @@ import com.internetstaff.parameterstore.application.port.out.ParameterStore; import lombok.RequiredArgsConstructor; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.shell.core.command.annotation.Argument; +import org.springframework.shell.core.command.annotation.Command; +import org.springframework.shell.core.command.annotation.Option; +import org.springframework.stereotype.Component; -@Command +@Component @RequiredArgsConstructor class Rm { private final ParameterStore parameterStore; + private final OptionPrompter promptingService; @Command(description = "Remove parameter", group = "Parameter Store") public String rm( - @Option(description = "Full path of parameter to remove", required = true) String name + @Option(description = "Force without prompting", shortName = 'f', defaultValue = "false") boolean force, + @Argument(index = 0, description = "Full path of parameter to remove") String name ) { - if (parameterStore.deleteParameter(name)) { - return "%s removed.".formatted(name); + if (force || promptingService.confirm("Are you sure you want to remove parameter '%s'?".formatted(name))) { + + if (parameterStore.deleteParameter(name)) { + return "%s removed.".formatted(name); + } else { + return "Parameter not found"; + } } else { - return "Parameter not found"; + return "Operation cancelled"; } } } \ No newline at end of file diff --git a/src/main/java/com/internetstaff/parameterstore/application/CurrentDirectoryService.java b/src/main/java/com/internetstaff/parameterstore/application/CurrentDirectoryService.java index 864a1d8..e1bff86 100644 --- a/src/main/java/com/internetstaff/parameterstore/application/CurrentDirectoryService.java +++ b/src/main/java/com/internetstaff/parameterstore/application/CurrentDirectoryService.java @@ -22,6 +22,17 @@ public String getCurrentDirectory() { return currentDirectory; } + @Override + public void setCurrentDirectory(String newDirectory) { + if (StringUtils.isBlank(newDirectory)) { + currentDirectory = DIRECTORY_SEPARATOR; + } else if (StringUtils.startsWith(newDirectory, DIRECTORY_SEPARATOR)) { + currentDirectory = normalize(newDirectory); + } else { + currentDirectory = normalize(this.currentDirectory + DIRECTORY_SEPARATOR + newDirectory); + } + } + private String normalize(String path) { return Path.of(path).normalize().toString(); } @@ -36,13 +47,14 @@ public String qualifyName(String name) { } @Override - public void setCurrentDirectory(String newDirectory) { - if (StringUtils.isBlank(newDirectory)) { - currentDirectory = DIRECTORY_SEPARATOR; - } else if (StringUtils.startsWith(newDirectory, DIRECTORY_SEPARATOR)) { - currentDirectory = normalize(newDirectory); - } else { - currentDirectory = normalize(this.currentDirectory + DIRECTORY_SEPARATOR + newDirectory); + public String baseName(String name) { + if (StringUtils.startsWith(name, currentDirectory)) { + String stripped = StringUtils.removeStart(name, currentDirectory); + if (StringUtils.startsWith(stripped, DIRECTORY_SEPARATOR)) { + return StringUtils.removeStart(stripped, DIRECTORY_SEPARATOR); + } + return stripped; } + return name; } } diff --git a/src/main/java/com/internetstaff/parameterstore/application/port/in/GetCurrentDirectoryUseCase.java b/src/main/java/com/internetstaff/parameterstore/application/port/in/GetCurrentDirectoryUseCase.java index d511e18..819b72d 100644 --- a/src/main/java/com/internetstaff/parameterstore/application/port/in/GetCurrentDirectoryUseCase.java +++ b/src/main/java/com/internetstaff/parameterstore/application/port/in/GetCurrentDirectoryUseCase.java @@ -4,4 +4,6 @@ public interface GetCurrentDirectoryUseCase { String getCurrentDirectory(); String qualifyName(String name); + + String baseName(String name); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 44c4bfd..1fe4a17 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -4,5 +4,3 @@ spring: shell: interactive: enabled: true - script: - enabled: false \ No newline at end of file diff --git a/src/test/java/com/internetstaff/parameterstore/application/CurrentDirectoryServiceTest.java b/src/test/java/com/internetstaff/parameterstore/application/CurrentDirectoryServiceTest.java index 4201786..978ba27 100644 --- a/src/test/java/com/internetstaff/parameterstore/application/CurrentDirectoryServiceTest.java +++ b/src/test/java/com/internetstaff/parameterstore/application/CurrentDirectoryServiceTest.java @@ -26,7 +26,6 @@ void testAbsolute(String initialDir, String command, String expectedDir) { @ParameterizedTest @CsvSource({ - "'',testfile,/testfile", "/testdir,testfile,/testdir/testfile", "/testdir,../testfile,/testfile" }) @@ -37,4 +36,17 @@ void testQualifyName(String currentDirectory, String name, String expectedName) assertThat(actual).isEqualTo(expectedName); } + @ParameterizedTest + @CsvSource({ + "/testdir, /testdir/testfile, testfile", + "/testdir, /testdir/subdir/testfile, subdir/testfile", + "/, /testfile, testfile", + "/testdir, /otherdir/testfile, /otherdir/testfile" + }) + void testBaseName(String currentDirectory, String name, String expectedBaseName) { + var currentDirectoryService = new CurrentDirectoryService(currentDirectory); + + var actual = currentDirectoryService.baseName(name); + assertThat(actual).isEqualTo(expectedBaseName); + } } \ No newline at end of file