= Picocli 2.0: 以少求多 //:作者: Remko Popma //:邮箱: rpopma@apache.org //:版本å·: 2.1.0-SNAPSHOT //:版本日期: 2017-11-15 :prewrap!: :source-highlighter: coderay :icons: font :imagesdir: ../images/ == 简介 Picocliæ˜¯ä¸€ä¸ªå•æ–‡ä»¶å‘½ä»¤è¡Œè§£æžæ¡†æž¶ï¼Œå®ƒå…è®¸æ‚¨åˆ›å»ºå‘½ä»¤è¡Œåº”ç”¨è€Œå‡ ä¹Žä¸éœ€è¦ä»£ç 。使用 `@Option` 或 `@Parameters` åœ¨æ‚¨çš„åº”ç”¨ä¸æ³¨é‡Šå—段,Picocli将分别使用命令行选项和ä½ç½®å‚æ•°å¡«å……è¿™äº›å—æ®µã€‚例如: [source,java] ---- @Command(name = "Greet", header = "%n@|green Hello world demo|@") class Greet implements Runnable { @Option(names = {"-u", "--user"}, required = true, description = "The user name.") String userName; public void run() { System.out.println("Hello, " + userName); } public static void main(String... args) { CommandLine.run(new Greet(), System.err, args); } } ---- å½“æˆ‘ä»¬æ‰§è¡Œè¿™ä¸ªç¨‹åºæ—¶ï¼ŒPicocli会解æžå‘½ä»¤è¡Œï¼Œå¹¶åœ¨è°ƒç”¨ `run` 方法之å‰å¡«å…… `userName` å—æ®µ: [source,bash] ---- $ java Greet -u picocli Hello, picocli ---- Picocli采用 http://picocli.info/#_ansi_colors_and_styles[Ansié¢œè‰²å’Œæ ·å¼]生æˆä½¿ç”¨å¸®åŠ©æ¶ˆæ¯ã€‚å¦‚æžœæˆ‘ä»¬è¿›è¡Œæ— æ•ˆè¾“å…¥æ¥è¿è¡Œä¸Šé¢çš„程åº(缺少必须的用户å选项), picocli会输出一æ¡é”™è¯¯å’Œä½¿ç”¨å¸®åŠ©ä¿¡æ¯: image:Greet-screenshot.png[错误信æ¯å’Œä½¿ç”¨å¸®åŠ©çš„å±å¹•截图] Picocliå¯ä»¥ç”Ÿæˆä¸€ä¸ª http://picocli.info/autocomplete.html[自动补é½]脚本,脚本å…许最终用户使用 `<TAB>` 命令行补全,从而æ¥å‘现哪些选项和å命令å¯ç”¨ã€‚您还å¯èƒ½å–œæ¬¢Picocli对 http://picocli.info/#_subcommands[å命令]å’Œ http://picocli.info/#_nested_sub_subcommands[嵌套å命令]åŠä»»ä½•深度å命令的支æŒã€‚ http://picocli.info[用户手册]详细æè¿°äº†picocli的功能。本文é‡ç‚¹ä»‹ç»Picocli 2.0版本ä¸å¼•入的新功能。 == 带ä½ç½®å‚数的混åˆé€‰é¡¹ 我们对解æžå™¨è¿›è¡Œäº†æ”¹è¿›ï¼ŒçŽ°åœ¨ä½ç½®å‚数现在å¯ä»¥ä¸Žå‘½ä»¤è¡Œä¸Šçš„选项混在一起。 image:whisk.png[] 以å‰ï¼Œä½ç½®å‚数必须紧éšé€‰é¡¹ã€‚ 从æ¤ç‰ˆæœ¬å¼€å§‹ï¼Œä»»ä½•éžé€‰é¡¹æˆ–åå‘½ä»¤çš„å‘½ä»¤è¡Œå‚æ•°éƒ½å°†è¢«è§£é‡Šä¸ºä½ç½®å‚数。 例如: [source,java] ---- class MixDemo implements Runnable { @Option(names = "-o") List<String> options; @Parameters List<String> positional; public void run() { System.out.println("positional: " + positional); System.out.println("options : " + options); } public static void main(String[] args) { CommandLine.run(new MixDemo(), System.err, args); } } ---- 使用选项和ä½ç½®å‚æ•°çš„æ··åˆæ¥è¿è¡Œä¸Šé¢çš„类表明,éžé€‰é¡¹è¢«è®¤ä¸ºæ˜¯ä½ç½®å‚数。例如: [source,bash] ---- $ java MixDemo param0 -o AAA param1 param2 -o BBB param3 positional: [param0, param1, param2, param3] options : [AAA, BBB] ---- 为了支æŒå…·æœ‰ä½ç½®å‚数的混åˆé€‰é¡¹ï¼Œå·²å¯¹è§£æžå™¨è¿›è¡Œæ›´æ”¹ã€‚ 从 picocl2.0开始,多值选项(数组ã€åˆ—è¡¨å’Œæ˜ å°„å—æ®µ)ä¸å†**默认为贪婪的**。 2.0版本详细æè¿°äº†æ¤å˜åŒ–和其他 https://github.com/remkop/picocli/releases/tag/v2.0.0#2.0-breaking-changes[潜在的é‡å¤§å˜æ›´]。 == å‘现集åˆç±»åž‹ Picocliå°†å‘½ä»¤è¡Œå‚æ•°çš„ http://picocli.info/#_strongly_typed_everything[自动类型转æ¢]æ‰§è¡Œåˆ°å¸¦æ³¨é‡Šå—æ®µçš„类型。命å选项和ä½ç½®å‚数都å¯ä»¥æ˜¯å¼ºç±»åž‹ã€‚ image:binoculars.jpg[] 在v2.0之å‰ï¼Œpicocli需è¦ä½¿ç”¨ `type` 属性对 `Collection` å’Œ `Map` å—æ®µè¿›è¡Œæ³¨é‡Šï¼Œ 以便能够进行类型转æ¢ã€‚å¯¹äºŽå…·æœ‰å…¶ä»–ç±»åž‹çš„å—æ®µï¼Œåƒæ•°ç»„å—æ®µå’Œå•值嗿®µï¼Œå¦‚ `int` 或 `java.io.File` å—æ®µï¼Œpicocliè‡ªåŠ¨ä»Žå—æ®µç±»åž‹ä¸æ£€æµ‹ç›®æ ‡ç±»åž‹ï¼Œä½†æ˜¯é›†åˆå’Œæ˜ å°„éœ€è¦æ›´è¯¦ç»†çš„æ³¨é‡Šã€‚例如: [source,java] ---- class Before { @Option(names = "-u", type = {TimeUnit.class, Long.class}) Map<TimeUnit, Long> timeout; @Parameters(type = File.class) List<File> files; } ---- 从v2.0开始,`type` 属性ä¸å†æ˜¯ `Collection` å’Œ `Map` å—æ®µçš„å¿…è¦å±žæ€§ï¼špicocli将从泛型类型推æ–集åˆå…ƒç´ 类型。 `type` 属性ä»ç„¶åƒä»¥å‰ä¸€æ ·å·¥ä½œï¼Œåªæ˜¯åœ¨å¤§å¤šæ•°æƒ…况下是å¯é€‰çš„。 çœç•¥ `type` å±žæ€§ä¼šåˆ é™¤ä¸€äº›é‡å¤çš„å†…å®¹ï¼Œä»Žè€Œä½¿ä»£ç æ›´ç®€æ´ã€æ›´æ¸…æ™°: [source,java] ---- class Current { @Option(names = "-u") Map<TimeUnit, Long> timeout; @Parameters List<File> files; } ---- 在上é¢çš„示例ä¸ï¼Œpicocli 2.0能够自动å‘çŽ°ï¼Œå‘½ä»¤è¡Œå‚æ•°åœ¨æ·»åŠ åˆ°åˆ—è¡¨ä¹‹å‰éœ€è¦è½¬æ¢ä¸º `File`,以åŠå¯¹äºŽæ˜ 射,需è¦å°†é”®è½¬æ¢ä¸º `TimeUnit`,将值转æ¢ä¸º `Long` 。 == 自动帮助 Picocliæä¾›äº†è®¸å¤šä¾¿åˆ©æ–¹æ³•,如 `run` å’Œ `call`,它们能够解æžå‘½ä»¤è¡Œå‚æ•°ã€å¤„ç†é”™è¯¯å¹¶è°ƒç”¨æŽ¥å£æ–¹æ³•æ¥æ‰§è¡Œåº”用。 从æ¤ç‰ˆæœ¬å¼€å§‹ï¼Œå½“ç”¨æˆ·åœ¨å‘½ä»¤è¡Œä¸æŒ‡å®šä¸€ä¸ªæ³¨é‡Šæœ‰ `versionHelp` 或 `usageHelp` 属性的选项时,这些便利方法还将自动输出使用帮助和版本信æ¯ã€‚ image:AskingForHelp.jpg[] 下é¢çš„ç¤ºä¾‹ç¨‹åºæ¼”示自动帮助: [source,java] ---- @Command(version = "Help demo v1.2.3", header = "%nAutomatic Help Demo%n", description = "Prints usage help and version help when requested.%n") class AutomaticHelpDemo implements Runnable { @Option(names = "--count", description = "The number of times to repeat.") int count; @Option(names = {"-h", "--help"}, usageHelp = true, description = "Print usage help and exit.") boolean usageHelpRequested; @Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit.") boolean versionHelpRequested; public void run() { // NOTE: code like below is no longer required: // // if (usageHelpRequested) { // new CommandLine(this).usage(System.err); // } else if (versionHelpRequested) { // new CommandLine(this).printVersionHelp(System.err); // } else { ... the business logic for (int i = 0; i < count; i++) { System.out.println("Hello world"); } } public static void main(String... args) { CommandLine.run(new AutomaticHelpDemo(), System.err, args); } } ---- 当使用 `-h` 或 `--help` 执行时,程åºè¾“出使用帮助: image:AutoHelpDemo-usage-screenshot.png[自动帮助范例的使用帮助消æ¯] 类似地,当使用 `-V` 或 `--version` 执行时,程åºè¾“出版本信æ¯: image:AutoHelpDemo-version-screenshot.png[自动帮助范例的版本信æ¯] 自动输出帮助的方法: * CommandLine::call * CommandLine::run * CommandLine::parseWithHandler (通过内置Run...â€‹å¥æŸ„) * CommandLine::parseWithHandlers (通过内置Run...â€‹å¥æŸ„) ä¸è‡ªåŠ¨è¾“å‡ºå¸®åŠ©çš„æ–¹æ³•: * CommandLine::parse * CommandLine::populateCommand == 更好的åå‘½ä»¤æ”¯æŒ æ¤ç‰ˆæœ¬æ·»åŠ äº†æ–°çš„ `CommandLine::parseWithHandler` 方法。这些方法æä¾›äº†ä¸Ž `run` å’Œ `call` 方法相åŒçš„æ˜“用性,但是对于嵌套的åå‘½ä»¤æœ‰äº†æ›´å¤§çš„çµæ´»æ€§å’Œæ›´å¥½çš„æ”¯æŒã€‚ // image:https://www.intersoft.no/wp-content/uploads/2015/11/duplicate.png[] image:strong_leadership.jpg[] 考虑具有å命令的应用需è¦åšä»€ä¹ˆ: 1. è§£æžå‘½ä»¤è¡Œã€‚ 2. å¦‚æžœç”¨æˆ·è¾“å…¥æ— æ•ˆï¼Œåˆ™å¯¹è§£æžå¤±è´¥å¤„çš„å命令输出错误消æ¯å’Œä½¿ç”¨å¸®åŠ©æ¶ˆæ¯ã€‚ 3. å¦‚æžœè§£æžæˆåŠŸï¼Œåˆ™æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²è¯·æ±‚顶层命令或å命令的使用帮助或版本信æ¯ã€‚如果是,则输出请求的信æ¯å¹¶é€€å‡ºã€‚ 4. å¦åˆ™ï¼Œæ‰§è¡Œä¸šåŠ¡é€»è¾‘ã€‚é€šå¸¸è¿™æ„å‘³ç€æ‰§è¡Œæœ€å…·ä½“çš„å命令。 Picocliæä¾›äº†ä¸€äº›æž„é€ å—æ¥å®Œæˆæ¤ä»»åŠ¡ï¼Œä½†æ˜¯å°†å®ƒä»¬è¿žæŽ¥åœ¨ä¸€èµ·å–决于应用。这ç§è¿žæŽ¥æœ¬è´¨ä¸Šæ˜¯æ ·æ¿ï¼Œä¸”它在应用之间éžå¸¸ç›¸ä¼¼ã€‚例如,以å‰ï¼Œå…·æœ‰å命令的应用通常包å«å¦‚下代ç : [source,java] ---- public static void main(String... args) { // 1. parse the command line CommandLine top = new CommandLine(new YourApp()); List<CommandLine> parsedCommands; try { parsedCommands = top.parse(args); } catch (ParameterException ex) { // 2. handle incorrect user input for one of the subcommands System.err.println(ex.getMessage()); ex.getCommandLine().usage(System.err); return; } // 3. check if the user requested help for (CommandLine parsed : parsedCommands) { if (parsed.isUsageHelpRequested()) { parsed.usage(System.err); return; } else if (parsed.isVersionHelpRequested()) { parsed.printVersionHelp(System.err); return; } } // 4. execute the most specific subcommand Object last = parsedCommands.get(parsedCommands.size() - 1).getCommand(); if (last instanceof Runnable) { ((Runnable) last).run(); } else if (last instanceof Callable) { Object result = ((Callable) last).call(); // ... do something with result } else { throw new ExecutionException("Not a Runnable or Callable"); } } ---- è¿™æ˜¯ç›¸å½“å¤§çš„æ ·æ¿ä»£ç é‡ã€‚Picocli 2.0æä¾›äº†ä¸€ç§ä¾¿åˆ©æ–¹æ³•,它å…许您将上述所有内容缩å‡ä¸ºä¸€è¡Œä»£ç ï¼Œè¿™æ ·æ‚¨å°±å¯ä»¥ä¸“注于您应用的业务逻辑: [source,java] ---- public static void main(String... args) { // This handles all of the above in one line: // 1. parse the command line // 2. handle incorrect user input for one of the subcommands // 3. automatically print help if requested // 4. execute one or more subcommands new CommandLine(new YourApp()).parseWithHandler(new RunLast(), System.err, args); } ---- 新的便利方法是 `parseWithHandler` 。您å¯ä»¥åˆ›å»ºè‡ªå·±çš„è‡ªå®šä¹‰å¥æŸ„或使用其ä¸ä¸€ä¸ªå†…ç½®å¥æŸ„。Picocliæä¾›ä¸€äº›å¸¸è§ç”¨ä¾‹çš„奿Ÿ„实现。 å†…ç½®çš„å¥æŸ„是 `RunFirst`ã€`RunLast` å’Œ `RunAll`ã€‚æ‰€æœ‰è¿™äº›å¥æŸ„都æä¾›äº†è‡ªåŠ¨å¸®åŠ©ï¼šå¦‚æžœç”¨æˆ·è¯·æ±‚usageHelp或versionHelp, 将输出所请求的信æ¯ï¼Œå¥æŸ„å°†è¿”å›žï¼Œè€Œæ— éœ€è¿›ä¸€æ¥å¤„ç†ã€‚奿Ÿ„希望所有命令都能执行 `java.lang.Runnable` 或 `java.util.concurrent.Callable`。 * `RunLast` 执行*最具体的*命令或å命令。例如,如果用户调用 `java Git commit -m "commit message"`, 那么picocli认为 `Git` 是顶层命令,`commit` 为å命令。在这个例åä¸ï¼Œ`commit` å命令是最具体的命令,所以 `RunLast` å°†åªæ‰§è¡Œå命令。 如果没有å命令,将执行顶层命令。现在 `RunLast` ç”±Picocli内部用于执行现有的 `CommandLine::run` å’Œ `CommandLine::call` 的便利方法。 * `RunFirst` åªæ‰§è¡Œ*首个*ã€é¡¶å±‚命令,并忽略å命令。 * `RunAll` 执行命令行ä¸å‡ºçŽ°çš„*顶层命令和所有å命令。 还有一个 `parseWithHandlers` 方法,它与å‰é¢çš„æ–¹æ³•类似,但é¢å¤–å…è®¸æ‚¨ä¸ºä¸æ£ç¡®çš„ç”¨æˆ·è¾“å…¥æŒ‡å®šä¸€ä¸ªè‡ªå®šä¹‰çš„å¥æŸ„。 === 改进的 'run' å’Œ 'call' 方法 `CommandLine::call` å’Œ `CommandLine::run` 便利方法现在支æŒå命令,并将执行用户指定的**最åŽä¸€ä¸ª**å命令。以å‰ï¼Œåå‘½ä»¤é€šå¸¸è¢«å¿½ç•¥ï¼Œåªæ‰§è¡Œé¡¶å±‚命令。 === 改进异常 最åŽï¼Œä»Žæ¤ç‰ˆæœ¬å¼€å§‹ï¼Œæ‰€æœ‰picocli异常都æä¾›äº†ä¸€ä¸ª `getCommandLine` 方法, è¯¥æ–¹æ³•è¿”å›žè§£æžæˆ–执行失败处的命令或å命令。 以å‰ï¼Œå¦‚果用户对带有å命令的应用æä¾›äº†æ— 效的输入,那么很难准确地指出哪个å命令未能解æžè¾“入。 == 结论 如果您已ç»åœ¨ä½¿ç”¨picocli,那么有必è¦å‡çº§v2.0。 å¦‚æžœæ‚¨ä¹‹å‰æ²¡æœ‰ä½¿ç”¨è¿‡picocli,我希望以上内容能让您有兴趣å°è¯•一下。 å…¶ä¸è®¸å¤šæ”¹è¿›æºäºŽç”¨æˆ·å馈和éšåŽçš„讨论。请ä¸è¦çŠ¹è±«ï¼Œåœ¨picocli https://github.com/remkop/picocli/issues[问题跟踪器]上æå‡ºé—®é¢˜ã€è¯·æ±‚æ·»åŠ æ–°åŠŸèƒ½æˆ–æä¾›å…¶ä»–å馈。 如果喜欢的è¯ï¼Œè¯·ç‚¹å‡» https://github.com/remkop/picocli[GitHub项目]给我们星,并告诉您的朋å‹ï¼