程式與外界溝通最古老、最普遍的介面就是命令列引數。Unix 哲學中「小工具 + 管線」的組合能力,
正建立在每個程式都能透過 argc/argv 接收參數之上。本單元對應 K&R §5.10,
從 echo 範例出發,實作簡易版 grep,並補充現代選項解析(getopt)與環境變數(getenv)。
學習目標
- 理解
argc/argv的記憶體結構與慣例(argv[0]、argv[argc] == NULL) - 能以指標運算走訪引數(K&R 風格
while (--argc > 0)) - 實作接受「選項旗標 + 位置參數」的程式(如
grep -n -v pattern) - 會使用 POSIX
getopt解析-x、-o value形式的選項 - 會用
getenv讀取環境變數,理解環境變數與引數的差別
對照章節 / 參考來源
- K&R §5.10 Command-line Arguments(
echo與 pattern-finding 程式) - C11 標準 §5.1.2.2.1(main 的兩種簽名與 argv 的保證)
- POSIX:
man 3 getopt、man 3 getenv - POSIX Utility Syntax Guidelines(選項設計慣例)
觀念講解
1. argc / argv 的結構
作業系統啟動程式時,把命令列切成字串陣列交給 main:
int main(int argc, char *argv[])
argc(argument count):引數個數,至少為 1(含程式名稱本身)。argv(argument vector):字串指標陣列;argv[0]慣例上是程式名稱,argv[argc]標準保證是NULL。
執行 ./prog -n hello 時的記憶體圖:
argc = 3
argv ──> [ptr0] ──> "./prog\0"
[ptr1] ──> "-n\0"
[ptr2] ──> "hello\0"
[NULL]
argv 的型別是 char **(指標的指標),與 char *argv[] 在參數位置等價。
標準允許程式修改 argv 指向的字串內容(getopt 就會重排它)。
2. 走訪引數的兩種風格
/* 索引風格:直觀 */
for (int i = 1; i < argc; i++)
printf("%s\n", argv[i]);
/* K&R 指標風格:echo 範例的寫法 */
while (--argc > 0)
printf("%s%s", *++argv, argc > 1 ? " " : "");
*++argv:先把 argv 往後移一格(跳過程式名),再取出該指標。
這種寫法緊湊但要小心副作用的順序,面試與讀舊碼時常見。
3. K&R 的 pattern-finding(簡易 grep)
K&R §5.10 把 §4.1 的「找出含有指定模式的行」改造成從命令列讀取模式:
while (getline(line, MAXLINE) > 0)
if (strstr(line, pattern) != NULL) {
printf("%s", line);
found++;
}
再加上兩個選項:-x(反相:印出「不含」模式的行)與 -n(印行號)。
選項解析的骨架是雙層迴圈——外層走引數、內層走一個引數內的多個旗標字母
(支援 -nx 合併寫法):
while (--argc > 0 && (*++argv)[0] == '-')
while ((c = *++argv[0]) != '\0')
switch (c) {
case 'x': except = 1; break;
case 'n': number = 1; break;
default: /* 未知選項 */ ;
}
注意 (*++argv)[0] 與 *++argv[0] 的不同:前者移動「引數陣列的游標」,
後者移動「目前字串內的游標」——正是上一單元複雜宣告的實戰。
4. POSIX getopt
手寫解析容易漏掉邊角(合併旗標、-o value 與 -ovalue、-- 終止符)。
POSIX 的 getopt 幫你處理這些慣例:
#include <unistd.h>
int opt;
while ((opt = getopt(argc, argv, "nvf:")) != -1) {
switch (opt) {
case 'n': line_numbers = 1; break;
case 'v': invert = 1; break;
case 'f': filename = optarg; break; /* f: 表示需要參數 */
case '?': /* 未知選項,getopt 已印錯誤訊息 */ break;
}
}
/* optind 是第一個「非選項」引數的索引 */
optstring 中字母後面的 : 表示該選項需要參數,參數會放在全域變數 optarg。
解析結束後 optind 之後才是位置參數。長選項(--verbose)需用 GNU getopt_long。
5. 環境變數 getenv
環境變數是另一條「由外向內」傳遞設定的通道,適合放不常變動的組態
(如 HOME、PATH、LANG):
const char *home = getenv("HOME");
if (home == NULL)
home = "/tmp"; /* 查不到時務必有預設值 */
引數 vs 環境變數的取捨:一次性、針對單次執行的用引數; 跨多個程式共享、屬於「執行環境」性質的用環境變數。
範例程式
| 檔案 | 內容 | 編譯 / 執行 |
|---|---|---|
01_echo_args.c |
argc/argv 結構解剖、索引與 K&R 指標兩種走訪風格 | cc -std=c11 -Wall -Wextra -o out 01_echo_args.c && ./out foo bar |
02_minigrep.c |
簡易 grep:-x/-n 手寫旗標解析;自建 /tmp 測試檔示範 | cc -std=c11 -Wall -Wextra -o out 02_minigrep.c && ./out |
03_getopt_env.c |
getopt 解析 -n/-v/-f file、getenv 讀環境變數 | cc -std=c11 -Wall -Wextra -o out 03_getopt_env.c && ./out |
三個程式在「不給任何引數」時都會用內建的示範資料自我展示;
你也可以自行加上引數觀察行為變化(例如 ./out -n -x main)。
常見錯誤與陷阱
- 假設
argc >= 2而直接使用argv[1]:使用者沒給引數時argv[1]是NULL, 對它做strcmp直接 segfault。永遠先檢查argc。 - 把
argv[0]當成「第一個引數」:argv[0]是程式名稱,實際引數從argv[1]開始。 注意某些環境下argv[0]可能是完整路徑,也可能只是名字。 - 混淆
(*++argv)[0]與*++argv[0]:後綴[]優先權高於前綴*與++,*++argv[0]解析為*++(argv[0]),移動的是字串內部游標。 - getopt 之後仍從
argv[1]讀位置參數:解析後應該從argv[optind]開始, 而且 getopt 可能已經重排過 argv。 - 修改
getenv回傳的字串:標準規定不可修改,且後續呼叫可能覆寫同一塊緩衝, 需要保留就strdup一份。
練習題
- 【易】寫一個
args_dump程式,逐行印出argc、每個argv[i]的索引、內容與長度。 - 【易】實作
sum程式:把所有引數當整數相加(用strtol), 遇到非數字引數時印出警告並跳過。 - 【中】為
02_minigrep.c加上-c選項:只印出匹配行數,不印內容(模仿grep -c)。 - 【中】用
getopt寫一個head複製品:支援-n N(預設 10), 從自建的 /tmp 測試檔讀取並印出前 N 行。 - 【難】實作
mini_env:不帶引數時印出environ中所有環境變數; 帶NAME=value形式引數時設定後執行後續命令(提示:extern char **environ、setenv、execvp)。 - 【難】手寫支援長選項的解析器:接受
--number、--invert、--file=NAME, 並支援--之後全部視為位置參數。
補充:選項設計的實務慣例
設計自己的命令列工具時,遵循 POSIX / GNU 慣例可以讓使用者「猜得到」用法:
- 短選項一個字母(
-n)、可合併(-nx);長選項雙破折號(--number)。 - 需要值的選項同時支援
-f file與-f=file/--file=name兩種寫法。 --之後的引數一律視為位置參數(即使長得像選項), 這是操作「檔名以 - 開頭」的唯一安全方法:rm -- -rf。- 無引數執行時:過濾器類工具應讀 stdin(如
grep、sort), 其他工具印出簡短 usage 並以非零碼離開。 - 離開碼慣例:0 成功、1 一般錯誤、2 用法錯誤(
grep則以 1 表示無匹配)。 - 訊息去向:正常輸出走 stdout(可被管線接走),
錯誤與診斷一律走 stderr(
fprintf(stderr, ...))。
延伸閱讀
- K&R §5.10;§4.1(pattern matching 的原始版本)
man 3 getopt、man 3 getopt_long(GNU 擴充)- POSIX.1-2017 XBD §12 Utility Conventions(選項語法慣例的官方定義)
- 《The Art of Unix Programming》(E. S. Raymond) 第 10 章:命令列介面設計
- musl libc 的
getopt.c原始碼(不到百行,適合閱讀)