[shell] if-else-fi === ###### tags: `OS / Ubuntu / shell script` ###### tags: `OS`, `Ubuntu`, `linux`, `shell script`, `if-else` <br> [TOC] <br> ## 常見的 `test` / `[ ... ]` / `[[ ... ]]` 的 **if 判斷式** > by gpt-5.2 (2025/10/22) > 小提醒:`[ ... ]` 是傳統 test,`[[ ... ]]` 是 bash 的加強版(比較不容易被字串空白、萬用字元干擾)。一般建議在 bash 腳本優先用 `[[ ... ]]`。 --- ### 檔案 / 目錄判斷 | 判斷式 | 意義 | 範例 | | --------------------- | ---------------------- | --------------------------- | | `-e path` | 存在(不管檔案或目錄) | `[[ -e /etc/hosts ]]` | | `-f path` | 是一般檔案 (regular file) | `[[ -f ./a.txt ]]` | | `-d path` | 是目錄 | `[[ -d /var/log ]]` | | `-L path` 或 `-h path` | 是符號連結 | `[[ -L /bin/sh ]]` | | `-r path` | 可讀 | `[[ -r ./a.txt ]]` | | `-w path` | 可寫 | `[[ -w ./a.txt ]]` | | `-x path` | 可執行 | `[[ -x ./script.sh ]]` | | `-s path` | 檔案大小 > 0 | `[[ -s ./a.txt ]]` | | `-b path` | block device(磁碟類) | `[[ -b /dev/sda ]]` | | `-c path` | char device(終端類) | `[[ -c /dev/tty ]]` | | `-p path` | named pipe (FIFO) | `[[ -p /tmp/fifo ]]` | | `-S path` | socket | `[[ -S /run/docker.sock ]]` | | `path1 -nt path2` | path1 比 path2 新(mtime) | `[[ a -nt b ]]` | | `path1 -ot path2` | path1 比 path2 舊(mtime) | `[[ a -ot b ]]` | | `path1 -ef path2` | 同一個檔案(同 inode) | `[[ a -ef b ]]` | --- ### 字串判斷(最常用的 -z / -n 在這) | 判斷式 | 意義 | 範例 | | ------------ | --------------------- | ------------------------ | | `-z s` | 字串長度為 0(空字串)(z: zero) | `[[ -z "$NAME" ]]` | | `-n s` | 字串長度非 0(非空) | `[[ -n "$NAME" ]]` | | `s1 = s2` | 相等 | `[[ "$A" = "ok" ]]` | | `s1 != s2` | 不相等 | `[[ "$A" != "ok" ]]` | | `s1 < s2` | 以字典序比較(在 `[[ ]]` 才安全) | `[[ "$A" < "$B" ]]` | | `s1 > s2` | 以字典序比較(在 `[[ ]]` 才安全) | `[[ "$A" > "$B" ]]` | | `s =~ regex` | 正規表示式匹配(bash `[[ ]]`) | `[[ "$A" =~ ^[0-9]+$ ]]` | - ### 更多 `s =~ regex` 範例 - `[[ ! "$file" =~ -scaler\.yaml$ ]]` --- ### 整數判斷(數值比較) | 判斷式 | 意義 | 範例 | | ----- | ---- | ------------------ | | `-eq` | 等於 | `[[ "$N" -eq 3 ]]` | | `-ne` | 不等於 | `[[ "$N" -ne 3 ]]` | | `-gt` | 大於 | `[[ "$N" -gt 3 ]]` | | `-ge` | 大於等於 | `[[ "$N" -ge 3 ]]` | | `-lt` | 小於 | `[[ "$N" -lt 3 ]]` | | `-le` | 小於等於 | `[[ "$N" -le 3 ]]` | --- ### 邏輯組合(AND / OR / NOT) | 寫法 | 意義 | 範例 | | ------------------ | ------------ | -------------------------------- | | `! cond` | 反相 | `[[ ! -f a.txt ]]` | | `cond1 && cond2` | AND(建議用這個) | `[[ -n "$A" ]] && [[ -f "$P" ]]` | | `cond1 \|\| cond2` | OR(建議用這個) | `[[ -d a ]] \|\| [[ -d b ]]` | | `cond1 -a cond2` | AND(舊式,不太建議) | `[ -n "$A" -a -f "$P" ]` | | `cond1 -o cond2` | OR(舊式,不太建議) | `[ -d a -o -d b ]` | --- ### 變數存在與否(bash 常用技巧) | 判斷式 | 意義 | 範例 | | ----------------- | -------------------------- | --------------------- | | `-v VAR` | 變數已宣告(bash `[[ ]]`) | `[[ -v PATH ]]` | | `${VAR+x}` | VAR 有設定(含空字串也算) | `[[ -n "${VAR+x}" ]]` | | `${VAR:-default}` | VAR 未設或為空 -> default | `X="${VAR:-foo}"` | | `${VAR-default}` | VAR 未設 -> default(空字串不算未設) | `X="${VAR-foo}"` | --- ### 最常見的 if 寫法範例(可直接套) ```bash if [[ -d "$DIR" && -n "$NAME" ]]; then echo "ok" else echo "not ok" fi ``` <br> --- ## if-condition ### `if [ -t 0 ]; then` - ### gpt-5 (2025/10/16) `[ -t 0 ]` 是在**測試「檔案描述元 0(stdin)」是不是接在一個終端機(TTY)」**。 * `-t`:test 的條件,表示「該檔案描述元是否為終端機」 * `0`:stdin(標準輸入)。`1` 是 stdout,`2` 是 stderr 所以: ```bash if [ -t 0 ]; then # 目前是在互動式終端機執行(可以安全地 read 提示) else # 非互動(例如被 pipe、重導向、CI),不要等待使用者輸入 fi ``` ### 小範例 * 直接跑腳本(互動):`-t 0` 為真 → 會出現 `read -p` 提示 * 管線/重導向(非互動):`echo x | ./script.sh` 或 `./script.sh < /dev/null` → `-t 0` 為假 → 不會提示 > 延伸:也常見用 `[ -t 1 ]` 檢查 stdout 是否是 TTY(決定要不要上色輸出)。 <br> --- ## 案例學習 ### `if <command>; then ... else ... fi` - ### gpt-5 (2025/09/03) ```bash if helm -n "${SLINKY_NAME}" status "${SLINKY_NAME}" >/dev/null 2>&1; then ... else ... fi ``` 這個 `if` 條件是在「用指令的**退出碼**(exit status)當布林值」: ### 執行順序與判斷邏輯 1. **先執行指令**:`helm -n "$SLINKY_NAME" status "$SLINKY_NAME"` * 這會查詢該命名空間裡是否有同名的 Helm release。 2. **輸出重導**:`>/dev/null 2>&1` * `>/dev/null`:把**標準輸出**(stdout)丟掉。 * `2>&1`:把**標準錯誤**(stderr)也重導到目前的 stdout(此時已丟到 `/dev/null`),所以畫面上不會看到任何輸出。 3. **if 判斷依據**:shell 以**退出碼**判斷真假 * **0**(成功)→ 進入 `then` 區塊。 * **非 0**(失敗)→ 進入 `else` 區塊。 --- ### 小重點(通用規則) * `if <command>; then ... else ... fi` 會用**指令的退出碼**判斷真假(0 為真、非 0 為假)。 * `>/dev/null 2>&1` 把**標準輸出與錯誤**都丟棄,避免干擾畫面。 * 想記錄錯誤碼可用: ```bash if some_cmd; then echo OK else rc=$? echo "FAIL with code: $rc" fi ``` <br> --- ### 用 `ls`:檢查檔案是否存在 - ### gpt-5 (2025/09/03) ```bash #!/usr/bin/env bash # Demo 1: branch by success/failure of a command (ls) if ls /etc/passwd >/dev/null 2>&1; then echo "[IF] /etc/passwd exists." else echo "[ELSE] /etc/passwd not found." fi ``` * 成功:找到檔案 → 退出碼 0 → 進 IF * 失敗:找不到檔案 → 非 0 → 進 ELSE <br> --- ### 用 `grep -q`:檢查文字是否出現 - ### gpt-5 (2025/09/03) ```bash #!/usr/bin/env bash # Demo 2: grep succeeds (exit 0) if pattern is found if grep -q "^root:" /etc/passwd >/dev/null 2>&1; then echo "[IF] Found 'root' user." else echo "[ELSE] 'root' user not found." fi ``` * 成功:找到關鍵字 → 0 → IF * 失敗:沒找到 / 讀檔錯誤 → 非 0 → ELSE <br> --- ### 用 `command -v`:檢查指令是否已安裝 - ### gpt-5 (2025/09/03) ```bash #!/usr/bin/env bash # Demo 3: check if a command is available (e.g., git) if command -v git >/dev/null 2>&1; then echo "[IF] 'git' is installed." else echo "[ELSE] 'git' is NOT installed." fi ``` * 成功:系統路徑找到該指令 → 0 → IF * 失敗:找不到 → 非 0 → ELSE <br> ### 用「命令列參數」與「互動輸入」教學 - ### gpt-5 (2025/09/03) 下面給你兩個最常見的 if-else 範例:用「命令列參數」與用「互動輸入」。 ### 1) 用參數判斷(沒有名字就顯示用法) **檔名:** `hello.sh` ```bash #!/usr/bin/env bash # Simple if-else "Hello" using an argument # If no name is given, show usage and exit with code 1 if [[ -z "$1" ]]; then echo "Usage: $0 <name>" exit 1 else echo "Hello, $1!" fi ``` **執行步驟** ```bash chmod +x hello.sh ./hello.sh # 會顯示用法並以狀態碼 1 結束 ./hello.sh Alice # 輸出:Hello, Alice! echo $? # 查看上一個指令的退出碼 ``` **重點說明** * `[[ -z "$1" ]]`:判斷第 1 個參數是否為空字串。 * `$0`:腳本自身檔名(在用法提示很好用)。 * `exit 1`:用非 0 代表「發生可預期的小錯」(例如使用方式不正確)。 --- ### 2) 用互動輸入判斷(空白就用預設 world) **檔名:** `hello_interactive.sh` ```bash #!/usr/bin/env bash # Simple if-else "Hello" using interactive input read -rp "Your name? " name # If name is empty, default to 'world'; else greet the given name if [[ -z "$name" ]]; then echo "Hello, world!" else echo "Hello, $name!" fi ``` **執行** ```bash chmod +x hello_interactive.sh ./hello_interactive.sh # 直接 Enter -> Hello, world! # 輸入 Alice -> Hello, Alice! ``` --- ### (加碼)if-elif-else 範例 ```bash #!/usr/bin/env bash name="$1" if [[ -z "$name" ]]; then echo "Usage: $0 <name>" exit 1 elif [[ "$name" == "world" ]]; then echo "Hello, World!" else echo "Hello, $name!" fi ``` > 以上就是 Ubuntu 上最精簡實用的 if-else「Hello, World」範例:一個用參數、一個用互動輸入,再加一個 `elif` 版本,足夠當快速入門模板。 <br> --- <br> ## 測試理解 ### 解讀:`if [ -n "$grp" ] && [ -d "/work/$grp" ] && [ -x "/work/$grp" ]; then` > by gpt-5.2 (2026/01/20) 這行是在做 **三個條件都成立才進入 if**(中間用 `&&` 串起來,代表 AND): ```sh if [ -n "$grp" ] && [ -d "/work/$grp" ] && [ -x "/work/$grp" ]; then ``` 逐段意思是: 1. `[ -n "$grp" ]` * `-n` = *string length is non-zero* * 也就是:`$grp` **不是空字串**(有抓到 group 名稱) * 例如 `grp=$(id -gn)` 失敗或回空,就不要亂用 `/work/` 去 cd。 2. `[ -d "/work/$grp" ]` * `-d` = *is a directory* * 也就是:`/work/<group>` **這個路徑存在且是目錄**。 3. `[ -x "/work/$grp" ]` * `-x` = *is executable*(對目錄來說代表**有「進入/搜尋」權限**,也就是 execute permission) * 也就是:你對 `/work/<group>` **有權限進去 cd**。 * 目錄的 `x` 權限很重要:沒有 `x`,就算你 `ls` 看到目錄也不能 `cd` 進去。 所以整句合起來就是: > 只有當 `grp` 有值,而且 `/work/$grp` 這個目錄存在,且你有權限進入它時,才去 `cd "/work/$grp"`;否則就停留在 `/work`。 補充一個小例子(直覺理解): * `grp=""` → 第一個條件失敗 → 不進 if * `/work/dev` 不存在 → 第二個條件失敗 → 不進 if * `/work/dev` 存在但權限是 `drw-r-----`(沒有 x)→ 第三個條件失敗 → 不進 if 如果你希望「有目錄就嘗試 cd,不行就算了」也可以簡化成一行: ```sh [ -n "$grp" ] && cd "/work/$grp" >/dev/null 2>&1 || true ``` 只是我原本那種寫法比較清楚也比較少做無謂嘗試。 <br> --- <br> ## `[ ];` vs `[[ ]];` - ### Question `if [ "$ENABLED" = true ]; then` 和 `if [[ "$ENABLED" = true ]]; then` 差在哪裡? - ### gpt-5 (2025/10/31) 短答:在你這段**有加引號、單純比對文字**的情況下,兩者效果一樣;但本質不同、行為在其他情境會分岐。 ### 差異重點 * `[ … ]` 是 **POSIX `test` 指令**(外部/內建指令),**可攜**到 /bin/sh。 * `[[ … ]]` 是 **Bash/Zsh 關鍵字**,**不可攜**到純 POSIX sh,但更安全、功能也更強。 ### 行為差異 1. **字詞分割 / 萬用字元展開** * `[ … ]`:會先做字詞分割與萬用字元展開,所以變數**必須加引號**。 * `[[ … ]]`:不做字詞分割與萬用字元展開,未加引號也較安全。 ```bash var= [ $var = true ] # 可能出現:unary operator expected [[ $var = true ]] # 不會報錯 ``` 2. **比對語意** * 在 `[[ … ]]` 裡,`=`/`==` 右側**未加引號時**支援 **glob 樣式比對**: ```bash [[ $x = t* ]] # x 以 't' 開頭 → true [ "$x" = t* ] # 這裡是把字面量 t* 拿來比對,不是樣式 ``` * `[[ … ]]` 還有 **正則**:`=~`(右側**不要加引號**) ```bash [[ $x =~ ^t(rue|RUE)$ ]] ``` 3. **字串大小比較符號 `<` `>`** * `[ … ]` 中 `<`、`>` 會被當成**重導向**,要跳脫: ```bash [ "$a" \< "$b" ] ``` * `[[ … ]]` 可直接用: ```bash [[ $a < $b ]] ``` 4. **條件組合** * `[[ … ]]` 可直接用 `&&`、`||`、`()` 分組: ```bash [[ cond1 && (cond2 || cond3) ]] ``` * `[ … ]` 通常要拆成多個 `[ ]` 配合 `&&` / `||`。 5. **變數存在性** * `[[ … ]]` 支援 `-v var`(Bash 4.2+)檢查變數是否已設定: ```bash [[ -v ENABLED ]] ``` ### 結論與建議 * 你的例子: ```bash if [ "$ENABLED" = true ]; then # 與 if [[ "$ENABLED" = true ]]; then ``` 兩者**結果相同**(因為已加引號、純文字比對)。 * 若腳本標頭是 `#!/usr/bin/env bash`,偏好用 `[[ … ]]`,較安全且可用樣式/正則。 * 若需要 **POSIX 可攜**(/bin/sh 也能跑),就用 `[ … ]`,並**確實為變數加引號**。 <br> --- ## `[ … ]`:會先做字詞分割與萬用字元展開,所以變數必須加引號。 > unary operator expected, - ### gpt-5 (round-1)(2025/10/31) 這個點超常踩坑的,我給你幾個最典型的。 ### 1. 空字串導致「一元運算子預期」 ```bash name="" # 沒有引號 if [ $name = foo ]; then echo ok fi ``` 執行時可能看到: ```text [: =: unary operator expected ``` 因為沒引號,`[ ... ]` 裡實際變成: ```bash [ = foo ] ``` 前面少了一個運算元,就報錯。改成: ```bash if [ "$name" = foo ]; then echo ok fi ``` 就沒事。 --- ### 2. 有空白的值被「拆成多個字」 ```bash file="my doc.txt" # 沒有引號 [ -f $file ] ``` 展開後會變成: ```bash [ -f my doc.txt ] ``` 對 shell 來說這是 4 個字:`[`, `-f`, `my`, `doc.txt`, 會判斷錯或報錯。正確寫法: ```bash [ -f "$file" ] ``` --- ### 3. 萬用字元被當成 pattern 去展開 ```bash pattern="*.log" # 沒有引號 [ $pattern = "*.log" ] ``` 假設目前目錄剛好有 `a.log b.log`,那 `$pattern` 會先被展開成 ```text a.log b.log ``` 整個式子就變成類似: ```bash [ a.log b.log = "*.log" ] ``` 語法就壞掉了。 正確寫法: ```bash [ "$pattern" = "*.log" ] ``` --- ### 4. 路徑裡剛好有星號的情況(比較少見但很壞) ```bash path="/tmp/*/data" [ -d $path ] # 糟糕,會在 /tmp 下展開 [ -d "$path" ] # 保留原字面 ``` --- ### 對照:為什麼 `[[ ... ]]` 比較不容易出事 ```bash name="" if [[ $name = foo ]]; then # 即使沒加引號也不會被拆字 echo ok fi ``` 因為 `[[ ... ]]` 裡不做 word splitting / pathname expansion,所以容錯高很多。 所以那句話的重點就是:「**在 `[ ... ]` 裡,用變數幾乎都要加雙引號**」,這是寫 Shell 的肌肉記憶之一。 <br> --- --- - ### gpt-5 (round-2)(2025/10/31) 重點是:**未加引號的變數會先被「字詞分割」與「萬用字元(globbing)展開」**,導致 `[ … ]` 看到的引數跟你想的不一樣。下面用最直觀的例子示範。 ### 1) 字詞分割(spaces 造成) ```bash x="foo bar" [ $x = "foo bar" ] && echo OK || echo NG # bash: [: too many arguments ← 被切成 5 個詞:[ , foo , = , foo , bar , ] ``` ✔ 正確寫法(加引號): ```bash [ "$x" = "foo bar" ] && echo OK || echo NG # OK ``` **空字串也會出事:** ```bash x= [ $x = true ] # bash: [: =: unary operator expected ``` ✔ 正確: ```bash [ "$x" = true ] # 不會報錯,條件為假 ``` --- ### 2) 萬用字元展開(globbing) 假設目錄下有 `a.txt b.txt`: ```bash pattern="*.txt" [ $pattern = "*.txt" ] && echo match || echo NG # bash: [: too many arguments ← $pattern 被展成 a.txt b.txt 再分割 ``` ✔ 正確: ```bash [ "$pattern" = "*.txt" ] && echo match || echo NG # match ``` 或是測檔案存在時也會被影響: ```bash file="*.txt" [ -e $file ] && echo exists || echo not # 可能變成:-e a.txt b.txt → too many arguments(或只剩第一個,語意錯) ``` ✔ 正確: ```bash [ -e "$file" ] && echo exists || echo not ``` --- ### 3) 為什麼 `[[ … ]]` 比較安全 `[[ … ]]` **不做字詞分割與萬用字元展開**(除非你刻意用模式比對),所以下面不會報錯: ```bash x="foo bar" [[ $x = "foo bar" ]] && echo OK # OK(即使沒加引號) ``` --- **結論**: * 在 `[ … ]` 裡:**一律為變數加引號**(`"$var"`)以避免分割與展開。 * 若腳本使用 Bash/Zsh,傾向用 `[[ … ]]`,更安全,也有額外功能(模式/正則)。 <br> {%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}