這段時間在公司充當救火員的角色,拯救了一個快要腐爛的項目。其中做的一個工作就是將其變成可配置的,這樣可以增加程序的靈活性,如果想改變程序的行爲,只需修改參數即可,而不是重新編譯。
首先我們需要把程序中可變的部分抽離出來,程序本身只處理業務邏輯,實現配置參數與功能代碼的解耦合。在 Linux 環境編程中,通常有兩種做法:
- 通過配置文件與程序進行交互
- 通過命令行選項參數進行交互
配置文件的格式可以是常見的 ini、xml、json,也可以是自定義的文件格式,對於配置項較多的程序,這種方式會更方便、更直觀。而命令行選項參數在 Linux 更加常見,幾乎所有 Linux 命令行工具都支持。本文就給大家講解,如何讓一個命令行程序支持選項參數。
命令行參數的管理
Linux 應用程序是從 main 函數開始執行的,如果需要帶選項參數,通常會這麼定義 main 函數:
int main(int argc, char *argv[])
{
/* do something */
}
- 第1個參數 argc 表示參數的數目,包含命令本身,也就是說如果命令後面不帶參數的話,argc 就等於 1。
- 第2個參數 argv 是字符指針數組,其成員依次指向各個參數,
argv[0]
指向命令本身,argv[1]
指向後面帶的第1個參數,指針數組最後一個成員爲 NULL,表示參數結束。
比如 ls -w 80
命令,其進程啓動之後拿到的 argc 和 argv 參數內容如下:
這裏要區分一下命令、選項、參數的概念。它們以空格隔開,第一個就是命令(如果使用管道,一個命令行中可以包含多個命令);選項和參數通常都是可選的,選項分爲短選項和長選項,比如這裏的 -w
是短選項,它對應的長選項是 --width
;參數 80 是對前面的選項 -w
的描述,並非所有選項都有參數,具體由程序本身決定。參數可以緊跟選項,也可以用空格隔開,對於長選項參數還可以使用等號,所以下面幾種寫法是等效的:
ls -w 80
ls -w80
ls --width 80
ls --width=80
命令行參數的識別
理解了上面這點,顯然我們要解析命令行的選項參數,只需要根據 argc 的值對 argv 進行拆解即可。但這樣會增加程序員的工作量,並且命令行的選項參數通常是隨意的,不會刻意讓某個參數處於第1或者第2的位置,因此,使用 Linux 爲我們提供的 getopt()
和 getopt_long()
函數進行命令行參數的識別是更好的選擇。
短選項參數
getopt()
函數用於解析命令行參數,其函數原型如下:
int getopt(int argc, char * const argv[], const char *optstring);
參數 | 描述 |
---|---|
argc | 命令參數的個數 |
argv | 指向這些參數的數組 |
optstring | 所有可能的參數字符串 |
返回值 | |
選項字符 | 返回識別成功的選項字符 |
‘?’ | 遇到無效的選項字符或缺少參數時 |
-1 | 當沒有其他參數供解析或者出錯時 |
第3個參數 optstring 可以是下列元素:
- 單個字符:表示該選項不帶參數;
- 單個字符後接一個冒號:表示該選項後必須跟一個參數,參數可以緊跟在選項後面,也可以以空格隔開;
- 單個字符後接兩個冒號:表示該選項後可以跟一個參數,參數必須緊跟在選項後面,不能以空格隔開。
PS:兩個冒號的用法比較奇特,而且跟長選項參數配合得不好,建議謹慎使用!
爲了完成參數識別,Linux 預設了幾個全局變量, getopt()
執行後的內容會暫存於此。
extern char *optarg;
extern int optind, opterr, optopt;
- optarg:指向當前選項參數(如果有的話)的指針
- optind:再次調用
getopt()
時的下一個 argv 指針的索引 - opterr:存儲錯誤代碼
- optopt:存儲未知或出錯(比如缺少參數)的選項
默認情況下,getopt 函數會重新排列命令行參數的順序,所有不可知或錯誤的命令行參數都排列到最後,當沒有其他參數供解析或者出錯時,getopt 將返回 -1,同時 optind 中將存放第一個未知或出錯選項的下標。
長選項參數
短選項參數言簡意賅,對於選項較少的程序是極好的。然而,畢竟只有26個英文字母,選項過多就顯得不夠用了,而且單個字符很容易重複,造成表意不明。因此,一套善解人意的選項參數,往往需要同時提供短選項和長選項,兼顧實用性和明義性。
Linux 爲我們提供了 getopt_long()
和 getopt_long_only()
兩個函數,其函數原型如下:
int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
int getopt_long_only(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
參數 | 描述 |
---|---|
argc | 命令參數的個數 |
argv | 指向這些參數的數組 |
optstring | 短選項參數字符串 |
longopts | 長選項參數標識 |
longindex | 記錄長選項的索引 |
返回值 | |
選項字符 | 返回識別成功的選項字符 |
‘?’ | 遇到無效的選項字符或缺少參數時 |
-1 | 當沒有其他參數供解析或者出錯時 |
可以看到,前3個參數與 getopt()
是一樣的。第4個參數 longopts 是我們要構建的長選項結構體數組,它包含選項的標識及其對應關係。第5個參數可用於記錄長選項的索引,如果沒有特別的選項參數結構,設置爲 NULL 即可。
option 結構體的定義如下:
struct option
{
const char *name; /* 長選項名 */
int has_arg; /* 0(no_argument)表示該參數後面不跟參數值 */
/* 1(required_argument)表示該參數後面一定要跟個參數值 */
/* 2(optional_argument)表示該參數後面可以跟,也可以不跟參數值 */
int *flag; /* 用於決定getopt_long()的返回值 */
/* 如果flag是NULL(通常情況),則返回對應的val值(下一個成員) */
/* 如果flag不是NULL,則將val值賦給flag所指向的內存,並返回0 */
int val; /* 和flag一起決定返回值 */
};
getopt_long_only()
的用法和 getopt_long()
相同,唯一的區別在輸入長選項的時候可以不用輸入--
而使用 -
。
使用示例
紙上得來終覺淺,絕知此事要躬行。下面通過一個簡單的示例,來看看命令行選項參數應該怎麼實現。
我們設計一個場景,該示例程序用於輸出某人說的某句話,默認輸出“Hello, World!”。一共有四個選項,如下:
短選項 | 長選項 | 是否帶參數 | 說明 |
---|---|---|---|
-h |
--help |
否 | 查看幫助 |
-v |
--version |
否 | 查看版本 |
-w |
--who |
是 | 設置名字 |
-s |
--say |
是 | 設置內容 |
首先引入必要的頭文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
許多命令行程序都支持查看幫助和版本信息,所以我們先實現好這兩個函數:
#define VER_MAJOR 0
#define VER_MINOR 1
#define VER_PATCH 1
static void show_usage(const char *cmd)
{
printf("Usage: %s [options] ... \n", cmd);
printf("This is a demo for how to use options\n\n");
printf(" -h, --help display this help and exit\n");
printf(" -v, --version output version information and exit\n");
printf(" -w, --who=NAME tell me what is your NAME\n");
printf(" -s, --say=CONTENT what CONTENT do you want to say\n\n");
exit(0);
}
static void show_version(void)
{
printf("version %d.%d.%d\n", VER_MAJOR, VER_MINOR, VER_PATCH);
exit(0);
}
接下來就是要構建 option 結構體,並調用 getopt_long()
進行選項參數的識別:
int main(int argc, char *argv[])
{
int option;
char *name = NULL;
char *content = "Hello, World!";
const char * const short_options = "hvw:s:";
const struct option long_options[] = {
{ "help", 0, NULL, 'h' },
{ "version", 0, NULL, 'v' },
{ "who", 1, NULL, 'w' },
{ "say", 1, NULL, 's' },
{ NULL, 0, NULL, 0 }
};
while ((option = getopt_long(argc, argv, short_options, long_options, NULL)) != -1)
{
switch (option)
{
case 'h':
show_usage(argv[0]);
break;
case 'v':
show_version();
break;
case 'w':
name = strdup(optarg);
break;
case 's':
content = strdup(optarg);
break;
case '?':
default :
printf("Error: option invalid\n");
exit(EXIT_FAILURE);
break;
}
}
if (name)
printf("%s: ", name);
printf("%s\n", content);
return 0;
}
好啦,程序非常簡單,編譯運行看看效果吧!