怎么基于Java編寫一個(gè)CLI工具?
CLI
CLI,全稱為命令行界面(Command Line Interface),是一種用于通過鍵盤輸入指令與操作系統(tǒng)進(jìn)行交互的軟件機(jī)制。這種界面是在圖形用戶界面得到普及之前使用最為廣泛的用戶界面,并且, 即使在當(dāng)前圖形用戶界面廣泛使用的環(huán)境下,CLI仍然有其獨(dú)特的優(yōu)勢和廣泛的應(yīng)用。
對于CLI,它的一個(gè)重要特性就是效率。用戶可以在一條文本命令中對多個(gè)文件執(zhí)行操作,而不需要在各個(gè)窗口之間切換,節(jié)省了大量時(shí)間。此外,如果你已經(jīng)熟悉了這些命令,那么你可以非??焖俚貫g覽系統(tǒng)并與之交互。
構(gòu)建CLI的工具很多,今天主要基于Java語言來實(shí)現(xiàn),其中Apache Commons CLI框架提供了這樣的便利。今天結(jié)合之前學(xué)習(xí)的graalVM提供的native-image工具,來生成一個(gè)exe類型的可執(zhí)行文件,由于graalVM的跨平臺性,我們還能生成各個(gè)平臺的CLI命令來輔助完成更多的工作。
Apache Commons CLI是一個(gè)用于編寫命令行界面的Java庫。它提供了一個(gè)靈活的框架,可以很容易地定義和解析命令行參數(shù)。這個(gè)庫的主要優(yōu)點(diǎn)是它可以處理各種類型的參數(shù),包括選項(xiàng)、位置參數(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.
一個(gè)合格的CLI基本都會提供help選項(xiàng)來展示,這個(gè)CLI的語法、選項(xiàng)以及功能描述。從上面的輸出可以看到help主要包括:
- 介紹:主要對命令的功能的描述,包括官網(wǎng)、版本以及一些內(nèi)在系數(shù)等
- 用法:包括命令語法格式、配置項(xiàng)、參數(shù)等信息
- 參數(shù)說明:具體配置項(xiàng)參數(shù)的說明,以及具體的功能描述
Common-CLI
- 定義階段:在Java代碼中定義Option參數(shù),定義參數(shù)、是否需要輸入值、簡單的描述等
- 解析階段:應(yīng)用程序傳入?yún)?shù)后,CLI進(jìn)行解析
- 詢問階段:通過查詢CommandLine詢問進(jìn)入到哪個(gè)程序分支中
定義階段
主要是借助Option類提供的API來構(gòu)建各種選項(xiàng)以及參數(shù)信息,下面是對應(yīng)API的描述:
返回值 | 方法名 | 說明 |
Options | addOption(Option opt) | 添加一個(gè)選項(xiàng)實(shí)例 |
Options | addOption(String opt, boolean hasArg, String description) | 添加一個(gè)只包含短名稱的選項(xiàng) |
Options | addOption(String opt, String description) | 添加一個(gè)只包含短名稱的選項(xiàng) |
Options | addOption(String opt, String longOpt, boolean hasArg, String description) | 添加一個(gè)包含短名稱和長名稱的選項(xiàng) |
Options | addOptionGroup(OptionGroup group) | 添加一個(gè)選項(xiàng)組 |
List | getMatchingOptions(String opt) | 獲得匹配選項(xiàng)的長名稱集合 |
Option | getOption(String opt) | 通過長名稱或短名稱獲得選項(xiàng) |
OptionGroup | getOptionGroup(Option opt) | 獲得選項(xiàng)所在的選項(xiàng)組 |
Collection | getOptions() | 獲得一個(gè)只讀的選項(xiàng)集合 |
List | getRequiredOptions() | 獲得必須的選項(xiàng)集合 |
boolean | hasLongOption(String opt) | 判斷是否存在選項(xiàng) |
boolean | hasOption(String opt) | 判斷是否存在選項(xiàng) |
boolean | hasShortOption(String opt) | 判斷是否存在選項(xiàng) |
解析階段
主要對輸入?yún)?shù)的解析,也就是main方法的參數(shù),默認(rèn)提供下面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;
}
解析的過程有時(shí)候會比較些復(fù)雜,示例中是針對單一選項(xiàng)的分支,當(dāng)多個(gè)選項(xiàng)混合使用時(shí),比如tar -zxvf xxx.tar.gz這樣的,當(dāng)然前提是我們定義的CLI支持這種風(fēng)格。
示例
下面通過一個(gè)簡單的示例看下如何構(gòu)建一個(gè)CLI的工具:該示例的作用是按指定格式輸出當(dāng)前日期:
clendar -o yyyy-MM-dd
- 定義配置項(xiàng)
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插件,主要幫助在打包時(shí)將依賴包一并寫入jar文件,同時(shí)將入口文件定義到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
如果安裝上面的配置,最終會在項(xiàng)目target目錄輸出一個(gè)以jar-with-dependencies為后綴的jar文件,通過下面的命令可以測試cli命令
java -jar ./target/calendar-jar-with-dependencies.jar -h
這樣的CLI可不是我們想要的,一來需要依賴JRE的運(yùn)行環(huán)境,同時(shí)調(diào)用極其不方便。
生成exe
如果你看過之前的文章,關(guān)于GraalVM的使用,按照文檔下載并配置好運(yùn)行環(huán)境后,可以通過下面的命令對上一步的jar文件進(jìn)一步處理
native-image -jar [jar] -o [name]
native-image -jar ./target/calendar-jar-with-dependencies.jar -o calendar
通過上面的命令會生成一個(gè)calendar.exe文件,這樣將其加入到環(huán)境變量后,則可以在windows平臺終端上使用了
對于不喜歡直接使用命令的,當(dāng)然這里也可以使用插件exec-maven-plugin,在maven生命周期package階段,自動執(zhí)行上面的命令,這樣整個(gè)過程只需要執(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í)行下面的命令接口看到預(yù)期的結(jié)果:
calendar.exe -O yyyy-MM-dd
總結(jié)
總的來說,Apache Commons CLI是一個(gè)非常強(qiáng)大的工具,可以幫助你輕松地處理命令行參數(shù)。無論你的應(yīng)用程序需要處理多少個(gè)參數(shù),或者這些參數(shù)的類型是什么, Commons CLI都可以提供幫助。