所有 UNIX 程式甚至那些具有圖形化使用者介面(graphical user interface,GUI)的程式,都能接受和處理命令列選項。對於某些程式,這是與其他程式或使用者進行互動的主要手段。具有可靠的複雜命令列參數處理機制,會使得您的應用程式更好、更有用。不過很多開發人員都將其寶貴的時間花在了編寫自己的命令列解析器,卻不使用 getopt(),而後者是一個專門設計來減輕命令列處理負擔的庫函數。請閱讀本文,以瞭解如何讓 getopt() 在全域結構中記錄命令參數,以便隨後隨時在整個程式中使用。
引言
在早期的 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. 系統標頭檔
#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() 允許在幾乎不進行額外工作的情況下處理更具描述性的長選項,這非常受開發人員的歡迎。
既然已經知道了如何方便地處理命令列選項,現在就可以集中精力改進您的程式的命令列,可以添加長選項支援,或添加之前由於不想向程式添加額外的命令列選項處理而擱置的任何其他選項。
不要忘記在某處記錄您所有的選項和參數,並提供某種類型的內建協助函數來為健忘的使用者提供協助。