本檔收錄 5 題由易到難的練習。建議流程:先蓋住解答自己寫,卡住再看
「解題思路」,寫完對照「參考解答」。每題完整程式碼在 exercises/:
cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_args_dump.c && /tmp/out
所有解答都採「雙模式」設計:不帶引數執行時用內建的模擬 argv 測資 展示結果;帶引數時則解析真正的命令列引數——兩條路共用同一套解析 邏輯,這正是把核心邏輯抽成函式的理由。
練習 1:完整傾印 argc / argv(難度:基礎)
題目
寫一個 args_dump 程式,逐行印出 argc、每個 argv[i] 的索引、
內容與長度,並驗證 argv[argc] == NULL 的標準保證。
模擬測資需包含:程式名稱、一般引數、含空白的引數(模擬 shell 引號)、
長得像選項的引數、數字引數。
執行範例:
argc = 5
argv[0] = "args_dump" (len=9)
argv[1] = "hello" (len=5)
...
解題思路
- argc/argv 的契約:
argc是引數個數(含程式名稱),argv[0]是程式名稱,argv[1..argc-1]是引數, 且 C 標準(5.1.2.2.1)保證argv[argc] == NULL。 - 雙模式架構:把傾印邏輯抽成
dump_args(int, char *[]),main只負責決定餵真引數還是模擬引數。模擬 argv 就是一個char *陣列,最後一格要親手放 NULL 才符合標準佈局。 - 「含空白的引數」在測什麼:shell 分詞發生在程式啟動前,
"two words"經過引號保護後是一個引數——程式看到的 argv 已經分好詞,裡面可以有任何字元。 - 邊界情況:引數長度用
strlen印出,證明引數是普通的 C 字串,沒有長度上限以外的特殊結構。
參考解答
/*
* ex1_args_dump.c — 練習 1:完整傾印 argc / argv
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex1_args_dump.c
* 執行:/tmp/out (用內建模擬引數示範)
* /tmp/out foo "bar baz" -x (也支援真的命令列引數)
*
* 逐行印出 argc、每個 argv[i] 的索引、內容與長度,
* 並驗證 argv[argc] == NULL 的標準保證。
*/
#include <stdio.h>
#include <string.h>
/* 核心邏輯獨立成函式:真實與模擬 argv 共用同一份程式碼 */
static void dump_args(int argc, char *argv[])
{
printf("argc = %d\n", argc);
for (int i = 0; i < argc; i++)
printf(" argv[%d] = \"%s\" (len=%zu)\n",
i, argv[i], strlen(argv[i]));
/* C 標準保證 argv[argc] 是 NULL——很多走訪寫法依賴這一點 */
printf(" argv[argc] %s NULL\n",
argv[argc] == NULL ? "==" : "!=");
}
int main(int argc, char *argv[])
{
if (argc > 1) { /* 真的從命令列拿到引數 */
printf("=== real command-line arguments ===\n");
dump_args(argc, argv);
return 0;
}
/* 沒帶引數:用寫死的模擬 argv 示範(模仿 shell 會傳的樣子) */
printf("=== simulated argv (run with args to use real ones) ===\n");
char *fake_argv[] = {
"args_dump", /* argv[0]:程式名稱 */
"hello",
"two words", /* shell 引號包起來的引數:含空白 */
"-v",
"42",
NULL, /* argv[argc] 必為 NULL */
};
int fake_argc = 5;
dump_args(fake_argc, fake_argv);
return 0;
}
程式碼講解
dump_args對真引數與假引數一視同仁——它只依賴 argc/argv 的 契約,不在乎資料來源。這種「注入測資」的結構讓命令列程式 變得可測試,是本單元所有解答的共同骨架。- 模擬 argv 的最後一格
NULL不是裝飾:argv[argc] == NULL的驗證行讀的就是它,少放會印出!=(或未定義行為)。 -v放在模擬測資裡提醒你:對「不解析選項」的程式而言,-v就只是一個普通字串,選項語意是解析器賦予的。- 易錯處:把
fake_argc寫成 6(把 NULL 也算進去)。 argc 計數不含結尾的 NULL。
範例輸出
=== simulated argv (run with args to use real ones) ===
argc = 5
argv[0] = "args_dump" (len=9)
argv[1] = "hello" (len=5)
argv[2] = "two words" (len=9)
argv[3] = "-v" (len=2)
argv[4] = "42" (len=2)
argv[argc] == NULL
練習 2:引數整數加總(難度:基礎)
題目
實作 sum 程式:把所有引數當十進位整數相加。要求用 strtol
做嚴格驗證——遇到非數字引數(如 abc、12x)時印警告到
stderr 並跳過,不中斷加總。負數要能正確處理。
模擬測資:10 20 abc -5 12x 30,預期 sum = 55。
解題思路
- 為什麼不用 atoi:
atoi("abc")回 0、atoi("12x")回 12, 無法區分「錯誤」與「真的是 0/12」,也不偵測溢位。strtol提供完整的錯誤回報管道。 - strtol 的三件套檢查(背下來,處處用得到):
-
end == s:一個字元都沒消化 → 完全不是數字; -*end != '\0':消化了一部分就停 → 有非法尾巴("12x"); -errno == ERANGE:值超出long範圍 → 溢位。 呼叫前要先errno = 0,因為 strtol 成功時不會清除 errno。 - 訊息流向:加總結果走 stdout(資料),警告走 stderr(診斷)。
這樣
./sum ... | tail -1之類的管線不會被警告汙染。 - 邊界情況:
-5是合法數字(strtol 處理正負號); 從argv[1]開始掃(跳過程式名稱)。
參考解答
/*
* ex2_sum_args.c — 練習 2:把引數當整數相加(strtol 嚴格驗證)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex2_sum_args.c
* 執行:/tmp/out (用內建模擬引數示範)
* /tmp/out 10 20 abc 30 (也支援真的命令列引數)
*
* 遇到非數字引數:印警告到 stderr 並跳過,不中斷加總。
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
/*
* strtol 的完整檢查三件套:
* 1. end == s -> 完全不是數字("abc")
* 2. *end != '\0' -> 有尾巴("12x")
* 3. errno == ERANGE -> 溢位("99999999999999999999")
*/
static int parse_int(const char *s, long *out)
{
char *end;
errno = 0;
long v = strtol(s, &end, 10);
if (end == s || *end != '\0')
return 0; /* 不是完整的十進位數字 */
if (errno == ERANGE)
return 0; /* 超出 long 範圍 */
*out = v;
return 1;
}
static long sum_args(int argc, char *argv[])
{
long sum = 0;
for (int i = 1; i < argc; i++) { /* 從 1 開始:跳過程式名稱 */
long v;
if (parse_int(argv[i], &v)) {
printf(" + %ld\n", v);
sum += v;
} else {
/* 診斷訊息一律走 stderr,別汙染 stdout 的資料流。
* 先沖掉 stdout 緩衝,兩條流的輸出順序才不會錯位。 */
fflush(stdout);
fprintf(stderr, " warning: '%s' is not a number, skipped\n",
argv[i]);
}
}
return sum;
}
int main(int argc, char *argv[])
{
if (argc > 1) {
printf("=== summing real arguments ===\n");
printf("sum = %ld\n", sum_args(argc, argv));
return 0;
}
printf("=== simulated: sum 10 20 abc -5 12x 30 ===\n");
char *fake_argv[] = {
"sum", "10", "20", "abc", "-5", "12x", "30", NULL,
};
printf("sum = %ld\n", sum_args(7, fake_argv));
return 0;
}
程式碼講解
parse_int把 strtol 的三件套包成「回傳成功與否 + 輸出參數」 的介面,呼叫端一行if就能用——驗證邏輯只寫一次。errno = 0;在 strtol 前面那行不能省:errno 是「黏性」的, 前一個函式留下的舊值會造成誤判。fflush(stdout);出現在寫 stderr 之前:stdout 接到管線時是 全緩衝、stderr 永遠不緩衝,不沖掉的話兩條流的訊息會錯位。- 易錯處:檢查順序上
*end != '\0'必須在end == s之後想——"abc"兩個條件都中,但""(空字串)只有end == s中; 合併成一個||條件最穩。
範例輸出
=== simulated: sum 10 20 abc -5 12x 30 ===
+ 10
+ 20
warning: 'abc' is not a number, skipped
+ -5
warning: '12x' is not a number, skipped
+ 30
sum = 55
練習 3:minigrep 加上 -c 選項(難度:進階)
題目
為 mini grep 加上 -c 選項:只印出匹配行數、不印內容
(模仿 grep -c)。要求:
- 用法
grep [-c] pattern file,引數錯誤回離開碼 2; - 模仿 grep 慣例:有匹配回 0、無匹配回 1;
- 測試檔由程式自建於
/tmp/,結束時刪除; - 模擬測資跑兩次:一次不帶
-c、一次帶-c。
解題思路
- 拆成兩層:
minigrep(pattern, fname, count_only)做事、run(argc, argv)解析引數。-c只是把「印行」換成「數行」, 核心迴圈完全共用,差別集中在兩個if。 - 匹配怎麼做:K&R 第 4 章手寫了
strindex;現代寫法直接用strstr(line, pattern)——回傳非 NULL 即該行含 pattern。 - 計數與離開碼:讓
minigrep回傳匹配行數,run據此決定 離開碼(>0→ 0、==0→ 1)。把「事實」與「政策」分開: 函式回報事實(幾行匹配),呼叫端決定政策(離開碼)。 - 測試檔生命週期:
make_test_file()寫死五行文字 → 跑兩組模擬 argv →remove(TESTFILE)清理。 測資設計成恰好 3 行含the,讓-c的輸出(3)可預期。 - 邊界情況:開檔失敗回 -1 並印錯誤到 stderr; 行緩衝區 512 對測試檔綽綽有餘(真 grep 要處理任意長行)。
參考解答
/*
* ex3_grep_count.c — 練習 3:minigrep 加上 -c(只印匹配行數)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex3_grep_count.c
* 執行:/tmp/out (內建模擬引數,含 -c 與不含 -c 各跑一次)
* /tmp/out -c pattern file (也支援真的命令列引數)
*
* 測試檔由程式自建於 /tmp/,結束時刪除。
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define TESTFILE "/tmp/ex3_grep_input.txt"
/*
* mini grep 核心:印出(或只計數)含 pattern 的行。
* count_only 非零時模仿 grep -c:只輸出匹配行數。
* 回傳匹配行數(呼叫端可據此決定離開碼,grep 慣例:無匹配回 1)。
*/
static int minigrep(const char *pattern, const char *fname, int count_only)
{
FILE *fp = fopen(fname, "r");
if (fp == NULL) {
fprintf(stderr, "grep: cannot open %s\n", fname);
return -1;
}
char line[512];
int matches = 0;
while (fgets(line, sizeof line, fp) != NULL) {
if (strstr(line, pattern) != NULL) { /* 子字串比對 */
matches++;
if (!count_only)
fputs(line, stdout); /* line 已含換行 */
}
}
fclose(fp);
if (count_only)
printf("%d\n", matches);
return matches;
}
/* 解析引數:支援 -c 選項,其後接 pattern 與檔名 */
static int run(int argc, char *argv[])
{
int count_only = 0;
int i = 1;
if (i < argc && strcmp(argv[i], "-c") == 0) {
count_only = 1;
i++;
}
if (argc - i != 2) {
fprintf(stderr, "usage: %s [-c] pattern file\n", argv[0]);
return 2; /* 用法錯誤慣例:離開碼 2 */
}
int n = minigrep(argv[i], argv[i + 1], count_only);
return n > 0 ? 0 : 1; /* grep 慣例:無匹配回 1 */
}
/* 建立寫死內容的測試檔 */
static void make_test_file(void)
{
FILE *fp = fopen(TESTFILE, "w");
if (fp == NULL) {
perror("fopen");
exit(1);
}
fputs("the quick brown fox\n"
"jumps over the lazy dog\n"
"pack my box with five dozen jugs\n"
"the rain in spain\n"
"hello world\n", fp);
fclose(fp);
}
int main(int argc, char *argv[])
{
if (argc > 1) /* 真實引數模式:檔案由使用者給 */
return run(argc, argv);
/* 模擬模式:自建測試檔,示範兩種用法 */
make_test_file();
printf("=== simulated: grep the %s ===\n", TESTFILE);
char *fake1[] = { "grep", "the", TESTFILE, NULL };
int rc1 = run(3, fake1);
printf("(exit status would be %d)\n", rc1);
printf("\n=== simulated: grep -c the %s ===\n", TESTFILE);
char *fake2[] = { "grep", "-c", "the", TESTFILE, NULL };
int rc2 = run(4, fake2);
printf("(exit status would be %d)\n", rc2);
remove(TESTFILE); /* 清理測試檔 */
return 0;
}
程式碼講解
count_only只影響兩處:匹配時不印行、結尾多印總數。 選項的實作成本與它對程式結構的侵入度成正比——好的分層讓 加選項變成小改動。fputs(line, stdout)而非printf("%s", line):line 是資料 不是格式字串;若用printf(line)而行內剛好有%,輕則亂印 重則當機(格式字串漏洞)。- 離開碼三態(0 有匹配/1 無匹配/2 用法錯誤)完全照真 grep 的 慣例——寫工具時遵循既有慣例,使用者與腳本才「猜得到」行為。
- 匹配行數
jumps over the lazy dog也算:strstr找的是 子字串,the出現在行中任何位置都中。 - 易錯處:忘記
remove(TESTFILE),/tmp 會累積垃圾檔; 或在run提前 return 的路徑忘了fclose(本解答的 fclose 在 minigrep 內,所有路徑都會經過)。
範例輸出
=== simulated: grep the /tmp/ex3_grep_input.txt ===
the quick brown fox
jumps over the lazy dog
the rain in spain
(exit status would be 0)
=== simulated: grep -c the /tmp/ex3_grep_input.txt ===
3
(exit status would be 0)
練習 4:getopt 版 head(難度:進階)
題目
用 POSIX 的 getopt 寫一個 head 複製品:支援 -n N(預設 10),
從自建的 /tmp 測試檔(12 行)讀取並印出前 N 行。要求:
-n的值用 strtol 嚴格驗證(拒絕-n abc、負數);- 非法選項或缺檔名時印 usage 並回離開碼 2;
- 模擬測資示範
head -n 3 檔案。
解題思路
- getopt 的心智模型:它是一台小狀態機,每呼叫一次吐出一個
選項字元;optstring
"n:"的冒號宣告「-n 帶值」,值放進全域optarg;掃完所有選項回 -1,此時optind指向第一個非選項 引數(檔名)。 - getopt 免費送你的東西:
-n 5與-n5兩種寫法、 選項合併(-ab=-a -b)、非法選項自動印錯誤訊息並回'?'。 手寫解析器要自己處理這些——這是練習 5 的主題。 - 值的驗證是你的事:getopt 只把字串塞進 optarg,
-n abc的abc要自己用 strtol 三件套驗證(複習練習 2)。 - head 主體:
while (printed < n && fgets(...))—— 兩個停止條件(讀滿 N 行、檔案讀完)自然合成一個條件式; 檔案不足 N 行時印到檔尾為止,不是錯誤。 - 邊界情況:
-n 0合法(印 0 行);n < 0拒絕; 缺檔名(optind >= argc)回用法錯誤。
參考解答
/*
* ex4_head_getopt.c — 練習 4:用 getopt 實作 head 複製品(-n N,預設 10)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex4_head_getopt.c
* 執行:/tmp/out (內建模擬引數:-n 3 讀自建測試檔)
* /tmp/out -n 5 file (也支援真的命令列引數)
*
* getopt 是 POSIX 函式(unistd.h),處理:選項合併、-n5 與 -n 5
* 兩種寫法、非法選項的錯誤訊息。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> /* getopt, optarg, optind */
#include <errno.h>
#define TESTFILE "/tmp/ex4_head_input.txt"
#define DEFAULT_N 10
/* 印出檔案前 n 行 */
static int head(const char *fname, long n)
{
FILE *fp = fopen(fname, "r");
if (fp == NULL) {
fprintf(stderr, "head: cannot open %s: %s\n",
fname, strerror(errno));
return 1;
}
char line[512];
long printed = 0;
while (printed < n && fgets(line, sizeof line, fp) != NULL) {
fputs(line, stdout);
printed++;
}
fclose(fp);
return 0;
}
/*
* 引數解析 + 執行。getopt 用法重點:
* - "n:" 的冒號表示 -n 需要一個值(getopt 會放進 optarg)
* - 回傳 '?' 表示非法選項或缺值
* - 解析結束後 optind 指向第一個「非選項」引數
*/
static int run(int argc, char *argv[])
{
long n = DEFAULT_N;
int opt;
while ((opt = getopt(argc, argv, "n:")) != -1) {
switch (opt) {
case 'n': {
char *end;
errno = 0;
n = strtol(optarg, &end, 10);
if (end == optarg || *end != '\0' || errno == ERANGE || n < 0) {
fprintf(stderr, "head: invalid line count '%s'\n", optarg);
return 2;
}
break;
}
default: /* getopt 已印過錯誤訊息 */
fprintf(stderr, "usage: %s [-n N] file\n", argv[0]);
return 2;
}
}
if (optind >= argc) { /* 沒給檔名 */
fprintf(stderr, "usage: %s [-n N] file\n", argv[0]);
return 2;
}
return head(argv[optind], n);
}
static void make_test_file(void)
{
FILE *fp = fopen(TESTFILE, "w");
if (fp == NULL) {
perror("fopen");
exit(1);
}
for (int i = 1; i <= 12; i++)
fprintf(fp, "line %02d of the test file\n", i);
fclose(fp);
}
int main(int argc, char *argv[])
{
if (argc > 1) /* 真實引數模式 */
return run(argc, argv);
/* 模擬模式:-n 3 讀自建的 12 行測試檔 */
make_test_file();
printf("=== simulated: head -n 3 %s ===\n", TESTFILE);
char *fake[] = { "head", "-n", "3", TESTFILE, NULL };
int rc = run(4, fake);
remove(TESTFILE); /* 清理測試檔 */
return rc;
}
程式碼講解
while ((opt = getopt(...)) != -1)加switch是 getopt 的 標準骨架,幾乎每個 Unix 工具的 main 都長這樣。case 'n'裡的大括號讓我們能在 case 內宣告char *end(C 的 case 標籤後不能直接接宣告,包個區塊即可)。default:同時接住'?'(非法選項)與其他意外值;getopt 自己 會往 stderr 印illegal option,我們只補 usage。- 注意 getopt 使用全域狀態(
optind),同一個行程重複解析 不同 argv 需要重設 optind(macOS 還要optreset); 本程式每次執行只解析一組,避開這個坑。 - 易錯處:忘記檢查
optind >= argc就取argv[optind]—— 沒給檔名時會解參考 NULL。
範例輸出
=== simulated: head -n 3 /tmp/ex4_head_input.txt ===
line 01 of the test file
line 02 of the test file
line 03 of the test file
練習 5:手寫長選項解析器(難度:挑戰)
題目
不用 getopt,手寫支援長選項的解析器,接受:
--number、--invert:布林選項;--file=NAME:帶值選項(=語法,空值報錯);--:之後的引數全部視為位置參數(即使長得像選項);- 未知選項報錯並回離開碼 2。
模擬測資需涵蓋:一般解析、-- 停止解析、未知選項被拒絕三種情境。
解題思路
- 先定資料結構:解析結果收進
struct options(旗標+file 值), 位置參數另外收集索引。解析器只填結構,不做業務邏輯—— 這讓它可以被三組測資重複呼叫驗證。 - 分類每個引數,順序很重要:
已越過 "--"?────── 是 ──> 位置參數 │否 不以 '-' 開頭?──── 是 ──> 位置參數 │否(單獨 "-" 慣例上代表 stdin,也歸位置參數) 是 "--"?────────── 是 ──> 設旗標,本身不算引數 │否 逐一比對已知長選項;都不中 -> 未知選項,報錯 --file=NAME的解析:strncmp(a, "--file=", 7)比對前綴, 值就是a + 7——指進原字串即可,不需要複製 (argv 的生命週期涵蓋整個程式)。記得擋掉空值--file=。--的語意:它是「選項區」與「位置參數區」的分界線。rm -- -rf能刪掉名為-rf的檔案,靠的就是這個機制—— 實作上只是一個布林旗標seen_dashdash。- 錯誤處理:未知選項立即返回 2(fail fast), 不要「跳過繼續」——拼錯的選項被默默忽略是最難查的 bug。
參考解答
/*
* ex5_long_options.c — 練習 5:手寫長選項解析器
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex5_long_options.c
* 執行:/tmp/out (內建三組模擬引數)
* /tmp/out --number --file=log.txt a b (也支援真的命令列引數)
*
* 支援:
* --number 布林選項
* --invert 布林選項
* --file=NAME 帶值選項(= 語法)
* -- 之後全部視為位置參數(即使長得像選項)
*/
#include <stdio.h>
#include <string.h>
struct options {
int number; /* --number 出現次數(當布林用) */
int invert; /* --invert */
const char *file; /* --file=NAME 的值,未給則 NULL */
};
/*
* 解析引數。回傳 0 成功、2 用法錯誤。
* 位置參數的索引收集進 pos[](呼叫端提供空間),個數寫進 *npos。
*/
static int parse_args(int argc, char *argv[], struct options *opt,
int pos[], int *npos)
{
int seen_dashdash = 0; /* 是否已越過 "--" */
memset(opt, 0, sizeof *opt);
*npos = 0;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
/* "--" 之後:一律當位置參數(處理檔名以 - 開頭的唯一安全法) */
if (seen_dashdash || a[0] != '-' || strcmp(a, "-") == 0) {
pos[(*npos)++] = i;
continue;
}
if (strcmp(a, "--") == 0) {
seen_dashdash = 1;
continue;
}
/* 至此必為 "--xxx" 形式的長選項 */
if (strcmp(a, "--number") == 0) {
opt->number = 1;
} else if (strcmp(a, "--invert") == 0) {
opt->invert = 1;
} else if (strncmp(a, "--file=", 7) == 0) {
if (a[7] == '\0') { /* --file= 後面是空的 */
fprintf(stderr, "error: --file requires a value\n");
return 2;
}
opt->file = a + 7; /* 指進原字串,零複製 */
} else if (strcmp(a, "--file") == 0) {
fprintf(stderr, "error: use --file=NAME\n");
return 2;
} else {
fprintf(stderr, "error: unknown option '%s'\n", a);
return 2;
}
}
return 0;
}
/* 印出解析結果 */
static void report(int argc, char *argv[])
{
struct options opt;
int pos[64];
int npos;
printf("command:");
for (int i = 0; i < argc; i++)
printf(" %s", argv[i]);
printf("\n");
fflush(stdout); /* 讓 stderr 的錯誤訊息不會與 stdout 錯位 */
if (parse_args(argc, argv, &opt, pos, &npos) != 0) {
printf(" -> parse failed (usage error)\n\n");
return;
}
printf(" number = %s\n", opt.number ? "on" : "off");
printf(" invert = %s\n", opt.invert ? "on" : "off");
printf(" file = %s\n", opt.file != NULL ? opt.file : "(none)");
printf(" positional:");
if (npos == 0)
printf(" (none)");
for (int i = 0; i < npos; i++)
printf(" \"%s\"", argv[pos[i]]);
printf("\n\n");
}
int main(int argc, char *argv[])
{
if (argc > 1) { /* 真實引數模式 */
report(argc, argv);
return 0;
}
/* 模擬模式:三組代表性測資 */
printf("=== case 1: options + value + positional ===\n");
char *c1[] = { "tool", "--number", "--file=data.txt", "in.c", NULL };
report(4, c1);
printf("=== case 2: -- stops option parsing ===\n");
/* "--invert" 在 -- 之後:必須被當成檔名而非選項 */
char *c2[] = { "tool", "--number", "--", "--invert", "-x", NULL };
report(5, c2);
printf("=== case 3: unknown option is rejected ===\n");
char *c3[] = { "tool", "--vebrose", NULL }; /* 打錯字 */
report(2, c3);
return 0;
}
程式碼講解
- 分類邏輯的判斷順序是本題核心:
seen_dashdash的檢查必須 最先做,否則--之後的--invert又會被當成選項—— case 2 測的就是這條路。 opt->file = a + 7;零複製的前提是 argv 活得比 options 久 (對命令列程式恆成立)。若 options 要存活超過 argv (例如塞進背景執行緒),就必須strdup。- 單獨的
"-"被歸為位置參數:Unix 慣例它代表 stdin (如cat -),不是選項。 --file不帶=的形式被明確拒絕並提示正確語法——比籠統的 unknown option 更友善。要支援--file NAME(空白分隔)也不難: 取argv[++i]即可,留作延伸。- 易錯處:用
strcmp(a, "--file=") == 0比對帶值選項—— 永遠不會中,因為後面還有值;帶值選項要用strncmp比前綴。
範例輸出
=== case 1: options + value + positional ===
command: tool --number --file=data.txt in.c
number = on
invert = off
file = data.txt
positional: "in.c"
=== case 2: -- stops option parsing ===
command: tool --number -- --invert -x
number = on
invert = off
file = (none)
positional: "--invert" "-x"
=== case 3: unknown option is rejected ===
command: tool --vebrose
error: unknown option '--vebrose'
-> parse failed (usage error)
常見錯誤解法對照
| 錯誤寫法 | 症狀 | 正解 |
|---|---|---|
用 atoi 解析引數 |
abc 變 0、12x 變 12,無法報錯 |
strtol 三件套(練習 2、4) |
| 警告訊息印到 stdout | 管線下游收到垃圾資料 | 診斷一律走 stderr(練習 2) |
printf(line) 印檔案內容 |
行內有 % 時亂印/當機 |
fputs(line, stdout)(練習 3) |
忘記檢查 optind >= argc |
缺檔名時解參考 NULL | 取位置參數前先檢查(練習 4) |
帶值長選項用 strcmp 全等比對 |
--file=x 永遠比不中 |
strncmp 比前綴(練習 5) |
忽略 -- 慣例 |
無法操作 - 開頭的檔名 |
seen_dashdash 旗標(練習 5) |