C 高階系列導航
1. C 程序的參數
在命令行中執行應用軟件時,常使用參數來完成對應用的配置或設置。因此,程序中對命令參數進行解析是非常常見且重要的功能。
在 C 語言中,當程序需要使用命令參數時,mian 函數必須爲以下形式:
int main(int argc, char* argv[])
{
// ...
}
參數對應着 main 函數的形參 argc
與 argv
。argc
爲命令的參數列表的個數,需注意命令本身就是參數之一,例如命令 ./app
的參數數爲 1。argv
爲參數列表,例如命令 ./app -m -p
的選項參數 -m
對應 argv[1]
,-p
對應 argv[2]
。
以下實驗將打印命令的參數列表:
#include <unistd.h>
int main(int argc, char* argv[])
{
for (int i = 0; i < argc; i++)
{
printf("argv[%d] : %s\n", i, argv[i]);
}
}
編譯後執行命令:output/Kapp -s -p 12345 -m
,打印結果如下:
argv[0] : output/Kapp
argv[1] : -s
argv[2] : -p
argv[3] : 12345
argv[4] : -m
可以看到,逐個解析命令參數並不困難。但當參數爲選項類型如以上的 -p
時,此時往往需連續解析後一參數 12345
並把解析結果拼接處理,這樣程序設計的複雜度將大大增加且擴展性很差。
有沒有方法可以方便管理選項類型參數呢?答案是有的。C 庫提供了 getopt 函數與 getopt_long 函數來管理命令的選項類型參數。
爲了簡化命名,以下將把選項類型參數簡稱爲選項,其餘皆稱爲參數,本質上兩者都是命令的參數。
2. getopt 函數
getopt 函數所屬頭文件 <unistd.h>
,函數原型爲:
int getopt(int argc, char* const argv[], const char* optstring);
形參 argc
與 argv
對應的就是 mian 函數的形參 argc
與 argv
,可以直接使用 main 函數的形參作爲傳參。
形參 optstring
用於註冊選項規則,表示以指定規則去解析 argv
中的選項,不同的規則可處理不同的選項情景。
optstring
規則如下:
格式 | 含義 | 示例 |
---|---|---|
單字符 | 該選項無參數 | "m" 表示選項 -m 無後續參數 |
單字符後接一個冒號 | 該選項必要有參數 | "p:" 表示選項 -p 的形式必須爲 -p(parameter) 或 -p (parameter) |
單字符後接兩個冒號 | 該選項的參數爲可選的 | "n::" 表示選項 -n 的形式可以爲 -n 或 -n(parameter) ,但不能爲 -n (parameter) (選項與參數間也有空格) |
需要注意的是,選項的格式必須爲 '-' + 字符
,getopt 函數纔會認爲該參數爲選項類型,否則會直接忽略。而命令選項是支持連續在一起的,即 -a -b -c
可合併爲 -abc
。不過最好只把相同規則的選項合併使用,不同規則的選項會導致解析結果混淆進而出錯。
getopt 函數的返回值存在以下規定:
- 當以規則
optstring
解析選項成功時,將返回選擇字符對應的 ASCII 碼值。- 當以規則
optstring
解析選項失敗時,將返回'?'
對應的 ASCII 碼值。- 當全部參數解析完成時,將返回 -1。
想必不少人到這裏都會有些懵圈:使用 getopt 函數解析選項成功後,解析結果哪去了呢?
其實在 <unistd.h>
包含的頭文件 <getopt.h>
中有以下兩個全局變量聲明:
extern char* optarg;
extern int optind;
getopt 函數一次只解析一個選項。optarg
指向的是解析後 argv
中選項對應的參數結果,例如以規則 "p:"
去解析 "-p 12345"
後,optarg
指向的是 "12345"
。當解析後無參數時,optarg
將指向 NULL。
optind
保存下一個解析參數的下標,該下標從 0 起算。
以下實驗將使用 getopt 來打印各命令選項及其參數:
#include <unistd.h>
int main(int argc, char* argv[])
{
int ret = 0;
while ((ret = getopt(argc, argv, "ab:c::")) != -1)
{
printf("ret = %c\t\t", ret);
printf("optarg = %s\t\t", optarg);
printf("optind = %d\t\t", optind);
printf("argv[optind] = %s\n", argv[optind]);
}
}
編譯後,執行命令:output/Kapp -a -a1
,打印結果如下:
ret = a optarg = (null) optind = 2 argv[optind] = -a1
ret = a optarg = (null) optind = 2 argv[optind] = -a1
output/Kapp: invalid option -- '1'
ret = ? optarg = (null) optind = 3 argv[optind] = (null)
命令中“-a”對應的規則爲 "a"
,即單字符。所以程序解析命令選項實際先解析了選項 -a
,再解析選項 -a
,最後解析選項 -1
失敗(-a1
等價於 -a -1
)。因此,getopt 函數解析了 3 次命令參數。
執行命令:output/Kapp -b0 -b -b123 -b 465
,打印結果如下:
ret = b optarg = 0 optind = 2 argv[optind] = -b
ret = b optarg = -b123 optind = 4 argv[optind] = -b
ret = b optarg = 465 optind = 6 argv[optind] = (null)
命令中“-b”對應的規則爲 "b:"
,即單字符接一個冒號。所以程序解析命令選項實際先解析了選項 -b0
得到參數 "0"
,再解析選項 -b
得到參數 "-b123"
,最後解析選項 -b
得到參數 "456"
。因此,使用規則單字符接一個冒號時對應的命令選項必須有參數,否則會把下一個選項誤認爲該規則對應的參數。
執行命令:output/Kapp -c -c100 -c 200
,打印結果如下:
ret = c optarg = (null) optind = 2 argv[optind] = -c100
ret = c optarg = 100 optind = 3 argv[optind] = -c
ret = c optarg = (null) optind = 4 argv[optind] = 200
命令中“-c”對應的規則爲 "c::"
,即單字符接兩個冒號。所以程序解析命令選項實際先解析了選項 -c
,再解析選項 -c100
得到參數 "100"
,最後解析選項 -c
,參數 200
將直接被忽略。
getopt 函數看起來已經能滿足基本的選項解析需求了,但熟悉 Linux 命令的開發者都知道命令選項除了有單破折號“-”的短選項形式外,還有雙破折號“–”的長選項形式,例如“–help”、“–prefix”等。顯然,getopt 函數無法滿足需求,此時可使用 getopt_long 函數。
3. getopt_long 函數
getopt_long 爲 GNU 在 getopt 函數基礎上所擴展的函數,所屬頭文件 <getopt.h>
。
函數原型如下:
int getopt_long(int argc, char* const argv[], const char* optstring, const struct option* longopts, int* longindex);
其中,前 3 個參數與 getopt 函數是一致的。getopt_long 函數包含了 getopt 函數的功能,並由後兩個參數完成解析長選項的功能拓展。
形參 longopts
用於註冊長選項的規則。其所屬結構體類型 struct option
在 <getopt.h>
中聲明如下:
struct option {
const char* name; // 選項名稱
int has_arg; // 指明參數類型
int* flag; // 當 flag 爲 NULL 時,返回 val;不爲空時而 *flag 爲 val,返回 0
int val; // 用於指定函數找到選項時的返回值或 flag 非空時指定 *flag 的值
};
name
爲長選項的名稱,has_arg
爲選擇 name
對應的規則,其可選以下選項:
選項 | 含義 | 示例 |
---|---|---|
no_argument | 等同於短選項的單字符規則,表明長選項不帶參數 | name 爲 "help" 表示選項 --help 無參數 |
required_argument | 等同於短選項的單字符接一個冒號規則,表明長選項必須帶參數 | name 爲 "prefix" 表示選項形式必須爲 --prefix (parameter) 或 --prefix=(parameter) |
optional_argument | 等同於短選項的單字符接兩個冒號規則,表明長選項的參數是可選的 | name 爲 "prefix" 表示選項形式必須爲 --prefix 或 --prefix=(parameter) |
longindex
用於記錄當前找到符合 longopts
的選項是第幾個元素的描述,即 longopts
的下標值。
getopt_long 函數的記錄解析結果與 getopt 函數一致,也是使用 optarg
來進行記錄。
以下實驗將使用 getopt_long 來打印各命令長選項:
#include <getopt.h>
int main(int argc, char* argv[])
{
int ret = -1;
int option_index = 0;
struct option long_options[] =
{
{"noarg", no_argument, NULL, 'n'},
{"reqarg", required_argument, NULL, 'r'},
{"optarg", optional_argument, NULL, 'o'},
{NULL, 0, NULL, 0}
};
while ((ret = getopt_long(argc, argv, "", long_options, &option_index)) != -1)
{
printf("ret = %c\t", ret);
printf("optarg = %s\t", optarg);
printf("optind = %d\t", optind);
printf("argv[optind] = %s\t", argv[optind]);
printf("option_index = %d\n", option_index);
}
}
編譯後,執行命令:output/Kapp --noarg --noarg=1
,打印結果如下:
ret = n optarg = (null) optind = 2 argv[optind] = --noarg=1 option_index = 2
output/Kapp: option '--noarg' doesn't allow an argument
ret = ? optarg = (null) optind = 3 argv[optind] = (null) option_index = 2
命令中的“–noarg”對應規則中的 "noarg"
。所以程序解析命令選項實際先解析了選項 --noarg
,再解析選項 --noarg=1
解析失敗。
執行命令:output/Kapp --reqarg=100 --reqarg --reqarg=200 --reqarg 300
,打印結果如下:
ret = r optarg = 100 optind = 2 argv[optind] = --reqarg option_index = 0
ret = r optarg = --reqarg=200 optind = 4 argv[optind] = --reqarg option_index = 0
ret = r optarg = 300 optind = 6 argv[optind] = (null) option_index = 0
命令中的“–reqarg”對應規則中的 "reqarg"
。所以程序解析命令選項實際先解析了選項 --reqarg=100
得到參數 "100"
,再解析選項 --reqarg
得到參數 "--reqarg=200"
,最後解析選項 --reqarg
得到參數 300
。可以看到,如果命令中“–reqarg”後不接參數時,函數會把下一選項當作選項對應的參數。
執行命令:output/Kapp --optarg --optarg=100 --optarg 200
,打印結果如下:
ret = o optarg = (null) optind = 2 argv[optind] = --optarg=1 option_index = 1
ret = o optarg = 100 optind = 3 argv[optind] = (null) option_index = 1
命令中的“–optarg”對應規則中的 "optarg"
。所以程序解析命令選項實際先解析了選項 --optarg
,再解析選項 --optarg=100
得到參數 "100"
,最後解析選項 --optarg
,參數 200
將直接被忽略。