-
Notifications
You must be signed in to change notification settings - Fork 424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add useful error messages when using list of arguments #1383
Comments
One way I can think of to improve the error message is to have more information in the error message of the type converter: if (!arg.toLowerCase().equals(arg))
throw new TypeConversionException("'" + arg + "' must be lower case");
// or: throw new TypeConversionException("Text must be lower case but was '" + arg + "'"); Thoughts? |
Oh wait, sorry.
Good catch, perhaps we can append the error message of the Would you be interested in providing a pull request for this? |
Hmm, I would guess that appending the original message could help - in some cases. But there are two separate cases that I see;
For case (2) the appending of the original message would give better feedback, but there's no tracing back to which flag the argument was sent to. Would be more useful with One way to counter both these possible scenarios would be to throw different exceptions for the two cases. Perhaps sub-classing the |
Adding another exception class would be one idea. It would be best if we can avoid that, would need to look in more detail... Agreed that Explicitly mentioning the name of the option whose parameter could not be type converted should solve both case 1 and 2, would it not? |
I'm not exactly sure of the flow through the Interpreter. If the reason for the Regarding the The current workaround for this would be to implement and set the parameterConsumer for each List-type option? |
I remember now; yes, I think that is what is happening. One idea is to modify the Interpreter to remember the exception as well as the option for which the argument is pushed back onto the stack; then, if the argument cannot be matched to any other option or positional parameter, show an error message that says something to the effect of "I tried this option, that gave me this error message, then I tried to treat it as a positional parameter, and that gave this other error..." |
@remkop I would like to try working on this issue. |
I have noticed that during the trace. The line:
Does not get called for the third parameter, I will continue to look at this issue. |
Great, thank you for looking at this, @MadFoal! |
I am still looking at this issue. So when we use a different class, in the example MyType, commandline does not process the second text argument "Bad Input." If you replace it with a "-t" it will work correctly. Additionally, if you instead of using MyType, instead use a primitive it works correctly as shown:
|
@remkop So this is interesting. When you run the test In section 6 of the manual it goes through all the multiple argument permutations in detail, and what we are attempting to do here is not explicitly permitted, using both flags Please feel free to correct me. I can open a PR to work on the documentation and build some additional tests cases to highlight exactly should pass for multiple test cases. |
@MadFoal good catch! What you are saying is that @StaffanArvidsson's conclusion that type conversion failed for the "Bad input" argument is incorrect. The custom type converter never even saw this argument. The default arity for an option of type I am guessing that the @StaffanArvidsson does this make sense? |
So, picocli is very flexible indeed and it's difficult to get a good overview of how to configure things at all levels. Also, java is slightly difficult to debug when comparing the final product and behaviour in the terminal vs what simple unit-tests do (in terms of eating quotes etc). In this case however, I changed my code slightly to check if your logic actually make sense and let's consider this example; public class BadErrorMsgTest {
// Case 1
@Option(names="-t",
split="\\s",
converter = MyConverter.class,
arity="1..*"
)
public List<MyType> args;
// Case 2
// @Option(names="-t",
// converter = MyConverter.class
// )
// public MyType args;
static class MyType {
String lowCaseStr;
private MyType(String txt){
lowCaseStr = txt;
}
public static class MyConverter implements ITypeConverter<MyType>{
@Override
public MyType convert(String arg) throws Exception {
System.out.printf("converting argument %s%n",arg);
if (!arg.toLowerCase().equals(arg))
throw new TypeConversionException("Text must be lower case but was '" + arg + "'");
return new MyType(arg);
}
}
public String toString() {return lowCaseStr;}
}
public static void main(String[] args) {
BadErrorMsg msgs = new BadErrorMsg();
// Case 1 - list
// new CommandLine(msgs).execute("-t","some txt", "Bad input");
// Case 2 - single argument
new CommandLine(msgs).execute("-t", "first-arg", "second-arg","some txt","Bad input");
}
} So now using
So to me this says that the converter actually sees all arguments until it fails (but with the "smart unquote" leading to "Bad input" being split into two). My guess is that the parser will continue until the exception being thrown and then back up and assume that |
So @remkop I do not think it solves the original problem/feature of providing better feedback to the user, the updated example code shows that there is a possibility for the ITypeConverter to throw a different exception or some other means of going about this. The parser should get that |
I wrote a PR for this issue. There are two ways to achieve the convert error test message, one way is to use the flag each time you want to supply an argument, and the second way is to use a comma delimiter instead of a white space character. |
@StaffanArvidsson Yes, that is exactly what happens. When If the type conversion fails, then the parser assumes that it has encountered a command line argument that is not a parameter of the The parser then proceeds to parse the "Bad input" parameter as a positional parameter, but the command does not have any positional parameters defined, resulting in the "Unmatched argument" error message.
I can see your point of view. One idea is what @MadFoal suggested: define the option as Thoughts? |
Thank you @remkop for the explanation! I tried to use To provide some context, I'm writing software which performs machine learning tasks using the CLI and some arguments are pretty complex and depend on which packages that have been dynamically loaded. One example is data-transformations that can take sub-arguments, say scaling attributes of certain columns. So I invented a way of specifying these sub-parameters with a syntax like; public class ImprovedErrorMsg {
@Option(names="-t",
split="\\s", //",",
converter = MyConverter.class,
arity="1..*"
)
public List<MyType> args;
@Parameters
List<String> positional;
static class MyType {
String className;
List<String> subArgs;
private MyType(String txt){
className = txt;
}
public void setSubArguments(List<String> args) {
// Do some validation - this would in reality be performed in unique implementation class
// Here simply say invalid if given more than 1 sub-argument
if (args == null)
return;
if (args.size()>1) {
throw new IllegalArgumentException(String.format("Invalid sub-arguments for class %s: %s",className,args));
}
// else accept the arguments
subArgs = args;
}
public static class MyConverter implements ITypeConverter<MyType>{
final static List<String> ALLOWED_VALUES= Arrays.asList("class1","class2");
final static String SUB_PARAM_SPLITTER = ":";
@Override
public MyType convert(String arg) throws Exception {
// Only there to check if something is checked or not
// System.out.printf("converting argument %s%n",arg);
String clsArg = arg;
List<String> subArgs = null;
if (arg.contains(SUB_PARAM_SPLITTER)) {
// Has sub-parameters
String[] splittedArgs = arg.split(SUB_PARAM_SPLITTER);
clsArg = splittedArgs[0]; // First portion is the main argument
subArgs = new ArrayList<>(Arrays.asList(splittedArgs));
subArgs.remove(0); // remove the main argument
}
for (String cls : ALLOWED_VALUES) {
if (cls.equalsIgnoreCase(clsArg)) {
MyType t = new MyType(cls);
try {
t.setSubArguments(subArgs);
return t;
} catch (IllegalArgumentException e) {
// Here we _know_ that the main argument was of the correct type,
// but the sub-argument was invalid somehow.
// wish to give more detailed info about these specifics
throw new TypeConversionException(e.getMessage());
}
}
}
// Not valid - but perhaps we can infer if it was a miss-spelling or non-loaded class or if the text was sent by mistake
if (arg.startsWith("class")) {
throw new TypeConversionException("Input "+arg+" currently not supported");
}
// Here the thing was likely sent to -t by mistake (or 'tried' and re-evaluated by the interpreter)
throw new TypeConversionException("Invalid input: "+arg);
}
}
public String toString() {return className;}
}
@Test
public void testCase() {
ImprovedErrorMsg msgs = new ImprovedErrorMsg();
// Case 1 - one valid, one which is miss-spelled or not currently available, rest should be directed to the "positional arguments"
try {
new CommandLine(msgs).parseArgs("-t", MyConverter.ALLOWED_VALUES.get(0), "class3", "second-arg","some txt","Bad input");
Assert.fail("There should be an exception thrown to give more detailed info to the user"); //TODO - this fails as "class3" is directed to "positional"
} catch (Exception e) {
e.printStackTrace();
Assert.assertTrue(e.getMessage().contains("class3 currently not supported"));
}
// Case 2 - only the concrete classes - no sub-arguments
msgs = new ImprovedErrorMsg();
new CommandLine(msgs).parseArgs("-t", MyConverter.ALLOWED_VALUES.get(0), MyConverter.ALLOWED_VALUES.get(1));
Assert.assertTrue(MyConverter.ALLOWED_VALUES.get(0).equalsIgnoreCase(msgs.args.get(0).className));
Assert.assertTrue(MyConverter.ALLOWED_VALUES.get(1).equalsIgnoreCase(msgs.args.get(1).className));
// Case 3 - with OK sub-arguments
msgs = new ImprovedErrorMsg();
new CommandLine(msgs).parseArgs("-t", MyConverter.ALLOWED_VALUES.get(0)+":param1", MyConverter.ALLOWED_VALUES.get(1)+":param2");
Assert.assertTrue(MyConverter.ALLOWED_VALUES.get(0).equalsIgnoreCase(msgs.args.get(0).className));
Assert.assertEquals("param1",msgs.args.get(0).subArgs.get(0));
Assert.assertTrue(MyConverter.ALLOWED_VALUES.get(1).equalsIgnoreCase(msgs.args.get(1).className));
Assert.assertEquals("param2",msgs.args.get(1).subArgs.get(0));
// Case 4 - with invalid sub-arguments (i.e. too many in this case)
msgs = new ImprovedErrorMsg();
try {
new CommandLine(msgs).parseArgs("-t", MyConverter.ALLOWED_VALUES.get(0)+":param1:param2", MyConverter.ALLOWED_VALUES.get(1)+":param3");
Assert.fail("Invalid sub-arguments");
} catch (Exception e) {
e.printStackTrace(); // TODO - remove in actual test-case
Assert.assertTrue(e.getMessage().contains("Invalid sub-arguments"));
}
}
} This might be outside of the scope of Picocli, that's up to you to decide. But thanks for providing a way of how this can be dealt with! |
I see. Another idea is to rearrange the logic a little bit. Specifically, don't specify the type converter in the annotation. List<MyType> args;
@Option(names="-t", split="\\s", arity="1..*")
public void setArgs(List<String> newValues) throws Exception {
args = convertList(newValues, MyType.class, new MyConverter());
}
static <T> List<T> convertList(List<String> values, Class<T> cls, ITypeConverter<T> converter) throws Exception {
List<T> result = new ArrayList<>();
for (String s : values) {
result.add(converter.convert(s));
}
return result;
} This has a bit more code than the original annotated fields approach, but should give better error messages. |
Ok thanks! I looked into trying that instead, and found that making subtypes of the TypeConversionException and using method-annotations did support some level of flexibility as I can then in the error/exception handling check for these subtype exceptions from the new CommandLine(msgs).parseArgs("-t", MyConverter.ALLOWED_VALUES.get(0)+":param1", MyConverter.ALLOWED_VALUES.get(1)+":param2", "pos1", "pos2"); will not put the |
What if you don't have |
I think I answered that earlier, when using e.g. |
Oh I see, yes. Can you run with |
Here's the debug-log, I also included the stack trace from the exception that is thrown as it might provide additional info. I ran this using the
I haven't used the @Parameters
List<String> positional; |
oh wait, |
Yes, e.g. for my example with data transformations - there can be any number of transformations that the user wish to apply, no way to restrict that in advance. That is also an issue that we've come across before, the error messages and bahaviour of the program differs depending on the order in wish the parameters are supplied to the CLI. |
Hm, you can tell your end users to use the Other than that, how can the parser determine whether a command line argument should be a parameter of the If there is some way to determine this programmatically, then you can use a parameter preprocessor for the |
Ok, I'll probably go with implementing those parameter processors for some of these cases, thanks for the tip! I think I've already done that for one of my options but might do that for all of them. This issue was opened in June so I've found some workarounds already but they are a bit ad hoc. Again, picocli is very flexible so it's difficult to get a good grasp on where to make these changes in the best way. |
Ok, great to hear you have been able to find solutions. Are you okay with where we got with this conversation? |
Yeah I think for this issue we found some sort of workaround. I have a separate issue with regards to ANSI rendering, I've not used ANIS before so it might be some lack of understanding on my part so I'll have to research it a bit more before opening a separate issue for it. But from spending quite a bit time tracking down strange output I've found that calling render on a long String with |
Great. Let me close this issue then. |
When having a list of arguments of a certain type, and the type conversion fails at one argument the
TypeConversionException
is converted to aUnmachedException
- leading to a generic error text such as;Unmatched argument at index 2: 'Bad input'
. I made a small example to show the different cases:It would be useful to have a way to discern between what is a truly unmatched input (e.g. a miss-spelled argument that is sent to the ITypeConverter by mistake) and simply that the input is poorly formatted. I understand that in the general case this might be hard to tell, but if the ITypeConverter have the logic to decide between the two cases it can give a better/more useful help text. Furthermore, if the 'Bad input' is given as the first argument to
-t
it will provide the more useful error message and not the Unmatched message.The text was updated successfully, but these errors were encountered: