引言
在早期的 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] - fooargv[1] - -vargv[2] - barargv[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. 系統頭文件
#in
clude <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; &nbs
p; 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': &n
bsp; 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() 允許在幾乎不進行額外工作的情況下處理更具描述性的長選項,這非常受開發人員的歡迎