一個靈活的程序應該是可配置的

這段時間在公司充當救火員的角色,拯救了一個快要腐爛的項目。其中做的一個工作就是將其變成可配置的,這樣可以增加程序的靈活性,如果想改變程序的行爲,只需修改參數即可,而不是重新編譯。

在這裏插入圖片描述

首先我們需要把程序中可變的部分抽離出來,程序本身只處理業務邏輯,實現配置參數與功能代碼的解耦合。在 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;
}

好啦,程序非常簡單,編譯運行看看效果吧!

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章