Parameter with optional argument

1,102 views
Skip to first unread message

Alexander Lembach

unread,
Apr 26, 2012, 2:37:52 PM4/26/12
to jcommander
Hello.

In my command line options I have two parameters which should be
usable as follows:

java Main -paramA arg1 arg2 -paramB arg3

I.e. there are two "main" parameters "paramA" and "paramB" of arity 2
and 1 respectively.

In Java:

@Parameter(names = "-paramA", arity = 2, description = "ParamA")
private List<String> paramA;

@Parameter(names = "-paramB", description = "ParamB")
private String paramB;

What I'd like to achieve is that the argument "arg2" is optional with
some specified default value, i.e. the command line should also be
accepted in this form (note that there is no "arg2"):

java Main -paramA arg1 -paramB arg3

This should have the same effect as if the user had entered "...-
paramA arg1 DefaultArg2 ...".

Is this possible with JCommander?

The problem is that if I declare "-paramA" to be of arity 2 and do not
specify "arg2", JCommander will interpret "-paramB" as the second
argument for paramA.

Thank you for your help!

Cédric Beust ♔

unread,
Apr 26, 2012, 2:39:28 PM4/26/12
to jcomm...@googlegroups.com
Hi Alexander,

Arities are static, so the short answer to your question is "no".

You should probably define both arguments as Lists and then validate these arities yourself after calling parse().

-- 
Cédric

Alexander Lembach

unread,
Apr 26, 2012, 3:13:31 PM4/26/12
to jcommander
Thank you Cédric for the quick reply.

> You should probably define both arguments as Lists and then validate these
> arities yourself after calling parse().

I tried to define both parameters as lists and of arity 1 (i.e. no
arity specification at all), but then I have to specify each argument
separately, like this:

@Parameter(names = "-paramA", description = "ParamA")
private List<String> paramA;

@Parameter(names = "-paramB", description = "ParamB")
private List<String> paramB;



java -paramA arg1 -paramA arg2 -paramB arg3

or

java -paramA arg1 -paramB arg3

The second usage example is OK but the first is not since I'd like to
have "-paramA arg1 arg2".

But anyway thank you for the quick response and for the good work.
I'll see how I get along...

Cédric Beust ♔

unread,
Apr 26, 2012, 3:33:55 PM4/26/12
to jcomm...@googlegroups.com
Hi Alexander,

You need to use variable arities, which are a feature in development (hence not yet documented). I also just fixed a bug in them, so if you want to test the code below, you'll have to pull from master right now. 

Here is how you do it:

  @Test
  public void multiList() {
    class Params implements IVariableArity {
      @Parameter(names = "-paramA", description = "ParamA", variableArity = true)
      private List<String> paramA = Lists.newArrayList();

      @Parameter(names = "-paramB", description = "ParamB", variableArity = true)
      private List<String> paramB = Lists.newArrayList();

      @Override
      public int processVariableArity(String optionName, String[] options) {
        int i = 0;
        while (i < options.length && !options[i].startsWith("-")) {
          if ("-paramA".equals(optionName)) paramA.add(options[i]);
          else paramB.add(options[i]);
          i++;
        }
        return i;
      }
    }

    String args[] = { "-paramA", "a1", "a2", "-paramB", "b1", "b2", "b3" };
    Params p = new Params();
    new JCommander(p, args).parse();
    Assert.assertEquals(p.paramA, Arrays.asList(new String[] { "a1", "a2" }));
    Assert.assertEquals(p.paramB, Arrays.asList(new String[] { "b1", "b2", "b3" }));
  }

Note: I think this is still too much work and that even when you use a variable arity, JCommander should take care of adding the parsed options to your field, not you. After I have fixed this, here is what you should just have to do:

      public int processVariableArity(String optionName, String[] options) {
        int i = 0;
        while (i < options.length && !options[i].startsWith("-")) {
          i++;
        }
        return i;
      }

In other words, you just tell JCommander how many options the current option should consume and it will take care of adding these to your List field.

Pushing this further, you shouldn't have to even do this and if your arg class doesn't implement IVariableArity, JCommander should just assume that your option will consume everything until the next option.

Bottom line: this is still a work in progress :-)

Does this make sense?

-- 
Cédric

Cédric Beust ♔

unread,
Apr 26, 2012, 3:49:05 PM4/26/12
to jcomm...@googlegroups.com
Actually, I just went ahead and implemented all these simplifications, here is how you do it now:

 public void multiVariableArityList() {
    class Params {
      @Parameter(names = "-paramA", description = "ParamA", variableArity = true)
      private List<String> paramA = Lists.newArrayList();

      @Parameter(names = "-paramB", description = "ParamB", variableArity = true)
      private List<String> paramB = Lists.newArrayList();
    }

    String args[] = { "-paramA", "a1", "a2", "-paramB", "b1", "b2", "b3" };
    Params p = new Params();
    new JCommander(p, args).parse();
    Assert.assertEquals(p.paramA, Arrays.asList(new String[] { "a1", "a2" }));
    Assert.assertEquals(p.paramB, Arrays.asList(new String[] { "b1", "b2", "b3" }));
  }

If you need additional flexibility in how you handle variable arities, have your arg class implement IVariableArity and return the number of options to skip. That's it.

This is in master now.

-- 
Cédric

-- 
Cédric




On Thu, Apr 26, 2012 at 12:13 PM, Alexander Lembach <al...@gmx.de> wrote:

Alexander Lembach

unread,
Apr 26, 2012, 4:55:30 PM4/26/12
to jcommander
> Pushing this further, you shouldn't have to even do this and if your arg
> class doesn't implement IVariableArity, JCommander should just assume that
> your option will consume everything until the next option.

This is exactly how I'd imagine it ... and how you did it! Thank you
very very much.

Now we just have to wait for the official release (the feature would
justify one ;-)

BTW: is there an "official" download URL for the "release" JAR and for
the "last built" JAR?

Cédric Beust ♔

unread,
Apr 26, 2012, 5:02:03 PM4/26/12
to jcomm...@googlegroups.com
Not at the moment, but you can always pull from github yourself, `mvn package` and then point your pom.xml to the local jar file with <systemPath>.

I'll push a new version on Maven Central in the next few days.

-- 
Cédric

Al Le

unread,
Apr 27, 2012, 2:34:45 AM4/27/12
to jcomm...@googlegroups.com
Hello Cédric.

> This is exactly how I'd imagine it ... and how you did it! Thank you
> very very much.

After having thought about this a bit more, I now think that your solution is good but not good enough (at least for my case). I'll explain.

I want to specify a set of 'key, value' pairs on the command line, with the 'value' part being optional (the default value should be, say, "MyDefault"). The command line should look like this:

java Main -pair key1 value1 -pair key2 -pair key3


With the current solution, I'd do it like this:

@Parameter(names = "-pair", variableArity = true)
private List<String> pairs;

But then, the following command line would be accepted (since 'variableArity' does not imply any restrictions on the number of values):

java Main -pair key1 value1 key2 key3 (*)

A remedy would be two optional attributes of the annotation: minArity (with the default 0) and maxArity (with the default MAX_INT). Specifying 'minArity' or 'maxArity' would imply 'variableArity=true'. With this correction, the command line (*) would no more be accepted. That's good.

But then there is still another problem. The following command line would be perfectly valid:

java Main -pair key1 value1 -pair key2 -pair key3 (**)

After parsing this command line, the list would have 4 elements in it: "key1", "value1", "key2", "key3". Now, how can I tell that 'key3' is actually a key and not the value for 'key2'? The solution could IMO be in introducing two addtional optional annotation parameters which could be used in combination with 'maxArity':

1. fillUpToMaxArity: if set to true, JCommander would always fill up the list to the specified maxArity (which must also be specified in this case), using some default value

2. fillUpWithValue: this would specify the value to use for filling up the list (with the default being null).


With these attributes, my definition would look like this:

@Parameter(names = "-pair", minArity = 1, maxArity = 2, fillUpToMaxArity = true, fillUpWithValue = "MyDefault")
private List<String> pairs;

After parsing the command line (**) above, the list would have the following elements: "key1", "value1", "key2", "MyDefault", "key3", "MyDefault".

What do you think?
--
Empfehlen Sie GMX DSL Ihren Freunden und Bekannten und wir
belohnen Sie mit bis zu 50,- Euro! https://freundschaftswerbung.gmx.de

Cédric Beust ♔

unread,
Apr 27, 2012, 12:42:06 PM4/27/12
to jcomm...@googlegroups.com
Hi Alexander,

I think this is becoming very, very specific, probably beyond what JCommander should offer.

I suggest you just use the variableArity support to get a list of all the keys/values and then perform your own processing to determine which are key/value pairs and which ones are just keys that should receive a default value.

Does this make sense?

-- 
Cédric

Alexander Lembach

unread,
Apr 27, 2012, 12:50:08 PM4/27/12
to jcommander
> I think this is becoming very, very specific, probably beyond what
> JCommander should offer.
>
> I suggest you just use the variableArity support to get a list of all the
> keys/values and then perform your own processing to determine which are
> key/value pairs and which ones are just keys that should receive a default
> value.

I'd do this but in my case, it's really impossible to tell what is a
key and what is a value since both can be arbitrary strings.

I'll try to create a patch and put it on github. You'll decide then
whether it's worth including into the official release.

OK?

Cédric Beust ♔

unread,
Apr 27, 2012, 12:52:51 PM4/27/12
to jcomm...@googlegroups.com
On Fri, Apr 27, 2012 at 9:50 AM, Alexander Lembach <al...@gmx.de> wrote:
> I think this is becoming very, very specific, probably beyond what
> JCommander should offer.
>
> I suggest you just use the variableArity support to get a list of all the
> keys/values and then perform your own processing to determine which are
> key/value pairs and which ones are just keys that should receive a default
> value.

I'd do this but in my case, it's really impossible to tell what is a
key and what is a value since both can be arbitrary strings.

Wait a minute... if *you* can't figure that out, how do you expect JCommander to? :-)

-- 
Cédric

Alexander Lembach

unread,
Apr 27, 2012, 12:55:28 PM4/27/12
to jcommander
> > I'd do this but in my case, it's really impossible to tell what is a
> > key and what is a value since both can be arbitrary strings.
>
> Wait a minute... if *you* can't figure that out, how do you expect
> JCommander to? :-)

With the help of the annotations I described in a previous post that
would be perfectly possible. Or have you been being ironic?

Cédric Beust ♔

unread,
Apr 27, 2012, 12:58:48 PM4/27/12
to jcomm...@googlegroups.com
Ah, ok. I was being serious. The point is that you could use your own annotations to help you parse these fields. Once you have done this, we could look into it and see if it's general enough to be provided by JCommander itself, but to be honest, I think it's too specific to your scenario to be worth it.

-- 
Cédric



 

Alexander Lembach

unread,
Apr 27, 2012, 1:29:30 PM4/27/12
to jcommander
> The point is that you could use your own
> annotations to help you parse these fields.

Do you mean that I could use my own annotations and then also provide
my own implementation of IVariableArity?

Cédric Beust ♔

unread,
Apr 27, 2012, 1:31:13 PM4/27/12
to jcomm...@googlegroups.com
Sure:

@Parameter(...)
@YourAnnotation(min = 1, max = 3)
private List<String>...

Then look up your annotation and parse accordingly.

-- 
Cédric

Alexander Lembach

unread,
Apr 27, 2012, 1:46:56 PM4/27/12
to jcommander
> > Do you mean that I could use my own annotations and then also provide
> > my own implementation of IVariableArity?
>
> Sure:
>
> @Parameter(...)
> @YourAnnotation(min = 1, max = 3)
> private List<String>...

Hrm... I'm trying to do that.I created my own annotation and annotated
my bean field with it. Now I try to create my own IVariableArity
implementation and have a problem: how can I get access to the
parameter (bean field) being parsed? Because I somehow need to access
my custom annotation.

Alexander Lembach

unread,
Apr 27, 2012, 3:47:13 PM4/27/12
to jcommander
Warning: the post is a bit lengthy, but it contains an error
descrioption (in JC implementation) and a fix for it, and also
outlines some limitations of the current "varArity" implementation.

This is what I've come up with so far. I have one parameter of type
List which can be specified in chunks of length 1 to 2. If less than 2
values are specified in a chunk, the chunk should be filled up to the
length 2. See my findings below (I've found an error and a problem in
the arity implementation).

---- ArityMinMax.java ----
package priv.jc.arity;

import static java.lang.annotation.ElementType.FIELD;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ FIELD })
public @interface ArityMinMax {

int min() default -1;
int max() default -1;

boolean fillToMaxArity() default false;
String fillingValue() default "X";

}


---- Main.java ----
package priv.jc.arity;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import com.beust.jcommander.IVariableArity;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;

public class Main implements IVariableArity {

private static final String FIELD_NAME = "param";

@Parameter(names = "-param", variableArity = true)
@ArityMinMax(min = 1, max = 2, fillToMaxArity = true)
public List<String> param = new ArrayList<String>();

public static void main(String[] args) {
Main t = new Main();
JCommander jc = new JCommander(t);
jc.parse(new String[]{"-param", "AAA", "-param", "BBB", "-param",
"CCC", "DDD"});
System.out.println("Param: " + t.param);
}

@Override
public int processVariableArity(String optionName, String[] options)
{
System.out.println("Variable arity parser called");
ArityMinMax amm = this.getArityDescription(FIELD_NAME);
int min = amm.min();
int max = amm.max();

int i = 0;
while (i < options.length && !options[i].startsWith("-")) {
System.out.println("Got value: '" + options[i] + "'");
i++;
}
if (max > 0 && i > max) {
throw new ParameterException("Max " + max
+ " values are allowed (offending value: "
+ options[i - 1] + ")");
}
if (min > 0 && i < min) {
throw new ParameterException("At least " + min + " values must be
specified");
}
if (max >= 0 && amm.fillToMaxArity()) {
List<String> targetList = getTargetList(FIELD_NAME);
int nToAdd = max - i;
System.out.println("Values to add: " + nToAdd);
for (int k = 0; k < nToAdd; k++) {
targetList.add(amm.fillingValue());
}
}
return i;
}

private List<String> getTargetList(String fieldName) {
try {
return (List<String>)
this.getClass().getDeclaredField(fieldName).get(this);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
private ArityMinMax getArityDescription(String fieldName) {
Field f;
try {
f = this.getClass().getDeclaredField(fieldName);
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
return (ArityMinMax) f.getAnnotation(ArityMinMax.class);
}

}
-------------------------------
---- Program output ----
Variable arity parser called
Got value: 'AAA'
Values to add: 1
Variable arity parser called
Got value: 'BBB'
Values to add: 1
Variable arity parser called
Got value: 'CCC'
Got value: 'DDD'
Values to add: 0
Param: [AAA, X, BBB, CCC, DDD]
--------

The result of the call is: Param: [AAA, X, BBB], i.e. not the value
I'd expected.

First, there is an error in the implementation in your master branch.
It stems from the fact that "processVariableArity" and
"processFixedArity" have different semantics. The former returns the
number of options processed, and the latter returns the resulting
index. Since varArity calls fixedArity, the semantics of varArity is
"spoiled". If a parameter with var arity is specified multiple times
(like in my case), the index is no more correct.

To fix this, I changed the semantics of "processFixedArity" to be the
same as in "processVariableArity". In code:

JCommander, private int processFixedArity(String[] args ... (line
749):

add the line "int originalIndex = index;" as the first line in the
method, and replace the last line ("return index") with "return index
- originalIndex".

In the main loop, replace "i = processFixedArity(args, i, pd,
fieldType)" with "i += processFixedArity(args, i, pd,
fieldType);" (change "=" to "+=").

That was the error. Now the problem which still is resent after the
fix.

The problem is that in processVariableArity I'm only supposed to find
out the real number of values to eat. If I assign values (which I want
to do to fill up the chunk), the values

a) Get assigned not at the end but in the beginning of the chunk
(because the "real" value assignment is done in JCommander *after*
processVariableArity)

b) If it's the first chunk, the assigned values are lost because the
collection is created anew (see ParameterDescription, line "if (l ==
null || fieldIsSetForTheFirstTime(isDefault))" and then "l =
newCollection(type)").

A remedy could be to delegate more responsibility to
"processVariableArity", i.e. not only to find out the number of the
options but also to really assign the values. The name already
suggests this.

But then you'd lose e.g. the type conversion infrastructure etc.

I don't know how to solve this best. The best solution would of course
be to just add the proposed attributes to the core JCommander
annotation! :-)
Reply all
Reply to author
Forward
0 new messages