【笨問題】CLI 參數為什麼有時要加 "--"? POSIX 參數慣例的冷知識
5 |
講到指令工具參數,Windows 老人的印象有可能還停留 /?、/S 之類的斜線表示法,例如:
DIR /S D:\TEMP
XCOPY D:\Data E:Data /S /E /H
但如果有追隨近年的開發主流,會發現 Windows 以外的世界,CLI 工具其實都在用另一套差不多的參數表示法:
git merge --no-ff -m "commit message" fix/crash-issue
npm install sax --force
curl -O --insecure --header 'Host: www.example.com' -I https://192.168.1.1/file.html
npx webpack init ./my-app --force --template=default
而隨著擁抱開源,近年微軟新推出的 CLI 工具也漸漸向 Linux 看齊:
# .NET SDK
dotnet publish -c Release -r win-x64 --no-self-contained -p:PublishSingleFile=true
# Chocolatey
choco install -y --no-progress git.install
# winget
winget install --id Microsoft.PowerToys --version 0.15.2
這套參數語法裡有個讓我迷惑的符號 "--",挺常在文件出現,但我一直沒有 100% 確定用法及使用時機,只知拿香跟著拜:
最近要自己寫 CLI 打算遵循這套參數做法,花了點時間研究,這才恍然大悟。
歸納這套參數語法有個共同特色,基本結構可分為[程式] [動作命令/Command] [選項/Option] [主參數/Argument]
四塊,[程式]與[命令]固定會在最前面(簡單工具則不需要命令,例如 curl),[主參數]與[選項]順序則可彈性組合搭配。
以 git merge --no-ff -v -m "message" feature/docker
為例,--no-ff、-m 是合併選項,而 feature/docker 則是合併對象參數(要合併的分支)。選項有兩種格式:-單一字元
(例如 -v、-m) 或 --英文字
(例如 --no-ff),選項後方有時會接參數,例如 -m "message"
;至於參數(本例中的 feature/docker,要合併的分支名稱)可以放在最後面,但也可以放在前面,寫成 git merge feature/docker --no-ff -m "message"
也 OK。
這套 CLI 通用的輸入參數語法規則,術語為 POSIX 參數語法慣例:
- 選項(Option)參數以 - 起首
- 選項名稱為單一字元(文數字)
- 有些選項需包含額外參數(Argument),例如:
-o output-file-path
、-m "message"
- 選項與其參數的分隔符號(空白)可加可不加,例如:
-o foo
等於-ofoo
- 可以用一個 - 接多個無參數選項,例如:
-abc
相當於-a -b -c
- 選項"通常"在非選項參數(如 Git 案例中的 feature/docker)前方 (註:此點非強制規定,許多 CLI 程式允許選項放後面)
- "--" 用來區隔選項及非選項參數,-- 後方的所有內容都視為非選項參數(即使加了 - 符號)
- 選項可任意順序排列,並可出現多次,如何解析由程式決定
- GNU 新增長版選項(Long Option)規則,使用 -- 串接一到三個英文字作為選項名稱,多英文字以 - 連接,例如: --name、--no-self-contained,或者英文字也可以用縮寫(例如:--no-ff,ff == Fast Forward)
- 長版選項可以寫成
--name=value
接收額外參數
理解此規則,我們就不難想出為什麼會需要 "--"。
舉個簡單例子,假設有個將 JSON 表格轉為 CSV 的 CLI 工具,用 json2csv data.json
解析 data.json 轉為 CSV 顯示於螢幕。程式支援 -f
選項,可沿用原檔名將結果存成 data.csv;但程式也支援自訂 CSV 檔名 -f result.csv
。由於 -f 後方可接自訂檔名也可以省略,若寫成 json2csv -f data.json
,程式就必須判斷 data.json 是給 -f 的選項參數,還是非選項參數(要處理的 JSON 檔名)。當然,這個案例加幾行邏輯自動判斷 data.json 到底是 -f 的參數還是 JSON 檔名不是難事,但更簡單且保證不出錯的方法是加上 -- 把 data.json 隔開(json2csv -f -- data.json
),明確告知 -- 後面都是非選項參數,不留半點模糊空間。
搞懂這點,回頭看文章開頭的兩張圖,心中不再有半點疑惑,讚!
2023-04-17 更新:本文以 GNU 的 POSIX 慣例 (GNU 版 getopt 指令的處理邏輯) 為準,與 POSIX.1-2017 / IEEE Std 1003.1-2017 規格 有些差異,例如:--name-of-option 這種 -- 接英文字的選項表示法是 GNU 新增的,IEEE 標準對於 -o foo、-ofoo 間的空白處理也有更嚴格的規定。文件也有提GNU 版是 POSIX 標準的擴充版 This is not what POSIX specifies; it is a GNU extension.
,感謝讀者 Explorer 補充。
This article explains POSIX arguments convension and why and when to use '--' in CLI arguments.
Comments
# by Explorer
作者有些地方解釋得不太正確,這邊給予補充/糾正: 1. 當選項後面可加可不加參數的時候,POSIX 的慣例是選項的字母跟選項的參數中間不得有空格,所以假如文中的-f選項可帶可不帶參數的時候…… -fresult.csv # 這樣寫才是正確的 -f"result.csv" # 這樣寫也正確,shell會自動把引號去除 -f result.csv # 這樣寫的話剖析器會認為-f 不帶參數,無論有沒有"--"區隔。 2. "--" 主要的目的是因應參數有可能以橫線記號開頭,例如以橫線開頭的檔名或是需要指定一個負號數字,範例: mkdir -m755 /root/foo/bar # 這會建立一個 /root/foo/bar 資料夾 mkdir -- -m755 /root/foo/bar # 這會建立兩個資料夾,一個為 -m755(檔名開頭為橫線),另一個為 /root/foo/bar 3. 如果命令選項後面的參數在該命令的規則裡是不可省略的話,選項的字母跟選項參數中間就可有空格,但也可以不用。以git commit 的-m選項為例,-m一定要指定訊息,但下面兩種寫法效果會相同: git commit -m"My message" git commit -m "My message" 寫shell script 的時候會建議使用前面的形式(選項字母跟選項參數中間不加空格)。
# by Raven
--OOXX 表示長參數偶爾會變成-OO-XX -O 短參數 單獨的 -- 類似 "\-h"
# by Jeffrey
to Explorer,感謝分享,原來有區分標準 POSIX 及 GNU 擴充版,文章說的應屬後者。(已補充於本文)
# by Forger
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html POSIX.1-2017 因為歷史因素的例外條件:(標準規格為了顧及向下相容性是容許變通的,不過宗旨還是希望未來的應用程式得完群遵守 12.2 語法規範,過去的應用程式就算了,以免破壞既有工具的相容性) 12.1 第 2 點載明,選用性選項 -c option_argument 的 -c 選項 以及 option_argument 選項參數 是用 <blank> 空格分隔,唯一一項例外是當 選項參數 [option_argument] 用方括弧包夾時,這反映了一種狀況,例如 [-f[option_argument]] 的 -f 選項跟 [option_argument] 選項參數 是緊鄰在一起的(沒有用空格分開),用來表示選用性 選項參數;然而,如果是必須性的選項參數,例如 [-c option_argument] 的 option_argument 是緊接著到下一個參數(用空格分開)。依據 12.2 語法規範,如果 選項 跟它的 選項參數 是分開的兩個獨立參數時,選項參數 就不能是選擇性的,所以不能是 [-c [option_argument]] 這樣寫。不過因為歷史因素,有以下幾點例外: 12.1 第 2 點 a 項目,依據 [-c option_argument] 用法格式的描述,依循標準的應用程式,應該把 -c 選項 以及 option_argument 選項參數,用兩個分開的獨立參數表示(也就是要用空格隔開,不能緊鄰在一起)。然而依循標準的實作,也應該允許 選項 跟它的 選項參數 是緊鄰在一起的,不必用空格分隔(也就是容許 [-coption_argument] 或 [-c"option argument"] 假如 選項參數 含有空格字元,雙引號會被 SHELL 移除)。 12.1 第 2 點 b 項目,依據 [-f[option_argument]] 用法格式的描述,依循標準的應用程式,應該把 -f 選項 以及 [option_argument] 選項參數,緊鄰的擺放在一起,不必用空格分隔。如果指令參數只有提供 -f 選項,則按照用法格式的描述,視為省略了 [option_argument] 選項參數;並且不能將緊接著的下一個參數,當成是 [option_argument] 選項參數。(也就是說不會有 [-f [option_argument]] 的情況發生,例如 -f ABC 的 -f 選項省略了 選項參數,而不是把 ABC 當作是 -f 選項的 [option_argument] 選項參數) 結論:規範不是盲目遵循,例如規則說不能怎樣,就不能做,那就類似程式語言當中的 Strict 嚴謹模式,但規範中有提到例外,也就是容許變通,不表示這樣做出來的應用程式,就沒有達到依循標準的目的。因此,用法格式的描述 [-c option_argument] 這樣寫,就表示你要 -cXXXX 或是 -c XXXX (用空格隔開),都是合法的。但是 [-f[option_argument]] 就不能這麼隨興,只能 -fXXXX 這樣使用,或是 -f 單獨使用,即便 -f XXXX (用空格隔開) 也當作省略了 [option_argument] 選項參數,而不是將 XXXX 當作是 選項參數。 12.2 語法規範 第 4 點,所有選項應該用 - 減號開頭。 12.2 語法規範 第 5 點,一般選項,例如 [-a][-b][-d|-e] 選項,不含有 option_argument 選項參數 的 選項,在使用一或多個此類選項時,最後一個選項可以緊接著另一個帶有 option_argument 選項參數 的 選項,只能有這麼一個參數在,那麼這些選項將能夠被接受並作為群組合併在一起,並且開頭只有一個 - 減號。 (也就是說 -a -b -d -c XXXX 一起用,可以簡化為 -abdc XXXX,而不能 -abcd XXXX 這樣使用,-c 的 c 選項只能在群組內最後一個選項,因為 -d 並沒有 option_argument 選項參數) 範例指令:netstat -nlp、tar -cvvf file.tar、git clean -xdf。 12.2 語法規範 第 10 點,第一個遇到的 -- 兩個減號 參數,如果不是 option_argument 選項參數,那它就是 選項 解析的終止條件。(換句話說 -c -- -- -? 第一個 -- 被當成是 -c 選項的 選項參數,第二個 -- 才是終止條件,-? 不視為是選項) 長參數、短參數:長參數 開頭有兩個減號,例如 [--file option_argument],反之,一般選項只有一個減號,就是 短參數,例如 [-f]。在正式常用的 Linux 工具,像是 man tar 查詢 tar 指令的說明文件,裡面會列出 -f, --file=ARCHIVE 這樣的描述,-f 短參數,--file=長參數檔名。然而 長參數 的 選項參數 不見得要用 = 分隔,也可以用空格分隔,雖然說明文件沒有這樣寫,但是可以這樣用。 此外,像是指令 cd - 或 su -,它們的 - 減號 是程式自己定義的,man su 說明文件的定義則是 -, -l, --login,而 ~ 這個家目錄的符號是 SHELL 定義的。像本篇的 -- 兩個減號,也是 SHELL 定義出來的功能。 12.2 語法規範 第 13 點,像是 tar | gzip - 或 echo -n '計算雜湊值,結尾不含換行' | sha256sum - 則表示從標準輸入的管線讀取內容,sha256sum 不加上 - 減號 效果相同。 12.2 語法規範 第 14 點,如果從第 3 點到第 10 點能夠判斷出這個參數是屬於選項的話,並且如果是選項群組的話,那麼它們即便沒有在開頭加上 - 減號,也是合法的選項規則。 (這也就是為什麼 tar cvvf file.tar 或 ps aux 也能運作的原因,即便不符合 第 4 點 規範,沒在開頭加上 - 減號) https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial https://www.nuget.org/packages/System.CommandLine 微軟官方的教學,使用此套件完美實現 Linux 以上所有長參數、短參數功能,微軟自己的 .NET CLI 也是使用此套件,命令提示字元並沒有 -- 兩個減號的機制,所以 dotnet run -- --file scl.runtimeconfig.json 是模擬出來的。 以上是從 Windows 平台,跨越到 Linux 平台 所必備的基本知識。
# by Jeffrey
to Forger,超專業!! (膜拜) 感謝分享。