代码风格
安全防御式写法, “不信任任何输入,任何操作都可能失败”。
规则:
| 规则 | 要求 |
|---|---|
| Shebang | #!/bin/bash(不推荐 /usr/bin/env bash) |
| 缩进 | 2 个空格,禁止制表符 |
| 行长 | 尽量不超过 80 字符 |
| 变量 | 局部小写下划线,环境变量/常量全大写下划线 |
| 函数 | 小写下划线,分层调用 |
| 条件 | 使用 [[ ... ]] 而非 [ ... ],除非 posix 兼容 |
| 命令替换 | 使用 $() 而不用反引号 |
| 管道 | 使用 | 时注意错误处理(pipefail) |
| 文件结构 | 包含头注释、函数注释 |
| 避免 eval | 严禁使用 eval |
| 引用 | 变量尽量用双引号包裹,除非明确需要分词 |
| 作用域 | 函数内必须使用 local 限定作用域。 |
写法对比:
| 场景 | 危险写法 | 防御写法 |
|---|---|---|
| 路径拼接 | rm -rf $1/* | [[ -d "$1" ]] && find "$1" -maxdepth 1 -delete |
| 变量引用 | echo $VAR | printf '%s\n' "${VAR}" |
| 检查命令成功 | grep pattern file > /dev/null | grep -q pattern file || { echo 'not found'; exit 1; } |
| 用户输入作为参数 | curl $URL | curl -- "${URL}" |
| 条件测试 | [ $a == $b ] | [[ "${a}" == "${b}" ]] |
词法
| 类型 | 示例 | 说明 |
|---|---|---|
| 空白 | space/tab | 分隔 token |
| 元字符 | | & ; ( ) < > | 参与语法 |
| 控制操作符 | || && & ; ;; ( ) | <newline> | 控制命令结构 |
| 普通字符 | 字母数字符号 | 构成 word |
# 开头 → 至行尾 | # 注释 | 构成注释 |
基础类型
变量
变量名
[_a-zA-Z][_a-zA-Z0-9]*参数类型
普通变量
位置参数:
$1 $2 ...特殊参数:
参数 含义 示例 $*所有位置参数,作为 一个单词,由 IFS 第一个字符连接 "$*"→"a b c"$@所有位置参数,作为 多个单词,保持原分词 "$@"→"a" "b" "c"$?最近命令的退出状态(0 = 成功) echo $?$$当前 Shell 的进程 ID(PID) 用来命名临时文件 $!最近后台(&)命令的 PID sleep 10 & echo $!$#位置参数个数 参数数量 $0当前脚本名或命令名 脚本自身名字 数组变量:
arr[0] arr[1]
变量赋值
name=value # 注意不要带空格
$name # 获取变量内容字符串
| 方式 | 行为 |
|---|---|
'...' | 全字面,不展开 |
"..." | 展开 $、反引号、少数 \ |
\c | 转义字符 |
$'...' | C 风格字符串 \n \t \xFF \ooo |
$"..." | 在 非 C/POSIX 的 locale 下,Bash 会尝试将字符串当作“可翻译文本” |
数组
声明数组
arr=(a b c)
arr[0]=a
arr[5]=x引用元素
${arr[0]}
${arr[i]}引用所有元素
${arr[*]} # 单词模式展开为一词,用 IFS 拼接
${arr[@]} # 每元素一词(常用于循环)
for x in "${arr[@]}"; do echo "$x"; done # 遍历数组数组长度
${#arr[@]}
${#arr[*]}取字符串元素长度
${#arr[i]} # 如果某个元素是字符串,计算其长度移除数组元素
unset arr[2] # 移除一个元素
unset arr # 整个数组命令格式
# [assignments...] command [args...] [redirections...]
# assignment 出现在命令名之前,仅影响该命令的环境
# redirection 可出现在任意位置(命令前、中、后)
# 如果 command 缺失,assignment 改变当前 shell 环境,否则不会
VAR=3 LS_COLORS=1 ls -l > out # 示例条件表达式
控制运算符
| 写法 | 意义 |
|---|---|
cmd1 ; cmd2 | 顺序 |
cmd1 & | 后台 |
cmd1 && cmd2 | 前真则后 |
cmd1 || cmd2 | 前假则后 |
[ ]和[[ ]]
| 特性 | [ ] | [[ ]] |
|---|---|---|
| 本质 | 等效于 test 命令 | 语言语法 |
| 是否 POSIX | 是 | 否(bash专属) |
| 会单词分割 | 是 | 否 |
| 会路径扩展 | 是 | 否 |
| 支持正则 | 否 | 是 |
支持 && | 否 | 是 |
单词分割:
str="a b" # 错误 [ $str = "a b" ] # 等效于 [ a b = "a b"] # 正确 [ "$str" = "a b"]路径扩展(Globbing)
字符串
| 表达式 | 意义 |
|---|---|
-z s | 空 |
-n s | 非空 |
s1 == s2 | 等 |
s1 != s2 | 不等 |
s1 < s2 | 字典序 |
数字比较
| 操作 | 含义 |
|---|---|
-eq | == |
-ne | != |
-lt | |
-le | <= |
-gt | > |
-ge | >= |
文件
| 测试 | 意义 |
|---|---|
-e/a f | 存在 |
-b f | block |
-c f | character special |
-d f | 目录 |
-f f | 普通文件 |
-g f | seduid |
-p f | pipe |
-r f | 可读 |
-w f | 可写 |
-x f | 可执行 |
-s f | 大小 > 0 |
f1 -nt f2 | 较新 |
f1 -ef f2 | 同 inode |
控制流
if
if list; then
list
elif list; then
list
else
list
fifor
for x in words; do list; done
for ((i=0; i<10; i++)); do ...; donewhile / until
while list; do list; done
until list; do list; donecase
case word in
pat) list ;;
pat2) list ;;
esacselect
select x in words; do list; done函数
name() { list; }规则:
- 无子进程(在当前 shell 执行)
$1 ...为函数参数return设置退出码local声明局部变量- 可以递归
扩展
扩展顺序(从上往下依次展开)
| 顺序 | 扩展 |
|---|---|
| 1 | 花括号扩展:生成组合字符串 |
| 2 | 波浪线扩展:展开成主目录、PWD、OLDPWD 等 |
| 3 | 参数展开:变量、数组、特殊参数等 |
| 4 | 命令替换:用命令输出替换 |
| 5 | 算术扩展:计算表达式 |
| 6 | 词拆分:按 IFS 拆成多个词 |
| 7 | 路径名扩展(通配符 glob) |
| 8 | 引用去除:删除未引用的 ' " \ |
花括号扩展(Brace Expansion)
bash
{a,b,c} # a b c,不进行语义分析,仅仅文本组合。
{1..3} # 1 2 3,可逆序,可字母
{1..10..2} # 1 3 5 7 9,可逆序(step不用符号),可字母
{a,b}{1,2} # a1 a2 b1 b2,可组合
{a,b{1,2},c} # a b1 b2 c,可嵌套
{,backup} # "" "backup",空字符
{01..05} # 数字之前允许有前导0不触发花括号扩展的情况:
- 被引号包裹
- 花括号里有
' " \ - 花括号无逗号且不是有效序列
\{a,b\}
波浪线(Tilde)扩展
| 表达式 | 展开结果 | 解释 | 示例 |
|---|---|---|---|
~ | $HOME | 当前用户主目录 | /home/yourname |
~user | 用户 user 的主目录 | 从 /etc/passwd 查找 | ~root → /root |
~+ | $PWD | 当前工作目录(当前路径) | /home/u/project |
~- | $OLDPWD | 上一次所在目录 | cd - 之前的路径 |
~N | 目录栈中第 N 个目录 | 相当于 dirs +N | ~0、~1、~2 |
参数展开(Parameter Expansion)
基本展开
${var} # 普通变量展开
${var:-default} # var 未设置或为空使用 default
${var:=default} # var 未设置或为空,用 default 并给 var 赋值
${var:+alt} # var 已设置且非空,设置值就会返回备用值
${var:?error} # var 未设置或为空,就报错并退出(非交互)子串
${var:offset:length} # 从 offset 起长 length 的字符。offset 可以为负数,但是前面必须有空格避免与-default 搞混了。前缀/后缀去除
${var#pat} # 删除最短匹配前缀
${var##pat} # 删除最长匹配前缀
${var%pat} # 删除最短后缀
${var%%pat} # 删除最长后缀字符串替换
${var/pat/repl} # 替换第一个匹配的 pat
${var//pat/repl} # 替换所有匹配的 pat
${var/#pat/repl} # 只在开头匹配到才替换
${var/%pat/repl} # 只在结尾匹配到才替换间接引用(indirect)
x=y;y=hello
echo ${!x} # hello匹配变量名
${!prefix*} # 匹配变量名列表(一个词),输出是变量名称
${!prefix@} # 匹配变量名列表(多个词),输出是变量名称命令替换(Command Substitution)
$(ls)
`ls` # 传统写法注意点:
- 会删除尾随换行符
- 嵌套必须用
$()形式 - 如果外面加上引号,会不进行词拆分
算术扩展(Arithmetic Expansion)
$((expr)) # 如果有变量不需 $- C 风格运算符:
+ - * / % << >> & | ^ && || - 比较:
< > <= >= == != - 赋值:
= += -= *= ... - 自增自减:
i++ ++i
支持 2 64 进制:
echo $((16#FF))拆词(Word Splitting)
由 IFS 控制,默认由 空格、tab、换行 拆分。
未引用按 IFS 拆词;引用后不拆词,非 IFS whitespace 会按单字符分隔,非 IFS whitespace 会按单字符分隔。
空参数在未引用时会被删除。
原始命令行 → 变量/命令展开 → 得到一个字符串 → 如果没被引号括住,按 IFS 拆 → 每个分段作为一个命令参数
IFS=:
x="a:b::c"
echo $x # a b c路径扩展(Globbing)
基本模式
| 模式 | 匹配含义 | 说明 |
|---|---|---|
* | 任意长度的字符串 | 包含空字符串 |
? | 任意单个字符 | 不含空 |
[abc] | 集合 | 匹配 a 或 b 或 c |
[a-z] | 范围 | 匹配 a z 中任意字符 |
[^a] / [!a] | 非集合 | 匹配不是 a 的任意字符 |
[]...] | 字符类 | 支持 POSIX 字符类 [:alpha:] 等 |
字符类(Character Classes)
除了下面的中括号要额外加个中括号
| 写法 | 匹配 |
|---|---|
[:alnum:] | 字母 + 数字 |
[:alpha:] | 字母 |
[:digit:] | 数字 |
[:lower:] | 小写字母 |
[:upper:] | 大写字母 |
[:blank:] | 空格、tab |
[:space:] | 空白字符 |
[:punct:] | 标点 |
[:graph:] | 可打印非空白字符 |
[:print:] | 可打印字符 |
[:xdigit:] | 十六进制字符 |
[:word:] | 字母、数字、下划线 _ |
[:cntrl:] | 控制字符 |
扩展通配符 (extglob)
| 模式 | 中文解释 |
|---|---|
?(pat) | pat 出现 0 次或 1 次 |
*(pat) | pat 出现 0 次或多次 |
+(pat) | pat 出现 1 次或多次 |
@(pat) | 只匹配 pat 之一 |
!(pat) | 不匹配 pat 的任意字符串 |
pat 可以是用 | 分隔的模式列表:
@(a|b|c)
!(foo|bar)未匹配时:
- 默认 → 保留原样
shopt -s nullglob→ 展开为空shopt -s failglob→ 报错
管道
管道本质上就是在子shell中运行命令,然后传输输入输出流,所以变量赋值是不会传递的。
cmd1 | cmd2 | cmd3 | ... # cmd1 的 stdout 会作为 cmd2 的 stdin,依次类推
! cmd1 | cmd2 # 管道的最终退出状态 = 最后一条命令的退出状态,加上!代表逻辑取反
echo ${PIPESTATUS[@]} # 如需每个命令的状态(Bash 扩展)
time cmd1 | cmd2 # 测量用了多少时间
time -p cmd1 | cmd2 # posix格式虽然不是 bash 语法,但是说明一下。命名管道,一个当成文件持续存在可以持续监听的管道。使用mkfifo建立
重定向
FD
文件描述符(File Descriptor,FD)就是一个表示“打开文件/设备/管道/终端”的整数数字。
可以把文件描述符当成一个管子,信息在里面流动,写入或者写出。
最常见的 3 个 FD 是默认打开的:
| FD | 名字 | 含义 | 默认连接到 |
|---|---|---|---|
| 0 | stdin | 标准输入 | 键盘 |
| 1 | stdout | 标准输出 | 终端屏幕 |
| 2 | stderr | 错误输出 | 终端屏幕 |
基本重定向
| 写法 | 创建新 FD? | 方向 | 行为说明 |
|---|---|---|---|
< file | 否(FD 0) | 读 | stdin(0)从 file 读取 |
> file | 否(FD 1) | 写 | stdout(1)写入 file(覆盖) |
>> file | 否(FD 1) | 写 | stdout 追加写入 file |
2> file | 否(FD 2) | 写 | stderr(2)写入 file |
2>> file | 否(FD 2) | 写 | stderr 追加写入 file |
&> file | 否(1、2) | 写 | stdout + stderr → file(覆盖) |
> | file | 否(1) | 写 |
创建或指定FD
| 写法 | 创建新 FD? | 方向 | 行为说明 |
|---|---|---|---|
n> file | 是(FD n) | 写 | 新 FD n 写 file(覆盖) |
n>> file | 是(FD n) | 写 | 新 FD n 追加写 file |
n< file | 是(FD n) | 读 | 新 FD n 读 file |
n<> file | 是(FD n) | 读写 | 新 FD n 读 + 写 file |
复制FD
让多个FD指向同一个打开文件
| 写法 | 创建新 FD? | 方向 | 行为说明 |
|---|---|---|---|
<& fd | 否 | 复制(读) | stdin(0)复制为 FD fd 所指向的文件 |
n<& fd | 否 | 复制(读) | FD n = FD fd(复制读端) |
>& fd | 否 | 复制(写) | stdout(1)复制为 FD fd |
n>& fd | 否 | 复制(写) | FD n = FD fd(复制写端) |
关闭FD
| 写法 | 创建新 FD? | 方向 | 行为说明 |
|---|---|---|---|
<&- | 否(FD 0) | 关闭 | 关闭 stdin(0) |
>&- | 否(FD 1) | 关闭 | 关闭 stdout(1) |
n<&- | 否 | 关闭 | 关闭 FD n(读) |
n>&- | 否 | 关闭 | 关闭 FD n(写) |
移动FD
| 写法 | 创建新 FD? | 方向 | 行为说明 |
|---|---|---|---|
n>&m- | 否 | 移动 | 将 FD m 移动到 FD n,并关闭 m |
Here字符串
| 写法 | 创建新 FD? | 方向 | 行为说明 |
|---|---|---|---|
<<EOF ... EOF | 否 | 输入 | Here Document:多行文本作为 stdin |
<<< "text" | 否 | 输入 | Here String:单行字符串作为 stdin |
进程替换
系统支持时可用,注意是作为文件:
<(cmd) # 输出作为文件提供
>(cmd) # 文件输入给 cmd
# 本质上等于下面的东西
temp_fifo = mkfifo
command > temp_fifo &
current_command < temp_fifo注意生效位置:
(list) # 子 shell(不会影响当前 shell)
{ list; } # 当前 shell(影响当前环境)关键内建命令
| 命令 | 作用 |
|---|---|
cd | 改变目录 |
exec | 改变 FD,或替换当前进程 |
read | 从 stdin 读入变量 |
printf | 格式化输出 |
local | 局部变量 |
declare | 声明变量类型(数组、整数等) |
set | 设置位置参数与 shell 选项 |
export | 导出变量 |
eval | 二次解析执行 |
test | 判断 |