怎么基于Java編寫一個CLI工具?
CLI
CLI,全稱為命令行界面(Command Line Interface),是一種用于通過鍵盤輸入指令與操作系統(tǒng)進行交互的軟件機制。這種界面是在圖形用戶界面得到普及之前使用最為廣泛的用戶界面,并且, 即使在當前圖形用戶界面廣泛使用的環(huán)境下,CLI仍然有其獨特的優(yōu)勢和廣泛的應(yīng)用。
對于CLI,它的一個重要特性就是效率。用戶可以在一條文本命令中對多個文件執(zhí)行操作,而不需要在各個窗口之間切換,節(jié)省了大量時間。此外,如果你已經(jīng)熟悉了這些命令,那么你可以非??焖俚貫g覽系統(tǒng)并與之交互。
構(gòu)建CLI的工具很多,今天主要基于Java語言來實現(xiàn),其中Apache Commons CLI框架提供了這樣的便利。今天結(jié)合之前學習的graalVM提供的native-image工具,來生成一個exe類型的可執(zhí)行文件,由于graalVM的跨平臺性,我們還能生成各個平臺的CLI命令來輔助完成更多的工作。
Apache Commons CLI是一個用于編寫命令行界面的Java庫。它提供了一個靈活的框架,可以很容易地定義和解析命令行參數(shù)。這個庫的主要優(yōu)點是它可以處理各種類型的參數(shù),包括選項、位置參數(shù)、可選參數(shù)等。
構(gòu)成
下面以native-image為例,通過在終端輸入native-image --help可以看到以下信息
_> native-image --help
GraalVM Native Image (https://www.graalvm.org/native-image/)
This tool can ahead-of-time compile Java code to native executables.
Usage: native-image [options] class [imagename] [options]
           (to build an image for a class)
   or  native-image [options] -jar jarfile [imagename] [options]
           (to build an image for a jar file)
   or  native-image [options] -m <module>[/<mainclass>] [options]
       native-image [options] --module <module>[/<mainclass>] [options]
           (to build an image for a module)
where options include:
    @argument files       one or more argument files containing options
    -cp <class search path of directories and zip/jar files>
    -classpath <class search path of directories and zip/jar files>
    --class-path <class search path of directories and zip/jar files>
                          A ; separated list of directories, JAR archives,
                          and ZIP archives to search for class files.
    -p <module path>
    --module-path <module path>...
                          A ; separated list of directories, each directory
                          is a directory of modules.一個合格的CLI基本都會提供help選項來展示,這個CLI的語法、選項以及功能描述。從上面的輸出可以看到help主要包括:
- 介紹:主要對命令的功能的描述,包括官網(wǎng)、版本以及一些內(nèi)在系數(shù)等
 - 用法:包括命令語法格式、配置項、參數(shù)等信息
 - 參數(shù)說明:具體配置項參數(shù)的說明,以及具體的功能描述
 
Common-CLI
- 定義階段:在Java代碼中定義Option參數(shù),定義參數(shù)、是否需要輸入值、簡單的描述等
 - 解析階段:應(yīng)用程序傳入?yún)?shù)后,CLI進行解析
 - 詢問階段:通過查詢CommandLine詢問進入到哪個程序分支中
 
定義階段
主要是借助Option類提供的API來構(gòu)建各種選項以及參數(shù)信息,下面是對應(yīng)API的描述:
返回值  | 方法名  | 說明  | 
Options  | addOption(Option opt)  | 添加一個選項實例  | 
Options  | addOption(String opt, boolean hasArg, String description)  | 添加一個只包含短名稱的選項  | 
Options  | addOption(String opt, String description)  | 添加一個只包含短名稱的選項  | 
Options  | addOption(String opt, String longOpt, boolean hasArg, String description)  | 添加一個包含短名稱和長名稱的選項  | 
Options  | addOptionGroup(OptionGroup group)  | 添加一個選項組  | 
List  | getMatchingOptions(String opt)  | 獲得匹配選項的長名稱集合  | 
Option  | getOption(String opt)  | 通過長名稱或短名稱獲得選項  | 
OptionGroup  | getOptionGroup(Option opt)  | 獲得選項所在的選項組  | 
Collection  | getOptions()  | 獲得一個只讀的選項集合  | 
List  | getRequiredOptions()  | 獲得必須的選項集合  | 
boolean  | hasLongOption(String opt)  | 判斷是否存在選項  | 
boolean  | hasOption(String opt)  | 判斷是否存在選項  | 
boolean  | hasShortOption(String opt)  | 判斷是否存在選項  | 
解析階段
主要對輸入?yún)?shù)的解析,也就是main方法的參數(shù),默認提供下面3中語法解析的支持:
- DefaultParser:提供了基礎(chǔ)的解析功能,能解析簡單的命令行參數(shù)。(比如:java -Djava.awt.headless=true -Djava.net.useSystemProxies=true Foo)
 - PosixParser:提供了解析POSIX形式參數(shù)的功能。(比如:tar -zxvf foo.tar.gz)
 - GnuParser:提供了解析長參數(shù)及Java命令中參數(shù)的功能。(比如:du --human-readable --max-depth=1)
 
詢問階段
基于上一步解析后,會將參數(shù)解析成CommandLine,結(jié)合Option中的配置,需要我們完成各種配置、參數(shù)匹配后的業(yè)務(wù)處理流程,類型下面這樣:
if( commandLine.hasOption("help") ){
          helper.printHelp("calendar [options] \n\nwhere options include:", null, options, null, false);
          return;
      }
      if( commandLine.hasOption("version") ){
          printResult("1.0.0");
          return;
      }解析的過程有時候會比較些復雜,示例中是針對單一選項的分支,當多個選項混合使用時,比如tar -zxvf xxx.tar.gz這樣的,當然前提是我們定義的CLI支持這種風格。
示例
下面通過一個簡單的示例看下如何構(gòu)建一個CLI的工具:該示例的作用是按指定格式輸出當前日期:
clendar -o yyyy-MM-dd- 定義配置項
 
private static Options initOptions() {
        Options options = new Options();
        options.addOption(Option.builder("H")
                .longOpt("help")
                .desc("show help information").build());
        options.addOption(Option.builder("V")
                .longOpt("version")
                .desc("show help information").build());
        options.addOption(Option.builder("O")
                .longOpt("out")
                .hasArg(true)
                .argName("fmt") // 只是定義
                .required(false)
                .desc("configure the date output format").build());
        return options;
    }- 解析參數(shù)
 
private static CommandLine parseArguments(Options options, String[] args){
        CommandLineParser parser = new DefaultParser();
        try {
            return parser.parse(options, args);
        } catch (ParseException e) {
            System.err.println(e.getMessage());
        }
        return null;
    }- 詢問階段
 
private static void handleCommand(Options options, CommandLine commandLine) {
        if(ArrayUtils.isEmpty(commandLine.getOptions()) ){
            printResult("Please specify options for calendar building or use --help for more info.");
            return;
        }
        if( commandLine.hasOption("help") ){
            helper.printHelp("calendar [options] \n\nwhere options include:", null, options, null, false);
            return;
        }
        if( commandLine.hasOption("version") ){
            printResult("1.0.0");
            return;
        }
        if( commandLine.hasOption("out") ){
            String fmt = commandLine.getOptionValue("out");
            if(StringUtils.isEmpty(fmt)){
                fmt = "yyyy-MM-dd HH:mm:ss";
            }
            printResult(DateFormatUtils.format(new Date(), fmt));
            return;
        }
        // calendar: 'x' is not a git command. See 'calendar --help'.
        helper.printHelp(String.format("calendar: '%s' is not a calendar command. See 'calendar --help'.", Arrays.toString(commandLine.getArgs())), options, false);
    }定義程序入口:
public static void main(String[] args) {
        // 定義階段
        Options options = initOptions();
        // 解析階段
        CommandLine commandLine = parseArguments(options, args);
        // 詢問階段
        handleCommand(options, commandLine);
    }打包
這里我們引入maven-assembly-plugin插件,主要幫助在打包時將依賴包一并寫入jar文件,同時將入口文件定義到manifest:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.3.0</version>
    <executions>
        <execution>
            <id>package-jar-with-dependencies</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>${main-class}</mainClass>
                    </manifest>
                </archive>
                <descriptorRefs>
                    <!-- bin,jar-with-dependencies,src,project -->
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
        </execution>
    </executions>
</plugin>可以直接通過maven插件或者下的命令,將上面的代碼打包成jar文件
mvn clean package測試jar
如果安裝上面的配置,最終會在項目target目錄輸出一個以jar-with-dependencies為后綴的jar文件,通過下面的命令可以測試cli命令
java -jar ./target/calendar-jar-with-dependencies.jar -h這樣的CLI可不是我們想要的,一來需要依賴JRE的運行環(huán)境,同時調(diào)用極其不方便。
生成exe
如果你看過之前的文章,關(guān)于GraalVM的使用,按照文檔下載并配置好運行環(huán)境后,可以通過下面的命令對上一步的jar文件進一步處理
native-image -jar [jar] -o [name]
native-image -jar ./target/calendar-jar-with-dependencies.jar -o calendar通過上面的命令會生成一個calendar.exe文件,這樣將其加入到環(huán)境變量后,則可以在windows平臺終端上使用了
對于不喜歡直接使用命令的,當然這里也可以使用插件exec-maven-plugin,在maven生命周期package階段,自動執(zhí)行上面的命令,這樣整個過程只需要執(zhí)行mvn clean package即可
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.1.0</version>
    <executions>
        <execution>
            <id>native-image-app</id>
            <phase>package</phase>
            <goals>
                <goal>exec</goal>
            </goals>
            <configuration>
                <environmentVariables>
                </environmentVariables>
                <!-- native-image -jar ./target/tool-jar-with-dependencies.jar -o tool -->
                <executable>native-image</executable>
                <arguments>
                    <argument>-jar</argument>
                    <argument>${project.basedir}/target/${project.build.finalName}-jar-with-dependencies.jar</argument>
                    <argument>-o</argument>
                    <argument>${project.build.finalName}</argument>
                </arguments>
            </configuration>
        </execution>
    </executions>
</plugin>測試exe
在終端執(zhí)行下面的命令接口看到預期的結(jié)果:
calendar.exe -O yyyy-MM-dd總結(jié)
總的來說,Apache Commons CLI是一個非常強大的工具,可以幫助你輕松地處理命令行參數(shù)。無論你的應(yīng)用程序需要處理多少個參數(shù),或者這些參數(shù)的類型是什么, Commons CLI都可以提供幫助。















 
 
 












 
 
 
 