Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -672,10 +672,10 @@ protected CommandMode getCommandMode() {

private String getCommandNameForHelp() {
// enforce kc.sh for ALL mode to ensure consistent line wrapping
if (getCommandMode() == CommandMode.ALL) {
return "kc.sh";
}
return Environment.getCommand();
return switch (getCommandMode()) {
case WIN -> "kc.bat";
default -> "kc.sh";
};
}

private void configureUsageHelpWidth(CommandLine cmd) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@

package org.keycloak.quarkus.runtime.configuration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import picocli.CommandLine;

/**
* Custom CommandLine.UnmatchedArgumentException with amended suggestions
*/
public class KcUnmatchedArgumentException extends CommandLine.UnmatchedArgumentException {

private static final int MAX_OPTION_SUGGESTIONS = 7;
private static final int MAX_COMMAND_SUGGESTIONS = 3;

public KcUnmatchedArgumentException(CommandLine commandLine, List<String> args) {
super(commandLine, args);
Expand All @@ -34,8 +41,52 @@ public KcUnmatchedArgumentException(CommandLine.UnmatchedArgumentException ex) {
super(ex.getCommandLine(), ex.getUnmatched());
}

/**
* see https://github.com/remkop/picocli/issues/2510 for issues with the
* default picocli logic
*/
@Override
public List<String> getSuggestions() {
return super.getSuggestions();
String unmatched = this.getUnmatched().get(0).toLowerCase();
List<String> candidates;
int maxSuggestions;
if (isUnknownOption()) {
candidates = super.getSuggestions(); // can be a lengthy list of all options
maxSuggestions = MAX_OPTION_SUGGESTIONS;
} else {
candidates = new ArrayList<String>();
for (Map.Entry<String, CommandLine> entry : commandLine.getCommandSpec().subcommands().entrySet()) {
if (!entry.getValue().getCommandSpec().usageMessage().hidden()) {
candidates.add(entry.getKey());
}
}
maxSuggestions = MAX_COMMAND_SUGGESTIONS;
}

return candidates.stream().map(c -> Map.entry(cosineSimilarity(unmatched, c.toLowerCase()), c))
.sorted((e1, e2) -> e2.getKey().compareTo(e1.getKey())).map(Map.Entry::getValue).limit(maxSuggestions).toList();
}

static double cosineSimilarity(String a, String b) {
Map<String, Integer> aFreq = bigramFrequency(a);
Map<String, Integer> bFreq = bigramFrequency(b);
double dot = dotProduct(aFreq, bFreq);
double normA = dotProduct(aFreq, aFreq);
double normB = dotProduct(bFreq, bFreq);
double denominator = Math.sqrt(normA * normB);
return denominator == 0 ? 0 : dot / denominator;
}

private static Map<String, Integer> bigramFrequency(String s) {
Map<String, Integer> freq = new HashMap<>();
for (int i = 0; i < s.length() - 1; i++) {
freq.merge(s.substring(i, i + 2), 1, Integer::sum);
}
return freq;
}

private static double dotProduct(Map<String, Integer> m1, Map<String, Integer> m2) {
return m1.entrySet().stream()
.collect(Collectors.summingDouble(e -> e.getValue() * (m2.getOrDefault(e.getKey(), 0))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ private void assertUnknownOption(NonRunningPicocli nonRunningPicocli) {
containsString(Help.defaultColorScheme(nonRunningPicocli.getColorMode())
.errorText("Unknown option: '--db-pasword'").toString()));
assertThat(nonRunningPicocli.getErrString(), containsString(
"Possible solutions: --db-url, --db-connect-timeout, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-pool-max-lifetime, --db-debug-jpql, --db-log-slow-queries-threshold, --db-tls-mode, --db-tls-trust-store-file, --db-tls-trust-store-type, --db-tls-trust-store-password, --db-mtls-key-store-file, --db-mtls-key-store-type, --db-mtls-key-store-password, --db-driver, --db, --db-url-full-<datasource>, --db-connect-timeout-<datasource>, --db-url-host-<datasource>, --db-url-database-<datasource>, --db-url-port-<datasource>, --db-url-properties-<datasource>, --db-username-<datasource>, --db-password-<datasource>, --db-schema-<datasource>, --db-pool-initial-size-<datasource>, --db-pool-min-size-<datasource>, --db-pool-max-size-<datasource>, --db-debug-jpql-<datasource>, --db-log-slow-queries-threshold-<datasource>, --db-tls-mode-<datasource>, --db-tls-trust-store-file-<datasource>, --db-tls-trust-store-type-<datasource>, --db-tls-trust-store-password-<datasource>, --db-mtls-key-store-file-<datasource>, --db-mtls-key-store-type-<datasource>, --db-mtls-key-store-password-<datasource>, --db-enabled-<datasource>, --db-driver-<datasource>, --db-kind-<datasource>"));
"--db-password, --db-password-<datasource>, --db-mtls-key-store-password, --db-tls-trust-store-password, --db-mtls-key-store-password-<datasource>, --db-tls-trust-store-password-<datasource>, --db"));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes! 🚀

}

@Test
Expand Down Expand Up @@ -1981,5 +1981,17 @@ public void poolMaxSizeLowAllowedWithKubernetesStack() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--db-pool-max-size=3", "--cache=ispn", "--cache-stack=kubernetes");
assertNoError(nonRunningPicocli);
}

@Test
public void commandSuggestions() {
NonRunningPicocli nonRunningPicocli = new NonRunningPicocli() {
@Override
protected CommandMode getCommandMode() {
return CommandMode.UNIX;
}
};
KeycloakMain.main(new String[] {"strt"}, nonRunningPicocli);
assertTrue(nonRunningPicocli.getErrString().contains("Did you mean: kc.sh start or kc.sh start-dev or kc.sh bootstrap-admin?"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void testServerDoesNotStartIfValidationFailDuringReAugStart(CLIResult res
@Launch({"start", "--db=dev-file", "--log=console", "--log-file-output=json", "--http-enabled=true", "--hostname-strict=false"})
public void testServerDoesNotStartIfDisabledFileLogOption(CLIResult result) {
result.assertError("Disabled option: '--log-file-output'. Available only when File log handler is activated");
result.assertError("--log, --log-async, --log-service-name, --log-service-environment, --log-console-output, --log-console-level, --log-console-format, --log-console-color, --log-console-async, --log-level, --log-level-<category>");
result.assertError("Possible solutions: --log-console-output, --log-level, --log, --log-console-level, --log-console-format, --log-console-async, --log-async");
}

@DryRun
Expand Down Expand Up @@ -114,7 +114,7 @@ public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(CLIResult
@Launch({"start-dev", "--log=console", "--log-file-output=json"})
public void testServerDoesNotStartDevIfDisabledFileLogOption(CLIResult result) {
result.assertError("Disabled option: '--log-file-output'. Available only when File log handler is activated");
result.assertError("Possible solutions: --log, --log-async, --log-service-name, --log-service-environment, --log-console-output, --log-console-level, --log-console-format, --log-console-color, --log-console-async, --log-level, --log-level-<category>");
result.assertError("Possible solutions: --log-console-output, --log-level, --log, --log-console-level, --log-console-format, --log-console-async, --log-async");
}

@DryRun
Expand All @@ -124,7 +124,7 @@ public void testServerDoesNotStartDevIfDisabledFileLogOption(CLIResult result) {
public void testServerStartDevIfEnabledFileLogOption(CLIResult result) {
result.assertNoError("Disabled option: '--log-file-output'. Available only when File log handler is activated");
result.assertError("Disabled option: '--log-console-color'. Available only when Console log handler is activated");
result.assertError("Possible solutions: --log, --log-async, --log-service-name, --log-service-environment, --log-file, --log-file-level, --log-file-format, --log-file-json-format, --log-file-output, --log-file-async, --log-file-rotation-enabled, --log-file-rotation-max-file-size, --log-file-rotation-max-backup-index, --log-file-rotation-file-suffix, --log-file-rotation-rotate-on-boot, --log-level, --log-level-<category>, --log-mdc-enabled");
result.assertError("Possible solutions: --log, --log-file, --log-level, --log-file-level, --log-file-json-format, --log-file-format, --log-file-async");
}

@DryRun
Expand Down
Loading