Skip to content

Bash 速查表

用于脚本编程的 Unix shell 和命令语言。

01

变量与字符串

变量与赋值

Bash 变量默认是无类型字符串;如果值是数字,算术运算可以工作。绝不在赋值中的 = 周围放空格——'name = Alice' 尝试运行名为 name 的命令。使用 $(...) 进行命令替换(优先于反引号)。export 使变量对子进程可用;readonly 使其不可变。环境变量(导出的)被您启动的脚本和程序继承。

bash
# no spaces around = in assignments
name="Alice"
age=30
PI=3.14
active=true

# use variables with $ prefix
echo "Hello, $name!"
echo "Age: $age"

# command substitution: capture command output
today=$(date +%Y-%m-%d)
files=$(ls | wc -l)
echo "Today is $today, $files files"

# readonly and environment variables
readonly MAX=100
export PATH="$PATH:/opt/bin"
env | grep PATH

字符串参数扩展

参数扩展是 Bash 最强大的功能之一,包含在 ${...} 中。${#var} 给出长度;${var:offset:length} 提取子字符串(负偏移量从末尾计数——注意 - 前面的空格)。,, 和 ^^ 转换大小写(Bash 4+)。:- 提供默认值而不设置变量;:= 作为副作用设置它;:? 以错误中止——非常适合脚本中的必需参数检查。

bash
s="Hello World"
echo ${#s}                # length: 11
echo ${s:0:5}             # substring: Hello
echo ${s:6}               # from index 6: World
echo ${s: -5}             # last 5 chars: World

# case conversion (Bash 4+)
echo ${s,,}               # lowercase: hello world
echo ${s^^}               # uppercase: HELLO WORLD
echo ${s^l}               # capitalize first if 'l': Hello World

# default values
echo ${name:-Guest}       # use Guest if name unset
echo ${name:=Guest}       # assign Guest if unset, then use
echo ${name:?required}    # error and exit if unset

字符串搜索与替换

/ 模式替换;// 替换所有;# 在开头匹配;% 在末尾匹配。## 和 %% 是贪婪的(最长匹配),而 # 和 % 是非贪婪的。这些对于路径操作至关重要:${path##*/} 提取基名(如 basename 命令),${path%/*} 提取目录(如 dirname)。掌握这些可以避免许多对外部命令(如 sed 和 cut)的调用。

bash
s="Hello World Hello"

# replace first occurrence
echo ${s/Hello/Hi}        # Hi World Hello

# replace all occurrences
echo ${s//Hello/Hi}       # Hi World Hi

# replace at beginning / end
echo ${s/#Hello/Hi}       # Hi World Hello (prefix)
echo ${s/%Hello/Hi}       # Hello World Hello (no suffix match)

# delete patterns (replace with empty)
echo ${s//Hello/}         #  World 
path="/usr/local/bin/app"
echo ${path##*/}          # app (longest prefix)
echo ${path%/*}           # /usr/local/bin (shortest suffix)

# extract filename and extension
file="report.tar.gz"
echo ${file%.*}           # report.tar (remove ext)
echo ${file##*.}          # gz (keep ext)

引用与特殊字符

单引号保留一切字面意义——没有变量扩展,没有转义处理(您甚至不能在单引号内包含单引号)。双引号允许 $、反引号和 \ 扩展,同时保留空格和特殊字符——几乎总是优先在变量周围使用双引号以防止分词和 globbing 错误。使用 $'...' 处理 ANSI-C 转义,如 \n 和 \t。

bash
# single quotes: literal, no expansion
echo 'Hello $USER'         # Hello $USER

# double quotes: expansion happens
echo "Hello $USER"         # Hello alice

# escape special chars with backslash
echo "Price: \\$5.00"      # Price: $5.00
echo 'It\'s a test'       # It's a test (escape inside single)

# concatenate strings
a="Hello"
b="World"
c="$a, $b!"
echo $c                    # Hello, World!

# multi-line string
msg="Line 1
Line 2
Line 3"
echo "$msg"

Read 与用户输入

read 捕获用户输入。-p 显示提示,-t 设置超时(返回非零),-a 读入数组。逐行读取文件时,始终使用 'IFS= read -r'——IFS= 保留前导/尾随空格,-r 禁用反斜杠转义以便反斜杠字面保留。这是逐行处理文件的规范安全模式。

bash
# read a single value
echo -n "Enter your name: "
read name
echo "Hi, $name!"

# read multiple values
read -p "First and last: " first last

# read with timeout (seconds)
read -t 5 -p "Quick! " answer || echo "too slow"

# read into an array
echo "Enter colors:"
read -a colors
echo "First: ${colors[0]}"

# read line by line from a file
while IFS= read -r line; do
    echo ">>> $line"
done < file.txt
02

数组

索引数组

Bash 数组是零索引的。@ 和 * 展开为所有元素——始终引用它们("${arr[@]}")以正确处理带空格的元素。${#arr[@]} 给出计数。+= 追加。unset 在索引中留下间隙;要重新索引,使用 arr=("${arr[@]}")。使用 ${arr[@]:start:count} 切片返回子集。

bash
# create and populate
fruits=("apple" "banana" "cherry")
fruits[3]="date"

# access elements
echo ${fruits[0]}         # apple
echo ${fruits[@]}         # all: apple banana cherry date
echo ${fruits[*]}         # same as @ in most contexts

# count and slice
echo ${#fruits[@]}        # count: 4
echo ${fruits[@]:1:2}     # elements 1-2: banana cherry

# append
fruits+=("elderberry")

# iterate
for f in "${fruits[@]}"; do
    echo "- $f"
done

# delete an element
unset fruits[1]            # removes "banana" (gap remains)

关联数组(映射)

关联数组(declare -A)将字符串键映射到值,就像其他语言中的字典——在 Bash 4+ 中可用。使用 ${!arr[@]} 获取键,${arr[@]} 获取值。-v 测试检查键是否存在。与索引数组不同,键是任意字符串,因此引用带空格的键至关重要。这些需要 Bash 4+(macOS 默认搭载 Bash 3)。

bash
# must declare before use (Bash 4+)
declare -A ages

# key-value assignment
ages[alice]=30
ages[bob]=25
ages["carol smith"]=28

# access by key
echo ${ages[alice]}       # 30
echo ${ages[bob]}         # 25

# all keys and values
echo ${!ages[@]}          # keys: alice bob carol smith
echo ${ages[@]}           # values: 30 25 28
echo ${#ages[@]}          # count: 3

# iterate keys
for name in "${!ages[@]}"; do
    echo "$name is ${ages[$name]}"
done

# check if key exists
if [[ -v ages[alice] ]]; then
    echo "alice exists"
fi

数组迭代模式

有三种主要迭代样式:按值(for x in "${arr[@]}")、按索引(for i in "${!arr[@]}")和 C 风格。当您需要位置时,按索引形式很有用。始终引用 "${arr[@]}" 以便包含空格的元素作为单个项保留。在循环中使用 += 构建数组是累积结果的惯用方式。

bash
arr=(one two three four five)

# iterate by value
for item in "${arr[@]}"; do
    echo "$item"
done

# iterate by index
for i in "${!arr[@]}"; do
    echo "[$i] = ${arr[$i]}"
done

# C-style indexed loop
for ((i=0; i<${#arr[@]}; i++)); do
    echo "${arr[$i]}"
done

# map/transform: build a new array
upper=()
for w in "${arr[@]}"; do
    upper+=("$(echo $w | tr a-z A-Z)")
done
echo "${upper[@]}"

从命令输出创建数组

mapfile/readarray(Bash 4+)一次性将行读入数组——对于大文件比 while 循环快得多。要拆分分隔字符串,设置 IFS 并使用 read -ra。对于文件名(可能包含空格或换行符),始终使用 find -print0 与 read -d '' 一起在 null 字节上拆分——文件名唯一安全的分隔符。< <(...) 进程替换为循环提供输入而不创建子 shell。

bash
# split a string into an array
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo ${parts[2]}          # c

# read lines of a file into an array
mapfile -t lines < file.txt
echo "${lines[0]}"        # first line
echo "${#lines[@]}"       # line count

# alternative: readarray (same as mapfile)
readarray -t lines2 < file.txt

# from command output (words split on IFS)
files=($(ls *.txt))
echo "${files[@]}"

# safer: read null-delimited output
while IFS= read -r -d '' f; do
    files+=("$f")
done < <(find . -name "*.txt" -print0)

特殊变量

这些特殊变量对于脚本编写至关重要。$0 是脚本路径;$1-$9 是位置参数(两位数使用 ${10})。始终引用 "$@" 以保留带空格的参数——"$*" 将它们连接成一个字符串。$? 给出退出状态(0 = 成功),是所有错误检查的基础。$$ 和 $! 对于 PID 文件和进程管理很有用。shift 丢弃 $1 并将其余的向下移动。

bash
# positional parameters
echo $0       # script name
echo $1       # first argument
echo $#       # argument count
echo $@       # all arguments (separate words)
echo "$@"     # all arguments (preserves quoting)
echo $*       # all arguments (single string)

# process and shell info
echo $$       # current shell PID
echo $!       # PID of last background command
echo $?       # exit status of last command (0 = success)
echo $-       # current shell option flags
echo $_       # last argument of previous command

# shift arguments
echo "$1 $2"  # a b
shift
echo "$1 $2"  # b c

# iterate all args safely
for arg in "$@"; do
    echo "arg: $arg"
done
03

控制流与测试

If / Elif / Else

[ ] 是 POSIX 测试命令(可移植但有限);[[ ]] 是 Bash 的增强版本,支持模式匹配(== 带通配符)、正则表达式(=~)、&& / || 运算符,并且无需引用变量。在 Bash 脚本中优先使用 [[ ]]。始终在 [ ] 中引用变量以避免空值或包含空格的值导致语法错误。-f、-r、-d、-e 测试检查文件存在和属性。

bash
# basic if
if [ "$age" -ge 18 ]; then
    echo "adult"
elif [ "$age" -ge 13 ]; then
    echo "teen"
else
    echo "child"
fi

# modern [[ ]] test (Bash-specific, safer)
if [[ $name == "Alice" ]]; then
    echo "hi Alice"
fi

# pattern matching in [[ ]]
if [[ $file == *.txt ]]; then
    echo "text file"
fi

# regex matching
if [[ $email =~ ^[a-z]+@[a-z]+.[a-z]+$ ]]; then
    echo "valid email"
fi

# multiple conditions
if [[ -f file.txt && -r file.txt ]]; then
    echo "readable file"
fi

测试运算符

文件测试(-f、-d、-r 等)检查文件系统属性。字符串测试使用 = 和 !=(或在 [[ ]] 中使用 ==);-z 检查空,-n 检查非空。整数比较使用 -eq、-ne、-lt、-gt、-le、-ge——绝不在 [ ] 中对整数使用 < >(它们会重定向!)。在 [[ ]] 和 (( )) 中您可以使用熟悉的 < > <= >= 运算符。记住这些运算符对于编写正确的条件语句至关重要。

bash
# file tests
[ -f file ]       # exists and is regular file
[ -d dir ]        # exists and is directory
[ -e path ]       # exists (any type)
[ -r file ]       # readable
[ -w file ]       # writable
[ -x file ]       # executable
[ -s file ]       # exists and is non-empty
[ file1 -nt file2 ]  # newer than
[ file1 -ot file2 ]  # older than

# string tests
[ -z "$s" ]       # empty string
[ -n "$s" ]       # non-empty string
[ "$a" = "$b" ]   # equal (use == in [[ ]])
[ "$a" != "$b" ]  # not equal

# integer tests
[ $n -eq 5 ]      # equal
[ $n -ne 5 ]      # not equal
[ $n -lt 5 ]      # less than
[ $n -gt 5 ]      # greater than
[ $n -le 5 ]      # less or equal
[ $n -ge 5 ]      # greater or equal

Case 语句

case 是 Bash 中 switch 的等价物——它将值与 glob 模式匹配。| 分隔替代项。;; 结束分支(如 break)。*) 模式是默认项。模式支持通配符(*、?、[abc])但不支持完整正则表达式。对于分派单个值,case 比长 if-elif 链更干净,它是构建命令行子命令(start/stop/restart)的标准方式。

bash
case $1 in
    start)
        echo "Starting service..."
        systemctl start app
        ;;
    stop)
        echo "Stopping service..."
        systemctl stop app
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    status)
        systemctl status app
        ;;
    --help|-h)
        echo "Usage: $0 {start|stop|restart|status}"
        ;;
    *)
        echo "Unknown command: $1" >&2
        exit 1
        ;;
esac

# pattern matching in case
case $file in
    *.jpg|*.png)    echo "image" ;;
    *.mp3|*.wav)    echo "audio" ;;
    *.txt)          echo "text" ;;
    *)              echo "other" ;;
esac

算术与 (( ))

$(( )) 计算算术表达式并返回结果。在 (( )) 内,变量名不需要 $——只需使用名称。(( )) 作为命令在结果非零时返回退出状态 0(真),使其非常适合数字条件。Bash 只做整数算术;对于浮点使用 bc 或 awk。运算符:+ - * / % ** 和 C 风格比较以及位运算符都可以工作。

bash
# arithmetic expansion
x=5
y=$((x + 3))
echo $y              # 8
echo $((x * 2))      # 10
echo $((2 ** 10))    # 1024 (exponent)
echo $((17 % 5))     # 2 (modulo)
echo $((17 / 5))     # 3 (integer division)

# (( )) condition: no $ needed, returns exit status
x=10
if (( x > 5 && x < 20 )); then
    echo "in range"
fi

# increment / decrement
((x++))             # post-increment
((x--))             # post-decrement
((++x))             # pre-increment

# bitwise operators
echo $((5 & 3))     # 1 (AND)
echo $((5 | 2))     # 7 (OR)
echo $((5 << 1))    # 10 (left shift)

短路与三元

&& 和 || 是短路运算符,兼作简洁的控制流:'cmd1 && cmd2' 仅在 cmd1 成功时运行 cmd2;'cmd1 || cmd2' 仅在 cmd1 失败时运行 cmd2。这在一行中替换简单的 if-else。'A && B || C' 模式模仿三元,但如果 B 可能失败则有微妙的 bug——对于健壮的代码,使用真正的 if 语句。冒号 (:) 是无操作命令,对于空分支很有用。

bash
# && and || as short-circuit control flow
[[ -f file.txt ]] && echo "exists"
[[ -f file.txt ]] || echo "missing"

# combined: do this OR fail
mkdir -p build && cd build && make || exit 1

# ternary-like (Bash doesn't have a real ternary)
result=$([[ $x -gt 0 ]] && echo "positive" || echo "non-positive")

# default value via &&
echo ${name:-"anonymous"}

# run command, fallback on failure
ping -c1 host && echo "up" || echo "down"

# null command as no-op
if condition; then
    :    # do nothing
fi
04

循环与迭代

For 循环

for 循环迭代单词列表。大括号扩展 {1..5} 生成序列(带可选步长 {start..end..step})。C 风格 for ((init; cond; update)) 最适合数字计数器。用 *.txt 迭代文件时,未引用的 glob 安全展开,但如果没有文件匹配,您会得到字面值 '*.txt'——启用 shopt -s nullglob 以获得空列表。

bash
# iterate a list
for fruit in apple banana cherry; do
    echo "$fruit"
done

# iterate an array
colors=("red" "green" "blue")
for c in "${colors[@]}"; do
    echo "Color: $c"
done

# range with brace expansion
for i in {1..5}; do
    echo "Number: $i"
done

# range with step
for i in {0..20..2}; do echo $i; done    # 0 2 4 ... 20

# C-style for loop
for ((i=0; i<5; i++)); do
    echo "Iteration $i"
done

# iterate command output
for file in *.txt; do
    echo "Processing $file"
done

While 与 Until 循环

while 在条件成功(退出 0)时运行;until 运行直到成功——它们是相反的。'while read' 模式是逐行处理文件的规范方式;始终使用 'IFS= read -r' 以确保安全。注意管道到 while 循环会在子 shell 中运行它,因此变量更改不会持久——当您需要保留变量时使用进程替换 '< <(cmd)' 代替。

bash
# while: runs while condition is true
count=0
while [ $count -lt 5 ]; do
    echo "Count: $count"
    ((count++))
done

# read lines from a file
while IFS= read -r line; do
    echo "Line: $line"
done < input.txt

# infinite loop with break
while true; do
    read -p "Quit? (y/n) " ans
    [[ $ans == "y" ]] && break
done

# until: runs until condition is true (opposite of while)
n=0
until [ $n -ge 3 ]; do
    echo "n=$n"
    ((n++))
done

# while reading from a pipeline
seq 1 5 | while read n; do
    echo "got $n"
done

Break、Continue 与 Select

break 退出循环(break N 退出 N 个嵌套循环);continue 跳到下一次迭代。select 从列表创建交互式编号菜单——它循环直到被 break。这些控制语句在 for、while 和 until 循环中工作。break N 形式对于逃离深度嵌套循环至关重要,但应谨慎使用,因为它会使代码更难理解。

bash
# break exits the loop
for i in {1..10}; do
    [[ $i -eq 5 ]] && break
    echo $i       # prints 1 2 3 4
done

# continue skips to next iteration
for i in {1..6}; do
    (( i % 2 == 0 )) && continue
    echo $i       # prints 1 3 5 (odd only)
done

# break out of nested loops
for i in 1 2 3; do
    for j in a b c; do
        [[ $j == "b" ]] && break 2   # break both loops
        echo "$i$j"
    done
done

# select menu
select opt in "Start" "Stop" "Quit"; do
    case $opt in
        Start) echo "starting" ;;
        Stop)  echo "stopping" ;;
        Quit)  break ;;
        *)     echo "invalid" ;;
    esac
done

安全地循环文件

解析 ls 输出是一个经典错误——带空格、换行符或特殊字符的文件名会破坏它。在 for 循环中直接使用 shell glob(*.txt),并引用变量。对于递归或复杂搜索,使用 find -print0 管道到 'read -d '''——null 字节是文件名唯一安全的分隔符。启用 nullglob 以便不匹配的 glob 产生空列表而不是字面模式。

bash
# BAD: breaks on filenames with spaces
for f in $(ls *.txt); do
    cat "$f"
done

# GOOD: glob expands safely
for f in *.txt; do
    [[ -f "$f" ]] || continue
    cat "$f"
done

# BEST: handle spaces, newlines, and no matches
shopt -s nullglob dotglob
for f in *.txt; do
    echo "Processing: $f"
done

# find with -print0 and while read -d ''
while IFS= read -r -d '' f; do
    echo "Found: $f"
done < <(find . -type f -name "*.txt" -print0)

# process files modified today
for f in $(find . -mtime -1 -print); do
    echo "Recent: $f"
done

范围与序列生成

大括号扩展 {a..b} 在解析时生成序列——快速且内置。它支持字母、数字、零填充和步长({start..end..step})。seq 是具有更多格式化选项(-f 用于 printf 风格)的外部命令。大括号扩展还创建多个参数:echo file{1..3}.{txt,log} 生成 6 个文件名。对于简单范围优先使用大括号扩展;当您需要自定义格式化时使用 seq。

bash
# brace expansion (not a loop, but generates lists)
echo {1..10}                 # 1 2 3 4 5 6 7 8 9 10
echo {a..e}                  # a b c d e
echo {01..10}                # zero-padded: 01 02 ... 10
echo file{1..3}.txt          # file1.txt file2.txt file3.txt

# seq command
seq 1 5                      # 1 2 3 4 5
seq 1 2 10                   # 1 3 5 7 9 (step 2)
seq -f "%03d" 1 5            # 001 002 003 004 005

# use seq in a for loop
for i in $(seq 1 5); do
    echo $i
done

# generate a list of dates
for d in $(seq -f "2024-01-%02g" 1 31); do
    echo "$d"
done
05

函数

定义与调用函数

Bash 中的函数用 name() 或 function name 定义。用空格分隔的参数(无括号)按名称调用它们。在函数内部始终使用 'local' 声明变量以避免污染全局作用域——没有它,赋值会泄漏出去。Bash 函数不能直接返回值(return 是退出状态 0-255);要返回字符串,echo 它并用 $() 捕获。

bash
# two equivalent syntaxes
greet() {
    echo "Hello, $1!"
}
function greet2() {
    echo "Hi, $1!"
}

# call with arguments (no parens)
greet "Alice"               # Hello, Alice!
greet2 "Bob"                # Hi, Bob!

# local variables (function-scoped)
counter() {
    local count=0
    ((count++))
    echo $count
}

# return a value via echo + command substitution
add() {
    echo $(($1 + $2))
}
result=$(add 3 4)           # 7

参数与形参

在函数内部,$1、$@、$# 指的是函数的参数,而不是脚本的——它们遮蔽脚本级参数。$FUNCNAME 持有函数名(对于调试很有用)。函数内部的 shift 只影响函数的参数。要将脚本的参数传递给函数,使用 func "$@"。这种遮蔽是为什么函数是可重用的构建块。

bash
# all positional params work inside functions
show_args() {
    echo "Function name: $FUNCNAME"
    echo "First arg:  $1"
    echo "Second arg: $2"
    echo "All args:   $@"
    echo "Arg count:  $#"
}

show_args a b c
# Function name: show_args
# First arg:  a
# Second arg: b
# All args:   a b c
# Arg count:  3

# pass all script args to a function
process "$@"

# shift within a function (local to it)
parse() {
    while (($#)); do
        echo "arg: $1"
        shift
    done
}

返回值与退出状态

Bash 的 'return' 只设置退出状态(0-255),因此函数兼作布尔测试。要返回字符串,echo 它并用 $() 捕获。对于多个值,使用 nameref(local -n)——Bash 4.3+——它允许函数按名称分配给调用者的变量。这是返回复杂数据的最干净方式。避免使用全局变量作为返回值,因为它使函数不可重入。

bash
# return sets exit status (0-255), not a value
is_even() {
    if (($1 % 2 == 0)); then
        return 0    # true/success
    else
        return 1    # false/failure
    fi
}

# use as a condition
if is_even 4; then
    echo "4 is even"
fi

# chain with && and ||
is_even 4 && echo "yes" || echo "no"

# return a string via echo (capture with $())
to_upper() {
    echo "${1^^}"
}
UP=$(to_upper hello)        # HELLO

# return multiple values via global or nameref
set_pair() {
    local -n _r=$1
    _r=(10 20)
}
set_pair result
echo "${result[@]}"       # 10 20

变量作用域

Bash 中的变量默认是全局的——即使在函数内部赋值也是如此!这是常见的 bug 来源。对于函数内部变量始终使用 'local'。'declare -g' 从函数内部显式创建全局变量。递归可以工作但在 Bash 中很慢(每次调用为 $() 分叉子 shell);谨慎使用。阶乘示例显示了模式:局部变量 + 递归调用 + 算术。

bash
# global by default
x=1
modify() {
    x=2          # changes the GLOBAL x!
    local y=3    # local to function
    echo "inside: x=$x y=$y"
}

modify
echo "outside: x=$x"       # x=2 (modified!)
echo "outside: y=$y"       # empty (y is local)

# declare -g for global inside a function
set_global() {
    declare -g NEW_VAR=42
}
set_global
echo $NEW_VAR              # 42

# recursion with local
factorial() {
    local n=$1
    if (( n <= 1 )); then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}
echo $(factorial 5)        # 120

库与 Source

source(或 .)在当前 shell 中执行文件,因此其函数和变量变得可用——这就是您构建可重用库的方式。常见模式是带有辅助函数的 utils.sh,脚本 source 它。与执行脚本(在子 shell 中运行)不同,sourced 代码可以修改调用者的环境。这也是 .bashrc 和 .bash_profile 工作的方式——它们在 shell 启动时被 source。

bash
# utils.sh — a reusable library
#!/bin/bash
log() {
    echo "[$(date +%H:%M:%S)] $*" >&2
}

die() {
    log "ERROR: $*"
    exit 1
}

confirm() {
    read -p "$1 [y/N] " ans
    [[ $ans =~ ^[Yy]$ ]]
}

# main.sh — use the library
source ./utils.sh        # or . ./utils.sh

log "Starting script"
confirm "Continue?" || die "aborted"
log "Done"

# source vs execute
# source: runs in current shell (shares variables)
# ./script.sh: runs in a subshell (isolated)
06

文本处理

grep — 模式搜索

grep 查找匹配模式的行。-i 忽略大小写,-v 反转,-n 显示行号,-r 递归,-E 使用扩展正则表达式(如 +、|、{})。-A/-B/-C 显示匹配周围的上下文行——对于理解错误非常有价值。在递归搜索期间使用 --include 过滤文件类型。如果找到匹配,grep 返回退出状态 0,使其在条件中很有用:if grep -q pattern file; then...

bash
# basic search
grep "error" app.log
grep -i "error" app.log          # case-insensitive
grep -v "debug" app.log          # invert (non-matching lines)
grep -c "error" app.log          # count matches
grep -n "error" app.log          # line numbers
grep -w "error" app.log          # whole word only

# recursive search
grep -rn "TODO" src/             # recursive + line numbers
grep -rl "TODO" src/             # files with matches only

# extended regex (or use egrep)
grep -E "error|warn|fatal" log
grep -E "^[0-9]{4}-" log         # lines starting with date

# context lines
grep -A 2 "error" log            # 2 lines after
grep -B 2 "error" log            # 2 lines before
grep -C 2 "error" log            # 2 lines before and after

# search multiple files
grep "function" *.js --include="*.js" -r .

sed — 流编辑器

sed 非交互式地编辑文本流。s 命令替换;g 使其全局。-i 就地编辑(始终先不带 -i 测试!)。分隔符可以是任何字符——对于路径使用 | 或 # 以避免转义斜杠。sed 逐行处理,因此多行操作需要 N 命令或保留空间。对于复杂编辑,awk 或 perl 可能更清晰。始终引用 sed 脚本以防止 shell 扩展。

bash
# substitute (replace)
sed 's/old/new/' file.txt           # first occurrence per line
sed 's/old/new/g' file.txt          # all occurrences (global)
sed 's/old/new/3' file.txt          # 3rd occurrence only
sed 's/old/new/gi' file.txt         # global + case-insensitive

# in-place editing
sed -i 's/foo/bar/g' file.txt       # edit file in place
sed -i.bak 's/foo/bar/g' file.txt   # keep a backup

# delete lines
sed '/^#/d' file.txt                # delete comment lines
sed '/^$/d' file.txt                # delete blank lines
sed '5d' file.txt                   # delete line 5
sed '5,10d' file.txt                # delete lines 5-10

# print specific lines
sed -n '10,20p' file.txt            # print lines 10-20
sed -n '/pattern/p' file.txt        # print matching lines

# use a different delimiter for paths
sed 's|/usr/local|/opt|g' paths.txt

# multiple commands
sed -e 's/a/A/g' -e 's/b/B/g' file.txt

awk — 列处理

awk 是用于列数据的小型编程语言。$1、$2... 是字段;$0 是整行;$NF 是最后一个字段。-F 设置输入分隔符;OFS 设置输出。BEGIN 在处理之前运行,END 在之后运行。NR 是记录(行)号;NF 是字段计数。awk 非常适合 CSV/TSV 处理、日志分析和生成报告——对于简单提取之外的任何内容,比 cut 强大得多。

bash
# print columns (default separator: whitespace)
awk '{print $1}' file.txt           # first column
awk '{print $1, $3}' file.txt       # columns 1 and 3
awk '{print $NF}' file.txt          # last column
awk -F: '{print $1}' /etc/passwd    # split on :

# filter and print
awk '$3 > 100' file.txt             # lines where col 3 > 100
awk '$1 == "ERROR" {print $2}' log  # col 2 of ERROR lines
awk 'NR == 5' file.txt              # 5th line only
awk 'NR >= 10 && NR <= 20' file.txt # lines 10-20

# BEGIN and END blocks
awk 'BEGIN {print "Start"} {sum += $1} END {print sum}' nums.txt

# field separator and output
awk -F, 'BEGIN {OFS="|"} {print $1, $2}' csv.txt

# count lines matching a pattern
awk '/error/' file.txt              # like grep
awk '/error/ {count++} END {print count}' log

# built-in variables: NR (row), NF (fields), FS, OFS
awk '{print NR, NF, $0}' file.txt

cut、tr、sort 与 uniq

cut 是简单的字段/字符提取——快速但有限(不支持引用)。tr 转换字符(非常适合大小写转换或分隔符交换),-d 删除。sort 排序行(-n 数字,-r 反向,-k 字段)。uniq 只移除相邻的重复项,因此始终先通过 sort 管道。uniq -c 与 sort -rn 是频率分析的经典模式:'sort | uniq -c | sort -rn' 显示最常见的行。

bash
# cut: extract fields or characters
cut -d: -f1 /etc/passwd            # field 1, delimiter :
cut -c1-5 file.txt                 # characters 1-5
cut -d, -f2,3 data.csv             # fields 2 and 3

# tr: translate or delete characters
echo "Hello" | tr 'a-z' 'A-Z'      # HELLO
echo "a,b,c" | tr ',' ' '          # a b c
echo "hello" | tr -d 'l'           # heo (delete)
tr -s ' ' < file.txt               # squeeze repeated spaces

# sort lines
sort file.txt                      # alphabetical
sort -n nums.txt                   # numeric
sort -rn nums.txt                  # reverse numeric
sort -t: -k3 -n /etc/passwd        # by 3rd field, numeric
sort -u file.txt                   # sort + unique

# uniq: filter or count adjacent duplicates
sort file.txt | uniq               # unique lines
sort file.txt | uniq -c            # count occurrences
sort file.txt | uniq -d            # only duplicates
sort file.txt | uniq -u            # only unique lines

管道与重定向

管道将 stdout 连接到 stdin,构建强大的管道。> 重定向 stdout(>> 追加),2> 重定向 stderr,&> 捕获两者。Here-doc(<<EOF)提供多行字符串;引用分隔符('EOF')禁用变量扩展。进程替换 <(cmd) 将命令的输出视为临时文件——对于在没有子 shell 的情况下提供 while 循环以及对于期望文件的命令(如 diff)至关重要。

bash
# pipe: chain commands (stdout of one -> stdin of next)
cat file | grep "x" | sort | uniq -c | sort -rn | head

# redirect stdout
echo "hello" > file.txt            # overwrite
echo "world" >> file.txt           # append

# redirect stderr
command 2> error.log               # stderr to file
command 2>&1                       # stderr to stdout
command > all.log 2>&1             # both to same file
command &> all.log                 # both (Bash shortcut)

# stdin from file
command < input.txt

# here-string
grep "x" <<< "some text with x"

# here-document (multi-line input)
cat << EOF
Line 1
Line 2 with $VAR expansion
EOF

# quoted delimiter disables expansion
cat << 'EOF'
Literal $VAR no expansion
EOF

# process substitution
diff <(ls dir1) <(ls dir2)         # compare outputs
while read line; do echo "$line"; done < <(grep x file)
07

文件与目录操作

find — 文件搜索

find 是最强大的文件搜索工具。-name 匹配 glob(使用 -iname 进行不区分大小写);-type 按 f/d/l 过滤;-mtime/-mmin 按修改时间过滤(- = 在之内,+ = 早于);-size 按大小过滤。-exec 对每个结果运行命令;{} 是文件名,\; 每个文件运行,+ 批处理它们。-delete 比 -exec rm 更安全地删除。始终先不带 -delete 测试 find!

bash
# search by name
find . -name "*.js"
find . -iname "*.JS"               # case-insensitive
find / -name "*.conf" 2>/dev/null  # suppress permission errors

# search by type
find . -type f -name "*.txt"       # files only
find . -type d -name src           # directories only
find . -type l                     # symlinks

# search by time and size
find . -mtime -7                   # modified < 7 days ago
find . -mtime +30                  # modified > 30 days ago
find . -mmin -60                   # modified < 60 min ago
find . -size +10M                  # larger than 10MB
find . -size -1k                   # smaller than 1KB
find . -empty                      # empty files/dirs

# act on results (-exec)
find . -name "*.log" -exec rm {} \;
find . -name "*.js" -exec wc -l {} +
find . -type f -exec chmod 644 {} \;

# safe deletion with -delete
find /tmp -type f -mtime +7 -delete

权限与所有权

权限是三个三元组:所有者、组、其他。每个数字是 r(4)+w(2)+x(1),所以 755 = rwxr-xr-x。chmod 更改权限;chown 更改所有者/组。符号表示法(u+x、g-w)对于增量更改更清晰。umask 为新文件设置默认权限(文件从 666 减去,目录从 777 减去)。对于 Web 服务器,目录 755 和文件 644 是标准;在生产中绝不要使用 777。

bash
# view permissions
ls -l file.txt
# -rw-r--r-- 1 user group 1024 Jan 1 10:00 file.txt
#  ^^^ ^^^ ^^^
#  owner group others

# chmod: change permissions
chmod 755 script.sh        # rwxr-xr-x
chmod 644 file.txt         # rw-r--r--
chmod +x script.sh         # add execute for all
chmod u+x,g-w file.txt     # symbolic: user +x, group -w
chmod -R 755 directory/    # recursive

# chown: change owner
chown alice file.txt
chown alice:developers file.txt
chown -R alice:developers project/

# umask: default permissions for new files
umask                      # show current (e.g. 022)
umask 077                  # new files: 600, dirs: 700

# numeric permission reference:
# 7=rwx 6=rw 5=rx 4=r 3=wx 2=w 1=x 0=none

复制、移动、移除与链接

cp -r 递归复制目录;-i 在覆盖之前提示(更安全);-p 保留时间戳和权限。mv 既是移动也是重命名。rm -rf 很危险——它递归删除而不提示;始终仔细检查路径。硬链接指向相同的 inode(相同文件,不能跨文件系统或链接目录);符号链接是路径引用(可以链接任何东西,但如果目标移动则断开)。使用 ln -s 创建符号链接。

bash
# copy
cp file.txt backup.txt
cp -r src/ dest/            # recursive (directories)
cp -i file.txt dest/        # interactive (prompt before overwrite)
cp -u file.txt dest/        # update (only if newer)
cp -p file.txt dest/        # preserve attributes

# move / rename
mv old.txt new.txt
mv file.txt /other/dir/
mv -i file.txt dest/        # prompt before overwrite

# remove
rm file.txt
rm -r directory/            # recursive
rm -f file.txt              # force (no error if missing)
rm -rf node_modules/        # force + recursive (DANGER)

# links
ln target hardlink          # hard link (same inode)
ln -s target symlink        # symbolic link (path reference)
ln -sf target symlink       # force recreate symlink

# view link target
readlink symlink
readlink -f symlink         # canonical absolute path

归档与压缩

tar 将文件捆绑到一个归档中;gzip/bzip2/xz 压缩它。标志:c=创建,x=提取,t=列出,f=文件,z=gzip,j=bzip2,J=xz,v=详细。.tar.gz 是 Unix 标准;.zip 在 Windows 上常见。bzip2 比 gzip 压缩更好但更慢;xz 最好但最慢。使用 -C 提取到特定目录。-k 标志在压缩时保留原始文件。

bash
# tar + gzip (most common)
tar -czf archive.tar.gz dir/      # create
tar -xzf archive.tar.gz           # extract
tar -xzf archive.tar.gz -C /opt/  # extract to /opt
tar -tzf archive.tar.gz           # list contents

# tar + bzip2 (better compression)
tar -cjf archive.tar.bz2 dir/
tar -xjf archive.tar.bz2

# zip / unzip
zip -r archive.zip dir/
unzip archive.zip
unzip archive.zip -d /target

# gzip / gunzip (single files)
gzip file.txt           # -> file.txt.gz
gunzip file.txt.gz      # -> file.txt
gzip -k file.txt        # keep original

# xz (best compression)
tar -cJf archive.tar.xz dir/
tar -xJf archive.tar.xz

# mnemonic: eXtract, Create, List, File, gZip, bZip2, xz

文件内容与比较

cat 转储整个文件;head/tail 显示两端;tail -f 流式实时更新(对于日志至关重要)。less 是带搜索 (/) 和导航的交互式分页器。diff 比较文件;-u 产生 patch 使用的统一格式。comm 比较两个排序的文件并显示每个独有的行或两者共有的行——对于比较列表很有用。在 comm 之前始终排序输入,因为它需要排序的文件。

bash
# view file contents
cat file.txt               # entire file
head -n 20 file.txt        # first 20 lines
tail -n 20 file.txt        # last 20 lines
tail -f app.log            # follow (live updates)
less file.txt              # pager (q to quit, / to search)

# line/word/char count
wc file.txt                # lines words bytes
wc -l file.txt             # lines only
wc -w file.txt             # words only

# compare files
diff file1.txt file2.txt
diff -u file1.txt file2.txt    # unified format (for patches)
diff -r dir1/ dir2/            # recursive

# patch
diff -u old new > change.patch
patch < change.patch
patch -R < change.patch        # reverse

# find duplicate lines across files
comm -12 <(sort a) <(sort b)   # lines in both
comm -23 <(sort a) <(sort b)   # only in a
comm -13 <(sort a) <(sort b)   # only in b
08

进程管理与信号

后台作业与作业控制

追加 & 在后台运行命令,立即返回。jobs 列出活动作业;fg/bg 在前台和后台之间移动它们。Ctrl+Z 暂停前台作业(发送 SIGTSTP)。wait 阻塞直到后台作业完成——对于启动并行工作的脚本至关重要。disown 从 shell 的作业表中移除作业,使其在您注销后继续运行(与 nohup 不同,它适用于已在运行的作业)。

bash
# run in background (append &)
sleep 100 &
# [1] 12345   (job 1, PID 12345)

# list jobs
jobs
jobs -l            # with PIDs

# bring to foreground / send to background
fg                 # foreground the current job
fg %1              # foreground job 1
bg %2              # background job 2 (continue running)

# suspend a running foreground job: Ctrl+Z
# then resume in background:
bg

# wait for background jobs
sleep 5 &
wait                # wait for all background jobs
wait $!             # wait for the last background job
wait 12345          # wait for specific PID

# disown: detach from shell (survives logout)
long_task &
disown %1

kill 与信号

kill 向进程发送信号。SIGTERM(默认)要求进程优雅退出——它可以清理。SIGKILL(-9)是强制和立即的;进程无法捕获或忽略它,因此它可能使资源处于不良状态。始终先尝试 SIGTERM,等待,然后仅在必要时使用 SIGKILL。killall/pkill 按名称终止。pkill -f 匹配完整命令行(更灵活)。使用 kill -l 列出所有信号名称。

bash
# send signals to processes
kill 12345              # SIGTERM (graceful, default)
kill -15 12345          # SIGTERM explicitly
kill -9 12345           # SIGKILL (force, cannot be caught)
kill -HUP 12345         # SIGHUP (reload config)

# kill by name
killall nginx           # kill all nginx processes
pkill -f "python app.py"  # match full command line

# common signals
# SIGTERM (15) - graceful termination (default)
# SIGKILL (9)  - force kill (cannot be caught/ignored)
# SIGINT (2)   - interrupt (Ctrl+C)
# SIGHUP (1)   - hangup (often reload config)
# SIGSTOP (19) - pause (cannot be caught)
# SIGCONT (18) - resume
# SIGTSTP (20) - terminal stop (Ctrl+Z)

# list all signals
kill -l

# priority: try SIGTERM first, SIGKILL as last resort
kill 12345; sleep 2; kill -9 12345 2>/dev/null

trap — 信号处理

trap 注册在脚本接收信号时运行的命令——对于清理至关重要。EXIT 伪信号在任何退出时触发(正常、错误或被杀死),使其非常适合移除临时文件。始终在创建它们清理的资源之前设置 trap。常见模式:trap cleanup EXIT INT TERM。trap '' SIGNAL 忽略它;trap - SIGNAL 恢复默认。这就是健壮脚本如何确保即使被中断也能清理。

bash
# clean up on exit (EXIT signal)
trap 'rm -f $TMPFILE' EXIT
TMPFILE=$(mktemp)
echo "working with $TMPFILE"
# when script exits (normally or via error), TMPFILE is removed

# handle Ctrl+C (SIGINT)
trap 'echo "Caught Ctrl+C, exiting"; exit 130' INT

# handle multiple signals
cleanup() {
    echo "Cleaning up..."
    rm -rf "$WORKDIR"
    exit
}
trap cleanup INT TERM EXIT

# ignore a signal
trap '' INT              # ignore Ctrl+C
trap - INT               # restore default behavior

# reload on SIGHUP
trap 'echo "reloading config"; source config.sh' HUP

# list current traps
trap -p

进程检查

ps 显示进程快照;top/htop 显示实时。pstree 显示父子层次结构。pgrep 按名称查找 PID(比 ps|grep 更安全)。lsof -i :PORT 找到哪个进程使用端口——对于调试'端口被使用'错误至关重要。ss(套接字统计)是 netstat 的现代替代品。ps aux | grep 模式无处不在,但 pgrep 更干净。使用 --sort 查找资源占用者。

bash
# list processes
ps aux                   # all processes (BSD style)
ps -ef                   # all processes (System V style)
ps -ef | grep nginx      # find nginx processes

# interactive viewer (like Task Manager)
top                      # real-time, all processes
htop                     # nicer interactive viewer (install separately)

# process tree
pstree                   # tree view
pstree -p                # with PIDs

# find process by name/port
pgrep -f "node app"      # print PIDs matching
pgrep -fl "node"         # full command line

# who's listening on a port
lsof -i :8080            # process using port 8080
ss -tlnp                 # all listening TCP ports
netstat -tlnp            # legacy alternative

# process resource usage
top -b -n 1 | head       # snapshot
ps aux --sort=-%mem | head  # top memory users

nohup、disown 与 tmux

nohup 使进程忽略 SIGHUP,使其在注销后存活——输出到 nohup.out。disown 对已在运行的后台作业实现相同效果。setsid 在新会话中启动进程,完全分离它。对于长时间运行的交互式工作,tmux 或 screen 更好:它们保持完整的终端会话存活,您可以稍后重新附加,因此即使是文本编辑器也能在断开连接后存活。这就是系统管理员管理远程服务器的方式。

bash
# nohup: run immune to hangups (survives logout)
nohup ./long_script.sh &
# output goes to nohup.out by default
nohup ./server.sh > server.log 2>&1 &

# disown: detach an already-running job
./long_task &
disown %1

# setsid: start in a new session
setsid ./daemon.sh &

# tmux: terminal multiplexer (persistent sessions)
tmux                    # start new session
tmux new -s work        # named session
tmux ls                 # list sessions
tmux attach -t work     # reattach
# inside tmux: Ctrl+B then D to detach
# processes keep running after detach

# screen: alternative to tmux
screen -S work
screen -ls
screen -r work

# check if a process is running
pgrep -x nginx && echo "running" || echo "stopped"
09

脚本与高级主题

Shebang 与脚本结构

shebang(#!)告诉内核使用哪个解释器。#!/usr/bin/env bash 最可移植。结构良好的脚本以 'set -euo pipefail' 开始以确保安全,定义 usage(),并使用 getopts(用于短标志)或手动循环(用于长标志)解析参数。OPTIND 跟踪下一个参数;shift 跳过已解析的选项以便 $1 是第一个位置参数。这种结构使脚本健壮且用户友好。

bash
#!/usr/bin/env bash
# ^ portable shebang (finds bash via PATH)
# #!/bin/bash          # absolute path (common)
# #!/usr/bin/bash       # on some systems
# #!/bin/sh             # POSIX sh (most portable)

set -euo pipefail        # strict mode (see below)

# script metadata
readonly PROGNAME=$(basename "$0")
readonly VERSION="1.0.0"

usage() {
    cat << EOF
Usage: $PROGNAME [OPTIONS] FILE
Options:
  -h, --help     Show this help
  -v, --version  Show version
  -o OUTPUT      Output file
EOF
}

# parse options with getopts
while getopts "hvo:" opt; do
    case $opt in
        h) usage; exit 0 ;;
        v) echo $VERSION; exit 0 ;;
        o) output=$OPTARG ;;
        *) usage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

严格模式(set -euo pipefail)

set -e 在任何命令失败(返回非零)时立即退出——及早捕获错误。set -u 将对未设置变量的引用视为错误。pipefail 使管道在其中的任何命令失败时返回非零(默认情况下,只有最后一个命令的状态重要)。它们一起捕获大多数 bug。使用 'cmd || true' 允许预期的失败,使用 'if ! cmd' 进行显式检查。某些命令(grep、test)合法地返回非零,因此包装它们。

bash
# the 'strict mode' trio
set -e             # exit on error (any non-zero status)
set -u             # error on unset variable
set -o pipefail    # pipeline fails if any command fails
# combined: set -euo pipefail

# example: without -e, errors are silently ignored
mkdir /root/nope   # fails (permission denied)
echo "still running"  # this runs anyway!

# with set -e, the script exits at the failure
set -e
mkdir /root/nope   # script exits here
echo "never reached"

# handle expected failures
set -e
if ! grep -q "x" file; then
    echo "not found"   # grep returning 1 won't kill script
fi

# temporarily disable -e
set +e
risky_command
set -e

# || true to ignore a command's failure
mkdir -p dir || true

调试技术

set -x(xtrace)在执行之前打印每个命令——#1 调试工具。PS4 自定义提示(添加行号有助于定位问题)。bash -n 不运行就检查语法。trap ERR 在任何错误发生时运行命令,非常适合记录哪里出了问题。对于复杂脚本,用 set -x/+x 包装可疑函数以只跟踪该部分。结合 set -e 在第一个错误处停止并检查。

bash
# trace execution: print each command before running
set -x              # turn on
set +x              # turn off
bash -x script.sh   # run with tracing from the start

# verbose: print input lines as read
set -v

# combine: -xv for full detail
bash -xv script.sh

# print commands with line numbers
export PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x

# check syntax without executing
bash -n script.sh   # syntax check only

# step through interactively
bash -x script.sh 2>&1 | less

# debug a specific function
debug_func() {
    set -x
    actual_func "$@"
    set +x
}

# use trap ERR to catch errors
trap 'echo "Error on line $LINENO" >&2' ERR

算术与数学

Bash 只用 $(( )) 做整数算术。对于浮点,将表达式管道到 bc(scale=N 设置小数位)或使用 awk。bc -l 加载数学库(sqrt、sin、cos 等)。$RANDOM 给出伪随机整数 0-32767;对于加密随机性使用 /dev/urandom。(( )) 命令支持 +=、-=、*=、/=、%= 和 ++/--,如 C。对于严肃的数学,Python 或 awk 是更好的选择。

bash
# integer arithmetic
echo $((5 + 3))         # 8
echo $((10 / 3))        # 3 (integer division)
echo $((10 % 3))        # 1 (modulo)
echo $((2 ** 8))        # 256 (exponent)

# floating point with bc
echo "scale=2; 10/3" | bc       # 3.33
echo "3.14 * 2" | bc            # 6.28
result=$(echo "scale=4; $a / $b" | bc)

# floating point with awk
awk 'BEGIN {printf "%.2f\n", 10/3}'   # 3.33
awk "BEGIN {print $a * $b}"

# math functions with bc -l
echo "scale=4; s(0)" | bc -l    # 0 (sine, radians)
echo "scale=4; sqrt(16)" | bc -l # 4.0000

# random numbers
echo $RANDOM              # 0-32767
echo $((RANDOM % 100))    # 0-99
head -c 4 /dev/urandom | od -An -tu4   # 32-bit random

# increment/decrement
n=5
((n++)); echo $n          # 6
((n+=10)); echo $n        # 16

正则表达式与模式匹配

Bash 有三个模式系统:用于文件名的 glob(*.txt、?、[abc])、用于更复杂匹配的扩展 glob(extglob:!()、@()、*())和 ERE 正则表达式([[ ]] 中的 =~)。=~ 将组捕获到 BASH_REMATCH(索引 0 = 完整匹配,1+ = 组)。正则表达式使用 ERE 语法(如 grep -E)。启用 extglob 以在文件名模式中进行强大的否定和交替。正则表达式是 Bash 用于输入验证的最有用功能之一。

bash
# glob patterns (filename matching)
*.txt          # any .txt file
file?.txt      # single char: file1.txt, fileA.txt
file[0-9].txt  # digit: file0.txt ... file9.txt
file[!0-9].txt # NOT a digit

# extended globs (shopt -s extglob)
shopt -s extglob
echo !(*.bak)       # everything except .bak files
echo @(*.jpg|*.png) # jpg or png
echo *(foo)         # zero or more "foo"

# regex with =~ in [[ ]]
if [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
    echo "valid email"
fi

# capture groups with BASH_REMATCH
if [[ $date =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
    echo "Year: ${BASH_REMATCH[1]}"
    echo "Month: ${BASH_REMATCH[2]}"
    echo "Day: ${BASH_REMATCH[3]}"
fi

# case-insensitive regex
shopt -s nocasematch
if [[ $s =~ ^hello ]]; then echo "starts with hello"; fi
10

grep 深入

基本 grep 模式

grep 使用模式搜索文本。-i 忽略大小写;-w 匹配整个单词(防止在搜索 'error' 时匹配 'errors');-v 反转(非匹配行);-c 计数;-n 显示行号;-l 只列出有匹配的文件名;-h 在搜索多个文件时抑制文件名前缀。默认情况下,grep 使用基本正则表达式(BRE),其中元字符需要反斜杠转义。使用 -E 进行扩展正则表达式(更清晰的语法)或 -F 进行固定字符串(无正则表达式,对于字面搜索更快)。

bash
# Basic search (literal string)
grep "error" logfile.txt

# Case-insensitive
grep -i "error" logfile.txt

# Whole word match
grep -w "error" logfile.txt

# Invert match (lines NOT matching)
grep -v "debug" logfile.txt

# Count matches
grep -c "error" logfile.txt

# Show line numbers
grep -n "error" logfile.txt

# Multiple files with filename prefix
grep "error" *.log

# Suppress filename prefix (single file behavior)
grep -h "error" *.log

# Only show filenames with matches
grep -l "error" *.log

grep 与正则表达式

grep -E(或 egrep)使用扩展正则表达式,语法更清晰:+、?、|、() 无需转义即可工作。^ 和 $ 锚定到行首/尾。[] 定义字符类;{} 指定重复。-P 启用 Perl 兼容正则表达式(PCRE),具有 \d、\w、前瞻等功能——但这是 GNU 特定的,不可移植。对于复杂正则表达式,考虑 ripgrep (rg),它更快,默认使用类似 PCRE 的语法。始终引用模式以防止 shell 解释特殊字符,如 $ 和 *。

bash
# Extended regex (-E or egrep)
grep -E "error|warning|fatal" logfile.txt

# Anchors: ^ start, $ end
grep -E "^ERROR:" logfile.txt      # lines starting with ERROR:
grep -E "completed$" logfile.txt   # lines ending with completed

# Character classes
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" dates.txt  # date format
grep -E "[A-Z][a-z]+" names.txt                  # Capitalized words

# Quantifiers
grep -E "ab+c" file.txt    # one or more b's
grep -E "ab*c" file.txt    # zero or more b's
grep -E "ab?c" file.txt    # zero or one b

# Alternation and grouping
grep -E "(cat|dog) food" file.txt

# PCRE (-P, GNU grep only)
grep -P "\d{3}-\d{4}" phones.txt  # \d for digits

grep 上下文与输出控制

上下文标志(-B、-A、-C)显示周围行,对于理解日志条目至关重要。-o 只输出匹配部分(对于提取 URL、数字等很有用)。--color 在终端中高亮匹配。-r 递归搜索(排除符号链接);-R 跟随符号链接。--include/--exclude/--exclude-dir 过滤要搜索的文件——对于大型代码库非常宝贵(始终排除 node_modules、.git、vendor)。-a 强制将二进制文件视为文本。对于代码搜索,ripgrep (rg) 是现代、更快的替代品,具有合理的默认值。

bash
# Show context: lines before/after match
grep -B 2 "error" log.txt    # 2 lines Before
grep -A 3 "error" log.txt    # 3 lines After
grep -C 2 "error" log.txt    # 2 lines Context (both)

# Only show matched part (not full line)
grep -o "https?://[^ ]+" urls.txt

# Color highlight matches
grep --color=auto "error" log.txt

# Recursive search
grep -r "TODO" ./src/
grep -R "TODO" ./src/        # follows symlinks

# Include/exclude file patterns
grep -r --include="*.py" "import" .
grep -r --exclude="*.test.js" "TODO" .
grep -r --exclude-dir=node_modules "TODO" .

# Binary files
grep -a "pattern" binary_file  # treat as text

grep 与 stdin 及管道

grep 在管道中最强大。[n]ginx 技巧防止 grep 匹配自己的进程:括号使模式不匹配进程列表中的字面 'grep' 字符串。zgrep 搜索压缩文件而无需手动解压。pgrep 是专门的进程查找器(比 ps | grep 更好)。grep -rl 列出包含模式的文件;管道到 xargs grep 在这些文件中搜索另一个模式——一种常见的代码考古技术。对于交互式代码搜索,使用专为源代码设计的 ripgrep 或 ack。

bash
# Search command output
ps aux | grep nginx
ps aux | grep "[n]ginx"  # trick: prevents matching grep itself

# Search compressed logs
zcat log.gz | grep "error"
zgrep "error" log.gz     # direct

# Chain multiple greps
cat log.txt | grep "error" | grep -v "timeout" | grep -c

# Extract and filter
grep -oE "[0-9.]+" response.txt | sort -n | uniq

# Find processes excluding grep
pgrep -f "node server.js"  # better than ps | grep

# Search command history
history | grep "git rebase"

# Find files containing pattern, then search more
grep -rl "config" . | xargs grep "database"

grep 退出状态与脚本

grep 的退出状态使其非常适合脚本:0(找到匹配)、1(无匹配)、2(错误)。-q(安静)抑制输出以进行纯条件检查。在 set -e 脚本中,grep 返回 1(无匹配)会退出脚本——使用 '|| true' 防止这种情况。while-read 模式一次处理一行匹配。grep -c 返回计数(无匹配则为 0),您可以进行数字比较。这种脚本功能使 grep 成为日志监控、验证脚本和 CI/CD 检查的构建块。

bash
# grep returns exit codes for scripting
# 0 = match found, 1 = no match, 2 = error

if grep -q "error" /var/log/syslog; then
    echo "Errors found!"
    # send alert, etc.
fi

# Silent check (-q), no output
grep -q "^$" file.txt && echo "has blank lines"

# Use in while loop
grep "pattern" file.txt | while read -r line; do
    echo "Found: $line"
done

# Count and branch
errors=$(grep -c "ERROR" log.txt)
if [ "$errors" -gt 10 ]; then
    echo "Too many errors: $errors"
fi

# grep with set -e (exits on non-zero)
set -e
grep "missing" file.txt || true  # prevent exit
11

sed 深入

sed 替换

sed(流编辑器)逐行转换文本。s 命令替换文本:s/pattern/replacement/flags。g(全局)替换每行所有出现;没有它,只替换第一个匹配。您可以将操作限制为特定行(按编号、范围或模式)。-i 就地编辑文件(危险——始终先不带 -i 测试,或使用 -i.bak 进行备份)。sed 独立处理每一行。分隔符不必是 /——当模式包含斜杠时(例如,文件路径)使用 s|old|new|g。

bash
# Basic substitution (first occurrence per line)
sed 's/old/new/' file.txt

# Replace all occurrences
sed 's/old/new/g' file.txt

# Case-insensitive (GNU sed)
sed 's/old/new/gi' file.txt

# Replace Nth occurrence
sed 's/old/new/2' file.txt  # 2nd occurrence only

# Only on lines matching pattern
sed '/error/s/old/new/' file.txt

# Only on specific line numbers
sed '5s/old/new/' file.txt      # line 5 only
sed '5,10s/old/new/g' file.txt  # lines 5-10
sed '$s/old/new/' file.txt      # last line

# In-place edit (modifies file)
sed -i 's/old/new/g' file.txt
sed -i.bak 's/old/new/g' file.txt  # keeps backup

sed 与正则表达式及捕获组

sed -E(或 -r)使用扩展正则表达式,语法更清晰(括号/大括号前无反斜杠)。捕获组在替换中引用为 \1、\2 等。& 表示完整匹配。日期重格式化示例显示了其威力:分别捕获年、月、日并重新排列。多个命令可以用分号链接(s/.../.../;s/.../.../)。始终转义模式中的特殊字符(.、*、[ 等)并转义替换中的 /(或使用不同的分隔符)。

bash
# Extended regex (-E or -r)
sed -E 's/[0-9]+/NUMBER/g' file.txt

# Capture groups with \1, \2 (BRE)
sed 's/\(foo\)\(bar\)/\2\1/' file.txt  # swap: foobar -> barfoo

# Capture groups with () (ERE, cleaner)
sed -E 's/(foo)(bar)/\2\1/' file.txt

# Reformat date: 2025-01-15 -> 15/01/2025
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' file.txt

# Add prefix to lines
sed 's/^/>> /' file.txt

# Add suffix
sed 's/$/ <<</' file.txt

# Remove leading/trailing whitespace
sed 's/^[ \t]*//;s/[ \t]*$//' file.txt

# Use & for full match
sed 's/[0-9]\{3\}/[&]/g' file.txt  # wrap 3 digits in brackets

sed 删除与打印

d 命令删除行;p 打印行。使用 -n(无自动打印),只有显式 p 命令产生输出——这将 sed 变为选择性打印机(如 grep)。删除空行(sed '/^$/d')是常见的清理。sed -n '5,10p' 等同于 sed -n '5,10p' 或 head/tail 组合。~ 语法(0~3p)每 3 行打印一次(GNU 扩展)。记住:没有 -n,sed 打印每一行(可能已修改);有 -n,除非使用 p,否则不打印。这种二元性使 sed 既是编辑器又是过滤器。

bash
# Delete lines
sed '5d' file.txt              # delete line 5
sed '5,10d' file.txt           # delete lines 5-10
sed '/pattern/d' file.txt      # delete matching lines
sed '/^$/d' file.txt           # delete blank lines
sed '/^[ \t]*$/d' file.txt    # delete whitespace-only lines
sed '$d' file.txt              # delete last line

# Print only specific lines (like head/tail)
sed -n '5p' file.txt           # print line 5 only
sed -n '5,10p' file.txt        # print lines 5-10
sed -n '/pattern/p' file.txt   # print matching lines (like grep)
sed -n '5,${/pattern/p}' file.txt  # from line 5 to end

# Print with line numbers
sed '=' file.txt | sed 'N;s/\n/\t/'

# Print every Nth line
sed -n '0~3p' file.txt  # every 3rd line (GNU)

sed 多行与保留空间

sed 的保留空间允许多行操作——模式空间(当前行)可以保存到保留空间并稍后检索。h/H 复制/追加到保留;g/G 从保留复制/追加;x 交换它们。行连接技巧(:a;N;$!ba;s/\n/ /g)将整个文件读入模式空间然后替换换行符。N 将下一行追加到模式空间。这些高级功能使 sed 图灵完备但也非常晦涩。对于复杂的多行转换,awk 或 Perl 更可读。使用 sed 进行简单的基于行的编辑;使用 awk 进行基于字段或多行逻辑。

bash
# Join lines (replace newline with space)
sed ':a;N;$!ba;s/\n/ /g' file.txt

# Join every 2 lines
sed 'N;s/\n/ /' file.txt

# Print paragraph (blocks separated by blank lines)
sed '/./{H;d;};x;s/\n/ /g' file.txt

# Reverse line order (tac alternative)
sed '1!G;h;$!d' file.txt

# Double-space a file
sed 'G' file.txt

# Remove last line of each paragraph
sed -n '/^$/{p;h;};/./{H;};/^$/{x;s/\n.*//;p;}' file.txt

# Hold space (h,H,g,G,x) stores lines between cycles
# h: copy pattern space to hold
# H: append pattern space to hold
# g: copy hold to pattern space
# G: append hold to pattern space
# x: exchange pattern and hold

sed 在管道与脚本中

sed 在文本转换的管道中表现出色。替换变量时,转义特殊字符(& 和 \)以防止解释。多个 -e 标志按顺序应用多个命令。-f 从文件读取命令(对于复杂脚本很有用)。CSV 到 TSV 转换显示了实际用途。对于脚本中的配置文件编辑,始终验证更改(sed 后 grep)并考虑使用专用工具(JSON 用 jq,YAML 用 yq)。sed 的优势是简单、快速、基于行的编辑——它是与 grep 和 awk 并列的 Unix 文本处理的主打工具。

bash
# Clean up output in pipeline
cat messy.txt | sed 's/[ \t]*$//' | sed '/^$/d' | sort

# Extract and transform
echo "name=John;age=30" | sed 's/;/\n/g' | sed 's/=/: /'

# Modify config files in scripts
sed -i "s/PORT=8080/PORT=$NEW_PORT/" config.env

# Escape special chars for safe substitution
ESCAPED=$(printf '%s\n' "$URL" | sed 's/[&\]/\\&/g')
sed "s|URL|$ESCAPED|g" template.txt

# Multiple substitutions
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# Read commands from file
sed -f script.sed input.txt

# Convert CSV to TSV
sed 's/,/\t/g' data.csv
12

awk 深入

awk 基础与字段

awk 自动将每行拆分为字段($1、$2、...、$NF)。-F 设置输入字段分隔符;OFS 设置输出分隔符。$0 是整行。NR(记录数)是行号;NF(字段数)是当前行的字段计数。awk 通过模式-动作对处理每一行:pattern { action }。如果没有模式,动作对每一行运行。如果没有动作,则打印该行。这使 awk 非常适合从 CSV、TSV、/etc/passwd 和日志文件中进行基于列的数据提取。

bash
# Print specific fields (default delimiter: whitespace)
awk '{print $1}' file.txt        # first field
awk '{print $1, $3}' file.txt    # first and third
awk '{print $NF}' file.txt       # last field
awk '{print $(NF-1)}' file.txt   # second-to-last

# Custom field separator
awk -F: '{print $1, $7}' /etc/passwd   # colon-separated
awk -F',' '{print $2}' data.csv        # CSV
awk -F'\t' '{print $1}' data.tsv      # TSV

# Print entire line ($0)
awk '{print}' file.txt
awk '{print NR, $0}' file.txt  # with line number

# NR = record (line) number
# NF = number of fields in current record
# NR and NF are built-in variables

# Change output separator
awk 'BEGIN{OFS="|"} {print $1, $2, $3}' file.txt

awk 模式与条件

awk 模式可以是正则表达式(/pattern/)、比较($3 > 100)、行号(NR == 5)或范围(/start/,/end/)。~ 和 !~ 将正则表达式应用于特定字段。条件可以用 &&、|| 和 ! 组合。这使 awk 成为强大的过滤器——比 grep 更具表现力,因为您可以按数字或字符串比较字段值。范围模式(/start/,/end/)打印两个标记之间的所有行(包含)。awk 每行评估条件;如果为真,则运行动作。没有动作时,默认为 {print}。

bash
# Pattern: only process matching lines
awk '/error/ {print}' log.txt          # like grep
awk '$3 > 100' data.txt                # 3rd field > 100
awk 'NR == 5' file.txt                 # line 5 only
awk 'NR >= 10 && NR <= 20' file.txt    # lines 10-20
awk 'NF == 0' file.txt                 # empty lines
awk '$1 == "ERROR"' log.txt            # first field equals ERROR

# Range pattern: from start to end
awk '/start/,/end/' file.txt

# Combine conditions
awk '$3 > 50 && $3 < 100 {print $1, $3}' data.txt
awk '$1 == "GET" || $1 == "POST" {print}' access.log

# Negation
awk '!/debug/ {print}' log.txt  # lines NOT containing debug

# Field comparison with regex
awk '$2 ~ /^[0-9]+$/' file.txt   # 2nd field is numeric
awk '$2 !~ /pattern/' file.txt   # 2nd field doesn't match

awk BEGIN/END 与变量

BEGIN 在读取任何输入之前运行(非常适合初始化、标题、变量设置)。END 在处理所有输入之后运行(非常适合摘要、总计)。用户变量不需要声明——它们默认为 0(数字)或空(字符串)。-v 将外部变量传递到 awk 中。这使 awk 成为用于数据处理的小型编程语言:sum、average、min、max、count——全部在一次遍历中。模式 'NR == 1 {max = $1}' 从第一行初始化 max,然后后续行更新它。这比多个 grep/sort/cut 管道高效得多。

bash
# BEGIN: runs before processing (setup)
# END: runs after processing (summary)
awk 'BEGIN {print "Total:"} {sum += $1} END {print sum}' nums.txt

# Count lines
awk 'END {print NR " lines"}' file.txt

# Sum a column
awk '{sum += $2} END {print "Total:", sum}' sales.csv

# Average
awk '{sum += $2; count++} END {print sum/count}' data.txt

# Find max value
awk 'NR == 1 {max = $1} $1 > max {max = $1} END {print max}' nums.txt

# User-defined variables (no declaration needed)
awk '{total += $1; if ($1 > max) max = $1}
     END {print "Sum:", total, "Max:", max, "Avg:", total/NR}'

# Pass variables with -v
awk -v threshold=50 '$1 > threshold {print}' data.txt
awk -v date="$(date +%Y-%m-%d)" '{print date, $0}' file.txt

awk 控制流

awk 有完整的控制流:if/else、for、while 和关联数组(键值)。这使其成为完整的数据处理语言。单词计数示例(words[$1]++)是经典的 awk 用例——计算列中每个值的出现次数,然后按频率排序。awk 中的数组是关联的(如字典),按字符串或数字索引。for (key in array) 迭代键(无序——通过 sort 管道)。awk 可以用单次高效遍历替换整个 cut、sort、uniq 和 wc 管道。对于复杂数据处理,awk 通常比链式 Unix 命令更清晰。

bash
# if-else
awk '{
  if ($3 >= 90) grade = "A"
  else if ($3 >= 80) grade = "B"
  else if ($3 >= 70) grade = "C"
  else grade = "F"
  print $1, grade
}' students.txt

# for loop
awk 'BEGIN {
  for (i = 1; i <= 5; i++) print i, i*i
}'

# for-in (iterate array)
awk '{count[$1]++} END {
  for (key in count) print key, count[key]
}' access.log | sort -rnk2

# while loop
awk '{
  i = 1
  while (i <= NF) {
    print i, $i
    i++
  }
}' file.txt

# Arrays (associative)
awk '{words[$1]++} END {
  for (w in words) print words[w], w
}' file.txt | sort -rn | head

awk 实用示例

这些实用示例显示了 awk 的实际威力。IP 频率计数对于日志分析至关重要。按扩展名的文件大小摘要使用 split() 提取扩展名。按列值过滤 CSV 替换复杂的 grep/sed 管道。百分位计算演示了 awk 的数学能力(asorti 对数组索引排序)。重新格式化列(更改分隔符和选择字段)是常见的 ETL 任务。awk 是结构化文本处理的首选工具——当数据有列/字段时,awk 几乎总是正确的选择。对于 JSON,使用 jq;对于 CSV,使用 awk 或 csvkit。

bash
# Top 10 IP addresses from access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# Or entirely in awk:
awk '{ip[$1]++} END {for (i in ip) print ip[i], i}' access.log | sort -rn | head

# Sum file sizes by extension
ls -l | awk '{split($NF, a, "."); ext=a[2]; size[ext]+=$5}
  END {for (e in size) print e, size[e]}' | sort -rnk2

# Filter CSV by column value
awk -F',' '$3 == "active" {print $1, $2}' users.csv

# Calculate response time percentiles from log
awk '{times[NR] = $2} END {
  n = asorti(times, sorted)
  print "p50:", sorted[int(n*0.5)]
  print "p95:", sorted[int(n*0.95)]
  print "p99:", sorted[int(n*0.99)]
}' response_times.txt

# Reformat columns
awk -F',' 'BEGIN{OFS="|"} {print $1, $2, $4}' data.csv
13

find 与 xargs

按名称与类型查找

find 按各种条件搜索文件系统。-name 匹配文件名(区分大小写);-iname 不区分大小写。-type f/d/l 按文件类型过滤。-path 匹配完整路径;-regex 使用完整路径正则表达式。-o(或)组合条件;使用 \( \) 进行分组。-maxdepth 限制递归深度(对于大型文件系统上的性能很重要)。find 输出路径;结合 -exec 或 xargs 对结果执行操作。始终引用模式以防止 shell glob 扩展。对于代码搜索,ripgrep (rg) 更快,但 find 对于文件系统操作更灵活。

bash
# Find by name (exact)
find . -name "config.txt"

# Case-insensitive name
find . -iname "config.txt"

# Wildcard pattern
find . -name "*.py"
find . -name "test_*"

# Find by type
find . -type f -name "*.js"   # files only
find . -type d -name "node_modules"  # directories
find . -type l                # symlinks

# Find by path (full path match)
find . -path "*/src/*.test.js"

# Find by regex
find . -regex ".*\.\(py\|js\)$"

# Multiple name patterns
find . \( -name "*.py" -o -name "*.js" \)

# Max depth (don't recurse too deep)
find . -maxdepth 2 -name "*.txt"

按时间与大小查找

find 的基于时间的搜索对于清理和审计至关重要。-mtime(修改)、-atime(访问)、-ctime(元数据更改)使用天;-mmin/-amin/-cmin 使用分钟。-(小于)和 +(大于)前缀值。大小使用后缀:c(字节)、k(KB)、M(MB)、G(GB)。-empty 查找零长度文件或空目录。-perm 检查权限:精确匹配(644)、所有位设置(-u+x)或任何位设置(/4000)。查找 SUID 文件(/4000)是一种安全审计技术。这些条件可以用 -a(和,默认)和 -o(或)组合。

bash
# Find by modification time
find . -mtime -1    # modified in last 24 hours
find . -mtime +7    # modified more than 7 days ago
find . -mtime 7     # modified exactly 7 days ago

# Access time (-atime) and change time (-ctime)
find . -atime -1    # accessed in last 24h
find . -ctime -1    # status changed in last 24h

# Minutes instead of days
find . -mmin -30    # modified in last 30 minutes

# Find by size
find . -size +100M   # larger than 100MB
find . -size -1k     # smaller than 1KB
find . -size 10M     # exactly 10MB

# Find empty files/directories
find . -empty -type f
find . -empty -type d

# Find by permissions
find . -perm 644     # exactly 644
find . -perm -u+x    # has execute for user
find / -perm /4000   # SUID files (security audit)

find -exec 与 -delete

-exec 对每个结果运行命令。{} 是文件名的占位符;\; 结束命令(每个文件运行一次);+ 批处理文件(一次运行所有文件——更高效)。-delete 移除匹配的文件(比 -exec rm 更快,但先用 -print 测试)。chmod 模式(目录 755,文件 644)是常见的 Web 服务器设置。对于接受多个文件的命令(grep、ls、wc),首选带 + 的 -exec。对于删除,始终先运行 -print 验证将要删除的内容,然后用 -delete 替换。-ok 而不是 -exec 每个文件提示确认。

bash
# Execute command on each result
find . -name "*.log" -exec ls -lh {} \;

# Execute with + (batch, more efficient)
find . -name "*.log" -exec ls -lh {} +

# Delete files (DANGEROUS - test first!)
find /tmp -name "*.tmp" -type f -delete
# Safer: use -exec rm
find /tmp -name "*.tmp" -type f -exec rm -i {} \;

# Execute a complex command
find . -name "*.py" -exec wc -l {} \; | sort -rn | head

# Change permissions
find . -type d -exec chmod 755 {} \;  # dirs to 755
find . -type f -exec chmod 644 {} \;  # files to 644

# Find and grep
find . -name "*.py" -exec grep "TODO" {} +

# Print before deleting (confirmation)
find . -name "*.bak" -print -delete

xargs 基础

xargs 将 stdin 转换为命令参数。它是 find 输出和不读取 stdin 的命令之间的桥梁。-n 限制每个命令的参数;-I {} 定义用于自定义位置的占位符。-0(与 find -print0 一起)正确处理带空格/换行符的文件名——始终使用这对以确保安全。-P 启用并行执行(非常适合 CPU 密集型任务,如图像转换)。-t(跟踪)在运行之前显示命令;-p 提示确认。当 -exec 太慢(每个文件一个进程)时,xargs 至关重要——xargs 高效地批处理参数。

bash
# Basic: take stdin and pass as arguments
echo "file1 file2 file3" | xargs rm

# One argument per line
find . -name "*.bak" | xargs rm

# Control arguments per command (-n)
echo "1 2 3 4 5" | xargs -n 2 echo  # echo 1 2; echo 3 4; echo 5

# Show commands before running (-t, verbose)
find . -name "*.log" | xargs -t rm

# Prompt before each command (-p)
find . -name "*.tmp" | xargs -p rm

# Handle filenames with spaces (-0 with find -print0)
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# Replace string (-I)
find . -name "*.py" | xargs -I {} cp {} /backup/

# Parallel execution (-P)
find . -name "*.png" | xargs -P 4 -I {} convert {} {}.thumb.png

# Limit with -L (lines per command)
cat urls.txt | xargs -L 1 curl -O

find + xargs 模式

find + xargs 是用于批量文件操作的经典 Unix 模式。-print0 | xargs -0 是用于带空格或特殊字符的文件名的安全组合。批量 grep 模式比 -exec grep 快得多(一个 grep 进程 vs 多个)。并行 xargs(-P N)大幅加快 CPU 密集型任务,如音频/视频转换。归档模式(查找旧日志,tar 它们)是常见的日志轮换技术。始终使用 -print0/-0 以确保健壮性——没有它,带空格、引号或换行符的文件名会破坏管道。这种组合是 Unix 系统管理的基础。

bash
# Find and bulk grep (efficient)
find . -name "*.py" -print0 | xargs -0 grep -l "import django"

# Find large files and show sizes
find . -type f -size +100M -print0 | xargs -0 ls -lhS

# Find and archive
find . -name "*.log" -mtime +30 -print0 |
  xargs -0 tar -czf old_logs.tar.gz

# Find and batch rename
find . -name "*.JPG" | xargs -I {} mv {} {}.bak
# Or with rename command:
find . -name "*.JPG" -exec rename 's/\.JPG$/.jpg/' {} +

# Find empty directories and remove
find . -type d -empty -print0 | xargs -0 rmdir

# Find recently modified files and copy
find . -name "*.py" -mtime -1 -print0 |
  xargs -0 -I {} cp {} /tmp/recent/

# Parallel processing
find . -name "*.wav" -print0 |
  xargs -0 -P 8 -I {} ffmpeg -i {} {}.mp3
14

高级 Bash 技术

Here Document 与 Here String

Here document(<< DELIMITER)将多行文本作为 stdin 提供给命令——对于生成配置文件、SQL 查询或任何多行输入很有用。未引用的分隔符允许变量扩展;引用的('EOF')将内容视为字面值。Here string(<<<)将单个字符串作为 stdin 提供——对于简单情况比 echo | command 更干净。分隔符可以是任何单词(EOF、END、DONE);约定是大写。Here doc 对于生成文件或与交互式程序(mysql、psql、ssh)交互的脚本至关重要。缩进的分隔符(<<-)去除前导制表符以提高可读性。

bash
# Here document: multi-line input to a command
cat << EOF
Line 1
Line 2 with $VARIABLE expansion
Line 3
EOF

# Quoted delimiter: no variable expansion
cat << 'EOF'
This $won't expand
\n stays literal
EOF

# Write to file
cat > config.txt << EOF
host=localhost
port=8080
EOF

# Here string: single string as stdin
grep "pattern" <<< "search this text"
read -r line <<< "hello world"

# With variables
name="Alice"
greeting=$(cat <<< "Hello, $name!")

# Pipe multi-line to command
mysql << EOF
USE mydb;
SELECT * FROM users;
EOF

参数扩展

参数扩展是 Bash 的内置字符串操作——对于简单操作不需要 sed/awk/cut。:- 提供默认值;# 和 ## 去除前缀;% 和 %% 去除后缀。文件扩展名示例非常常见:${file##*.} 获取扩展名,${file%.*} 获取不带扩展名的基名。/ 替换第一个匹配;// 替换所有。^^ 和 ,, 转换大小写(Bash 4+)。这些操作比生成外部命令更快。掌握参数扩展以编写高效、可读的 Bash 脚本,无需不必要的子 shell。

bash
# Default values
echo ${name:-"default"}    # use "default" if name unset/empty
echo ${name:="default"}    # set name to "default" if unset/empty

# String length
echo ${#var}

# Substring extraction
var="Hello World"
echo ${var:0:5}    # Hello (start:length)
echo ${var:6}      # World (start to end)

# Remove from beginning (# shortest, ## longest)
file="archive.tar.gz"
echo ${file#*.}     # tar.gz (remove up to first .)
echo ${file##*.}    # gz (remove up to last .)

# Remove from end (% shortest, %% longest)
echo ${file%.*}     # archive.tar (remove from last .)
echo ${file%%.*}    # archive (remove from first .)

# Replace
echo ${var/World/Bash}    # Hello Bash (first match)
echo ${var//l/L}          # HeLLo WorLd (all matches)

# Case conversion (Bash 4+)
echo ${var^^}   # HELLO WORLD (uppercase)
echo ${var,,}   # hello world (lowercase)

trap 与信号处理

trap 为信号和事件注册处理程序。EXIT 在脚本终止时触发(正常、错误或被杀死)——非常适合清理(临时文件、锁)。INT(Ctrl-C)、TERM(kill)、HUP(终端关闭)是常见信号。临时文件模式(trap 'rm -f' EXIT)确保即使脚本失败也能清理。DEBUG 在每个命令之前触发——对于跟踪很有用。始终引用 trap 命令(单引号防止立即扩展)。trap 对于健壮脚本至关重要:没有它,临时文件累积,锁可能在失败时不释放。始终自己清理。

bash
# Clean up on exit (any reason)
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}
trap cleanup EXIT

# Handle specific signals
trap 'echo "Interrupted!"; cleanup' INT TERM
trap 'echo "Ctrl-C pressed"' INT

# Ignore a signal
trap '' INT  # ignore Ctrl-C

# Reset trap
trap - INT  # restore default behavior

# Common pattern: temp file cleanup
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
echo "data" > "$TMPFILE"
# ... use TMPFILE ...
# File is removed when script exits (even on error)

# Debug trap (trace execution)
trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

调试 Bash 脚本

set -x 跟踪执行(打印每个命令带 + 前缀)——主要的调试工具。set -euo pipefail(严格模式)及早捕获错误:-e 在非零时退出,-u 捕获变量名中的拼写错误,pipefail 使管道正确失败(没有它,只有最后一个命令的退出代码重要)。bash -n 不运行就检查语法。PS4 自定义跟踪前缀(显示 file:line 非常有帮助)。trap ERR 在任何错误时触发(带 -e),为您提供行号上下文。对于复杂脚本,在可疑部分添加 set -x 而不是全局。在生产脚本中始终使用严格模式。

bash
# Trace execution: print each command before running
bash -x script.sh
# Or within script:
set -x   # enable tracing
set +x   # disable tracing

# Verbose: print input lines as read
bash -v script.sh

# Strict mode (recommended for all scripts)
set -euo pipefail
# -e: exit on error
# -u: error on undefined variable
# -o pipefail: pipe fails if any command fails
# -o pipefail: pipe fails if any command fails

# Print specific debug info
echo "DEBUG: var=$var" >&2  # to stderr

# Check syntax without executing
bash -n script.sh

# PS4: customize trace prompt
export PS4='+${BASH_SOURCE}:${LINENO}: '
bash -x script.sh

# Trap ERR for error handling
trap 'echo "Error on line $LINENO"' ERR

curl 与 wget

curl 和 wget 是必不可少的 HTTP 客户端。curl 用于 API 测试(支持所有 HTTP 方法、标头、JSON)。-d 发送 POST 数据;-H 设置标头;-X 指定方法;-L 跟随重定向;-O 用远程文件名保存;-s 安静(用于脚本)。wget 专为下载优化(用 -c 恢复,用 -r 递归)。对于 REST API 测试,curl 带有 -H 'Content-Type: application/json' 和 -d 用于 JSON 正文是标准。-w '%{http_code}' 只提取状态代码用于脚本。对于复杂 API 测试,考虑 httpie(更简单的语法)或 postman。

bash
# Basic HTTP request
curl https://api.example.com/data
curl -O https://example.com/file.zip  # save to file

# HTTP methods
curl -X POST https://api.example.com/users
curl -X PUT -d '{"name":"Alice"}' https://api.example.com/users/1
curl -X DELETE https://api.example.com/users/1

# Headers and JSON
curl -H "Content-Type: application/json" \
     -H "Authorization: Bearer TOKEN" \
     -d '{"key":"value"}' \
     https://api.example.com/data

# Download with wget
wget https://example.com/file.zip
wget -q https://example.com/file.zip  # quiet
wget -c https://example.com/file.zip  # continue/resume

# Follow redirects
curl -L https://short.url/abc

# Save response, show headers
curl -s -o response.json -w "%{http_code}" https://api.example.com

# Download with progress bar
curl --progress-bar -O https://example.com/largefile.iso

# POST form data
curl -d "name=Alice&[email protected]" https://api.example.com/form
15

信号与 Trap

trap 清理处理程序

trap 为信号注册处理程序。EXIT 是特殊的——它在 shell 因任何原因退出时触发,使其非常适合清理。始终在 trap 中清理锁文件、临时目录和子进程。要 trap 的常见信号:INT(Ctrl-C)、TERM(默认 kill)、HUP(终端关闭)、EXIT。

bash
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}

# catch INT (Ctrl-C), TERM, EXIT
trap cleanup INT TERM EXIT

# normal exit also triggers EXIT trap
echo "Working..."
sleep 30
# cleanup runs automatically

# remove trap
trap - EXIT                      # disable
trap - INT TERM EXIT             # disable all

带清理的临时文件

绝不要用 $$ 或 $RANDOM 手动生成临时文件名——可预测的路径会招致符号链接攻击。mktemp 原子地创建唯一命名的文件/目录。与 EXIT 上的 trap 配对以保证即使出错或中断也能清理。设置 TMPDIR 环境变量控制位置。

bash
TMPDIR=$(mktemp -d)
TMPFILE=$(mktemp)

cleanup() {
  rm -rf "$TMPDIR" "$TMPFILE"
}
trap cleanup EXIT

# use temp files safely
echo "data" > "$TMPFILE"
process_data < "$TMPFILE"

# mktemp is safer than $RANDOM — avoids symlink attacks
# -d creates a directory
# -p /var/tmp specifies parent
# mktemp -t myapp.XXXXXX   # use TMPDIR env

脚本中的信号处理

对于长时间运行的脚本,使用由信号处理程序翻转的标志变量来优雅地跳出主循环。这让您完成当前迭代、持久化状态并干净地退出。避免在 trap 处理程序本身内部做繁重的工作——只设置标志。

bash
#!/bin/bash
# graceful shutdown for a long-running script

RUNNING=1
stop() {
  echo "Stopping..."
  RUNNING=0
}
trap stop INT TERM

while [ $RUNNING -eq 1 ]; do
  echo "Tick $(date)"
  sleep 1
done

echo "Done."

# send signals: kill -TERM <pid> or Ctrl-C

timeout 与 watch

timeout 运行命令并在持续时间后杀死它(默认 SIGTERM)。退出代码 124 表示超时。使用 --kill-after 在进程忽略 SIGTERM 时升级到 SIGKILL。与循环结合构建健康检查。watch 用于面向人类的周期性显示,而不是脚本。

bash
# timeout kills a command after N seconds
timeout 30 slow_command
timeout 5s curl https://slow.example.com
timeout --signal=KILL 60 build.sh  # escalate to SIGKILL
timeout -k 5 30 command            # kill -9 after 5s grace

# exit status: 124 if timed out
timeout 2 sleep 10
echo $?                            # 124

# watch reruns a command
watch -n 1 date
watch -n 5 'kubectl get pods'

# tlimit-style loops
while true; do
  timeout 5 curl -s https://api.example.com/health || echo "down"
  sleep 60
done

忽略与重新引发

trap '' SIGNAL 忽略信号(便于不可中断的关键部分)。要将信号转发给子进程,用 $! 捕获其 PID 并重新发送。当启用 set -e 时,ERR trap 在任何命令失败时触发——便于通过 $LINENO 记录失败的行号。

bash
# ignore a signal entirely
trap '' INT                       # ignore Ctrl-C
trap - INT                        # restore default

# re-raise / propagate to children
trap 'kill -TERM $childpid; wait $childpid' TERM

# forward signals to a subprocess
childpid=
run_child() {
  some_server &
  childpid=$!
  wait $childpid
}
trap 'kill -TERM $childpid' TERM INT
run_child

# exit on any error
set -e
trap 'echo "Failed at line $LINENO"' ERR
16

函数与库

定义函数

函数将可重用命令分组。两种语法:name() {...} 和 function name {...}。函数内部的参数是 $1、$2 等——$0 仍然是脚本名。$@ 展开为所有参数(始终引用为 "$@" 以保留带空格的参数)。$# 是计数。

bash
# two equivalent syntaxes
greet() {
  echo "Hello, $1!"
}

function greet {                  # also valid
  echo "Hello, $1!"
}

greet Alice                       # Hello, Alice!
greet "Bob Smith"                 # quoted args

# arguments: $1, $2, ... $@, $#, $*
echo_args() {
  echo "Count: $#"
  echo "All: $@"
  echo "First: $1"
  for arg in "$@"; do echo "- $arg"; done
}

返回值与局部变量

return 设置退出状态(0=成功,1-255=失败)——它不返回值。要获取值,echo 它并用 $() 捕获。在函数内部使用 local 声明变量以避免污染全局作用域。local 在递归函数中至关重要。

bash
# return is exit status (0-255), not a value
is_even() {
  if (( $1 % 2 == 0 )); then return 0; else return 1; fi
}

if is_even 4; then echo "even"; fi

# capture stdout as a value
get_date() { date +%Y-%m-%d; }
today=$(get_date)

# local variables
counter() {
  local count=0                  # scoped to function
  ((count++))
  echo $count
}

source 库

source(或 .)在当前 shell 中执行文件——因此函数、变量和别名变得可用。这就是您构建可重用库的方式。BASH_SOURCE[0] vs $0 区分 source vs 执行——对于既是库又是可运行脚本的文件很方便。

bash
# library file: lib/utils.sh
log() {
  echo "[$(date +%H:%M:%S)] $*" >&2
}

CONFIG_PATH="/etc/myapp"

# main script
source lib/utils.sh              # or: . lib/utils.sh
log "Starting..."
echo "Config: $CONFIG_PATH"

# check if sourced vs executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "Running directly"
else
  echo "Sourced"
fi

递归函数

Bash 支持递归,但它很慢(每次调用为 $() 生成子 shell)并且堆栈深度有限(~1000s)。对于计算密集型工作,优先使用 awk、Python 或外部工具。在递归函数中始终使用 local 变量——否则它们会在调用之间相互覆盖。

bash
# factorial
factorial() {
  local n=$1
  if (( n <= 1 )); then
    echo 1
  else
    local prev=$(factorial $((n - 1)))
    echo $((n * prev))
  fi
}
factorial 5                      # 120

# fibonacci
fib() {
  if (( $1 < 2 )); then echo $1; return; fi
  echo $(( $(fib $(($1 - 1))) + $(fib $(($1 - 2))) ))
}

# caution: bash recursion is slow and limited depth
# for heavy work, use awk, python, or external tools

默认和可选参数

${1:-default} 在 $1 未设置或为空时替换默认值。${1:?message} 在缺失时以错误退出——非常适合必需参数。shift 从参数列表中弹出 $1。结合 case + shift 是在 bash 函数中解析标志和位置参数的惯用方式。

bash
# default values
greet() {
  local name="${1:-World}"        # default if unset or empty
  local greeting="${2:-Hello}"
  echo "$greeting, $name!"
}
greet                            # Hello, World!
greet Alice                      # Hello, Alice!
greet Alice Hi                   # Hi, Alice!

# required argument
require_arg() {
  : "${1:?Usage: require_arg <name>}"   # exits with message if missing
  echo "Got: $1"
}

# shift through args
process() {
  while (( $# )); do
    case "$1" in
      -v) verbose=1; shift ;;
      *)  files+=("$1"); shift ;;
    esac
  done
}
17

数组与关联数组

索引数组

索引数组使用基于 0 的整数键。迭代时始终引用 "${arr[@]}" 以正确处理带空格的元素。${#arr[@]} 是计数。${!arr[@]} 给出索引(对于稀疏数组很有用)。unset 'arr[i]'(引用)移除元素。

bash
# declare and assign
fruits=("apple" "banana" "cherry")
fruits+=("date")                 # append
fruits[10]="kiwi"                # sparse

# access
echo "${fruits[0]}"             # first
echo "${fruits[@]}"             # all values
echo "${#fruits[@]}"            # count
echo "${!fruits[@]}"            # indices

# iterate
for f in "${fruits[@]}"; do
  echo "$f"
done

# slice
echo "${fruits[@]:1:2}"         # 2 items starting at index 1

# delete
unset 'fruits[1]'

关联数组(映射)

关联数组(declare -A)是 bash 4+ 功能(macOS 默认搭载 bash 3——使用 brew install bash)。键是字符串。-v 测试检查键是否存在。用 ${!arr[@]} 迭代键。macOS 默认 /bin/bash 是 3.2——使用 -A 的脚本需要 #!/usr/bin/env bash 指向较新的 bash。

bash
# requires bash 4+
declare -A ages
ages["alice"]=30
ages["bob"]=25
ages["carol"]=42

# access
echo "${ages["alice"]}"
echo "${ages[@]}"               # all values
echo "${!ages[@]}"              # all keys

# iterate keys
for name in "${!ages[@]}"; do
  echo "$name => ${ages[$name]}"
done

# check key exists
if [[ -v ages["alice"] ]]; then echo "yes"; fi

# count
echo "${#ages[@]}"

# delete
unset 'ages[bob]'

读入数组

mapfile(又名 readarray)是将行吸入数组的最快方式——始终使用 -t 去除尾随换行符。while-read 循环可移植但较慢。要拆分分隔字符串,设置 IFS 并使用 read -ra。< <(cmd) 进程替换避免子 shell 变量作用域问题。

bash
# read lines into array
mapfile -t lines < file.txt      # bash 4+
echo "${lines[0]}"

# alternative (portable)
lines=()
while IFS= read -r line; do
  lines+=("$line")
done < file.txt

# split string into array
IFS=',' read -ra csv <<< "a,b,c,d"
echo "${csv[2]}"                # c

# from command
mapfile -t files < <(find . -name "*.js")

# process each
for f in "${files[@]}"; do
  echo "Processing $f"
done

数组操作

Bash 数组缺少许多内置操作(没有 .indexOf、.reverse、.unique)。对于这些,编写循环或通过 sort/uniq 管道。唯一数组技巧使用关联数组作为集合。对于复杂数组操作,考虑 awk 或真正的编程语言——bash 数组最适合简单列表。

bash
# append
arr=(a b c)
arr+=(d e)                       # (a b c d e)

# length
echo "${#arr[@]}"

# slice
echo "${arr[@]:1:2}"            # (b c)

# find index (no built-in; loop)
idx=-1
for i in "${!arr[@]}"; do
  if [[ "${arr[$i]}" == "b" ]]; then idx=$i; break; fi
done

# reverse
n=${#arr[@]}
for ((i=n-1; i>=0; i--)); do
  echo "${arr[$i]}"
done

# unique (preserve order)
declare -A seen
unique=()
for x in "${arr[@]}"; do
  [[ -v seen[$x] ]] && continue
  seen[$x]=1
  unique+=("$x")
done

用数组嵌套数据

Bash 没有原生嵌套数组。变通方法:(1) 将记录存储为分隔符连接的字符串并用 IFS 拆分,(2) 使用带复合键(如 "r,c")的关联数组,(3) 使用变量间接 ${!name}。对于真正的嵌套数据,切换到 jq + JSON 或真正的语言。

bash
# array of "records" (delimiter-based)
users=("alice:30:admin" "bob:25:user")
for u in "${users[@]}"; do
  IFS=':' read -r name age role <<< "$u"
  echo "$name ($age) - $role"
done

# simulate 2D with name-mangled vars
declare -A grid
grid["0,0"]=1; grid["0,1"]=2
grid["1,0"]=3; grid["1,1"]=4
for key in "${!grid[@]}"; do
  IFS=',' read -r r c <<< "$key"
  echo "[$r,$c] = ${grid[$key]}"
done

# arrays of arrays (not directly supported)
# workaround: use indirection
rows=("row0" "row1")
row0=(a b c)
row1=(d e f)
ref="${rows[0]}[@]"
echo "${!ref}"                   # a b c
18

字符串操作

参数扩展

参数扩展是 bash 最强大的字符串工具。# 和 ## 从前面去除(最短/最长匹配);% 和 %% 从末尾去除。/ 替换第一个,// 替换所有。这些避免生成子 shell——对于简单操作比通过 sed 管道快得多。

bash
path="/var/log/app.log"

# length
echo "${#path}"                 # 16

# substring (offset:length)
echo "${path:5}"                # log/app.log
echo "${path:5:3}"              # log

# remove from front (shortest/longest)
echo "${path#*/}"               # var/log/app.log
echo "${path##*/}"              # app.log (basename)

# remove from end
echo "${path%/*}"               # /var/log (dirname)
echo "${path%%.*}"              # /var/log/app

# replace
echo "${path/log/LOG}"          # /var/LOG/app.log (first)
echo "${path//log/LOG}"         # all occurrences

大小写转换

Bash 4+ 添加了 ,,(小写)、^^(大写)、^(首字母大写)、,(首字母小写)。declare -u/-l 自动转换赋值。对于较旧的 bash(macOS 默认),使用 tr 或 awk。在规范化用户输入或构建标识符时,大小写转换很常见。

bash
s="Hello World"

# bash 4+
echo "${s,,}"                   # hello world (lower)
echo "${s^^}"                   # HELLO WORLD (upper)
echo "${s^}"                    # Hello World (capitalize first)
echo "${s,}"                    # hello World (lowercase first)

# pattern-based
echo "${s^^[aeiou]}"            # hEllO wOrld (vowels upper)

# portable (pre-bash 4)
echo "$s" | tr 'a-z' 'A-Z'
echo "$s" | awk '{print toupper($0)}'

# declare typed variables
declare -u UPPER="hello"         # HELLO
declare -l LOWER="WORLD"         # world

拆分与连接

拆分:设置 IFS 并使用 read -ra。连接:${arr[*]} 与 IFS 设置可以工作但很棘手——join_by 函数更可靠。printf '%s' "$d%s" 技巧将分隔符前置到每个剩余参数,产生干净的连接。tr 可以在分隔符和换行符之间转换。

bash
# split on delimiter
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo "${parts[2]}"              # c
for p in "${parts[@]}"; do echo "$p"; done

# join with delimiter (bash 4+)
arr=(a b c d)
joined=$(IFS=,; echo "${arr[*]}")
echo "$joined"                   # a,b,c,d

# custom joiner
join_by() {
  local d=$1; shift
  local first=$1; shift
  printf '%s' "$first"
  printf '%s' "$d%s" "$@"
}
join_by " | " a b c              # a | b | c

# split into lines
tr ',' '\n' <<< "a,b,c"

搜索与测试

模式匹配:== 带通配符(* ? [..])执行 glob 匹配。=~ 执行扩展正则表达式;捕获组进入 BASH_REMATCH。shopt -s nocasematch 使 [[ ]] 比较不区分大小写。expr 是遗留的——对于新代码优先使用 [[ ]]。

bash
s="Hello World"

# substring search
if [[ "$s" == *"World"* ]]; then echo "contains"; fi
if [[ "$s" == Hello* ]]; then echo "starts with"; fi
if [[ "$s" == *World ]]; then echo "ends with"; fi

# regex match
if [[ "$s" =~ ^H[a-z]+\sW[a-z]+$ ]]; then echo "match"; fi
echo "${BASH_REMATCH[0]}"       # whole match
echo "${BASH_REMATCH[1]}"       # first group

# case-insensitive
shopt -s nocasematch
[[ "$s" == *world* ]] && echo "yes"
shopt -u nocasematch

# index (no built-in; use expr)
expr "$s" : ".*World"            # length up to match

printf 格式化

printf 比 echo 更强大——支持 C 风格格式说明符(%s、%d、%f、%x)、宽度/精度和对齐。%()T 格式化时间而无需生成 date 命令。printf -v 将结果存储在变量中。printf '=%.0s' {1..40} 是重复字符 N 次的巧妙技巧。

bash
# basic
printf '%s\n' "hello" "world"   # one per line
printf '%s = %d\n' "count" 42

# width and padding
printf '%10s\n' "right"         # right-aligned, width 10
printf '%-10s|\n' "left"        # left-aligned
printf '%05d\n' 42              # 00042 (zero-padded)
printf '%.2f\n' 3.14159         # 3.14

# date formatting
printf 'Today: %(%Y-%m-%d)T\n' -1

# to variable
printf -v today '%(%Y%m%d)T' -1
echo "$today"                    # 20260621

# repeat
printf '=%.0s' {1..40}; echo     # 40 equals signs
19

正则表达式

bash 正则表达式与 =~

=~ 使用 ERE(扩展正则表达式)。捕获组填充 BASH_REMATCH(索引 0 = 整个匹配,1+ = 组)。引用字符串但不引用正则表达式(引用正则表达式使其成为字面值)。对于不区分大小写的匹配,使用 shopt -s nocasematch。正则表达式是 POSIX ERE——没有 \d、\w;使用 [0-9]、[A-Za-z0-9_]。

bash
s="user_42"

if [[ "$s" =~ ^([a-z]+)_([0-9]+)$ ]]; then
  echo "Match!"
  echo "Name: ${BASH_REMATCH[1]}"   # user
  echo "ID: ${BASH_REMATCH[2]}"     # 42
fi

# email-ish check
email="[email protected]"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "valid"
fi

# case-insensitive
shopt -s nocasematch
[[ "$s" =~ USER ]] && echo "yes"
shopt -u nocasematch

grep 正则表达式模式

grep 默认使用 BRE(基本正则表达式),其中 +、?、|、() 需要反斜杠。-E 启用 ERE(更清晰的语法)。-P 启用 PCRE,带 \d、\w、\b、前瞻——最强大但可移植性较差(仅 GNU grep)。对于必须在 macOS/BSD 上运行的脚本,优先使用 -E。

bash
# basic (BRE) — default
grep 'foo' file
grep 'a\.' file                 # literal dot (escape in BRE)
grep 'a*b' file                  # zero or more a

# extended (ERE) — -E
grep -E 'a+b' file               # one or more
grep -E 'a|b' file               # alternation
grep -E '(ab)+' file             # grouping
grep -E '[0-9]{3}' file          # quantifier

# perl (PCRE) — -P
grep -P '\d{3}' file            # digits
grep -P '\bword\b' file        # word boundary
grep -P '(?<=foo)bar' file       # lookbehind

# anchors
grep -E '^Error' file            # line start
grep -E 'done$' file             # line end
grep -E '^Error.*timeout$' file

sed 正则表达式替换

sed 默认使用 BRE(捕获组需要 \( \),反向引用 \1)。-E 切换到 ERE(更清晰的 () 和 +、?、|)。I 标志使替换不区分大小写(GNU sed)。当模式包含斜杠时使用不同的分隔符(s|...|...|)。[[:space:]] 对于空白是可移植的。

bash
# BRE (default)
sed 's/foo/bar/' file
sed 's/a\.b/x/' file            # literal dot
sed 's/a\(b\)c/\1/' file      # capture with backrefs

# ERE with -E
sed -E 's/a+b/x/' file
sed -E 's/(foo|bar)/X/g' file
sed -E 's/([0-9]{4})/Year: \1/' file

# case-insensitive
sed 's/foo/bar/I' file
sed 's/[fF][oO][oO]/bar/g' file

# common recipes
sed -E 's/^[[:space:]]+//' file  # trim leading whitespace
sed -E 's/[[:space:]]+$//' file  # trim trailing
sed -E 's/ +/ /g' file           # collapse spaces
sed -E 's#https?://[^ ]+##g' file # strip URLs

awk 正则表达式

awk 使用 ERE。~ 运算符针对正则表达式测试字符串。/pat1/, /pat2/ 是范围模式(从 pat1 到 pat2 包含匹配)。gsub 进行就地全局替换。gawk 的带第三个参数的 match() 将组捕获到数组中——很方便但不能移植到 mawk/POSIX awk。

bash
# match operator ~
awk '$1 ~ /^[0-9]+/' file        # field 1 is numeric
awk '$0 !~ /debug/' file         # line doesn't contain
awk '/^Error/,/^$/' file         # from Error to blank line

# case-insensitive
awk 'BEGIN { IGNORECASE=1 } /error/' file
awk 'tolower($0) ~ /error/' file

# gsub (global substitute)
awk '{ gsub(/foo/, "bar"); print }' file
awk '{ gsub(/[0-9]+/, "N"); print }' file

# match with capture (gawk)
awk 'match($0, /id=([0-9]+)/, m) { print m[1] }' file

# split on regex
awk '{ n = split($0, parts, /[,;\t]/); print n }' file

常见正则表达式配方

这些配方涵盖常见的文本提取任务。-o 只打印匹配部分。注意正则表达式不适合解析 HTML、JSON 或 XML——使用适当的解析器(JSON 用 jq,XML 用 xmllint)。对于 IPv4,此正则表达式匹配格式但不匹配有效范围(255.255.255.255 vs 999.999.999.999)。

bash
# IPv4
grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log

# URL
grep -Eo 'https?://[^[:space:]]+' file

# email
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file

# date YYYY-MM-DD
grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' file

# hex color
grep -Eo '#[0-9A-Fa-f]{6}' file

# strong password (8+, upper, lower, digit)
grep -E '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,}$' file

# trim whitespace
sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' file

# extract domain from URL
sed -E 's#https?://([^/]+).*#\1#' file
20

调试

set 选项

set -e 在任何命令失败时退出(脚本必备)。set -u 捕获变量名中的拼写错误。set -o pipefail 使管道在任何部分失败时失败(没有它,只有最后一个命令的退出代码重要)。set -x 跟踪执行。组合为 -euo pipefail,这是'严格模式'——最安全的默认值。

bash
#!/bin/bash
set -e                           # exit on error
set -u                           # error on unset variable
set -o pipefail                  # pipe fails if any command fails
set -x                           # print commands (trace)
# or combined:
set -euo pipefail

# shorthand
set -Eeuxo pipefail              # -E: ERR trap inherits

# strict mode for robust scripts
set -euo pipefail
IFS=$'\n\t'

# trace specific section
set -x
complex_logic
set +x

# debug a script
bash -x script.sh
bash -xv script.sh               # verbose + trace

用 -x 跟踪

set -x 在执行之前打印每个命令,默认前缀为 +(PS4)。自定义 PS4 以包含 file:line:function 使跟踪对于调试更有用。BASH_XTRACEFD 将跟踪重定向到不同的 FD,因此您可以将跟踪输出与 stdout/stderr 分开。

bash
# trace entire script
bash -x script.sh

# trace inside script
set -x
echo "tracing on"
set +x

# customize PS4 (the trace prompt)
export PS4='+ ${BASH_SOURCE:-shell}:${LINENO}: ${FUNCNAME[0]:-main}() '
set -x
my_function
set +x

# trace to file
exec 5> /tmp/trace.log
BASH_XTRACEFD=5
set -x
# ... commands ...
set +x
exec 5>&-

错误处理与 ERR trap

ERR trap 在命令失败时触发(带 set -e)。trap 内部的 $? 给出失败的退出代码。$LINENO 显示失败发生的位置。caller 内置命令打印 file:line:function——循环它产生堆栈跟踪。set -E 使 ERR trap 也在函数内部触发(否则在函数上下文中禁用)。

bash
set -e
set -E                           # ERR trap inherits functions

on_error() {
  local exit_code=$?
  local line=$1
  echo "ERROR: command failed at line $line with exit code $exit_code" >&2
  echo "Stack:" >&2
  local i=0
  while caller $i >&2; do ((i++)); done
  exit $exit_code
}
trap 'on_error $LINENO' ERR

# now any failing command triggers the handler
false                            # triggers on_error

# caller builtin gives file:line:func
# stack trace via: while caller $i; do ((i++)); done

shellcheck linter

shellcheck 是 shell 脚本的事实上的 linter——捕获引用问题、未使用的变量、常见陷阱,并建议惯用法。在每个脚本上运行它。最常见的修复是 SC2086:引用变量以防止分词和 globbing。许多 CI 管道要求 shellcheck 通过。

bash
# install: apt install shellcheck (or brew, or use shellcheck.net)
# run:
shellcheck script.sh
shellcheck -x script.sh          # trust includes (no SC1090 warnings)
shellcheck -S warning script.sh  # only warnings+
shellcheck --exclude=SC2086 *.sh # exclude specific rules

# common warnings:
# SC2086: double-quote to prevent globbing (use "$var")
# SC2046: quote $(cmd) to prevent word splitting
# SC2034: unused variable
# SC2155: declare and assign separately (masks return code)
# SC2004: $/${} unnecessary in arithmetic

# in script: disable inline
# shellcheck disable=SC2086
echo $unquoted_var

# disable for next line
# shellcheck disable=SC2086
echo $args

日志与诊断

带时间戳和颜色的分级日志记录器(DEBUG/INFO/WARN/ERROR)使脚本更容易调试。始终记录到 stderr 以便 stdout 保持干净用于数据。支持 --verbose 标志(启用 set -x)和 --dry-run(打印命令而不执行)。ANSI 颜色代码:31=红色,33=黄色,32=绿色,90=灰色。

bash
# logging function with levels
log() {
  local level=$1; shift
  local msg="$*"
  local ts=$(date '+%Y-%m-%d %H:%M:%S')
  local color
  case $level in
    DEBUG) color='\033[90m' ;;
    INFO)  color='\033[32m' ;;
    WARN)  color='\033[33m' ;;
    ERROR) color='\033[31m' ;;
  esac
  printf "%b[%s] [%s] %s\033[0m\n" "$color" "$ts" "$level" "$msg" >&2
}

log INFO "Starting process"
log WARN "Disk space low"
log ERROR "Connection failed"

# verbose flag
[[ $VERBOSE ]] && set -x

# dry-run mode
if [[ $DRY_RUN ]]; then
  echo "Would: $cmd"
else
  eval "$cmd"
fi
21

安全

引用与注入

Bash 安全的 #1 规则:引用每个变量扩展。未引用的 $var 经历分词和 globbing,导致 bug 和注入。对于带动态参数的命令,使用数组(cmd=(ls "$dir"); "${cmd[@]}")而不是字符串。绝不要 eval 或 $() 用户输入到 shell 字符串中。

bash
# ALWAYS quote variables
file="my file.txt"
rm $file                         # BAD: rm my file.txt (3 args!)
rm "$file"                       # GOOD: rm "my file.txt"

# use arrays for command + args
files=("a.txt" "b file.txt" "c.txt")
rm "${files[@]}"                # each file as separate arg, safely

# avoid eval
eval "echo $user_input"          # DANGEROUS
echo "$user_input"               # safe

# avoid command substitution in unsafe places
# BAD: filename from user, executed
cmd="ls $userdir"
$cmd                             # word-splitting + globbing

# GOOD: use array
cmd=(ls "$userdir")
"${cmd[@]}"

安全的临时文件与密钥

mktemp 创建具有不可预测名称和 600 权限的文件——免受符号链接竞争攻击。始终用 trap 清理。从环境变量读取密钥,绝不要硬编码。避免将密钥作为命令行参数传递(通过 ps 可见)——使用 stdin、环境变量或带 600 权限的配置文件。

bash
# safe temp files
tmp=$(mktemp)                    # atomic, unpredictable name
trap 'rm -f "$tmp"' EXIT

# safe temp dir
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

# secrets: read from env, never hardcode
api_key="${API_KEY:?Set API_KEY env var}"

# don't leak secrets to process list (ps)
# BAD: curl -H "Authorization: Bearer $TOKEN" ...
# GOOD: pass via stdin or config file
curl -H @- <<EOF https://api.example.com
Authorization: Bearer $TOKEN
EOF

# restrict file perms for secrets
umask 077
echo "$api_key" > ~/.secrets/api
chmod 600 ~/.secrets/api

输入验证

在使用之前验证所有用户输入。正则表达式检查格式;算术检查范围。对于文件路径,拒绝 ..(遍历)和绝对路径,除非明确允许。tr -cd 去除不在允许集中的字符——对于清理标识符很有用。白名单(case)比黑名单对于允许的操作更安全。

bash
# validate numeric input
read -p "Enter a number: " n
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
  echo "Not a number" >&2; exit 1
fi

# validate range
if (( n < 1 || n > 100 )); then
  echo "Out of range" >&2; exit 1
fi

# validate file path (no traversal)
path="$1"
case "$path" in
  *..*|/*) echo "Invalid path" >&2; exit 1 ;;
esac

# sanitize filename
safe=$(echo "$name" | tr -cd 'A-Za-z0-9._-')

# whitelist allowed values
case "$action" in
  start|stop|restart) ;;
  *) echo "Invalid action" >&2; exit 1 ;;
esac

权限降级与 setuid

setuid shell 脚本不安全——大多数系统忽略脚本上的 setuid 位。使用带严格 sudoers 条目(特定命令,无密码)的 sudo 代替。尽早通过 exec sudo -u 降级 root。对于能力(例如,绑定端口 80),使用 setcap 而不是以 root 运行。chmod 700 保持脚本对所有者私有。

bash
# drop privileges in a script running as root
if [[ $EUID -eq 0 ]]; then
  exec sudo -u appuser "$0" "$@"
fi

# check for root
if [[ $EUID -ne 0 ]]; then
  echo "Requires root" >&2; exit 1
fi

# setuid scripts are DANGEROUS — avoid them
# instead, use sudoers entries
# /etc/sudoers.d/myapp:
#   appuser ALL=(root) NOPASSWD: /usr/local/bin/myapp.sh

# restrict script permissions
chmod 700 script.sh              # only owner can run
chown appuser:appuser script.sh

# use capsh instead of full root for fine-grained caps
# setcap 'cap_net_bind_service=+ep' /usr/bin/myapp

签名与完整性

始终验证下载脚本的校验和和签名,尤其是以 root 运行的安装程序。sha256sum -c 与已知良好的哈希比较。GPG 签名证明完整性和真实性(签名者的身份)。将 curl 管道到 sha256sum 让您在执行之前查看哈希——但更安全的模式是下载-然后验证-然后执行。

bash
# verify checksum
sha256sum -c file.sha256
# file.sha256 contains: <hash>  <filename>

# generate checksums
sha256sum release.tar.gz > release.tar.gz.sha256
sha256sum *.iso > isos.sha256

# GPG sign and verify
gpg --sign file.txt              # creates file.txt.gpg
gpg --verify file.txt.gpg
gpg --detach-sig --armor file.tar.gz
gpg --verify file.tar.gz.asc file.tar.gz

# download + verify in one go
curl -fsSL https://example.com/install.sh | sha256sum
curl -fsSL https://example.com/install.sh -o install.sh
sha256sum -c <(echo "expected_hash  install.sh")
22

网络

curl 高级

curl 通过 URL 传输数据。-X 设置方法,-d 发送数据,-H 设置标头。-O 用远程文件名保存。-I 只获取标头。-L 跟随重定向。-u user:pass 用于认证。-v 用于详细。

bash
# POST with data
curl -X POST -d "name=Alice" https://api.example.com
# JSON POST
curl -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' https://api.example.com
# Download with progress
curl -O https://example.com/file.zip
# Headers only
curl -I https://example.com

wget

wget 非交互式地下载文件。-m 镜像站点。-c 恢复中断的下载。-r 递归,-l 限制深度。-b 在后台运行。对于递归下载比 curl 更好。

bash
# Download file
wget https://example.com/file.zip
# Mirror website
wget -m https://example.com
# Resume download
wget -c https://example.com/largefile.zip
# Recursive download
wget -r -l 2 https://example.com
# Background
wget -b https://example.com/file.zip

ssh 与 scp

ssh 连接到远程机器。-i 指定密钥文件。-L 创建本地端口转发。scp 通过 SSH 复制文件。-r 递归目录。使用 ssh-copy-id 安装密钥。配置 ~/.ssh/config 用于别名。

bash
# SSH connect
ssh user@host
# SSH with key
ssh -i ~/.ssh/id_rsa user@host
# Port forwarding
ssh -L 8080:localhost:80 user@host
# Copy file
scp file.txt user@host:/path/
# Copy directory
scp -r dir/ user@host:/path/

netstat 与 ss

netstat 和 ss 显示网络连接。-t TCP,-u UDP,-l 监听,-n 数字,-p 进程。ss 比 netstat 更快更详细。用于查找哪个进程使用端口或诊断连接问题。

bash
# List listening ports
netstat -tlnp
ss -tlnp
# Show all connections
netstat -an
ss -tunap
# Show routing table
netstat -r
# Show interface stats
netstat -i

ping 与 traceroute

ping 测试可达性和延迟(-c 限制计数)。traceroute 显示到主机的路径。dig 查询 DNS 记录(A、MX、NS、TXT)。+short 给出简洁输出。用于网络诊断和 DNS 故障排除。

bash
# Check connectivity
ping -c 4 google.com
# Trace route
traceroute google.com
# DNS lookup
dig example.com
dig +short example.com
# Get DNS records
dig MX example.com
dig NS example.com
23

Shell 脚本深入

函数

函数将命令分组。local 创建函数作用域变量。$1、$2 是参数。return 设置退出状态(0-255)。用 $() 捕获输出。函数必须在使用之前定义。使用 source 从文件加载。

bash
greet() {
    local name="$1"
    echo "Hello, $name"
    return 0
}
greet "Alice"
# Return value via $?
result=$(greet "Bob")

条件语句

[ ] 是测试命令。-f 文件,-d 目录,-z 空字符串,-n 非空。= 用于字符串,-eq/-ne/-gt/-lt 用于数字。始终引用变量以处理空格。[[ ]] 是 bash 增强版,带正则表达式。

bash
if [ -f "/path/file" ]; then
    echo "File exists"
elif [ -d "/path" ]; then
    echo "Directory exists"
else
    echo "Not found"
fi
# String comparison
if [ "$str" = "hello" ]; then ... fi
# Numeric
if [ "$num" -gt 10 ]; then ... fi

循环

for 迭代列表。C 风格 for 使用 (( ))。while 读取直到条件失败。while read 安全地逐行处理文件。始终引用变量。使用 find 进行递归文件迭代。

bash
# For loop
for i in 1 2 3; do
    echo $i
done
# C-style
for ((i=0; i<5; i++)); do echo $i; done
# While loop
while read line; do
    echo "$line"
done < file.txt
# Iterate files
for f in *.txt; do process "$f"; done

数组

数组使用括号。${arr[@]} 展开所有元素。${#arr[@]} 是长度。始终引用 "${arr[@]}" 以处理空格。declare -A 创建关联数组(bash 4+)。使用 read -a 拆分为数组。

bash
# Declare array
fruits=("apple" "banana" "cherry")
# Access
echo ${fruits[0]}      # apple
echo ${fruits[@]}      # all
echo ${#fruits[@]}     # length
# Iterate
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done
# Associative array
declare -A config
config[key]="value"

错误处理

set -e 在出错时退出,-u 在未定义变量上,pipefail 捕获管道失败。trap ERR 处理错误。trap EXIT 运行清理。command -v 检查命令是否存在。始终以 set -euo pipefail 开始脚本。

bash
set -e  # Exit on error
set -u  # Error on undefined variable
set -o pipefail  # Pipeline fails if any command fails
set -euo pipefail  # All three (recommended)
trap 'echo "Error on line $LINENO"' ERR
trap 'cleanup' EXIT
# Check command result
if ! command -v docker &> /dev/null; then
    echo "docker not installed"
fi
24

文件操作深入

find 高级

find 按条件搜索文件。-type f/d 用于文件/目录。-size +1M 大于 1MB。-mtime -7 在 7 天内修改。-exec 对每个匹配运行命令。{} 是文件名;\; 结束命令。

bash
# By name
find . -name "*.txt"
# By type and size
find . -type f -size +1M
# By modification time
find . -mtime -7  # Modified in last 7 days
# Execute command
find . -name "*.log" -exec gzip {} \;
# Delete
find . -name "*.tmp" -delete

tar 与压缩

tar 组合文件;-z 用 gzip 压缩。-c 创建,-x 提取,-t 列出,-v 详细,-f 文件名。对于 .tar.bz2 使用 -j。zip/unzip 处理 ZIP 格式。使用 -C 提取到特定目录。

bash
# Create tar.gz
tar -czvf archive.tar.gz dir/
# Extract
tar -xzvf archive.tar.gz
# List contents
tar -tzvf archive.tar.gz
# zip/unzip
zip -r archive.zip dir/
unzip archive.zip

rsync

rsync 高效同步文件,只传输差异。-a 归档模式(保留属性),-v 详细,-z 压缩。--delete 移除源中不存在的文件。src/ 上的尾部斜杠很重要:有它,复制内容;没有它,复制目录。

bash
# Sync local
rsync -av src/ dest/
# Sync to remote
rsync -avz src/ user@host:/path/
# Delete files not in source
rsync -av --delete src/ dest/
# Dry run
rsync -av --dry-run src/ dest/
# Exclude
rsync -av --exclude='*.log' src/ dest/

文件权限

权限:r=4,w=2,x=1。755 = rwxr-xr-x(所有者完全,其他读/执行)。644 = rw-r--r--(文件)。u/g/o/a = 用户/组/其他/所有。chmod -R 递归。chown 更改所有权。

bash
# Symbolic
chmod u+x file      # User execute
chmod g-w file      # Group write off
chmod a=r file      # All read only
# Numeric
chmod 755 file      # rwxr-xr-x
chmod 644 file      # rw-r--r--
# Change owner
chown user:group file
# Recursive
chmod -R 755 dir/

ln 与符号链接

硬链接指向相同的 inode(仅相同文件系统)。符号链接(-s)指向路径。硬链接在原始文件删除后仍然存活。符号链接在目标移动后断开。使用 readlink 解析符号链接。

bash
# Hard link
ln file.txt hardlink.txt
# Symbolic link
ln -s /path/to/target symlink
# Find broken symlinks
find . -type l ! -exec test -e {} \; -print
# Copy symlink target
cp -L symlink dest/
25

常见陷阱

未引用的变量

未引用的变量在空格上拆分和 glob。$(ls) 在文件名包含空格时中断。使用 glob(*.txt)或 find -print0 与 read -d。始终引用变量:"$var"。使用 IFS= read -r 进行安全行读取。

bash
# BUG: breaks with spaces
for f in $(ls); do echo "$f"; done
# GOOD: handle spaces
for f in *.txt; do echo "$f"; done
# Or with find
find . -name "*.txt" -print0 | while IFS= read -r -d '' f; do
    echo "$f"
done

cp vs mv

cp 复制,mv 移动。两者都可以在无警告的情况下覆盖。使用 -i 在覆盖之前提示。使用 -n 永不覆盖。mv 在同一文件系统上是原子的,对于锁和原子更新很有用。

bash
# cp: copy (original remains)
cp file.txt backup.txt
# mv: move/rename (original removed)
mv file.txt newname.txt
# Common mistake: overwriting
cp important.txt /tmp/  # If /tmp/important.txt exists, overwritten!
# Use -i for safety
cp -i file.txt /tmp/

rm 危险

rm -rf 很危险,尤其是带变量。空变量导致 rm -rf /。始终引用并检查变量。考虑 trash-cli 进行可恢复的删除。绝不要粗心地运行 rm -rf / 或用 sudo。

bash
# DANGEROUS: recursive force
rm -rf /  # Never do this!
rm -rf $VAR/  # If $VAR is empty, becomes rm -rf /
# Safer: check first
if [ -n "$VAR" ]; then
    rm -rf "$VAR/"
fi
# Use trash-cli instead
trash-put file.txt

Shebang

shebang(#!)指定解释器。#!/usr/bin/env bash 比 #!/bin/bash 更可移植。使用 /bin/sh 获得最大可移植性(避免 bashism)。始终用 chmod +x 使脚本可执行。

bash
#!/bin/bash          # bash script
#!/usr/bin/env bash  # Portable bash
#!/usr/bin/env python3  # Python
#!/bin/sh            # POSIX sh (more portable)
# Make executable
chmod +x script.sh
./script.sh

退出状态

$? 持有最后一条命令的退出状态。0 = 成功,非零 = 失败。在另一个命令覆盖它之前立即检查。最好直接使用 if command。在脚本中始终以有意义的状态代码退出。

bash
# Check last command
if [ $? -eq 0 ]; then
    echo "Success"
fi
# Better: direct check
if command; then
    echo "Success"
else
    echo "Failed"
fi
# Exit with status
exit 0  # Success
exit 1  # Failure

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。