[轉] Java 命令行交互-JCommander


[From] https://github.com/Sayi/sayi.github.com/issues/32

 

我喜歡簡單,什么是簡單?正如若干字符組成的命令行。

有時候我們用Java開發了一個小工具,希望通過命令行(CLI)或者圖形界面直接調用。命令行相較於圖形界面,實現迅速,交互更接近於程序員人群,本文主要介紹Java在命令行交互上的應用,我們不妨先看看命令行的兩種風格:

  • POSIX風格 tar -zxvf foo.tar.gz
  • Java風格 java -Djava.awt.headless=true -Djava.net.useSystemProxies=true Foo

JCommander介紹

JCommander是Java解析命令行參數的工具,作者是cbeust,他的開源測試框架testNG相信很多程序員都有耳聞。

根據官方文檔,我簡單總結了JCommander的幾個特點:

  • 注解驅動
    它的核心功能命令行參數定義是基於注解的,這也是我選擇用它的主要原因。我們可以輕松做到命令行參數與屬性的映射,屬性除了是String類型,還可以是Integer、boolean,甚至是File、集合類型。

  • 功能豐富
    它同時支持文章開頭的兩種命令行風格,並且提供了輸出幫助文檔的能力(usage()),還提供了國際化的支持。

  • 高度擴展
    下文會詳述。

在看具體應用示例前,我們先讀懂核心注解@Parameter的源碼(你大可以跳過下面這段長長的源碼,直接看示例),以此來了解它向我們展示了哪些方面的能力:

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

  /**
   * An array of allowed command line parameters (e.g. "-d", "--outputdir", etc...).
   * If this attribute is omitted, the field it's annotating will receive all the
   * unparsed options. There can only be at most one such annotation.
   */
  String[] names() default {};

  /**
   * A description of this option.
   */
  String description() default "";

  /**
   * Whether this option is required.
   */
  boolean required() default false;

  /**
   * The key used to find the string in the message bundle.
   */
  String descriptionKey() default "";

  /**
   * How many parameter values this parameter will consume. For example,
   * an arity of 2 will allow "-pair value1 value2".
   */
  public static int DEFAULT_ARITY = -1;
  int arity() default DEFAULT_ARITY;

  /**
   * If true, this parameter is a password and it will be prompted on the console
   * (if available).
   */
  boolean password() default false;

  /**
   * The string converter to use for this field. If the field is of type <tt>List</tt>
   * and not <tt>listConverter</tt> attribute was specified, JCommander will split
   * the input in individual values and convert each of them separately.
   */
  Class<? extends IStringConverter<?>> converter() default NoConverter.class;

  /**
   * The list string converter to use for this field. If it's specified, the
   * field has to be of type <tt>List</tt> and the converter needs to return
   * a List that's compatible with that type.
   */
  Class<? extends IStringConverter<?>> listConverter() default NoConverter.class;

  /**
   * If true, this parameter won't appear in the usage().
   */
  boolean hidden() default false;

  /**
   * Validate the parameter found on the command line.
   */
  Class<? extends IParameterValidator>[] validateWith() default NoValidator.class;

  /**
   * Validate the value for this parameter.
   */
  Class<? extends IValueValidator>[] validateValueWith() default NoValueValidator.class;

  /**
   * @return true if this parameter has a variable arity. See @{IVariableArity}
   */
  boolean variableArity() default false;

  /**
   * What splitter to use (applicable only on fields of type <tt>List</tt>). By default,
   * a comma separated splitter will be used.
   */
  Class<? extends IParameterSplitter> splitter() default CommaParameterSplitter.class;
  
  /**
   * If true, console will not echo typed input
   * Used in conjunction with password = true
   */
  boolean echoInput() default false;

  /**
   * If true, this parameter is for help. If such a parameter is specified,
   * required parameters are no longer checked for their presence.
   */
  boolean help() default false;
  
  /**
   * If true, this parameter can be overwritten through a file or another appearance of the parameter
   * @return nc
   */
  boolean forceNonOverwritable() default false;

  /**
   * If specified, this number will be used to order the description of this parameter when usage() is invoked.
   * @return
   */
  int order() default -1;
  
}

 

JCommander 應用示例

在一般應用場景,我們可能只需要設置@Parameter以下幾個屬性值:

  • names 設置命令行參數,如-old
  • required 設置此參數是否必須
  • description 設置參數的描述
  • order 設置幫助文檔的順序
  • help 設置此參數是否為展示幫助文檔或者輔助功能

下面是一個完整的示例,它用來比較兩份文檔,然后輸出差異。源碼在https://github.com/Sayi/swagger-diff上。

/**
 * 
 * @author Sayi
 * @version
 */
public class CLI {

  private static final String OUTPUT_MODE_MARKDOWN = "markdown";

  @Parameter(names = "-old", description = "old api-doc location:Json file path or Http url", required = true, order = 0)
  private String oldSpec;

  @Parameter(names = "-new", description = "new api-doc location:Json file path or Http url", required = true, order = 1)
  private String newSpec;

  @Parameter(names = "-v", description = "swagger version:1.0 or 2.0", validateWith = RegexValidator.class, order = 2)
  @Regex("(2\\.0|1\\.0)")
  private String version = SwaggerDiff.SWAGGER_VERSION_V2;

  @Parameter(names = "-output-mode", description = "render mode: markdown or html", validateWith = RegexValidator.class, order = 3)
  @Regex("(markdown|html)")
  private String outputMode = OUTPUT_MODE_MARKDOWN;

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

  @Parameter(names = "--version", description = "swagger-diff tool version", help = true, order = 6)
  private boolean v;

  public static void main(String[] args) {
    CLI cli = new CLI();
    JCommander jCommander = JCommander.newBuilder().addObject(cli).build();
    jCommander.parse(args);
    cli.run(jCommander);
  }

  public void run(JCommander jCommander) {
    if (help) {
      jCommander.setProgramName("java -jar swagger-diff.jar");
      jCommander.usage();
      return;
    }
    if (v) {
      JCommander.getConsole().println("1.2.0");
      return;
    }

    //SwaggerDiff diff = null;
  }
}

 

運行命令行查看幫助文檔,輸出結果如下:

$ java -jar swagger-diff.jar --help
Usage: java -jar swagger-diff.jar [options]
  Options:
  * -old
      old api-doc location:Json file path or Http url
  * -new
      new api-doc location:Json file path or Http url
    -v
      swagger version:1.0 or 2.0
      Default: 2.0
    -output-mode
      render mode: markdown or html
      Default: markdown
    --help

    --version
      swagger-diff tool version

 

這個示例像我們展示了JCommander注解的強大,我們僅僅使用注解就完成了所有參數的定義。注意,對於boolean為true的參數,我們只需要輸入參數名,比如--help,而不是--help=true

示例中使用了usage()方法即可完美的輸出幫助文檔。

JCommander擴展:增加正則表達式校驗

JCommander是高度擴展的,兩個核心接口定義了擴展的能力。

IStringConverter支持String類型的參數值可以轉化為任意其他類型的屬性。

/**
 * An interface that converts strings to any arbitrary type.
 * 
 * If your class implements a constructor that takes a String, this
 * constructor will be used to instantiate your converter and the
 * parameter will receive the name of the option that's being parsed,
 * which can be useful to issue a more useful error message if the
 * conversion fails.
 * 
 * You can also extend BaseConverter to make your life easier.
 * 
 * @author cbeust
 */
public interface IStringConverter<T> {
  /**
   * @return an object of type <T> created from the parameter value.
   */
  T convert(String value);
}

 

IParameterValidator支持參數值的校驗。

/**
 * The class used to validate parameters.
 *
 * @author Cedric Beust <cedric@beust.com>
 */
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;

}

 

在閱讀上文示例中,可能會有些許疑問,比如@Regex是什么注解,JCommander並沒有提供正則表達式校驗參數值的功能。

對於很多參數,我們都有校驗的場景,比如值只能是幾個可選值,或者是在一定范圍內,IParameterValidator 和IParameterValidator2實現了參數校驗了功能,接下來我們將基於接口IParameterValidator2擴展JCommander,同樣,我們只需要使用注解即可。

  1. 自定義正則注解,這樣我們就可以在需要正則校驗的屬性上,設置表達式,如@Regex("(2\\.0|1\\.0)")
package com.deepoove.swagger.diff.cli;

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 Regex {

  String value() default "";

}

 

  1. 實現RegexValidator,當有Regex注解的時候,解析正則表達式,應用校驗規則。注意這段代碼使用了反射,可能並不是最優雅的方式,但是在不修改JCommander源碼的情況下,可能是最好的方式了
package com.deepoove.swagger.diff.cli;

import java.lang.reflect.Field;
import java.util.regex.Pattern;

import com.beust.jcommander.IParameterValidator2;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;

public class RegexValidator implements IParameterValidator2 {

  private static final String PARAMETERIZED_FIELD_NAME = "field";

  @Override
  public void validate(String name, String value) throws ParameterException {
    return;
  }

  @Override
  public void validate(String name, String value, ParameterDescription pd)
      throws ParameterException {
    Parameterized parameterized = pd.getParameterized();
    Class<? extends Parameterized> clazz = parameterized.getClass();
    try {
      Field declaredField = clazz.getDeclaredField(PARAMETERIZED_FIELD_NAME);
      declaredField.setAccessible(true);
      Field paramField = (Field) declaredField.get(parameterized);
      Regex regex = paramField.getAnnotation(Regex.class);
      if (null == regex) return;
      String regexStr = regex.value();
      if (!Pattern.matches(regexStr, value)) { throw new ParameterException(
          "Parameter " + name + " should match " + regexStr + " (found " + value + ")"); }
    } catch (NoSuchFieldException e) {
      return;
    } catch (IllegalArgumentException e) {
      return;
    } catch (IllegalAccessException e) {
      return;
    }
  }
}

 

  1. 使用正則注解和正則校驗類
@Parameter(names = "-v",  validateWith = RegexValidator.class)
@Regex("(2\\.0|1\\.0)")
private String version = "2.0";

 

至此,正則校驗已完成。

更多More: Apache Commons CLI

從源碼中可以看到,JCommander默認提供了不少轉化器。

----IStringConverter
  \--BaseConverter
     --\--BigDecimalConverter
     --\--BooleanConverter
     --\--DoubleConverter
     --\--FloatConverter
     --\--IntegerConverter
     --\--ISO8601DateConverter
     --\--LongConverter
     --\--PathConverter
     --\--URIConverter
     --\--URLConverter
 \--EnumConverter
 \--InetAddressConverter
 \--FileConverter

Java在命令行交互的應用,還有很多工具。另一個使用比較廣泛的是Apache Commons CLI: http://commons.apache.org/proper/commons-cli/index.html,它比JCommander支持更多的命令行風格,但是擴展能力不夠。

https://github.com/Sayi/sayi.github.com/issues/32

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM