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