Skip to content

Classic Shell Scripting 讀書筆記 (四)

  • Ops

波浪展開

  • 若命令行第一個字串開頭為波浪符號 (~),則執行波浪展開,目的是轉換使用者根目錄的絕對路徑
  • 可以直接或間接方式指定展開的使用者,使用 ~ 則轉換當前使用者,使用 ~{username} 則會從 /etc/passwd 查找特定用戶的根目錄並替換
  • 好處是簡潔,及避免在程序裡把路徑寫死,壞處是可移植性差,許多商用 UNIX 的 Bourne Shell 不支援

通配符 wildcard

  • Shell 會將命令行提供的模式,至換成符合模式的一組排序過的文件名
  • 範圍表示法雖然方便,但你不應該對包含在範圍的字串有太多的假設,比較安全的方式是:分別指定所有大寫字母、小寫字母、數字、或子範圍,避免使用像 [a-Z][A-z] 這樣的用法
  • 使用範圍更大的問題是不同 locale 間的可移植性
  • 習慣上,當執行通配符展開時,UNIX Shell 會忽略以點號(.)開頭的文件,這些文件通常是配置文件或啟動文件

基本通配符

命令替換 command substitution

  • 將命令替換語句替換為執行結果
  • 形式有兩種:使用反引號(`),或將命令括在 $() 裡,在內嵌的用法上,第二種用法有利於增加可讀性,且內嵌的雙引號不須再進行(\)轉義

範例:根據使用的 shell 歸類使用者

expr 命令

  • 「UNIX 少數設計得不嚴謹卻又難用的命令」,不建議使用,可以以 test 或 $((...)) 代替
  • 通常在命令替換語句中使用,通過打印的方式將值返回標準輸出
  • 支持 32 及 64 位元的算術運算,幾乎不會有 overflow 的問題

引用 quoting

用來防止 Shell 將某些你想要的東西解釋成不同意義,有三種方式:
* 反斜槓轉義
* 單引號:強制將字符都看作字面上的意義(即便是反斜槓),不可再內嵌單引號
* 雙引號:同單引號的引用功能,差別在於,使用雙引號引用會確切處理轉義字符、變量、算術、命令替換,雙引號其中的單引號不具特殊意義

執行順序

Shell 從標準輸入或腳本讀取的每一行稱為管道(pipeline),一個管道包含多個命令,以 0 或特殊符號隔開,Shell 會分割這些命令並設置 I/O

  1. 將命令分割成 token: 使用 meta 字符組 |;&&||
  2. 檢查每個命令第一個 token ,看是否為一個開放關鍵字,藉此判斷命令是否為複合命令(compound command),若關鍵字並非 if{( 開始符號,而是中間結尾部份如 else, done 則發出錯誤訊號
  3. 檢查第一個單詞是否為別名(alias),如果匹配,便替代別名,並回到步驟 1
  4. 執行波浪展開:開頭為 ~ 注:以下步驟皆針對所有單詞
  5. 執行變量替換:開頭為 $
  6. 執行命令替換:$(string) 或 `string`
  7. 執行算術表達式(arithmetic expression):$((string))
  8. 使用 $IFS 定義的字符,對展開後的結果進行分割
  9. 執行通配符展開:對 *?[...] 執行文件名生成(filename generation)
  10. 使用第一個單詞執行命令

eval 語句

告知 Shell 取出並執行 eval 的參數

說明:
* $cmd 至步驟 5 才做變量展開,在步驟 1 時管道符號並沒有拿來分割,因此 |more 被視為 ls 的參數
* eval $cmd:先針對 $cmd 做第一次處理,其中包含變量展開,接著執行第二次,此時管道符號以能被在步驟 1 找到並拿來分割命令

subShell v.s. 代碼區塊 (code block)

subShell

指被括在圓括號 () 裡的命令,這些命令會在另外的進程中執行

範例:讓一小組命令在不同目錄下執行

code block

指被括在花括號 {} 裡的命令,這些命令會在同一個進程中執行,必須將結束關鍵字放置於換行符號或分號之後

主要差異在於子腳本是否與主腳本共享狀態,若使用代碼區塊,cd 命令會影響主腳本,也會影響變量賦值, exit 則會中止整個腳本。簡單來說,如果希望子腳本不要影響父腳本,使用 subShell,反之,使用代碼區塊

內建(Built-in)命令

Shell 本身執行命令,而非在另外的進程中執行外部程序,POSIX 又將命令區分成特殊(special)、一般(reqular)

命令查找次序是 1.特殊命令 2.Shell 函數 3.一般命令 4.外部命令,可以自定義函數來覆寫或擴展一般命令

有些內建命令只是為了增加效率,如 truefalsetest

範例: 擴展 cd 命令

特殊內建命令的特性

  • 特殊命令若出現語法錯誤,會導致 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 資源限制長度的參數

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *