目录

基础

健壮脚本

  1. 使用正确的 Shebang
#!/bin/bash
# 或若需兼容 POSIX:
#!/bin/sh
  1. 启用严格模式
set -euo pipefail
  1. 妥善处理变量
# 始终用双引号包裹变量,防止空格、通配符展开等问题:
echo "$my_var"   # 安全
echo $my_var     # 危险!若含空格或 * 会出错
# 提供默认值(避免依赖外部环境)
name="${USER:-unknown}"
  1. 检查命令执行结果
# 不要假设命令一定成功
if ! mkdir -p "$dir"; then
  echo "Error: failed to create directory $dir" >&2
  exit 1
fi
# 或使用 || 简写(适用于简单场景):
cp file.txt dest/ || { echo "Copy failed"; exit 1; }
  1. 安全处理文件路径
if [[ ! -f "$config_file" ]]; then
  echo "Config file missing: $config_file" >&2
  exit 1
fi
  1. 正确使用临时文件
temp_file=$(mktemp) || { echo "Failed to create temp file"; exit 1; }
trap 'rm -f "$temp_file"' EXIT  # 自动清理
  1. 处理信号(如 Ctrl+C)
cleanup() {
  echo "Caught signal, cleaning up..."
  rm -f "$temp_file"
  exit 1
}
trap cleanup INT TERM
信号编号触发场景
EXIT/00脚本正常或异常退出时(最常用)
INT2用户按Ctrl+C(中断)
TERM15收到kill命令(默认信号)
ERR-任一命令返回非零退出码(需配合set -e)
  1. 日志与错误输出
  1. 参数解析与验证
while getopts "f:v" opt; do
  case $opt in
    f) file="$OPTARG" ;;
    v) verbose=1 ;;
    *) echo "Usage: $0 [-f file] [-v]" >&2; exit 1 ;;
  esac
done

# 验证必需参数
if [[ -z "${file:-}" ]]; then
  echo "Error: -f is required" >&2
  exit 1
fi

getopts 用法

while getopts "选项字符串" ; do
  case $变 in
    项1) ... ;;
    项2) ... ;;
    \?)   ... ;;  # 处理无效选项
    :)    ... ;;  # 处理缺少参数(如果选项需要参数)
  esac
done
# 跳过已处理的选项(可选,用于处理非选项参数)
shift $((OPTIND - 1))
  1. 选项字符串(optstring)
  1. 特殊变量
  1. 静默错误模式(抑制默认错误信息)
行为默认模式f:vh静默模式:f:vh
遇到无效选项(如-xgetopts自动输出错误到 stderr:script.sh: illegal option -- x 并将opt设为?不输出任何错误;将opt设为?,由脚本自行处理
遇到缺少参数(如-f后无值)getopts自动输出错误:script.sh: option requires an argument -- f并将opt设为?不输出错误;将opt设为 :

,由脚本自行处理

ℹ️note

关键点:只有在静默模式下,才会触发 case 中的 : 分支。 在默认模式下,缺少参数也会变成 ?(和无效选项一样),无法区分。

字符问题

shell变量

  1. var_name="runoob",变量名和等号之间不能有空格,因为空格通常用于分隔命令和参数
  2. 使用变量:$var_name${var_name},加大括号是为了帮助解释器识别变量的边界。$ 相当于从变量中取值(解引用),不加 $ 则表示变量本身
  3. 只读变量:readonly var_name,变量值不可被改变
  4. 删除变量:unset var_name,删除后不能再次使用,不能删除只读变量

变量类型

字符串变量

var1='zmy_'$var_name'_flm' # 输出 zmy_runoob_flm
var2='zmy_$var_name_flm' # 输出 zmy_$var_name_flm
var1="Hi, "$var_name"! \n" # 输出 Hi, runoob
var2="Hi, ${var_name}! \n" # 输出 Hi, runoob
string="runoob is a great site"
echo `expr index "$string" io` # 输出 4

整数变量

# 使用 (( )) 算术表达式(推荐)
a=2
((a++))      # a 自增 1 → a=3
((a--))      # 自减
((a += 1))   # 等价于 a = a + 1
((a = a + 1))

# 使用 $(( )) 算术扩展
a=2
a=$((a + 1))   # a=3
a=$((a * 2))   # a=6

# 使用 let 命令(较老,不推荐)
a=2
let a=a+1
let "a += 1"   # 带空格时需加引号

数组(不支持多维数组)

# 获取数组的某个值
value=${my_array[n]}

# 使用 @ 或 * 获取数组中所有元素
${my_array[@]}
${my_array[*]}

# 使用 ! 获取数组中所有键
${!my_array[@]}
${!my_array[*]}

# 获取数组元素的个数
length=${#my_array[@]}; 
length=${#my_array[*]}

# 获取单个元素的长度
length=${#my_array[n]}

# 判断关联数组中是否存在键
if [[ -n "${my_array[$key]+exists}" ]]; then
    echo "$key 存在"
fi

if [[ -v "my_array[$key]" ]]; then
    echo "$key 存在"
fi

# 判断普通数组中是否存在值
[[ "${my_array[*]}" =~ "$value" ]] && echo "存在" || echo "不存在"

mapfile

# 如果不指定数组名,数据将默认存储到 MAPFILE 数组中。
mapfile []  < 
# 或
mapfile []  
ℹ️note
  • 回调是在读取过程中触发,不是读取完成后
  • 每次回调时,数组只包含当前已读取的行
  • 执行回调时,会传递两个参数 $1: 当前正在读取的行号,从 0 开始,$2: 当前正在读取的行内容
  • 利用回调时的数组长度(等于 $1 参数)

内置特殊变量

命令行参数变量
进程和状态变量
脚本信息变量
执行环境变量
行和文件描述符
调试相关变量
扩展和匹配变量

注释

:<<EOF(EOF也可以使用其他符号:COMMENT ' !)
注释内容
注释内容
注释内容
EOF

: '
这是注释的部分。
可以有多行内容。
'

参数扩展

shell命令执行顺序

  1. 分词 (Tokenization) # 分割成 tokens: echo, "当前目录: $PWD", *.txt
  2. 展开 (Expansion) # 参数展开在这里发生 ↓ a. 大括号展开 {} # 最先 b. 波浪号展开 ~ # 第二 c. 参数展开 $ # 第三 ← 这里! d. 命令替换 $() `` # 第四 e. 算术展开 $(( )) # 第五 f. 单词分割 # 第六 g. 文件名展开 * ? [...] # 最后
  3. 引用移除 (Quote Removal) # 移除引号
  4. 命令执行 (Execution) # 执行命令
# 无引号:Shell 扩展
echo *.txt           # Shell 扩展
# 单引号:完全字面
echo '*.txt'         # 输出: *.txt
# 双引号:变量扩展,但通配符不扩展
echo "$HOME/*.txt"   # 输出: /home/user/*.txt
❗️important

除了 ${variable:=word}${variable::=word}(强制修改变量为 word 无论原变量是否为空,Bash 4.4+ 新增)之外,其他变量扩展不会修改变量的值,仅是返回扩展结果

💡tip

pattern 必须能够匹配开头(对于 % )或者结尾(对于 # ),如果只匹配了中间部分则无法删除

ℹ️note

其中 /string 部分可省略,表示替换为空,即删除,例如 ${url//:*} 删除 :之后的所有内容

shell运算符

val=`expr 2 + 2` # 运算符要和表达式间要有空格,不能是 expr 2+2,写在`(())`中不受此限制
# 假设 a=10,b=20
`expr $a + $b` # 30
`expr $a - $b` # -10
`expr $a \* $b` # 200
`expr $a / $b` # 2
`expr $a % $b` # 取余 0
a=$b # 把b的值赋给a
[ $a == $b ] # 数值比较 false
[ $a != $b ] # true

条件表达式要放在方括号中,并且要有空格[$a==$b]是错误的

[ $a -eq $b ] # a==b equal
[ $a -ne $b ] # a!=b not equal
[ $a -ge $b ] # a≥b great equal
[ $a -le $b ] # a≤b less equal
[ $a -gt $b ] # a>b great than
[ $a -lt $b ] # a<b less than

[ ! false ] # ! 非运算
[ $a -lt $b -a $b -gt $a ] # -a 与运算 and
[ $a -lt $b -o $b -gt $a ] # -o 或运算 or
# 优先级:! > -a > -o

[ $a -lt $b && $b -gt $a ] # && 逻辑的and
[ $a -lt $b || $b -gt $a ] # || 逻辑的or
# a="abc", b="efg"
[ $a = $b ] # a==b 
[ $a != $b ] # a!=b
[ -z $a ] # 字符串长度为0返回true,a.size()==0
[ -n $a ] # 字符串长度不为0返回true,a.size()!=0
[ $a ] # 字符串不为空返回true a.isNotEmpty()
[ -b $file ] # file是块设备文件返回true
[ -c $file ] # file是字符设备文件返回true
[ -d $file ] # file是目录返回true
[ -f $file ] # file是普通文件返回true(既不是目录也不是设备文件)
[ -g $file ] # file设置了SGID位返回true
[ -k $file ] # file设置了粘着位(Sticky Bit)返回true
[ -p $file ] # file是有名管道返回true
[ -u $file ] # file设置了SUID位返回true
[ -r $file ] # file可读返回true
[ -w $file ] # file可写返回true
[ -x $file ] # file可执行返回true
[ -s $file ] # file为不为空返回true(文件大小大于0)
[ -e $file ] # file存在返回true(包括目录)
[ -S $file ] # file是socket文件返回true
[ -L $file ] # file存在并且是一个符号连接返回true
[ -t $fd   ] # 检查文件描述符 fd 是否连接到一个终端
[ -v $var  ] # 检查变量 var 是否设置(Bash 4.2+)
printf "%-10s %-8s %-4s\n"   重kg  
printf "%-10s %-8s %-4.2f\n"   66.1234
printf "%-10s %-8s %-4.2f\n"   48.6543
printf "%-10s %-8s %-4.2f\n"   47.9876
a=100
b=200
if test $[a] -eq $[b]
then 
    echo '两数相等!'
else
    echo '两数不相等!'
fi
# 输出:两数不相等! []执行基本的算数运算:res=$[a+b] res为300

控制流

if语句

# 多行写法
if condition; then
     command1
else
     commandN
fi
# 一行写法
if [ $(ps -ef | grep -c "ssh") -gt 1 ]; then echo 'true'; fi

if [ $a -gt $b ]
then
    echo 'a > b'
elif [ $a -eq $b ]
then
    echo 'a = b'
else
    echo 'a < b'
fi

for循环

# 多行写法
for var in item1 item2 ... itemN
do
    command1
    command2
    ...
done

# 一行写法
for var in item1 item2 ... itemN; do command1; command2... done;

while、until循环

while [until] [ $a -gt $b ] # : 冒号用于无限循环,也可使用 true,for (( ; ; ))
do 
    command
done

while read FILM # read 读取键盘信息,按<Ctrl-D>结束
do
    echo "是的!$FILM 是一个好网站"
done

case语句

case $var in 
var1|var2|var3|...)
    command1
    ...
    ;;
varN)
    command2
    ...
    ;;
*) # 以上未匹配时捕获
    command3
    ...
    ;;
esac

shell函数

# 函数定义
# 通过 $1 $2 ... $n 获取参数,当n>=10时,需使用${n}获取参数
[ function ] fun (){
    action;
    [return int;]
}

# 函数调用 只需用函数名 fun
fun arg1 agr2 ... agrN

shell输入/输出重定向

命令说明
command > file将 stdout 重定向到 file
command >> file将 stdout 追加到 file
command < file将 stdin 重定向到 file
n > file将文件描述符为n的文件重定向到file
n >> file将文件描述符为n的文件追加到file
n>&m将 FD n 输出重定向 FD m 指向的文件,让 n 变成 m 的只写副本(如果 m 是只写)
n<&m将 FD n 输入重定向 FD m 指向的文件,让 n 变成 m 的只读副本(如果 m 是只读)
exec n>&m打开文件描述符 n(复制 m)
exec n>&-关闭文件描述符 n
<<tag abc tag将开始标记 tag 和结束标记 tag 之间的内容 abc 作为输入
ℹ️note
  • 输入输出重定向的底层逻辑是打开一个文件描述符 fd,然后让 FD 0, 1, n 指向这个文件描述符 fd,fd = open("file", O_WRONLY|O_CREAT|O_APPEND) dup2(fd, x)
  • 默认 FD:如果不写 n,>>> 默认操作 FD 1,< 默认操作 FD 0。
  • &m 的理解:& 的作用是 “类型标记”,告诉 Shell 解析器:“后面的数字是文件描述符编号,而不是文件名”。(类似于指针解引用)
  • 命令通常是从 stdin 读取输入,并将输出写入 stdout 或 stderr,重定向改变的是 stdin,stdout 和 stderr 的指向,但命令依然从 std 读取或输出,效果上看是从文件读取和输出,实际是由 std 作为中间连接
# 同时有输入和输出重定向
< input.txt grep "error" > errors.txt 2> /dev/null

# 混合位置
> output.txt < input.txt command args
2> errors.log command < input.txt > output.txt

# 这会修改当前 Shell 进程的 FD 表!
exec 3> /tmp/log.txt

# 后续所有使用 FD 3 的命令都会写入 /tmp/log.txt
echo "line1" >&3
echo "line2" >&3

# 甚至新的命令也受影响(如果它们继承 FD 3)
bash -c 'echo "line3" >&3'  # 也会写入 /tmp/log.txt

# 恢复:关闭 FD 3
exec 3>&-
# 子 Shell 中的重定向
(
    exec 3> /tmp/sub.txt
    echo "in sub" >&3
)

# 父 Shell 的 FD 3 不存在(子 Shell 的 FD 表已销毁)
echo "test" >&3
# 报错:Bad file descriptor
# 管道中的重定向只影响对应命令
cat file.txt | grep "pattern" > out.txt

# cat 的 stdout → 管道
# grep 的 stdin ← 管道,stdout → out.txt
# 两个命令的 FD 表独立,互不影响
# source 在当前 Shell 中执行,不 fork
source script.sh

# 如果 script.sh 中有 exec 重定向,会影响当前 Shell

实用技巧:保存和恢复 FD

# FD 3 也指向 FD 1 指向的文件,相当于记录原 FD 1 的指向(复制 FD 1)
exec 3>&1

# 现在可以安全地重定向 FD 1
echo "to file" > /tmp/output.txt

# 让 FD 1 再指向 FD 3 所指向的文件,相当于恢复 FD 1 原来的指向
exec 1>&3

# 关闭 FD 3
exec 3>&-

exec

echo "hello" > >(cat)  # 实际步骤:
# 1. 创建 /dev/fd/63
# 2. echo "hello" → /dev/fd/63
# 3. cat 从 /dev/fd/63 读取并输出

进程替换和管道对比

# 比较两个命令的输出
diff <(command1) <(command2)

# 将输出同时传递给多个命令
# tee从 log.txt读取数据,并同时输出到所有进程替换的临时文件描述符
# 同时输出到终端
tee >(grep "error" > errors.txt) >(grep "warning" > warnings.txt) < log.txt
# 相当于 
cat log.txt|tee >(grep "error" > errors.txt) >(grep "warning" > warnings.txt)
💡tip
  • 可以将进程替换视为一个临时文件
  • ls > >(grep "txt") 中,>(grep "txt") 会生成 /dev/fd/63,ls 的结果输出通过此文件,grep 从此文件中查找 txt。
  • cat <(echo "hello") 中,<(echo "hello") 会生成 /dev/fd/45echo "hello" 的结果写入此文件,cat 读取此文件

管道、HereDocument, HereString, - stdin

管道 (|):内核级的数据通道

HereDocument 和 HereString

写法行为场景
<<EOF保留所有前导空白(Tab 和空格)。内容中的缩进会原样保留。需要严格保持格式(如 Python 代码、Makefile、YAML)。
<<-EOF仅去除前导 Tab 字符,保留空格。允许你在脚本中对 Here Doc 内容进行缩进以提高可读性。大多数 Shell 脚本场景,希望代码整洁且内容不受缩进影响。
<< 'EOF'禁止变量展开和命令替换,但保留所有前导空白。内容包含 $、` 或 \ 且不希望被 Shell 解析时。
<<-'EOF'禁止变量展开,且仅去除前导 Tab 字符。最推荐的通用写法:既整洁又安全。
💡tip
  • - 决定是否删除前导tab
  • ' 单引号决定内容是否展开

- 的特殊性

彩色输出