Shell 概览

自动关联目录:Shell 概览

Shell 的一切麻烦几乎都来自同一个事实:命令在被执行前要经过多轮展开(expansion),而引号决定了哪些展开会发生。

展开顺序

1. 大括号展开   {a,b} → a b
2. 波浪号展开   ~ → $HOME
3. 参数展开     $var、${var}
4. 命令替换     $(cmd)
5. 算术展开     $((1+2))
6. 单词拆分     按 IFS 把结果切成多个词
7. 路径名展开   * ? [...]
8. 引号移除     去掉语法层面的引号

单词拆分(第 6 步)是绝大多数 bug 的来源:包含空格的变量不加引号,就会被拆成多个参数。

files="a b.txt"
rm $files      # 实际是 rm a b.txt —— 删错文件
rm "$files"    # 一个参数

引号

引号行为
无引号全部展开 + 单词拆分 + 通配符
"双引号"展开 $var $(cmd) $((..)),但不做单词拆分与通配符
'单引号'完全字面量,什么展开都不做
$'...'支持转义序列(\n \t)

经验法则:变量一律加双引号;只有在确实需要拆分时才不加(而且通常有更好的写法)。

变量

name=value          # 等号两侧不能有空格
echo "$name"

${name:-default}    # 未定义/为空时用默认值
${name:=default}    # 同时赋值
${name:?msg}        # 未定义则报错退出
${#name}            # 长度
${name%.txt}        # 去掉尾部 .txt
${name/#old/new}    # 替换开头
特殊变量含义
$0脚本名
$1 … $9位置参数
$#参数个数
$@所有参数(加引号时保持独立:"$@")
$*所有参数(合成一个字符串)
$?上一条命令的退出码
$$当前进程 PID

"$@" 是转发参数的正确写法,$* 会把参数揉成一团。

重定向与管道

写法含义
> file覆盖写 stdout
>> file追加
2> filestderr
&> file / > file 2>&1两者一起
< file读 stdin
``
<<< "text"here-string
<<EOF ... EOFhere-document

注意 cmd > out.txt 2>&1 的顺序:2>&1 必须在重定向之后,否则 stderr 指向的还是旧的目标。

还有 sudo echo x > /etc/file 这种经典问题——重定向由 shell 做,不受 sudo 影响,要用 tee:

echo x | sudo tee /etc/file > /dev/null

条件与循环

if [[ -f "$path" && ! -L "$path" ]]; then
    ...
elif [[ "$n" -gt 10 ]]; then
    ...
fi

for f in *.log; do echo "$f"; done
while IFS= read -r line; do echo "$line"; done < file

[[ ]] 常用判断:

判断含义
-f -d -e -L文件/目录/存在/符号链接
-r -w -x可读/可写/可执行
-z -n空/非空字符串
== !=字符串比较(右侧不加引号则为模式匹配)
=~正则匹配
-eq -ne -lt -gt数值比较

函数

log() {
    printf '[%s] %s\n' "$(date +%T)" "$*"
}
  • 返回值用 return 0/1(0 表示成功)
  • 想返回字符串就用 echo + 命令替换
  • 用 local 声明局部变量

常见陷阱

陷阱说明
cd 失败继续执行用 `cd dir
管道吞掉错误码set -o pipefail
文件名以 - 开头用 -- 分隔:rm -- -file
grep 无匹配返回 1在 set -e 下会中断脚本
while read 在子 shell 里改变量无效管道会起子 shell,改用重定向或 shopt -s lastpipe
CRLF 换行符Windows 编辑过的脚本报 ^M,用 dos2unix