[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 %}