波浪展開
- 若命令行第一個字串開頭為波浪符號 (~),則執行波浪展開,目的是轉換使用者根目錄的絕對路徑
- 可以直接或間接方式指定展開的使用者,使用
~
則轉換當前使用者,使用~{username}
則會從 /etc/passwd 查找特定用戶的根目錄並替換 - 好處是簡潔,及避免在程序裡把路徑寫死,壞處是可移植性差,許多商用 UNIX 的 Bourne Shell 不支援
通配符 wildcard
- Shell 會將命令行提供的模式,至換成符合模式的一組排序過的文件名
- 範圍表示法雖然方便,但你不應該對包含在範圍的字串有太多的假設,比較安全的方式是:分別指定所有大寫字母、小寫字母、數字、或子範圍,避免使用像
[a-Z]
或[A-z]
這樣的用法 - 使用範圍更大的問題是不同 locale 間的可移植性
- 習慣上,當執行通配符展開時,UNIX Shell 會忽略以點號(.)開頭的文件,這些文件通常是配置文件或啟動文件
基本通配符
命令替換 command substitution
- 將命令替換語句替換為執行結果
- 形式有兩種:使用反引號(`),或將命令括在
$()
裡,在內嵌的用法上,第二種用法有利於增加可讀性,且內嵌的雙引號不須再進行(\
)轉義
1 2 3 4 5 |
$ echo outer `echo inner1 `echo inner2` inner1` outer outer inner1 inner2 inner1 outer $ echo outer $(echo inner1 $(echo inner2) inner1) outer outer inner1 inner2 inner1 outer |
範例:根據使用的 shell 歸類使用者
1 2 3 4 5 6 7 8 9 |
rm -f /tmp/*.mailing-list while IFS=: read user passwd uid gid name home Shell do Shell=${Shell:-/bin/sh} # 空值表示使用 /bin/sh file="/tmp/$(echo $Shell | sed -e 's;^/;;' -e 's;/;-;g').mailing-list" echo $user, >> $file done |
expr 命令
- 「UNIX 少數設計得不嚴謹卻又難用的命令」,不建議使用,可以以 test 或
$((...))
代替 - 通常在命令替換語句中使用,通過打印的方式將值返回標準輸出
- 支持 32 及 64 位元的算術運算,幾乎不會有 overflow 的問題
引用 quoting
用來防止 Shell 將某些你想要的東西解釋成不同意義,有三種方式:
* 反斜槓轉義
* 單引號:強制將字符都看作字面上的意義(即便是反斜槓),不可再內嵌單引號
* 雙引號:同單引號的引用功能,差別在於,使用雙引號引用會確切處理轉義字符、變量、算術、命令替換,雙引號其中的單引號不具特殊意義
執行順序
Shell 從標準輸入或腳本讀取的每一行稱為管道(pipeline),一個管道包含多個命令,以 0 或特殊符號隔開,Shell 會分割這些命令並設置 I/O
- 將命令分割成 token: 使用 meta 字符組
|
、;
、&&
、||
- 檢查每個命令的第一個 token ,看是否為一個開放關鍵字,藉此判斷命令是否為複合命令(compound command),若關鍵字並非
if
、{
、(
開始符號,而是中間結尾部份如else
,done
則發出錯誤訊號 - 檢查第一個單詞是否為別名(alias),如果匹配,便替代別名,並回到步驟 1
- 執行波浪展開:開頭為
~
注:以下步驟皆針對所有單詞 - 執行變量替換:開頭為
$
- 執行命令替換:
$(string)
或 `string` - 執行算術表達式(arithmetic expression):
$((string))
- 使用 $IFS 定義的字符,對展開後的結果進行分割
- 執行通配符展開:對
*
、?
、[...]
執行文件名生成(filename generation) - 使用第一個單詞執行命令
eval 語句
告知 Shell 取出並執行 eval 的參數
1 2 3 4 5 6 7 8 |
~$ cmd="ls | more" ~$ $cmd ls: 無法存取 '|': 沒有此一檔案或目錄 ls: 無法存取 'more': 沒有此一檔案或目錄 ~$ eval $cmd file1 file2 |
說明:
* $cmd
至步驟 5 才做變量展開,在步驟 1 時管道符號並沒有拿來分割,因此 |
及 more
被視為 ls 的參數
* eval $cmd
:先針對 $cmd 做第一次處理,其中包含變量展開,接著執行第二次,此時管道符號以能被在步驟 1 找到並拿來分割命令
subShell v.s. 代碼區塊 (code block)
subShell
指被括在圓括號 ()
裡的命令,這些命令會在另外的進程中執行
範例:讓一小組命令在不同目錄下執行
1 2 |
$ tar -cf - . | (cd /newdir; tar -xpf -) |
code block
指被括在花括號 {}
裡的命令,這些命令會在同一個進程中執行,必須將結束關鍵字放置於換行符號或分號之後
1 2 3 4 5 |
cd /some/directory || { echo could not change to /some/directory! >&2 exit 1 } |
主要差異在於子腳本是否與主腳本共享狀態,若使用代碼區塊,cd 命令會影響主腳本,也會影響變量賦值, exit 則會中止整個腳本。簡單來說,如果希望子腳本不要影響父腳本,使用 subShell,反之,使用代碼區塊。
內建(Built-in)命令
Shell 本身執行命令,而非在另外的進程中執行外部程序,POSIX 又將命令區分成特殊(special)、一般(reqular)
命令查找次序是 1.特殊命令 2.Shell 函數 3.一般命令 4.外部命令,可以自定義函數來覆寫或擴展一般命令
有些內建命令只是為了增加效率,如 true
、false
、test
範例: 擴展 cd 命令
1 2 3 4 5 6 |
cd () { command cd "$@" # 告訴 Shell 要避開函數的查找並訪問真正的命令 x=$(pwd) PS1="${x##*/}\$ " } |
特殊內建命令的特性
- 特殊命令若出現語法錯誤,會導致 Shell 退出;一般命令反之
- 特殊命令若標明變量賦值,則變量會繼續向後沿用
set 命令
POSIX 為了保留歷史的兼容性而存在的命令,可以做的事相當廣泛,也因此有些難懂,最常用來:
* 排序顯示所有 Shell 變量的名稱和值
* 改變位置參數,set -- [arguments]
,以提供的參數取代位置參數
* 打開或停用 Shell 選項,以 -
打開 +
關閉,變量 $-
用來表示當前已打開的 Shell 選項
腳本範例: pathfind
- 17-18: 為避免利用輸入字段分隔符攻擊 Shell 腳本,將 IFS 重設為標準值——空格+定位符(tab)+換行(newline)
- 20-23: 為了防止呼叫到欺騙程序,將 PATH 設為最小值,並透過 export 將安全查找路徑繼承給子進程
- 48-52: 變量命名規則——小寫變量為本地函數或主程序代碼使用;大寫變量被整個程序全局性共享
- 48: 使用字串 yes/no 比起 1/0 更有可讀性,對運行的資源消耗很微小
- GNU 鼓勵使用長的、描述性的選項名稱,而非舊的、隱蔽式的單一選項字符
- 57: 在 Shell 裡並沒有一種簡單透過指定前綴來匹配長名稱的作法,因此在此提供所有可能的替代用法
- 60: 以 GNU 處理 –help 的慣例是在標準輸出上,顯示簡短摘要,並以代碼 0 退出
- 60: 問號 (?) 是 Shell 的通配符,須用引號括起來
- 68: 匹配剩下的所有選項
- 71: 匹配所有條件以外的所有狀態,有點像是 C 的 switch 語句的 default 選擇器,同時也代表:「所有可能的選擇都考慮到了」
- 81: 使用者提供的環境變量可能是 PATH,為了安全因素檢查並重設
- 83: 可透過 “$envvar” 取得環境變量的名稱,但要使用的是它的變量展開,第一次變量展開完形同 ‘\${XXX}’,再傳到 eval 做第二次展開,其中兩邊的單引號是為了避免更進一步的展開
- 87: 開始健康檢查(sanity check),避免 garbage-in, garbage out
- 96: 遇到空列表,不做任何事,只成功退出
- 119: 限制累加的錯誤退出碼不得高於 125
腳本測試
理想上的測試,應結合有效參數與至少一個以上的無效參數,假設腳本有 3 個參數,各有 {i, j, k} 種縮寫方式,則有 (i+1) * (j+1) * (k+1) 種組合,每種組合都必須搭配 0 – 3 (至少要 3 個)個參數做測試。
著名的安全漏洞
- 竄改輸入字段分隔符 IFS
- 竄改查找路徑,以惡意命令替換可信的命令
- 將反引號命令、Shell meta 字符、控制字符(含 NUL 與換行符號)置入參數中
- 傳送超過 Shell 資源限制長度的參數