Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/1823[#1823]: Fix IDEasy creates duplicate entries in .gitconfig
* https://github.com/devonfw/IDEasy/issues/1724[#1724]: Add gui commandlet
* https://github.com/devonfw/IDEasy/issues/1853[#1853]: Add ARM releases for VSCode on Mac
* https://github.com/devonfw/IDEasy/issues/1643[#1643]: Improve CLI error messages for unknown options and property values

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/44?closed=1[milestone 2026.05.001].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1146,11 +1146,40 @@ public int run(CliArguments arguments) {
}
activateLogging(cmd);
verifyIdeMinVersion(false);
if (result != null) {

if (cmd != null && result instanceof ValidationState res) {
final CliArgument arg = res.getCliArgument();
if (arg != null) {
if (arg.getValue() != null) {
// --flag=value with an invalid value: reconstruct the error message here to control order
String exMsg = res.getParseExceptionMessage();
if (exMsg != null) {
step.error(Property.INVALID_ARGUMENT + ": {}", arg.getValue(), arg.getKey(), cmd.getName(), exMsg);
} else {
step.error(Property.INVALID_ARGUMENT, arg.getValue(), arg.getKey(), cmd.getName());
}
String hint = res.getParseHint();
if (hint != null) {
LOG.error(Property.INVALID_ARGUMENT_HELP_MULTIPLE, hint);
}
} else {
// Unknown option flag or positional value with invalid content
step.error("Option {} not found for commandlet {}.", arg.get(), cmd.getName());
String hint = res.getParseHint();
if (hint != null) {
LOG.error(Property.INVALID_ARGUMENT_HELP_MULTIPLE, hint);
}
}
IdeLogLevel.INTERACTION.log(LOG, "To see the available options and arguments call the following command:\n"
+ "ide {} help", cmd.getName());
return 1;
}
}
if (result != null && (!(result instanceof ValidationState) || ((ValidationState) result).getCliArgument() == null) ) {
LOG.error(result.getErrorMessage());
step.error("Invalid arguments: {}", current.getArgs());
IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
}
step.error("Invalid arguments: {}", current.getArgs());
IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
return 1;
} catch (Throwable t) {
activateLogging(cmd);
Expand Down Expand Up @@ -1542,6 +1571,7 @@ public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
if (currentProperty == null) {
LOG.trace("No option or next value found");
ValidationState state = new ValidationState(null);
state.setCliArgument(currentArgument);
state.addErrorMessage("No matching property found");
return state;
}
Expand All @@ -1562,6 +1592,9 @@ public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
if (!matches) {
ValidationState state = new ValidationState(null);
state.addErrorMessage("No matching property found");
state.setCliArgument(currentArgument);
state.setParseHint(currentProperty.getAndClearLastParseHint());
state.setParseExceptionMessage(currentProperty.getAndClearLastParseExceptionMessage());
return state;
}
currentArgument = arguments.current();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ public void setValueAsString(String valueAsString, IdeContext context) {
setValue(b);
}

@Override
protected String getValidValuesErrorHint(IdeContext context, Commandlet commandlet) {

return "'true', 'yes', 'false', 'no'";
}

@Override
protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
CompletionCandidateCollector collector) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devonfw.tools.ide.property;

import java.util.stream.Collectors;

import com.devonfw.tools.ide.commandlet.Commandlet;
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
import com.devonfw.tools.ide.context.IdeContext;
Expand Down Expand Up @@ -58,6 +60,15 @@ protected void completeValue(String arg, IdeContext context, Commandlet commandl
}
}

@Override
protected String getValidValuesErrorHint(IdeContext context, Commandlet commandlet) {

return context.getCommandletManager().getCommandlets().stream()
.map(Commandlet::getName)
.map(n -> "'" + n + "'")
.collect(Collectors.joining(", "));
}

@Override
public Commandlet parse(String valueAsString, IdeContext context) {

Expand Down
15 changes: 15 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/property/EnumProperty.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.devonfw.tools.ide.property;

import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.devonfw.tools.ide.commandlet.Commandlet;
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
Expand Down Expand Up @@ -48,6 +50,19 @@ public V parse(String valueAsString, IdeContext context) {
throw new IllegalArgumentException(String.format("Invalid Enum option: %s", valueAsString));
}

/**
* @return All possible EnumValues as string, delimited by a comma.
*/
public String getEnumValuesAsString() {
return Stream.of(this.valueType.getEnumConstants()).map(c -> String.format("'%s'", c.toString().toLowerCase(Locale.ROOT))).collect(Collectors.joining(", "));
}

@Override
protected String getValidValuesErrorHint(IdeContext context, Commandlet commandlet) {

return getEnumValuesAsString();
}

@Override
protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {

Expand Down
47 changes: 42 additions & 5 deletions cli/src/main/java/com/devonfw/tools/ide/property/Property.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public abstract class Property<V> {

private static final Logger LOG = LoggerFactory.getLogger(Property.class);

private static final String INVALID_ARGUMENT = "Invalid CLI argument '{}' for property '{}' of commandlet '{}'";
public static final String INVALID_ARGUMENT = "Invalid CLI argument '{}' for property '{}' of commandlet '{}'";
public static final String INVALID_ARGUMENT_HELP_MULTIPLE = "Did you mean one of [{}]?";

private static final String INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE = INVALID_ARGUMENT + ": {}";

Expand All @@ -53,6 +54,10 @@ public abstract class Property<V> {
/** @see #getValue() */
protected final List<V> value = new ArrayList<>();

private String lastParseHint;

private String lastParseExceptionMessage;

/**
* The constructor.
*
Expand Down Expand Up @@ -306,19 +311,51 @@ public void setValueAsString(String valueAsString, IdeContext context) {
*/
public final boolean assignValueAsString(String valueAsString, IdeContext context, Commandlet commandlet) {

this.lastParseHint = null;
this.lastParseExceptionMessage = null;
try {
setValueAsString(valueAsString, context);
return true;
} catch (Exception e) {
if (e instanceof IllegalArgumentException) {
LOG.warn(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName());
} else {
LOG.warn(INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE, valueAsString, getNameOrAlias(), commandlet.getName(), e.getMessage());
if (!(e instanceof IllegalArgumentException)) {
this.lastParseExceptionMessage = e.getMessage();
}
this.lastParseHint = getValidValuesErrorHint(context, commandlet);
return false;
}
}

/**
* @return the hint string for the last failed parse (e.g. "Did you mean one of [...]?"), and clears it. Returns {@code null} if no hint is available.
*/
public String getAndClearLastParseHint() {

String h = this.lastParseHint;
this.lastParseHint = null;
return h;
}

/**
* @return the exception message from the last failed parse if the exception was not an {@link IllegalArgumentException}, and clears it. Returns {@code null}
* otherwise.
*/
public String getAndClearLastParseExceptionMessage() {

String m = this.lastParseExceptionMessage;
this.lastParseExceptionMessage = null;
return m;
}

/**
* @param context the {@link IdeContext}.
* @param commandlet the {@link Commandlet} owning this property.
* @return a formatted string of valid values to show as a hint when an invalid value is given, or {@code null} if no hint is available.
*/
protected String getValidValuesErrorHint(IdeContext context, Commandlet commandlet) {

return null;
}

/**
* @return the {@code null} value.
*/
Expand Down
12 changes: 12 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/property/ToolProperty.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devonfw.tools.ide.property;

import java.util.stream.Collectors;

import com.devonfw.tools.ide.commandlet.Commandlet;
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
import com.devonfw.tools.ide.context.IdeContext;
Expand Down Expand Up @@ -68,6 +70,16 @@ public ToolCommandlet parse(String valueAsString, IdeContext context) {
return context.getCommandletManager().getRequiredToolCommandlet(valueAsString);
}

@Override
protected String getValidValuesErrorHint(IdeContext context, Commandlet commandlet) {

return context.getCommandletManager().getCommandlets().stream()
.filter(c -> c instanceof ToolCommandlet)
.map(Commandlet::getName)
.map(n -> "'" + n + "'")
.collect(Collectors.joining(", "));
}

@Override
protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devonfw.tools.ide.validation;

import com.devonfw.tools.ide.cli.CliArgument;

/**
* Implementation of {@link ValidationResult} as a mutable state that can collect errors dynamically.
*/
Expand All @@ -9,6 +11,11 @@ public class ValidationState implements ValidationResult {

private StringBuilder errorMessage;

/**
* Field for the {@link CliArgument} that was the reason for a failed validation.
*/
private CliArgument cliArgument;

/**
* The default constructor for no property.
*/
Expand Down Expand Up @@ -68,4 +75,50 @@ public void add(ValidationResult result) {
}
}
}

private String parseHint;

private String parseExceptionMessage;

/**
* @param cliArgument The {@link CliArgument} that failed the validation.
*/
public void setCliArgument(CliArgument cliArgument) {
this.cliArgument = cliArgument;
}

/**
* @return The {@link CliArgument} that failed the validation.
*/
public CliArgument getCliArgument() {
return this.cliArgument;
}

/**
* @param parseHint the hint to display when a parse error occurred (e.g. a list of valid values).
*/
public void setParseHint(String parseHint) {
this.parseHint = parseHint;
}

/**
* @return the hint for the failed parse, or {@code null} if none.
*/
public String getParseHint() {
return this.parseHint;
}

/**
* @param parseExceptionMessage the message from a non-{@link IllegalArgumentException} parse failure.
*/
public void setParseExceptionMessage(String parseExceptionMessage) {
this.parseExceptionMessage = parseExceptionMessage;
}

/**
* @return the exception message from a non-{@link IllegalArgumentException} parse failure, or {@code null}.
*/
public String getParseExceptionMessage() {
return this.parseExceptionMessage;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.devonfw.tools.ide.version;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -88,8 +90,31 @@ public static VersionIdentifier resolveVersionPattern(GenericVersionRange versio
return vi;
}
}
List<VersionIdentifier> closest = findClosestVersions(version, versions, 5);
String closestStr = closest.stream().map(Object::toString).collect(Collectors.joining(", "));
throw new CliException(
"Could not find any version matching '" + version + "' - there are " + versions.size() + " version(s) available but none matched!");
"Could not find any version matching '" + version + "' - there are " + versions.size()
+ " version(s) available but none matched!\nDid you mean one of: " + closestStr + "?");
}

private static List<VersionIdentifier> findClosestVersions(GenericVersionRange version, List<VersionIdentifier> versions, int maxCount) {

if (version instanceof VersionIdentifier vi && !vi.isPattern()) {
long requestedMajor = vi.getStart().getNumber();
List<VersionIdentifier> majorMatches = new ArrayList<>();
for (VersionIdentifier v : versions) {
if (v.getStart().getNumber() == requestedMajor) {
majorMatches.add(v);
if (majorMatches.size() >= maxCount) {
break;
}
}
}
if (!majorMatches.isEmpty()) {
return majorMatches;
}
}
return versions.size() <= maxCount ? versions : versions.subList(0, maxCount);
}

/**
Expand Down
Loading
Loading