Linux下很多程序甚至那些具有圖形用戶界面(graphical user interface,GUI)的程序,都能接受和處理命令行選項。對于某些程序,這是與用戶進(jìn)行交互的主要手段。具有可靠的復(fù)雜命令行參數(shù)處理機制,會使得您的應(yīng)用程序更好、更有用。getopt() 是一個專門設(shè)計來減輕命令行處理負(fù)擔(dān)的庫函數(shù)。
1、命令行參數(shù)
命令行程序設(shè)計的首要任務(wù)是解析命令行參數(shù),GUI派的程序員很少關(guān)心這個。這里,對參數(shù)采用了一種比較通俗的定義:命令行上除命令名之外的字符串。參數(shù)由多項構(gòu)成,項與項之間用空白符彼此隔開。
參數(shù)進(jìn)一步分為選項 和操作數(shù) 。選項用于修改程序的默認(rèn)行為或為程序提供信息,比較老的約定是以短劃線開頭。選項后可以跟隨一些參數(shù),稱為選項參數(shù)。剩下的就是操作數(shù)了。
2、POSIX約定
POSIX表示可移植操作系統(tǒng)接口: Portable Operating System Interface,電氣和電子工程師協(xié)會(Institute of Electrical and Electronics Engineers,IEEE)最初開發(fā) POSIX 標(biāo)準(zhǔn),是為了提高 UNIX 環(huán)境下應(yīng)用程序的可移植性。然而,POSIX 并不局限于 UNIX。許多其它的操作系統(tǒng),例如 DEC OpenVMS 和 Microsoft Windows NT,都支持 POSIX 標(biāo)準(zhǔn)。
下面是POSIX標(biāo)準(zhǔn)中關(guān)于程序名、參數(shù)的約定:
程序名不宜少于2個字符且不多于9個字符;
程序名應(yīng)只包含小寫字母和阿拉伯?dāng)?shù)字;
選項名應(yīng)該是單字符活單數(shù)字,且以短橫‘-‘為前綴;
多個不需要選項參數(shù)的選項,可以合并。(譬如:foo -a -b -c ---->foo -abc)
選項與其參數(shù)之間用空白符隔開;
選項參數(shù)不可選。
若選項參數(shù)有多值,要將其并為一個字串傳進(jìn)來。譬如:myprog -u "arnold,joe,jane"。這種情況下,需要自己解決這些參數(shù)的分離問題。
選項應(yīng)該在操作數(shù)出現(xiàn)之前出現(xiàn)。
特殊參數(shù)‘--'指明所有參數(shù)都結(jié)束了,其后任何參數(shù)都認(rèn)為是操作數(shù)。
選項如何排列沒有什么關(guān)系,但對互相排斥的選項,如果一個選項的操作結(jié)果覆蓋其他選項的操作結(jié)果時,最后一個選項起作用;如果選項重復(fù),則順序處理。
允許操作數(shù)的順序影響程序行為,但需要作文檔說明。
讀寫指定文件的程序應(yīng)該將單個參數(shù)'-'作為有意義的標(biāo)準(zhǔn)輸入或輸出來對待。
3、GNU長選項
GNU鼓勵程序員使用--help、--verbose等形式的長選項。這些選項不僅不與POSIX約定沖突,而且容易記憶,另外也提供了在所有GNU工具之間保持一致性的機會。GNU長選項有自己的約定:
對于已經(jīng)遵循POSIX約定的GNU程序,每個短選項都有一個對應(yīng)的長選項。
額外針對GNU的長選項不需要對應(yīng)的短選項,僅僅推薦要有。
長選項可以縮寫成保持惟一性的最短的字串。
選項參數(shù)與長選項之間或通過空白字符活通過一個'='來分隔。
選項參數(shù)是可選的(只對短選項有效)。
長選項允許以一個短橫線為前綴。
4、基本的命令行處理技術(shù)
C程序通過argc和argv參數(shù)訪問它的命令行參數(shù)。argc是整型數(shù),表示參數(shù)的個數(shù)(包括命令名)。main()函數(shù)的定義方式有兩種,區(qū)別僅在于argv如何定義:
int main(int argc, char *argv[])
{
……
} int main(int argc, char **argv)
{
……
}
當(dāng) C 運行時庫的程序啟動代碼調(diào)用 main() 時,已經(jīng)對命令行進(jìn)行了處理。argc 參數(shù)包含參數(shù)的計數(shù)值,而 argv 包含指向這些參數(shù)的指針數(shù)組。argv[0]是程序名。
一個很簡單的命令行處理技術(shù)的例子是echo程序,它可以將參數(shù)輸出到標(biāo)準(zhǔn)設(shè)備上,用空格符隔開,最后換行。若命令行第一個參數(shù)為-n,那么就不會換行。
清單1:
#include <stdio.h>
int main(int argc, char **argv)
{
int i, nflg;
nflg = 0;
if(argc > 1 && argv[1][0] == '-' && argv[1][1] == 'n'){
nflg++;
argc--;
argv++;
}
for(i=1; i<argc; i++){
fputs(argv[i], stdout);
if(i < argc-1)
putchar(' ');
}
if(nflg == 0)
putchar('\n');
return 0;
}
echo程序中,對于命令行參數(shù)的解析是手動實現(xiàn)的。很久以前,Unix支持小組為了簡化對于命令行參數(shù)的解析,開發(fā)了getopt()函數(shù) ,同時提供了幾個外部變量,使得編寫遵守POSIX的代碼變得更加容易了。
5、命令行參數(shù)解析函數(shù) —— getopt()
getopt()函數(shù)聲明如下:
#include <unistd.h>
int getopt(int argc, char * const argv[], const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;
該函數(shù)的argc和argv參數(shù)通常直接從main()的參數(shù)直接傳遞而來。optstring是選項字母組成的字串。如果該字串里的任一字符后面有冒號,那么這個選項就要求有選項參數(shù)。
當(dāng)給定getopt()命令參數(shù)的數(shù)量 (argc )、指向這些參數(shù)的數(shù)組 (argv ) 和選項字串 (optstring ) 后,getopt() 將返回第一個選項,并設(shè)置一些全局變量。使用相同的參數(shù)再次調(diào)用該函數(shù)時,它將返回下一個選項,并設(shè)置相應(yīng)的全局變量。如果不再有可識別的選項,將返回 -1 ,此任務(wù)就完成了。
getopt() 所設(shè)置的全局變量包括:
char *optarg ——當(dāng)前選項參數(shù)字串(如果有)。
int optind ——argv的當(dāng)前索引值。當(dāng)getopt()在while循環(huán)中使用時,循環(huán)結(jié)束后,剩下的字串視為操作數(shù),在argv[optind]至argv[argc-1]中可以找到。
int opterr——這個變量非零時,getopt()函數(shù)為“無效選項”和“缺少參數(shù)選項,并輸出其錯誤信息。
int optopt ——當(dāng)發(fā)現(xiàn)無效選項字符之時,getopt()函數(shù)或返回'?'字符,或返回':'字符,并且optopt包含了所發(fā)現(xiàn)的無效選項字符。
以下面的程序為例:
選項:
-n —— 顯示“我的名字”。
-g —— 顯示“我女朋友的名字”。
-l —— 帶參數(shù)的選項.
清單2:
#include <stdio.h>
#include <unistd.h>
int main (int argc, char **argv)
{
int oc; /*選項字符 */
char *b_opt_arg; /*選項參數(shù)字串 */
while((oc = getopt(argc, argv, "ngl:")) != -1)
{
switch(oc)
{
case 'n':
printf("My name is Lyong.\n");
break;
case 'g':
printf("Her name is Xxiong.\n");
break;
case 'l':
b_opt_arg = optarg;
printf("Our love is %s\n", optarg);
break;
}
}
return 0;
}
運行結(jié)果:
$ ./opt_parse_demo -n
My name is Lyong.
$ ./opt_parse_demo -g
Her name is Xxiong.
$ ./opt_parse_demo -l forever
Our love is forever
$ ./opt_parse_demo -ngl forever
My name is Lyong.
Her name is Xxiong.
Our love is forever
6、改變getopt()對錯誤命令行參數(shù)信息的輸出行為
不正確的調(diào)用程序在所難免,這種錯誤要么是命令行選項無效,要么是缺少選項參數(shù)。正常情況下,getopt()會為這兩種情況輸出自己的出錯信息,并且返回'?'。為了驗證此事,可以修改一下上面的清單2中的代碼。
清單3:
#include <stdio.h>
#include <unistd.h>
int main (int argc, char **argv)
{
int oc; /*選項字符 */
char *b_opt_arg; /*選項參數(shù)字串 */
while((oc = getopt(argc, argv, "ngl:")) != -1)
{
switch(oc)
{
case 'n':
printf("My name is Lyong.\n");
break;
case 'g':
printf("Her name is Xxiong.\n");
break;
case 'l':
b_opt_arg = optarg;
printf("Our love is %s\n", optarg);
break;
case '?':
printf("arguments error!\n");
break;
}
}
return 0;
}
輸入一個錯誤的命令行,結(jié)果如下:
$ ./opt_parse_demo -l
./opt_parse_demo: option requires an argument -- l
arguments error!
如果不希望輸出任何錯誤信息,或更希望輸出自定義的錯誤信息??梢圆捎靡韵聝煞N方法來更改getopt()函數(shù)的出錯信息輸出行為:
在調(diào)用getopt()之前,將opterr設(shè)置為0,這樣就可以在getopt()函數(shù)發(fā)現(xiàn)錯誤的時候強制它不輸出任何消息。
如果optstring參數(shù)的第一個字符是冒號,那么getopt()函數(shù)就會保持沉默,并根據(jù)錯誤情況返回不同字符,如下:
“無效選項” —— getopt()返回'?',并且optopt包含了無效選項字符(這是正常的行為)。
“缺少選項參數(shù)” —— getopt()返回':',如果optstring的第一個字符不是冒號,那么getopt()返回'?',這會使得這種情況不能與無效選項的情況區(qū)分開。
清單4:
#include <stdio.h>
#include <unistd.h>
int main (int argc, char **argv)
{
int oc; /*選項字符 */
char ec; /*無效的選項字符*/
char *b_opt_arg; /*選項參數(shù)字串 */
while((oc = getopt(argc, argv, ":ngl:")) != -1)
{
switch(oc)
{
case 'n':
printf("My name is Lyong.\n");
break;
case 'g':
printf("Her name is Xxiong.\n");
break;
case 'l':
b_opt_arg = optarg;
printf("Our love is %s\n", optarg);
break;
case '?':
ec = (char)optopt;
printf("無效的選項字符 \' %c \'!\n", ec);
break;
case ':':
printf("缺少選項參數(shù)!\n");
break;
}
}
return 0;
}
測試結(jié)果:
$ ./opt_parse_demo -a
無效的選項字符 ' a '!
$ ./opt_parse_demo -l
缺少選項參數(shù)!
7、GNU提供的getopt()函數(shù)的特點
上面所設(shè)計的getopt()函數(shù)是UNIX支持小組提供的,其執(zhí)行時一碰到不以'-'開始的命令行參數(shù)就停止尋找選項。而GNU提供的getopt()函數(shù)與之不同,它會掃描整個命令行來尋找選項。當(dāng)調(diào)用GNU getopt()函數(shù)并處理命令行參數(shù)的時候,它重新排列argv中的元素,這樣當(dāng)重排結(jié)束時,所有選項都被移動到前面并且那些繼續(xù)檢查argv [optind]至argv[argc-1]中剩余參數(shù)的代碼仍正常工作,但在任何情況下,碰到特殊參數(shù)'--'就結(jié)束對選項的掃描。
可以輸入一個亂序的命令行,查看opt_parse_demo的輸出:
$ ./opt_parse_demo -l forever a b c d -g -n
Our love is forever
Her name is Xxiong.
My name is Lyong.
GNU getopt()第二個特點是可以在optstring中使用特殊的首字符改變getopt()的默認(rèn)行為:
optstring[0] = '+',這樣就與UNIX支持小組提供的getopt()很相近了。
optstring[0] = '-',會在optarg中得到命令行中的每個參數(shù)。
以上兩種情況下,':'可以作為第二個字符使用。
GNU getopt()第三個特點是optstring中的選項字符后面接兩個冒號,就允許該選項有可選的選項參數(shù)。在選項參數(shù)不存在的情況下,GNU getopt()返回選項字符并將optarg設(shè)置為NULL。
8、GNU長選項命令行解析
20 世紀(jì) 90 年代,UNIX 應(yīng)用程序開始支持長選項,即一對短橫線、一個描述性選項名稱,還可以包含一個使用等號連接到選項的參數(shù)。
GNU提供了getopt-long()和getopt-long-only()函數(shù)支持長選項的命令行解析,其中,后者的長選項字串是以一個短橫線開始的,而非一對短橫線。
getopt_long() 是同時支持長選項和短選項的 getopt() 版本。下面是它們的聲明:
#include <getopt.h>
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);
getopt_long()的前三個參數(shù)與上面的getopt()相同,第4個參數(shù)是指向option結(jié)構(gòu)的數(shù)組,option結(jié)構(gòu)被稱為“長選項表”。longindex參數(shù)如果沒有設(shè)置為 NULL,那么它就指向一個變量,這個變量會被賦值為尋找到的長選項在longopts中的索引值,這可以用于錯誤診斷。
option結(jié)構(gòu)在getopt.h中的聲明如下:
struct option{
const char *name;
int has_arg;
int *flag;
int val;
};
對結(jié)構(gòu)中的各元素解釋如下:
const char *name
這是選項名,前面沒有短橫線。譬如"help"、"verbose"之類。
int has_arg
描述了選項是否有選項參數(shù)。如果有,是哪種類型的參數(shù),此時,它的值一定是下表中的一個。
符號常量 數(shù)值 含義
no_argument 0 選項沒有參數(shù)
required_argument 1 選項需要參數(shù)
optional_argument 2 選項參數(shù)可選
int *flag
如果這個指針為NULL,那么 getopt_long()返回該結(jié)構(gòu)val字段中的數(shù)值。如果該指針不為NULL,getopt_long()會使得它所指向的變量中填入val字段中的數(shù)值,并且getopt_long()返回0。如果flag不是NULL,但未發(fā)現(xiàn)長選項,那么它所指向的變量的數(shù)值不變。
int val
這個值是發(fā)現(xiàn)了長選項時的返回值,或者flag不是NULL時載入*flag中的值。典型情況下,若flag不是NULL,那么val是個真/假值,譬如1或0;另一方面,如果flag是NULL,那么 val通常是字符常量,若長選項與短選項一致,那么該字符常量應(yīng)該與optstring中出現(xiàn)的這個選項的參數(shù)相同。
每個長選項在長選項表中都有一個單獨條目,該條目里需要填入正確的數(shù)值。數(shù)組中最后的元素的值應(yīng)該全是0。數(shù)組不需要排序,getopt_long()會進(jìn)行線性搜索。但是,根據(jù)長名字來排序會使程序員讀起來更容易。
以上所說的flag和val的用法看上去有點混亂,但它們很有實用價值,因此有必要搞透徹了。
大部分時候,程序員會根據(jù)getopt_long()發(fā)現(xiàn)的選項,在選項處理過程中要設(shè)置一些標(biāo)記變量,譬如在使用getopt()時,經(jīng)常做出如下的程序格式:
int do_name, do_gf_name, do_love; /*標(biāo)記變量*/
char *b_opt_arg;
while((c = getopt(argc, argv, ":ngl:")) != -1)
{
switch (c){
case 'n':
do_name = 1;
case 'g':
do_gf_name = 1;
break;
break;
case 'l':
b_opt_arg = optarg;
……
}
}
當(dāng)flag不為NULL時,getopt_long*()會為你設(shè)置標(biāo)記變量。也就是說上面的代碼中,關(guān)于選項'n'、'l'的處理,只是設(shè)置一些標(biāo)記,如果flag不為NULL,時,getopt_long()可以自動為各選項所對應(yīng)的標(biāo)記變量設(shè)置標(biāo)記,這樣就能夠?qū)⑸厦娴膕witch語句中的兩種種情況減少到了一種。下面給出一個長選項表以及相應(yīng)處理代碼的例子。
清單5:
#include <stdio.h>
#include <getopt.h>
int do_name, do_gf_name;
char *l_opt_arg;
struct option longopts[] = {
{ "name", no_argument, &do_name, 1 },
{ "gf_name", no_argument, &do_gf_name, 1 },
{ "love", required_argument, NULL, 'l' },
{ 0, 0, 0, 0},
};
int main(int argc, char *argv[])
{
int c;
while((c = getopt_long(argc, argv, ":l:", longopts, NULL)) != -1){
switch (c){
case 'l':
l_opt_arg = optarg;
printf("Our love is %s!\n", l_opt_arg);
break;
case 0:
printf("getopt_long()設(shè)置變量 : do_name = %d\n", do_name);
printf("getopt_long()設(shè)置變量 : do_gf_name = %d\n", do_gf_name);
break;
}
}
return 0;
}
在進(jìn)行測試之前,再來回顧一下有關(guān)option結(jié)構(gòu)中的指針flag的說明吧。
如果這個指針為NULL,那么 getopt_long()返回該結(jié)構(gòu)val字段中的數(shù)值。如果該指針不為NULL,getopt_long()會使得它所指向的變量中填入val字段中的數(shù)值,并且getopt_long()返回0。如果flag不是NULL,但未發(fā)現(xiàn)長選項,那么它所指向的變量的數(shù)值不變。
下面測試一下:
$ ./long_opt_demo --name
getopt_long()設(shè)置變量 : do_name = 1
getopt_long()設(shè)置變量 : do_gf_name = 0
$ ./long_opt_demo --gf_name
getopt_long()設(shè)置變量 : do_name = 0
getopt_long()設(shè)置變量 : do_gf_name = 1
$ ./long_opt_demo --love forever
Our love is forever!
$ ./long_opt_demo -l forever
Our love is forever!
測試過后,應(yīng)該有所感觸了。關(guān)于flag和val的討論到此為止。下面總結(jié)一下get_long()的各種返回值的含義:
返回值
含 義
0
getopt_long()設(shè)置一個標(biāo)志,它的值與option結(jié)構(gòu)中的val字段的值一樣
1
每碰到一個命令行參數(shù),optarg都會記錄它
'?'
無效選項
':'
缺少選項參數(shù)
'x'
選項字符'x'
-1
選項解析結(jié)束
從實用的角度來說,我們更期望每個長選項都對應(yīng)一個短選項,這種情況下,在option結(jié)構(gòu)中,只要將flag設(shè)置為NULL,并將val設(shè)置為長選項所對應(yīng)的短選項字符即可。譬如上面清單5中的程序,修改如下。
清單6:
#include <stdio.h>
#include <getopt.h>
int do_name, do_gf_name;
char *l_opt_arg;
struct option longopts[] = {
{ "name", no_argument, NULL, 'n' },
{ "gf_name", no_argument, NULL, 'g' },
{ "love", required_argument, NULL, 'l' },
{ 0, 0, 0, 0},
};
int main(int argc, char *argv[])
{
int c;
while((c = getopt_long(argc, argv, ":l:", longopts, NULL)) != -1){
switch (c){
case 'n':
printf("My name is LYR.\n");
break;
case 'g':
printf("Her name is BX.\n");
break;
case 'l':
l_opt_arg = optarg;
printf("Our love is %s!\n", l_opt_arg);
break;
}
}
return 0;
}
測試結(jié)果如下:
$ ./long_opt_demo --name --gf_name --love forever
My name is LYR.
Her name is BX.
Our love is forever!
$ ./long_opt_demo -ng -l forever
My name is LYR.
Her name is BX.
Our love is forever!
9、在LINUX之外的系統(tǒng)平臺上使用GNU getopt()或getopt_long()
只要從GNU程序或GNU C Library(GLIBC)的CVS檔案文件中copy源文件即可(
http://sourceware.org/glibc/)。所需源文件是 getopt.h、getopt.c和getoptl.c,將這些文件包含在你的項目中。另外,你的項目中最好也將COPYING.LIB文件包含進(jìn)去,因為GNU LGPL(GNU 程序庫公共許可證)的內(nèi)容全部包括在命名為COPYING.LIB 的文件中。
10、結(jié)論
程序需要能夠快速處理各個選項和參數(shù),且要求不會浪費開發(fā)人員的太多時間。在這一點上,無論是GUI(圖形用戶交互)程序還是CUI(命令行交互)程序,都是其首要任務(wù),其區(qū)別僅在于實現(xiàn)方式的不同。GUI通過菜單、對話框之類的圖形控件來完成交互,而CUI使用了純文本的交互方式。在程序開發(fā)中,許多測試程序用CUI來完成是首選方案。
getopt() 函數(shù)是一個標(biāo)準(zhǔn)庫調(diào)用,可允許您使用直接的 while/switch 語句方便地逐個處理命令行參數(shù)和檢測選項(帶或不帶附加的參數(shù))。與其類似的 getopt_long() 允許在幾乎不進(jìn)行額外工作的情況下處理更具描述性的長選項,這非常受開發(fā)人員的歡迎。