使用 getopt() 進行命令列處理

helloxchen發表於2010-11-05
Chris Herborth (), 自由撰稿人, 作家
Chris Herborth 的照片
Chris Herborth 是一位屢獲殊榮的高階技術作者,十餘年來撰寫了許多篇作業系統和程式設計方面的文章。在陪伴兒子 Alex 或妻子 Lynette 之餘,他設計、編寫和研究(也就是玩)影片遊戲。

簡介: 所有 UNIX® 程式甚至那些具有圖形使用者介面(graphical user interface,GUI)的程式,都能接受和處理命令列選項。對於某些程式,這是與其他程式或使用者進行互動的主要手段。具有可靠的複雜命令列引數處理機制,會使得您的應用程式更好、更有用。不過很多開發人員都將其寶貴的時間花在了編寫自己的命令列解析器,卻不使用 getopt(),而後者是一個專門設計來減輕命令列處理負擔的庫函式。請閱讀本文,以瞭解如何讓 getopt() 在全域性結構中記錄命令引數,以便隨後隨時在整個程式中使用。

在早期的 UNIX® 中,其命令列環境(當時的唯一使用者介面)包含著數十種小的文字處理工具。這些工具非常小,通常可很好地完成一項工作。這些工具透過較長的命令管道連結在一起,前面的程式將其輸出傳遞給下一個程式以作為輸入,整個過程由各種命令列選項和引數加以控制。

正是 UNIX 的這方面的特徵使其成為了極為強大的處理基於本文的資料的環境,而這也是其在公司環境中的最初用途之一。在命令管道的一端輸入一些文字,然後在另一端檢索經過處理的輸出。

命令列選項和引數控制 UNIX 程式,告知它們如何動作。作為開發人員,您要負責從傳遞給您程式的 main() 函式的命令列發現使用者的意圖。本文將演示如何使用標準 getopt()getopt_long() 函式來簡化命令列處理工作,並討論了一項用於跟蹤命令列選項的技術。

本文包含的示例程式碼(請參見下載)是使用 C 開發工具(C Development Tooling,CDT)在 Eclipse 3.1 中編寫的;getopt_demogetopt_long_demo 專案是 Managed Make 專案,均使用 CDT 的程式生成規則構建。在專案中沒有包含 Makefile,如果需要在 Eclipse 外編譯程式碼,可以自己方便地生成一個。

如果尚未嘗試過 Eclipse(請參閱參考資料),真的應該嘗試一下——這是一個優秀的整合開發環境(integrated development environment,IDE),其每個新版本都有較大的提升。這是來自“強硬派” EMACS 和 Makefile 開發人員的作品。

在編寫新程式時,首先遇到的障礙之一就是如何處理控制其行為的命令列引數。這包括從命令列傳遞給您程式的 main() 函式的一個整數計數(通常名為 argc)和一個指向字串的指標陣列(通常名為 argv).可以採用兩種實質一樣的方式宣告標註 main() 函式,如清單 1 中所示。



                
int main( int argc, char *argv[] );
int main( int argc, char **argv );

第一種方式使用的是指向 char 指標陣列,現在似乎很流行這種方式,比第二種方式(其指標指向多個指向 char 的指標)略微清楚一些。由於某些原因,我使用第二種方式的時間更多一些,這可能源於我在高中時艱難學習 C 指標的經歷。對於所有的用途和目的,這兩種方法都是一樣的,因此可以使用其中您自己最喜歡的方式。

當 C 執行時庫的程式啟動程式碼呼叫您的 main() 時,已經對命令列進行了處理。argc 引數包含引數的計數值,而 argv 包含指向這些引數的指標陣列。對於 C 執行時庫,arguments 是程式的名稱,程式名後的任何內容都應該使用空格加以分隔。

例如,如果使用引數 -v bar 執行一個名為 foo 程式,您的 argc 將設定為 4,argv 的設定情況將如清單 2 中所示。



                
argv[0] - foo
argv[1] - -v
argv[2] - bar
argv[3] - 

一個程式僅有一組命令列引數,因此我要將此資訊儲存在記錄選項和設定的全域性結構中。對程式有意義的要跟蹤的任何內容都可以記錄到此結構中,我將使用結構來幫助減少全域性變數的數量。正如我在網路服務設計文章(請參閱參考資料)所提到的,全域性變數非常不適合用於執行緒化程式設計中,因此要謹慎使用。

示例程式碼將演示一個假想的 doc2html 程式的命令列處理。該 doc2html 程式將某種型別的文件轉換為 HTML,具體由使用者指定的命令列選項控制。它支援以下選項:

  • -I——不建立關鍵字索引。
  • -l lang——轉換為使用語言程式碼 lang 指定的語言。
  • -o outfile.html——將經過轉換的文件寫入到 outfile.html,而不是列印到標準輸出。
  • -v——進行轉換時提供詳細資訊;可以多次指定,以提高診斷級別。
  • 將使用其他檔名稱來作為輸入文件。

您還將支援 -h-?,以列印幫助訊息來提示各個選項的用途。

getopt() 函式位於 unistd.h 系統標頭檔案中,其原型如清單 3 中所示:



                
int getopt( int argc, char *const argv[], const char *optstring );

給定了命令引數的數量 (argc)、指向這些引數的陣列 (argv) 和選項字串 (optstring) 後,getopt() 將返回第一個選項,並設定一些全域性變數。使用相同的引數再次呼叫該函式時,它將返回下一個選項,並設定相應的全域性變數。如果不再有識別到的選項,將返回 -1,此任務就完成了。

getopt() 所設定的全域性變數包括:

  • optarg——指向當前選項引數(如果有)的指標。
  • optind——再次呼叫 getopt() 時的下一個 argv 指標的索引。
  • optopt——最後一個已知選項。

對於每個選項,選項字串 (optstring) 中都包含一個對應的字元。具有引數的選項(如示例中的 -l-o 選項)後面跟有一個 : 字元。示例所使用的 optstringIl:o:vh?(前面提到,還要支援最後兩個用於列印程式的使用方法訊息的選項)。

可以重複呼叫 getopt(),直到其返回 -1 為止;任何剩下的命令列引數通常視為檔名或程式相應的其他內容。

讓我們對 getopt_demo 專案的程式碼進行一下深入分析;為了方便起見,我在此處將此程式碼拆分為多個部分,但您可以在可下載原始碼部分獲得完整的程式碼(請參見下載)。

清單 4 中,可以看到系統演示程式所使用的系統標頭檔案;標準 stdio.h 提供標準 I/O 函式原型,stdlib.h 提供 EXIT_SUCCESSEXIT_FAILUREunistd.h 提供 getopt()



                
#include 
#include 
#include 

清單 5 顯示了我所建立的 globalArgs 結構,用於以合理的方式儲存命令列選項。由於這是個全域性變數,程式中任何位置的程式碼都可以訪問這些變數,以確定是否建立關鍵字索引、生成何種語言等等事項。最好讓 main() 函式外的程式碼將此結構視為一個常量、只讀儲存區,因為程式的任何部分都可以依賴於其內容。

每個命令列選擇都有一個對應的選項,而其他變數用於儲存輸出檔名、指向輸入檔案列表的指標和輸入檔案數量。



                
struct globalArgs_t {
    int noIndex;                /* -I option */
    char *langCode;             /* -l option */
    const char *outFileName;    /* -o option */
    FILE *outFile;
    int verbosity;              /* -v option */
    char **inputFiles;          /* input files */
    int numInputFiles;          /* # of input files */
} globalArgs;

static const char *optString = "Il:o:vh?";

選項字串 optString 告知 getopt() 可以處理哪個選項以及哪個選項需要引數。如果在處期間遇到了其他選項,getopt() 將顯示一個錯誤訊息,程式將在顯示了使用方法訊息後退出。

下面的清單 6 包含一些從 main() 引用的用法訊息函式和文件轉換函式的小存根。可以對這些存根進行自由更改,以用於更為有用的目的。



                
void display_usage( void )
{
    puts( "doc2html - convert documents to HTML" );
    /* ... */
    exit( EXIT_FAILURE );
}

void convert_document( void )
{
    /* ... */
}

最後,如清單 7 中所示,在 main() 函式中使用此結構。和優秀的開發人員一樣,您需要首先初始化 globalArgs 結構,然後才開始處理命令列引數。在您的程式中,可以藉此設定在一定情況下合理的預設值,以便在以後有更合適的預設值時更方便地對其進行調整。



                
int main( int argc, char *argv[] )
{
    int opt = 0;
    
    /* Initialize globalArgs before we get to work. */
    globalArgs.noIndex = 0;     /* false */
    globalArgs.langCode = NULL;
    globalArgs.outFileName = NULL;
    globalArgs.outFile = NULL;
    globalArgs.verbosity = 0;
    globalArgs.inputFiles = NULL;
    globalArgs.numInputFiles = 0;

清單 8 中的 while 迴圈和 switch 語句是用於本程式的命令列處理的程式碼部分。只要 getopt() 發現選項,switch 語句將確定找到的是哪個選項,將能在 globalArgs 結構中看到具體情況。當 getopt() 最終返回 -1 時,就完成了選項處理過程,剩下的都是您的輸入檔案了。



                
opt = getopt( argc, argv, optString );
    while( opt != -1 ) {
        switch( opt ) {
            case 'I':
                globalArgs.noIndex = 1; /* true */
                break;
                
            case 'l':
                globalArgs.langCode = optarg;
                break;
                
            case 'o':
                globalArgs.outFileName = optarg;
                break;
                
            case 'v':
                globalArgs.verbosity++;
                break;
                
            case 'h':   /* fall-through is intentional */
            case '?':
                display_usage();
                break;
                
            default:
                /* You won't actually get here. */
                break;
        }
        
        opt = getopt( argc, argv, optString );
    }
    
    globalArgs.inputFiles = argv + optind;
    globalArgs.numInputFiles = argc - optind;

既然已經完成了引數和選項的收集工作,接下來就可以執行程式所設計的任何功能(在本例中是進行文件轉換),然後退出(清單 9)。



                
convert_document();
    
    return EXIT_SUCCESS;
}

好,工作完成,非常漂亮。現在就可以不再往下讀了。不過,如果您希望程式符合 90 年代末期的標準並支援 GNU 應用程式中流行的 選項,則請繼續關注下面的內容。

在 20 世紀 90 年代(如果沒有記錯的話),UNIX 應用程式開始支援長選項,即一對短橫線(而不是普通 選項所使用的單個短橫線)、一個描述性選項名稱還可以包含一個使用等號連線到選項的引數。

幸運的是,可以透過使用 getopt_long() 向程式新增長選項支援。您可能已經猜到了,getopt_long() 是同時支援長選項和短選項的 getopt() 版本。

getopt_long() 函式還接受其他引數,其中一個是指向 struct option 物件陣列的指標。此結構相當直接,如清單 10 中所示。



                
struct option {
    char *name;
    int has_arg;
    int *flag;
    int val;
};

name 成員是指向長選項名稱(帶兩個短橫線)的指標。has_arg 成員設定為 no_argumentoptional_argument, 或 required_argument(均在 getopt.h 中定義)之一,以指示選項是否具有引數。如果 flag 成員未設定為 NULL,在處理期間遇到此選項時,會使用 val 成員的值填充它所指向的 int 值。如果 flag 成員為 NULL,在 getopt_long() 遇到此選項時,將返回 val 中的值;透過將 val 設定為選項的 short 引數,可以在不新增任何其他程式碼的情況下使用 getopt_long()——處理 while loopswitch 的現有 getopt() 將自動處理此選項。

這已經變得更為靈活了,因為各個選項現在可以具有可選引數了。更重要的是,僅需要進行很少的工作,就可以方便地放入現有程式碼中。

讓我們看看如何使用 getopt_long() 來對示例程式進行更改(getopt_long_demo 專案可從下載部分獲得)。

由於 getopt_long_demo 幾乎與剛剛討論的 getopt_demo 程式碼一樣,因此我將僅對更改的程式碼進行說明。由於現在已經有了更大的靈活性,因此還將新增對 --randomize 選項(沒有對應的短選項)的支援。

getopt_long() 函式在 getopt.h 標頭檔案(而非 unistd.h)中,因此將需要將該標頭檔案包含進來(請參見清單 11)。我還包含了 string.h,因為將稍後使用 strcmp() 來幫助確定處理的是哪個長引數。



                
#include 
#include 

您已經為 --randomize 選項在 globalArgs 中新增了一個標誌(請參見清單 12),並建立了 longOpts 陣列來儲存關於此程式支援的長選項的資訊。除了 --randomize 外,所有的引數都與現有短選項對應(例如,--no-index 等同於 -I)。透過在選項結構中包含其短選項等效項,可以在不向程式新增任何其他程式碼的情況下處理等效的長選項。



                
struct globalArgs_t {
    int noIndex;                /* -I option */
    char *langCode;             /* -l option */
    const char *outFileName;    /* -o option */
    FILE *outFile;
    int verbosity;              /* -v option */
    char **inputFiles;          /* input files */
    int numInputFiles;          /* # of input files */
    int randomized;             /* --randomize option */
} globalArgs;

static const char *optString = "Il:o:vh?";

static const struct option longOpts[] = {
    { "no-index", no_argument, NULL, 'I' },
    { "language", required_argument, NULL, 'l' },
    { "output", required_argument, NULL, 'o' },
    { "verbose", no_argument, NULL, 'v' },
    { "randomize", no_argument, NULL, 0 },
    { "help", no_argument, NULL, 'h' },
    { NULL, no_argument, NULL, 0 }
};

清單 13getop() 呼叫更改為了 getopt_long(),除了 getopt() 的引數外,它還接受 longOpts 陣列和 int 指標 (longIndex)。當 getopt_long() 返回 0 時,longIndex 所指向的整數將設定為當前找到的長選項的索引。



                
opt = getopt_long( argc, argv, optString, longOpts, &longIndex );
    while( opt != -1 ) {
        switch( opt ) {
            case 'I':
                globalArgs.noIndex = 1; /* true */
                break;
                
            case 'l':
                globalArgs.langCode = optarg;
                break;
                
            case 'o':
                globalArgs.outFileName = optarg;
                break;
                
            case 'v':
                globalArgs.verbosity++;
                break;
                
            case 'h':   /* fall-through is intentional */
            case '?':
                display_usage();
                break;

            case 0:     /* long option without a short arg */
                if( strcmp( "randomize", longOpts[longIndex].name ) == 0 ) {
                    globalArgs.randomized = 1;
                }
                break;
                
            default:
                /* You won't actually get here. */
                break;
        }
        
        opt = getopt_long( argc, argv, optString, longOpts, amp;longIndex );
    }

我還新增了 0 的 case,以便處理任何不與現有短選項匹配的長選項。在此例中,只有一個長選項,但程式碼仍然使用 strcmp() 來確保它是預期的那個選項。

這樣就全部搞定了;程式現在支援更為詳細(對臨時使用者更加友好)的長選項。

UNIX 使用者始終依賴於命令列引數來修改程式的行為,特別是那些設計作為小工具集合 (UNIX 外殼環境)的一部分使用的實用工具更是如此。程式需要能夠快速處理各個選項和引數,且要求不會浪費開發人員的太多時間。畢竟,幾乎沒有程式設計為僅處理命令列引數,開發人員更應該將精力放在程式所實際進行的工作上。

getopt() 函式是一個標準庫呼叫,可允許您使用直接的 while/switch 語句方便地逐個處理命令列引數和檢測選項(帶或不帶附加的引數)。與其類似的 getopt_long() 允許在幾乎不進行額外工作的情況下處理更具描述性的長選項,這非常受開發人員的歡迎。

既然已經知道了如何方便地處理命令列選項,現在就可以集中精力改進您的程式的命令列,可以新增長選項支援,或新增之前由於不想向程式新增額外的命令列選項處理而擱置的任何其他選項。

不要忘記在某處記錄您所有的選項和引數,並提供某種型別的內建幫助函式來為健忘的使用者提供幫助。

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/24790158/viewspace-1040986/,如需轉載,請註明出處,否則將追究法律責任。

相關文章