Apache Commons CLI 簡介
1. 概述
在本教程中,我們將探索 Java Apache Commons CLI 程式庫。它是一個框架,可以幫助開發人員以高效、標準的方式建立現有軟體工具的命令列介面 (CLI)。
該程式庫支援定義 CLI 選項及其基本驗證,可加快 CLI 的開發速度。它有助於解析命令列參數及其值。最後,參數值可以傳遞給實作該工具的底層服務。
值得注意的是,Apache Commons CLI 庫也用於 Apache 的多個產品,包括 Kafka、Maven、Ant 和 Tomcat。
我們將討論 Apache Commons CLI 中的一些重要類,然後在範例程式中使用它們來展示其功能。
2. CLI 的主要關注點
CLI 透過協助自動化執行一系列與其領域相關的任務,為工具提供了優勢。此外,在當今世界,DevOps 工程師在沒有 CLI 的情況下工作是不可想像的。
除了工具底層實現的挑戰之外,所有 CLI 都需要處理一些基本要求:
- 解析命令列參數,提取參數值,並將它們傳遞給底層服務
- 以一定的格式顯示幫助訊息
- 顯示版本
- 處理缺少的必需選項
- 處理未知選項
- 處理互斥選項
3. 重要課程
讓我們來看看Apache Commons CLI 函式庫的重要類別:
Option
、 OptionGroup
和Options
類別幫助定義 CLI 。所有 CLI 選項的定義都包含在Options
類別中。 CommandLineParser
類別的parse()
方法使用Options
類別來解析命令列。如果出現任何偏差, parse()
方法會拋出適當的異常。解析後,可以進一步探測CommandLine
類別以提取 CLI 選項的值(如果有)。
最後,提取的值可以傳遞給實作 CLI 工具的底層服務。
與CommandLineParser
類別中的parse()
方法類似, HelpFormatter
也使用Options
類別來顯示 CLI 工具的說明文字。
4. 實施
讓我們進一步探索 Apache Commons CLI 庫類,並了解它們如何幫助一致、快速地建立 CLI 工具。
4.1.先決條件 Maven 依賴項
首先,讓我們在pom.xml
檔案中加入必要的Maven 依賴項:
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.6.0</version>
</dependency>
4.2.定義、解析和探測命令列參數
考慮使用psql
CLI 連接到 PostgreSQL 資料庫的命令:
psql -h PGSERVER -U postgres -d empDB
或者:
psql --host PGSERVER -username postgres -dbName empDB
這兩個指令都需要資料庫伺服器主機、使用者名稱和資料庫名稱的輸入參數。第一個指令使用短選項名稱,而第二個指令使用長選項名稱。 username
和dbName
是必要選項,而host
是可選的。如果缺少主機,則預設情況下,我們將 localhost 視為主機值。
現在,讓我們定義、解析和探測命令列參數:
@Test
void whenCliOptionProvided_thenParseAndExtractOptionAndArgumentValues() throws ParseException {
Options options = new Options();
Option hostOption = createOption("h", "host", "HOST", "Database server host", false);
Option userNameOption = createOption("U", "username", "USERNAME", "Database user name", true);
Option dbNameOption = createOption("d", "dbName", "DBNAME", "Database name to connect to", true);
options.addOption(hostOption)
.addOption(dbNameOption)
.addOption(userNameOption);
String[] commandWithShortNameOptions = new String[] { "-h", "PGSERVER", "-U", "postgres", "-d", "empDB" };
parseThenProcessCommand(options, commandWithShortNameOptions, "h", "U", "d" );
String[] commandWithLongNameOptions = new String[] { "--username", "postgres", "--dbName", "empDB" };
parseThenProcessCommand(options, commandWithShortNameOptions, "host", "username", "dbName" );
}
為了定義命令中的選項,我們透過呼叫方法createOption()
建立了與每個輸入選項對應的Option
物件:
Option createOption(String shortName, String longName, String argName, String description, boolean required) {
return Option.builder(shortName)
.longOpt(longName)
.argName(argName)
.desc(description)
.hasArg()
.required(required)
.build();
}
我們使用Option.Builder
類別來設定 CLI 中輸入選項的短名稱、長名稱、參數名稱和描述。此外,在建構器類別中的required()
方法的幫助下,我們認為先前定義的-U
和-d
選項是強制性的。
最後,我們將分別將帶有短名稱選項和長名稱選項的參數傳遞給方法parseThenProcessCommand()
:
void parseThenProcessCommand(Options options, String[] commandArgs, String hostOption,
String usernameOption, String dbNameOption) throws ParseException {
CommandLineParser commandLineParser = new DefaultParser();
CommandLine commandLine = commandLineParser.parse(options, commandArgs);
String hostname = commandLine.hasOption("h") ? commandLine.getOptionValue(hostOption) : "localhost";
String username = commandLine.getOptionValue(usernameOption);
String dbName = commandLine.getOptionValue(dbNameOption);
if (commandLine.hasOption("h")) {
assertEquals("PGSERVER", hostname);
} else {
assertEquals("localhost", hostname);
}
assertEquals("postgres", userName);
assertEquals("empDB", dbName);
createConnection(hostname, username, dbName);
}
有趣的是,該方法可以處理具有短名稱和長選項名稱的命令。 CommandLineParser class
解析參數,然後我們透過呼叫CommandLine
物件的getOptionValue()
方法來擷取它們的值。由於host
是可選的,我們呼叫CommandLine
類別中的hasOption()
方法來偵測它是否存在。如果它不存在,我們將其值替換為預設的localhost.
最後,我們透過呼叫方法createConnection()
將值傳遞給底層服務。
4.3.處理缺少的強制選項
在大多數 CLI 中,當缺少強制選項時,應顯示錯誤。假設psql
指令中缺少強制host
選項:
psql -h PGSERVER -U postgres
讓我們看看如何處理這個問題:
@Test
void whenMandatoryOptionMissing_thenThrowMissingOptionException() {
Options options = createOptions();
String[] commandWithMissingMandatoryOption = new String[]{"-h", "PGSERVER", "-U", "postgres"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(MissingOptionException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithMissingMandatoryOption);
} catch (ParseException e) {
assertTrue(e instanceof MissingOptionException);
handleException(new RuntimeException(e));
throw e;
}
});
}
當我們呼叫CommandLineParser
類別中的parse()
方法時,它會拋出MissingOptionException
,指示缺少所需的選項d
。接下來,我們呼叫方法handleException()
來管理異常。
假設存在d
選項,但缺少其參數:
psql -h PGSERVER -U postgres -d
現在,讓我們看看如何處理這個問題:
@Test
void whenOptionArgumentIsMissing_thenThrowMissingArgumentException() {
Options options = createOptions();
String[] commandWithOptionArgumentOption = new String[]{"-h", "PGSERVER", "-U", "postgres", "-d"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(MissingArgumentException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithOptionArgumentOption);
} catch (ParseException e) {
assertTrue(e instanceof MissingArgumentException);
handleException(new RuntimeException(e));
throw e;
}
});
}
當在CommandLineParser
上呼叫parse()
方法時,由於-d
選項旁邊缺少參數,因此會引發MissingArgumentException
。再往下,我們呼叫handleException()
來管理異常。
4.4.處理無法辨識的選項
有時,在執行命令時,我們會提供無法識別的選項:
psql -h PGSERVER -U postgres -d empDB -y
我們提供了一個不正確的、不存在的-y
選項。我們來看看程式碼中是如何處理的:
@Test
void whenUnrecognizedOptionProvided_thenThrowUnrecognizedOptionException() {
Options options = createOptions();
String[] commandWithIncorrectOption = new String[]{"-h", "PGSERVER", "-U", "postgres", "-d", "empDB", "-y"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(UnrecognizedOptionException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithIncorrectOption);
} catch (ParseException e) {
assertTrue(e instanceof UnrecognizedOptionException);
handleException(new RuntimeException(e));
throw e;
}
});
}
parse()
方法在遇到未知的-y
選項時拋出UnrecogniedOptionException
。隨後,我們呼叫handleException()
來管理執行階段異常。
4.5.處理互斥選項
考慮使用cp
在 Unix 平台中複製檔案的命令:
cp -i -f file1 file2
-i
選項在覆蓋檔案之前進行提示;但是, -f
選項會覆蓋檔案而不提示。這兩個選項是相互衝突的,因此不應一起使用。
讓我們嘗試實作此驗證:
@Test
void whenMutuallyExclusiveOptionsProvidedTogether_thenThrowAlreadySelectedException() {
Option interactiveOption = new Option("i", false, "Prompts the user before overwriting the existing files");
Option forceOption = new Option("f", false, "Overwrites the existing files without prompting");
OptionGroup optionGroup = new OptionGroup();
optionGroup.addOption(interactiveOption)
.addOption(forceOption);
Options options = new Options();
options.addOptionGroup(optionGroup);
String[] commandWithConflictingOptions = new String[]{"cp", "-i", "-f", "file1", "file2"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(AlreadySelectedException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithConflictingOptions);
} catch (ParseException e) {
assertTrue(e instanceof AlreadySelectedException);
handleException(new RuntimeException(e));
throw e;
}
});
}
首先,我們使用其建構函式而不是Option.Builder
類別來創建了相關的Option
物件。這也是實例化Option
類別的另一種方法。
OptionGroup
類別有助於對互斥選項進行分組。因此,我們將這兩個選項加入OptionGroup
物件中。然後,我們將OptionGroup
物件新增至Options
物件。最後,當我們呼叫CommandLineParser
類別上的parse()
方法時,它引發了AlreadySelectedException,
表示有衝突的選項。
4.6.顯示幫助文字
格式化說明文字並將其顯示在終端上是所有 CLI 工具的共同關注點。因此,Apache Commons CLI 也在HelpFormatter
類別的幫助下解決了這個問題。
讓我們以psql
CLI 為例:
@Test
void whenNeedHelp_thenPrintHelp() {
HelpFormatter helpFormatter = new HelpFormatter();
Options options = createOptions();
options.addOption("?", "help", false, "Display help information");
helpFormatter.printHelp("psql -U username -h host -d empDB", options);
}
HelpFormatter
類別中的printHelp()
方法使用包含 CLI 定義的Options
物件來顯示說明文字。此方法的第一個參數會產生頂部顯示的 CLI 的使用文字。
讓我們來看看HelpFormatter
類別產生的輸出:
usage: psql -U username -h host -d empDB
-?,--help Display help information
-d,--dbName <DBNAME> Database name to connect to
-h,--host <HOST> Database server host
-U,--username <USERNAME> Database user name
5. 結論
在本文中,我們討論了 Apache Commons CLI 函式庫可幫助以標準化方式快速且有效率地建立 CLI 的能力。此外,該庫簡潔易懂。
值得注意的是,還有其他函式庫,如 JCommander、Airline 和 Picocli,它們同樣有效率且值得探索。與 Apache Commons CLI 不同,它們都支援註解。
與往常一樣,所使用的程式碼可以在 GitHub 上找到。