歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Unix知識 >> Unix基礎知識

使用getopt() 、getopt_long()、getopt_long_only()進行命令行處理

引言

在早期的 UNIX 中,其命令行環境(當時的唯一用戶界面)包含著數十種小的文本處理工具。這些工具非常小,通常可很好地完成一項工作。這些工具通過較長的命令管道鏈接在一起,前面的程序將其輸出傳遞給下一個程序以作為輸入,整個過程由各種命令行選項和參數加以控制。

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

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

開始之前

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

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

命令行

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

清單 1. 聲明 main() 函數的兩種方式

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

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

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

當 C 運行時庫的程序啟動代碼調用您的 main() 時,已經對命令行進行了處理。argc 參數包含參數的計數值,而 argv 包含指向這些參數的指針數組。對於 C 運行時庫,arguments 是程序的名稱,程序名後的任何內容都應該使用空格加以分隔。

例如,如果使用參數 -v bar www.ibm.com 運行一個名為 foo 程序,您的 argc 將設置為 4,argv 的設置情況將如清單 2 中所示。

清單 2. argv 的內容

argv[0] - foo

argv[1] - -v

argv[2] - bar

argv[3] - www.ibm.com

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

示例代碼將演示一個假想的 doc2html 程序的命令行處理。該 doc2html 程序將某種類型的文檔轉換為 HTML,具體由用戶指定的命令行選項控制。它支持以下選項:

-I——不創建關鍵字索引。

-l lang——轉換為使用語言代碼 lang 指定的語言。

-o outfile.html——將經過轉換的文檔寫入到 outfile.html,而不是打印到標准輸出。

-v——進行轉換時提供詳細信息;可以多次指定,以提高診斷級別。

將使用其他文件名稱來作為輸入文檔。

您還將支持 -h 和 -?,以打印幫助消息來提示各個選項的用途。

簡單命令行處理: getopt()

getopt() 函數位於 unistd.h 系統頭文件中,其原型如清單 3 中所示:

清單 3. getopt() 原型

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

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

getopt() 所設置的全局變量包括:

optarg——指向當前選項參數(如果有)的指針。

optind——再次調用 getopt() 時的下一個 argv 指針的索引。

optopt——最後一個已知選項。

對於每個選項,選項字符串 (optstring) 中都包含一個對應的字符。具有參數的選項(如示例中的 -l 和 -o 選項)後面跟有一個 : 字符。示例所使用的 optstring 為 Il:o:vh?(前面提到,還要支持最後兩個用於打印程序的使用方法消息的選項)。

可以重復調用 getopt(),直到其返回 -1 為止;任何剩下的命令行參數通常視為文件名或程序相應的其他內容。

getopt() 的使用

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

在清單 4 中,可以看到系統演示程序所使用的系統頭文件;標准 stdio.h 提供標准 I/O 函數原型,stdlib.h 提供 EXIT_SUCCESS和EXIT_FAILURE,unistd.h 提供 getopt()。

清單 4. 系統頭文件

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

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

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

清單 5. 全局參數存儲和選項字符串

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() 引用的用法消息函數和文檔轉換函數的小存根。可以對這些存根進行自由更改,以用於更為有用的目的。

清單 6. 存根

void display_usage( void )

{

puts( "doc2html - convert documents to HTML" );

/* ... */

exit( EXIT_FAILURE );

}

void convert_document( void )

{

/* ... */

}

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

清單 7. 初始化

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 時,就完成了選項處理過程,剩下的都是您的輸入文件了。

清單 8. 使用 getopt() 處理 argc/argv

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)。

清單 9. 開始工作

convert_document();

 

return EXIT_SUCCESS;

}

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

復雜命令行處理: getopt_long()

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

幸運的是,可以通過使用 getopt_long() 向程序添加長選項支持。您可能已經猜到了,getopt_long() 是同時支持長選項和短選項的getopt() 版本。

getopt_long() 函數還接受其他參數,其中一個是指向 struct option 對象數組的指針。此結構相當直接,如清單 10 中所示。

清單 10. getopt_long() 的選項

struct option {

char *name;

int has_arg;

int *flag;

int val;

};

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

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

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

使用 getopt_long()

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

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

清單 11. 其他頭文件

#include <getopt.h>

#include <string.h>

您已經為 --randomize 選項在 globalArgs 中添加了一個標志(請參見清單 12),並創建了 longOpts 數組來存儲關於此程序支持的長選項的信息。除了 --randomize 外,所有的參數都與現有短選項對應(例如,--no-index 等同於 -I)。通過在選項結構中包含其短選項等效項,可以在不向程序添加任何其他代碼的情況下處理等效的長選項。

清單 12. 擴展後的參數

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 }

};

清單 13 將 getop() 調用更改為了 getopt_long(),除了 getopt() 的參數外,它還接受 longOpts 數組和 int 指針 (longIndex)。當getopt_long() 返回 0 時,longIndex 所指向的整數將設置為當前找到的長選項的索引。

清單 13. 新的經改進的選項處理

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() 允許在幾乎不進行額外工作的情況下處理更具描述性的長選項,這非常受開發人員的歡迎。

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

不要忘記在某處記錄您所有的選項和參數,並提供某種類型的內置幫助函數來為健忘的用戶提供幫助。

下載

au-getopt_demo.zip

au-getopt_long_demo.zip

Copyright © Linux教程網 All Rights Reserved