代码风格

安全防御式写法, “不信任任何输入,任何操作都可能失败”。

规则:

规则要求
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 $VARprintf '%s\n' "${VAR}"
检查命令成功grep pattern file > /dev/nullgrep -q pattern file || { echo 'not found'; exit 1; }
用户输入作为参数curl $URLcurl -- "${URL}"
条件测试[ $a == $b ][[ "${a}" == "${b}" ]]

词法

类型示例说明
空白space/tab分隔 token
元字符| & ; ( ) < >参与语法
控制操作符|| && & ; ;; ( ) | <newline>控制命令结构
普通字符字母数字符号构成 word
# 开头 → 至行尾# 注释构成注释

基础类型

变量

变量名

[_a-zA-Z][_a-zA-Z0-9]*

参数类型

变量赋值

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专属)
会单词分割
会路径扩展
支持正则
支持 &&
  1. 单词分割:

    str="a b"
    
    # 错误
    [ $str = "a b" ]
    # 等效于
    [ a b = "a b"]
    
    # 正确
    [ "$str" = "a b"]
  2. 路径扩展(Globbing)

字符串

表达式意义
-z s
-n s非空
s1 == s2
s1 != s2不等
s1 < s2字典序

数字比较

操作含义
-eq==
-ne!=
-lt
-le<=
-gt>
-ge>=

文件

测试意义
-e/a f存在
-b fblock
-c fcharacter special
-d f目录
-f f普通文件
-g fseduid
-p fpipe
-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
fi

for

for x in words; do list; done
for ((i=0; i<10; i++)); do ...; done

while / until

while list; do list; done
until list; do list; done

case

case word in
  pat) list ;;
  pat2) list ;;
esac

select

select x in words; do list; done

函数

name() { list; }

规则:

扩展

扩展顺序(从上往下依次展开)

顺序扩展
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

不触发花括号扩展的情况:

  1. 被引号包裹
  2. 花括号里有 ' " \
  3. 花括号无逗号且不是有效序列
  4. \{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)) # 如果有变量不需 $

支持 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)

未匹配时:

管道

管道本质上就是在子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名字含义默认连接到
0stdin标准输入键盘
1stdout标准输出终端屏幕
2stderr错误输出终端屏幕

基本重定向

写法创建新 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判断