1. **命令替换与变量赋值**
    
2. ** `test` 与 `expr` 的区别**
    
3. **字符串比较 vs 数值比较**
    
4. **未定义变量陷阱**
    
5. **位运算与逻辑非**
    
6. ** `while` 与 `until` 循环**
    

---

## 一、命令替换与变量赋值

这是最容易出错的部分，关键要分清：

### 1. `ret= command` 不是赋值命令输出

bash

ret= expr length "hello"

Shell 实际理解为：

1. 临时设置环境变量 `ret` 为空；
    
2. 执行命令 `expr length "hello"`；
    
3. `expr` 输出 `5`；
    
4. Shell 又把 `5` 当成命令执行，所以报：
    
    text
    
    command not found: 5
    

也就是说：

bash

ret= command

不是“把 command 的结果赋给 ret”，而是“临时环境变量 + 执行命令”。

### 2. 要赋值命令输出，必须用命令替换

正确写法：

bash

ret=$(expr length "hello")

或者传统写法：

bash

ret= `expr length "hello"`

推荐 `$(...)`，因为更清晰、支持嵌套、不容易出错。

### 3. 等号后不能有空格

错误：

bash

ret= `expr length "hello"`

这会被解析成：

bash

ret= 5

然后 `5` 被当成命令，报 `command not found: 5`。

正确：

bash

ret= `expr length "hello"`
ret=$(expr length "hello")

### 4. `echo` 只是输出参数

bash

echo expr length "hellp"5

输出：

text

expr length hellp 5

因为 `echo` 不计算表达式，只把参数原样输出。引号会被 Shell 去掉。

而：

bash

echo `expr length "hello"`

会先执行命令替换，得到 `5`，再变成：

bash

echo 5

所以输出 `5`。

### 5. 计算字符串长度更推荐 Shell 内置写法

bash

str="hello"
echo ${#str}   # 5

不必依赖 `expr`。

---

## 二、`test` 与 `expr` 的区别

### 1. `test` / `[ ]`

用于条件判断，常和 `if`、`while`、`until` 搭配。

bash

test 10 -lt 20
[ 10 -lt 20 ]

特点：

- Shell 内置，快；
    
- 不输出结果；
    
- 只用退出码表示真假：
    
    - 成立：`$? = 0`
        
    - 不成立：`$? = 1`
        
- 运算符明确：
    
    - `-lt` 小于
        
    - `-gt` 大于
        
    - `-eq` 等于
        
    - `-ne` 不等于
        
    - `-le` 小于等于
        
    - `-ge` 大于等于
        

### 2. `expr`

用于表达式计算。

bash

expr 10 \< 20

特点：

- 外部命令，慢；
    
- 会输出 `1` 或 `0`；
    
- 同时也有退出码：
    
    - 成立：`$? = 0`
        
    - 不成立：`$? = 1`
        
    - 语法错误：`$? = 2`
        
- 符号通常要转义：
    
    - `>` 写成 `\>`
        
    - `<` 写成 `\<`
        
    - `&` 写成 `\&`
        
    - `|` 写成 `\|`
        
    - `!` 写成 `\!`
        

### 3. 怎么选

条件判断优先：

bash

[ ... ]
[[ ... ]]
(( ... ))

需要表达式结果时才考虑：

bash

result=$(expr ...)

现代 Bash/Zsh 更推荐：

bash

(( 10 < 20 ))
result=$(( 10 < 20 ))

### 4. `expr` 能不能做逻辑求值？

能，但不好用。

bash

expr 10 \> 5
expr 1 \& 0
expr 1 \| 0
expr \! 1

它输出 `1/0`，退出码也遵循 Shell 惯例。但语法笨拙、需要转义、性能差，所以条件判断仍优先用 `test`、`[ ]`、`[[ ]]`、`(( ))`。

---

## 三、字符串比较 vs 数值比较

这是 `test` 里非常重要的区别。

bash

a=1
b=01
c=2

### 1. `=` 是字符串比较

bash

test $a = $b
echo $?

结果是 `1`，表示 false。

因为：

- `$a` 是 `"1"`
    
- `$b` 是 `"01"`
    
- 字符串 `"1"` 和 `"01"` 不相等。
    

### 2. `-eq` 是数值比较

bash

test $a -eq $b
echo $?

结果是 `0`，表示 true。

因为：

- `1` 和 `01` 在数值上都等于 `1`。
    

### 3. `-lt` 数值小于

bash

test $a -lt $c
echo $?

结果是 `0`，表示 true。

因为 `1 < 2`。

### 4. 总结

|写法|比较类型|例子|结果|
|---|---|---|---|
| `=` |字符串| `"1" = "01"` |false|
| `-eq` |数值| `1 == 01` |true|
| `-lt` |数值| `1 < 2` |true|

关键记忆：

- `=`、`!=` 是字符串比较；
    
- `-eq`、`-ne`、`-lt`、`-gt`、`-le`、`-ge` 是数值比较；
    
- `01` 和 `1` 字符串不同，数值相同。
    

---

## 四、未定义变量陷阱

这是 `test` 里另一个容易踩坑的地方。

bash

test -n $f
echo $?

如果 `$f` 未定义，Shell 会把它扩展为空字符串。

于是命令可能变成：

bash

test -n

`-n` 后面没有参数。某些 Shell 对这种情况处理不符合 POSIX，可能返回 `0`，让你误以为“非空”。

而：

bash

test -z $f

会变成类似：

bash

test -z ""

空字符串确实为空，所以返回 `0`。

于是出现：

bash

test -n $f   # 返回 0
test -z $f   # 返回 0

看起来没区别，其实是 `test -n $f` 的解析异常。

### 安全写法

始终给变量加引号：

bash

test -n "${f}"
test -z "${f}"

或者：

bash

test -n "${f:-}"
test -z "${f:-}"

Bash/Zsh 更推荐：

bash

[[ -n ${f} ]]
[[ -z ${f} ]]

### 结论

- 未定义变量通常视为空字符串；
    
- 但 `test -n $f` 可能因为缺少参数而异常返回 `0`；
    
- 不要写裸变量，必须加引号；
    
- 推荐 `[[ ... ]]`。
    

---

## 五、位运算与逻辑非

### 1. 位运算

Bash/Zsh 用 `$(( ))`：

bash

echo $(( 5 & 3 ))   # 1，按位与
echo $(( 5 | 3 ))   # 7，按位或
echo $(( 5 ^ 3 ))   # 6，按位异或
echo $(( ~5 ))      # -6，按位取反
echo $(( 5 << 1 ))  # 10，左移
echo $(( 5 >> 1 ))  # 2，右移

`expr` 只支持部分：

bash

expr 5 \& 3
expr 5 \| 3

不支持 `^`、`~`、`<<`、`>>`。

`awk` 支持更完整：

bash

awk 'BEGIN{print and(5,3)}'
awk 'BEGIN{print or(5,3)}'
awk 'BEGIN{print xor(5,3)}'
awk 'BEGIN{print compl(5)}'
awk 'BEGIN{print lshift(5,1)}'
awk 'BEGIN{print rshift(5,1)}'

### 2. 逻辑非

条件判断中：

bash

if ! [ 1 -eq 2 ]; then
    echo "not equal"
fi

`!` 会反转命令退出码。

算术逻辑非：

bash

echo $(( !0 ))  # 1
echo $(( !1 ))  # 0
echo $(( !5 ))  # 0

`expr` 也支持：

bash

expr \! 0   # 1
expr \! 1   # 0

### 3. 推荐

- 位运算：优先 `$(( ))`；
    
- 逻辑非：条件判断用 `!`，算术中用 `$(( ! ))`；
    
- `expr` 支持有限，不作为首选。
    

---

## 六、`while` 与 `until` 循环

### 1. 核心区别

|循环|条件逻辑|执行时机|
|---|---|---|
| `while` |条件为真时循环|条件成立就继续|
| `until` |条件为真时停止|条件不成立就继续|

简单记：

- `while`：当条件为真时循环；
    
- `until`：直到条件为真时停止。
    

### 2. 示例：从 1 数到 3

`while`：

bash

count=1
while [ $count -le 3 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

`until`：

bash

count=1
until [ $count -gt 3 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

两者输出相同。

因为：

- `while [ $count -le 3 ]`：只要 `count <= 3` 就循环；
    
- `until [ $count -gt 3 ]`：直到 `count > 3` 才停止。
    

逻辑相反，效果可以相同。

### 3. 示例：等待文件

`while`：

bash

while [ ! -f "/tmp/ready.txt" ]; do
    echo "Waiting..."
    sleep 1
done

`until`：

bash

until [ -f "/tmp/ready.txt" ]; do
    echo "Waiting..."
    sleep 1
done

### 4. 怎么选

- “满足条件时继续” → `while`
    
- “直到条件满足才停止” → `until`
    
- 条件容易写成正逻辑 → `while`
    
- 条件容易写成反逻辑 → `until`
    

### 5. 关于“`until` 至少执行一次”

这句话对 Shell 标准 `until` 不准确。

标准 `until`：

bash

until [ condition ]; do
    commands
done

条件在开头，先判断，再决定是否执行循环体。

如果一开始条件就为真，循环体一次也不执行。

例如：

bash

count=0
until [ $count -lt 1 ]; do
    echo "Count: $count"
    count=$((count - 1))
done

`[ 0 -lt 1 ]` 成立，`until` 条件为真时停止，所以循环体一次也不执行。

如果要模拟“至少执行一次”，要手动把条件放到末尾：

bash

until false; do
    commands
    [ condition ] && break
done

示例：

bash

count=0
until false; do
    echo "Count: $count"
    count=$((count + 1))
    [ $count -ge 3 ] && break
done

这才至少执行一次。

所以：

- Shell 标准 `until`：条件在开头，不保证至少执行一次；
    
- 条件后置的 `until`：可以至少执行一次；
    
- 不要把它和其他语言的 `do...until` 混淆。
    

---

## 总体知识框架

可以记成下面这张图：

text

Shell 易混点
├── 命令替换与赋值
│   ├── ret= command 不是赋值命令输出
│   ├── ret=$(command) 才是命令替换赋值
│   ├── 等号后不能有空格
│   └── echo 只输出参数，不计算表达式
│
├── test 与 expr
│   ├── test/[ ]：条件判断，退出码 0/1
│   ├── expr：表达式计算，输出 1/0，也有退出码
│   ├── 条件判断优先 test、[[ ]]、(( ))
│   └── expr 语法老、慢、需转义
│
├── 比较运算
│   ├── =、!= 是字符串比较
│   ├── -eq、-lt、-gt 等是数值比较
│   └── "1" 和 "01" 字符串不同，数值相同
│
├── 未定义变量
│   ├── 未定义变量通常视为空
│   ├── test -n $f 可能异常返回 0
│   ├── 必须加引号：test -n "${f}"
│   └── 推荐 [[ -n ${f} ]]
│
├── 位运算与逻辑非
│   ├── 位运算优先 $(( ))
│   ├── 逻辑非用 !
│   ├── 算术逻辑非用 $(( ! ))
│   └── expr 支持有限
│
└── while 与 until
    ├── while：条件为真时循环
    ├── until：条件为真时停止
    ├── 标准 until 条件在开头，不保证至少执行一次
    └── 要至少执行一次，需条件后置 + break

一句话总结：

**Shell 里最容易混的是“命令替换、条件测试、比较类型、未定义变量、循环条件方向”这几件事。记住：赋值命令输出用 `$(...)`，条件判断用 `test` / `[ ]` / `[[ ]]`，字符串比较用 `=`，数值比较用 `-eq` 等，变量要加引号，`while` 是条件真继续，`until` 是条件真停止。**