jcommander使用指南

举报
从大数据到人工智能 发表于 2022/03/28 00:41:13 2022/03/28
【摘要】 总览在Java中经常会遇到需要输入参数的情况,JCommander 是一个非常小的 Java 框架,可以轻松解析命令行参数。 下文完整解析JCommander的用法。例如您可以使用选项描述注释字段:import com.beust.jcommander.Parameter;public class Args { @Parameter private List<String> param...

总览

在Java中经常会遇到需要输入参数的情况,JCommander 是一个非常小的 Java 框架,可以轻松解析命令行参数。 下文完整解析JCommander的用法。

例如您可以使用选项描述注释字段:

import com.beust.jcommander.Parameter;

public class Args {
  @Parameter
  private List<String> parameters = new ArrayList<>();

  @Parameter(names = { "-log", "-verbose" }, description = "Level of verbosity")
  private Integer verbose = 1;

  @Parameter(names = "-groups", description = "Comma-separated list of group names to be run")
  private String groups;

  @Parameter(names = "-debug", description = "Debug mode")
  private boolean debug = false;
}

然后你只需让 JCommander 解析:

Args args = new Args();
String[] argv = { "-log", "2", "-groups", "unit" };
JCommander.newBuilder()
  .addObject(args)
  .build()
  .parse(argv);

Assert.assertEquals(jct.verbose.intValue(), 2);

其他例子:

class Main {
    @Parameter(names={"--length", "-l"})
    int length;
    @Parameter(names={"--pattern", "-p"})
    int pattern;

    public static void main(String ... argv) {
        Main main = new Main();
        JCommander.newBuilder()
            .addObject(main)
            .build()
            .parse(argv);
        main.run();
    }

    public void run() {
        System.out.printf("%d %d", length, pattern);
    }
}
$ java Main -l 512 --pattern 2
512 2

类型的选择

代表参数的字段可以是任何类型。 默认支持基本类型(整数、布尔值等… ),您可以编写类型转换器来支持任何其他类型(文件等… )。

Boolean

当在 boolean 或 Boolean 类型的字段上找到 Parameter 注释时,JCommander 将其解释为参数为0的选项:

@Parameter(names = "-debug", description = "Debug mode")
private boolean debug = false;

这样的参数在命令行上不需要任何额外的参数,如果在解析过程中检测到,相应的字段将设置为 true。 如果您想定义一个默认为 true 的布尔参数,您可以将其声明为具有 1 的元数。然后用户必须明确指定他们想要的值:

@Parameter(names = "-debug", description = "Debug mode", arity = 1)
private boolean debug = true;

使用以下任一方式调用:

program -debug true
program -debug false

当在 String、Integer、int、Long 或 long 类型的字段上找到 Parameter 注释时,JCommander 将解析以下参数并尝试将其转换为正确的类型:

@Parameter(names = "-log", description = "Level of verbosity")
private Integer verbose = 1;
java Main -log 3

将导致字段 verbose 接收值 3。但是:

$ java Main -log test

将导致抛出异常。

Lists

当在 List 类型的字段上找到 Parameter 注释时,JCommander 会将其解释为可以多次出现的选项:

@Parameter(names = "-host", description = "The host")
private List<String> hosts = new ArrayList<>();

将允许您解析以下命令行:

$ java Main -host host1 -verbose -host host2

当 JCommander 完成上述行的解析时,字段 hosts 将包含字符串“host1”和“host2”。

Password

如果您的参数之一是密码或您不希望在历史记录中显示或明确显示的其他值,则可以将其声明为密码类型,然后 JCommander 将要求您在控制台中输入它:

public class ArgsPassword {
  @Parameter(names = "-password", description = "Connection password", password = true)
  private String password;
}

当你运行你的程序时,你会得到如下提示:

Value for -password (Connection password):

在 JCommander 恢复之前,您需要在此时键入值。

显示输入

在 Java 6 中,默认情况下,您将无法看到您在提示符下输入的密码(Java 5 和更低版本将始终显示密码)。 但是,您可以通过将 echoInput 设置为 true 来覆盖它(默认为 false,此设置仅在密码为 true 时有效):

public class ArgsPassword {
  @Parameter(names = "-password", description = "Connection password", password = true, echoInput = true)
  private String password;
}

自定义类型(转换器和拆分器)

将参数绑定到自定义类型或更改 JCommander 拆分参数的方式(默认为逗号拆分),JCommander 提供了两个接口 IStringConverter 和 IParameterSplitter。

自定义类型 - 单值

使用@Parameter 的converter= 属性或实现IStringConverterFactory。

通过声明的方式

默认情况下,JCommander 仅将命令行解析为基本类型(字符串、布尔值、整数和长整数)。 很多时候,您的应用程序实际上需要更复杂的类型(例如文件、主机名、列表等)。 为此,您可以通过实现以下接口来编写类型转换器:

public interface IStringConverter<T> {
  T convert(String value);
}

例如,这是一个将字符串转换为文件的转换器:

public class FileConverter implements IStringConverter<File> {
  @Override
  public File convert(String value) {
    return new File(value);
  }
}

然后,您需要做的就是使用正确的类型声明您的字段并将转换器指定为属性:

@Parameter(names = "-file", converter = FileConverter.class)
File file;

JCommander 附带了一些常见的转换器(有关更多信息,请参阅 IStringConverter 的实现)。

提示:

如果转换器用于列表字段:

@Parameter(names = "-files", converter = FileConverter.class)
List<File> files;

应用程序调用如下:

$ java App -files file1,file2,file3

JCommander 会将字符串 file1,file2,file3 拆分为 file1,file2,file3 并将其一一提供给转换器。

有关解析值列表的替代解决方案,请参阅自定义类型 - 列表值。

通过工厂方法的方式

如果您使用的自定义类型在您的应用程序中出现多次,则必须在每个注释中指定转换器可能会变得乏味。 为了解决这个问题,您可以使用 IStringConverterFactory:

public interface IStringConverterFactory {
  <T> Class<? extends IStringConverter<T>> getConverter(Class<T> forType);
}

例如,假设您需要解析表示主机和端口的字符串:

$ java App -target example.com:8080

定义持有类

public class HostPort {
  public HostPort(String host, String port) {
     this.host = host;
     this.port = port;
  }

  final String host;
  final Integer port;
}

以及创建此类实例的字符串转换器:

class HostPortConverter implements IStringConverter<HostPort> {
  @Override
  public HostPort convert(String value) {
    String[] s = value.split(":");
    return new HostPort(s[0], Integer.parseInt(s[1]));
  }
}

工厂很简单:

public class Factory implements IStringConverterFactory {
  public Class<? extends IStringConverter<?>> getConverter(Class forType) {
    if (forType.equals(HostPort.class)) return HostPortConverter.class;
    else return null;
  }

您现在可以使用 HostPort 类型作为不带任何 converterClass 属性的参数:

public class ArgsConverterFactory {
  @Parameter(names = "-hostport")
  private HostPort hostPort;
}

您需要做的就是将工厂添加到您的 JCommander 对象中:

ArgsConverterFactory a = new ArgsConverterFactory();
JCommander jc = JCommander.newBuilder()
    .addObject(a)
    .addConverterFactory(new Factory())
    .build()
    .parse("-hostport", "example.com:8080");

Assert.assertEquals(a.hostPort.host, "example.com");
Assert.assertEquals(a.hostPort.port.intValue(), 8080);

使用字符串转换器工厂的另一个优点是您的工厂可以来自依赖注入框架。

自定义类型 - 列表值

使用 @Parameter 注释的 listConverter= 属性并分配自定义 IStringConverter 实现以将字符串转换为值列表。

通过声明的方式

如果您的应用程序需要复杂类型的列表,请通过实现与以前相同的接口来编写列表类型转换器:

public interface IStringConverter<T> {
  T convert(String value);
}

其中 T 是一个列表。

例如,这是一个将字符串转换为 List<File> 的列表转换器:

public class FileListConverter implements IStringConverter<List<File>> {
  @Override
  public List<File> convert(String files) {
    String [] paths = files.split(",");
    List<File> fileList = new ArrayList<>();
    for(String path : paths){
        fileList.add(new File(path));
    }
    return fileList;
  }
}

然后,您需要做的就是使用正确的类型声明您的字段并将列表转换器指定为属性:

@Parameter(names = "-files", listConverter = FileListConverter.class)
List<File> file;

现在,如果您像以下示例一样调用应用程序:

$ java App -files file1,file2,file3

拆分

使用 @Parameter 注释的 splitter= 属性并分配自定义 IParameterSplitter 实现来处理参数在子部分中的拆分方式。

通过声明的方式

默认情况下,JCommander 会尝试以逗号分隔 List 字段类型的参数。

要将参数拆分到其他字符上,您可以通过实现以下接口编写自定义拆分器:

public interface IParameterSplitter {
  List<String> split(String value);
}

例如,这是一个拆分器,它使用分号拆分字符串:

public static class SemiColonSplitter implements IParameterSplitter {
    public List<String> split(String value) {
      return Arrays.asList(value.split(";"));
    }
}

然后,您需要做的就是使用正确的类型声明您的字段并将拆分器指定为属性:

@Parameter(names = "-files", converter = FileConverter.class, splitter = SemiColonSplitter.class)
List<File> files;

JCommander 会将字符串 file1;file2;file3 拆分为 file1、file2、file3 并将其一一提供给转换器。

参数验证

参数验证可以通过两种不同的方式执行:在单个参数级别或全局。

单个参数验证

您可以通过提供一个实现以下接口的类来要求 JCommander 对您的参数执行早期验证:

public interface IParameterValidator {
 /**
   * Validate the parameter.
   *
   * @param name The name of the parameter (e.g. "-host").
   * @param value The value of the parameter that we need to validate
   *
   * @throws ParameterException Thrown if the value of the parameter is invalid.
   */
  void validate(String name, String value) throws ParameterException;
}

这是一个示例实现,它将确保参数是一个正整数:

public class PositiveInteger implements IParameterValidator {
 public void validate(String name, String value)
      throws ParameterException {
    int n = Integer.parseInt(value);
    if (n < 0) {
      throw new ParameterException("Parameter " + name + " should be positive (found " + value +")");
    }
  }
}

在 @Parameter 注释的 validateWith 属性中指定实现此接口的类的名称:

@Parameter(names = "-age", validateWith = PositiveInteger.class)
private Integer age;

尝试将负整数传递给此选项将导致抛出 ParameterException。

可以指定多个验证器:

@Parameter(names = "-count", validateWith = { PositiveInteger.class, CustomOddNumberValidator.class })
private Integer value;

全局参数验证

使用 JCommander 解析参数后,您可能希望对这些参数执行额外的验证,例如确保两个互斥参数未同时指定。 由于此类验证涉及所有潜在的组合,JCommander 不提供任何基于注释的解决方案来执行此验证,因为这种方法必然会受到 Java 注释的本质的限制。 相反,您应该简单地在 Java 中对 JCommander 刚刚解析的所有参数执行此验证。

主要参数

到目前为止,我们看到的所有@Parameter 注释都定义了一个名为names 的属性。 您可以定义一个(最多一个)参数而不使用任何此类属性。 此参数可以是 List<String> 或单个字段(例如 String 或具有转换器的类型,例如 File),在这种情况下,只需要一个主要参数。

@Parameter(description = "Files")
private List<String> files = new ArrayList<>();

@Parameter(names = "-debug", description = "Debugging level")
private Integer debug = 1;

将允许您解析:

$ java Main -debug file1 file2

私有参数

参数也可以是私有的:

public class ArgsPrivate {
  @Parameter(names = "-verbose")
  private Integer verbose = 1;

  public Integer getVerbose() {
    return verbose;
  }
}
ArgsPrivate args = new ArgsPrivate();
JCommander.newBuilder()
    .addObject(args)
    .build()
    .parse("-verbose", "3");
Assert.assertEquals(args.getVerbose().intValue(), 3);

参数分隔符

默认情况下,参数由空格分隔,但您可以更改此设置以允许使用不同的分隔符:

$ java Main -log:3

或者

$ java Main -level=42

您使用 @Parameters 注释定义分隔符:

@Parameters(separators = "=")
public class SeparatorEqual {
  @Parameter(names = "-level")
  private Integer level = 2;
}

多重描述

您可以将参数描述分布在多个类上。 例如,您可以定义以下两个类:

public class ArgsMaster {
  @Parameter(names = "-master")
  private String master;
}

public class ArgsSlave {
  @Parameter(names = "-slave")
  private String slave;
}

并将这两个对象传递给 JCommander:

ArgsMaster m = new ArgsMaster();
ArgsSlave s = new ArgsSlave();
String[] argv = { "-master", "master", "-slave", "slave" };
JCommander.newBuilder()
    .addObject(new Object[] { m , s })
    .build()
    .parse(argv);

Assert.assertEquals(m.master, "master");
Assert.assertEquals(s.slave, "slave");

@ 语法

JCommander 支持 @ 语法,它允许您将所有选项放入文件中并将此文件作为参数传递:

/tmp/parameters

-verbose
file1
file2
file3
$ java Main @/tmp/parameters

参数的多个值

固定参数数量

如果您的某些参数需要多个值,例如以下示例,在 -pairs 之后需要两个值:

$ java Main -pairs slave master foo.xml

那么您需要使用 arity 属性定义您的参数并将该参数设为 List<String>:

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

您不需要为 boolean 或 Boolean 类型的参数(默认 arity 为 0)以及 String、Integer、int、Long 和 long 类型(默认 arity 为 1)的参数指定 arity。

另外,请注意,对于定义元数的参数,只允许使用 List<String>。 如果您需要的参数是 Integer 或其他类型(此限制是由于 Java 的擦除),您将不得不自己转换这些值。

可变参数

您可以指定一个参数可以接收不定数量的参数,直到下一个选项。 例如:

program -foo a1 a2 a3 -bar
program -foo a1 -bar

这样的参数可以用两种不同的方式解析。

使用列表

如果以下参数的数量未知,则您的参数必须是 List<String> 类型,并且您需要将布尔变量 Arity 设置为 true:

@Parameter(names = "-foo", variableArity = true)
public List<String> foo = new ArrayList<>();

使用类

或者,您可以根据出现的顺序定义一个将存储以下参数的类:

static class MvParameters {
  @SubParameter(order = 0)
  String from;

  @SubParameter(order = 1)
  String to;
}

@Test
public void arity() {
  class Parameters {
    @Parameter(names = {"--mv"}, arity = 2)
    private MvParameters mvParameters;
  }

  Parameters args = new Parameters();
  JCommander.newBuilder()
          .addObject(args)
          .args(new String[]{"--mv", "from", "to"})
          .build();

  Assert.assertNotNull(args.mvParameters);
  Assert.assertEquals(args.mvParameters.from, "from");
  Assert.assertEquals(args.mvParameters.to, "to");
}

多个选项名称

您可以指定多个选项名称:

@Parameter(names = { "-d", "--outputDirectory" }, description = "Directory")
private String outputDirectory;

将允许以下两种语法:

$ java Main -d /tmp
$ java Main --outputDirectory /tmp

其他选项配置

您可以通过几种不同的方式配置如何查找选项:

  • JCommander#setCaseSensitiveOptions(boolean):指定选项是否区分大小写。 如果使用 false 调用此方法,则“-param”和“-PARAM”被视为相等。
  • JCommander#setAllowAbbreviatedOptions(boolean):指定用户是否可以传递缩写选项。 如果使用 true 调用此方法,则用户可以通过“-par”来指定名为 -param 的选项。 如果缩写名称不明确,JCommander 将抛出 ParameterException。

必选和可选参数

如果您的某些参数是强制性的,您可以使用 required 属性(默认为 false):

@Parameter(names = "-host", required = true)
private String host;

如果未指定此参数,JCommander 将抛出异常,告诉您缺少哪些选项。

默认值

为参数指定默认值的最常见方法是在声明时初始化字段:

private Integer logLevel = 3;

对于更复杂的情况,您可能希望能够在多个主要类中重用相同的默认值,或者能够在一个集中的位置(例如 .properties 或 XML 文件)中指定这些默认值。 在这种情况下,您可以使用 IDefaultProvider:

public interface IDefaultProvider {
  /**
   * @param optionName The name of the option as specified in the names() attribute
   * of the @Parameter option (e.g. "-file").
   *
   * @return the default value for this option.
   */
  String getDefaultValueFor(String optionName);
}

通过将此接口的实现传递给您的 JCommander 对象,您现在可以控制将哪个默认值用于您的选项。 请注意,此方法返回的值随后将传递给字符串转换器(如果有),从而允许您为所需的任何类型指定默认值。

例如,这是一个默认提供程序,它将为除“-debug”之外的所有参数分配默认值 42:

private static final IDefaultProvider DEFAULT_PROVIDER = new IDefaultProvider() {
  @Override
  public String getDefaultValueFor(String optionName) {
    return "-debug".equals(optionName) ? "false" : "42";
  }
};

// ...

JCommander jc = JCommander.newBuilder()
    .addObject(new Args())
    .defaultProvider(DEFAULT_PROVIDER)
    .build()

Help参数

如果您的参数之一用于显示一些帮助或用法,则需要使用帮助属性:

@Parameter(names = "--help", help = true)
private boolean help;

如果您省略此布尔值,JCommander 将在尝试验证您的命令并发现您未指定某些必需参数时发出错误消息。

更复杂的语法(命令)

诸如 git 或 svn 之类的复杂工具可以理解一整套命令,每个命令都有自己特定的语法:

$ git commit --amend -m "Bug fix"

上面的“commit”等词在 JCommander 中称为“commands”,您可以通过为每个命令创建一个 arg 对象来指定它们:

@Parameters(separators = "=", commandDescription = "Record changes to the repository")
private class CommandCommit {

  @Parameter(description = "The list of files to commit")
  private List<String> files;

  @Parameter(names = "--amend", description = "Amend")
  private Boolean amend = false;

  @Parameter(names = "--author")
  private String author;
}

@Parameters(commandDescription = "Add file contents to the index")
public class CommandAdd {

  @Parameter(description = "File patterns to add to the index")
  private List<String> patterns;

  @Parameter(names = "-i")
  private Boolean interactive = false;
}

然后你用你的 JCommander 对象注册这些命令。

在解析阶段之后,您在 JCommander 对象上调用 getParsedCommand(),并根据返回的命令,您知道要检查哪个 arg 对象(如果您想在命令行上出现第一个命令之前支持选项,您仍然可以使用主 arg 对象):

CommandMain cm = new CommandMain();
CommandAdd add = new CommandAdd();
CommandCommit commit = new CommandCommit();
JCommander jc = JCommander.newBuilder()
    .addObject(cm)
    .addCommand("add", add);
    .addCommand("commit", commit);
    .build();

jc.parse("-v", "commit", "--amend", "--author=cbeust", "A.java", "B.java");

Assert.assertTrue(cm.verbose);
Assert.assertEquals(jc.getParsedCommand(), "commit");
Assert.assertTrue(commit.amend);
Assert.assertEquals(commit.author, "cbeust");
Assert.assertEquals(commit.files, Arrays.asList("A.java", "B.java"));

异常

每当 JCommander 检测到错误时,它都会抛出 ParameterException。 请注意,这是一个运行时异常,因为此时您的应用程序可能未正确初始化。 此外,ParameterException 包含 JCommander 实例,如果您需要显示一些帮助,也可以在其上调用 usage()。

使用

您可以在用于解析命令行的 JCommander 实例上调用 usage() 以生成程序理解的所有选项的摘要:

Usage: <main class> [options]
  Options:
    -debug          Debug mode (default: false)
    -groups         Comma-separated list of group names to be run
  * -log, -verbose  Level of verbosity (default: 1)
    -long           A long number (default: 0)

您可以通过在 JCommander 对象上调用 setProgramName() 来自定义程序的名称。 前面有星号的选项是必需的。

您还可以通过设置@Parameter 注解的 order 属性来指定调用 usage() 时每个选项的显示顺序:

class Parameters {
    @Parameter(names = "--importantOption", order = 0)
    private boolean a;

    @Parameter(names = "--lessImportantOption", order = 3)
    private boolean b;

隐藏参数

如果您不希望某些参数出现在用法中,可以将它们标记为“隐藏”:

@Parameter(names = "-debug", description = "Debug mode", hidden = true)
private boolean debug = false;

国际化

您可以将参数的描述国际化。 首先使用类顶部的@Parameters 注释来定义消息包的名称,然后在所有需要翻译的@Parameters 上使用descriptionKey 属性而不是description。 此 descriptionKey 是消息包中字符串的键:

@Parameters(resourceBundle = "MessageBundle")
private class ArgsI18N2 {
  @Parameter(names = "-host", description = "Host", descriptionKey = "host")
  String hostName;
}

你的包需要定义这个键:

host: Hôte

然后,JCommander 将使用默认语言环境来解析您的描述。

参数委托

如果您在同一个项目中编写许多不同的工具,您可能会发现这些工具中的大多数都可以共享配置。 虽然您可以对对象使用继承来避免重复此代码,但对实现的单一继承的限制可能会限制您的灵活性。 为了解决这个问题,JCommander 支持参数委托。

当 JCommander 在您的一个对象中遇到使用 @ParameterDelegate 注释的对象时,它的行为就好像该对象已添加为描述对象本身一样:

class Delegate {
  @Parameter(names = "-port")
  private int port;
}

class MainParams {
  @Parameter(names = "-v")
  private boolean verbose;

  @ParametersDelegate
  private Delegate delegate = new Delegate();
}

上面的示例指定了一个委托参数 Delegate,然后在 MainParams 中引用该参数。 您只需将 MainParams 对象添加到您的 JCommander 配置中即可使用委托:

MainParams p = new MainParams();
JCommander.newBuilder().addObject(p).build()
    .parse("-v", "-port", "1234");
Assert.assertTrue(p.isVerbose);
Assert.assertEquals(p.delegate.port, 1234);

动态参数

JCommander 允许您指定编译时未知的参数,例如“-Da=b -Dc=d”。 此类参数使用 @DynamicParameter 注释指定,并且必须是 Map<String, String> 类型。 动态参数允许在命令行中出现多次:

@DynamicParameter(names = "-D", description = "Dynamic parameters go here")
private Map<String, String> params = new HashMap<>();

您可以使用属性 assignment 指定不同于 = 的分配字符串。

自定义使用格式

JCommander 允许您自定义 JCommander#usage() 方法的输出。 您可以通过继承 IUsageFormatter 然后调用 JCommander#setUsageFormatter(IUsageFormatter) 来做到这一点。

仅打印参数名称的用法格式化程序示例,由新行分隔,如下所示:

class ParameterNamesUsageFormatter implements IUsageFormatter {

    // Extend other required methods as seen in DefaultUsageFormatter

    // This is the method which does the actual output formatting
    public void usage(StringBuilder out, String indent) {
        if (commander.getDescriptions() == null) {
            commander.createDescriptions();
        }

        // Create a list of the parameters
        List<ParameterDescription> params = Lists.newArrayList();
        params.addAll(commander.getFields().values());

        // Append all the parameter names
        if (params.size() > 0) {
            out.append("Options:\n");

            for (ParameterDescription pd : params) {
                out.append(pd.getNames()).append("\n");
            }
        }
    }
}

JCommander 在其他语言中的使用

Kotlin

class Args {
    @Parameter
    var targets: List<String> = arrayListOf()

    @Parameter(names = arrayOf("-bf", "--buildFile"), description = "The build file")
    var buildFile: String? = null

    @Parameter(names = arrayOf("--checkVersions"),
               description = "Check if there are any newer versions of the dependencies")
    var checkVersions = false
}

Groovy

import com.beust.jcommander.*

class Args {
  @Parameter(names = ["-f", "--file"], description = "File to load. Can be specified multiple times.")
  List<String> file
}

new Args().with {
  JCommander.newBuilder().addObject(it).build().parse(argv)
  file.each { println "file: ${new File(it).name}" }
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。