变量与字符串
变量与赋值
Bash 变量默认是无类型字符串;如果值是数字,算术运算可以工作。绝不在赋值中的 = 周围放空格——'name = Alice' 尝试运行名为 name 的命令。使用 $(...) 进行命令替换(优先于反引号)。export 使变量对子进程可用;readonly 使其不可变。环境变量(导出的)被您启动的脚本和程序继承。
# 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+)。:- 提供默认值而不设置变量;:= 作为副作用设置它;:? 以错误中止——非常适合脚本中的必需参数检查。
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)的调用。
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。
# 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 禁用反斜杠转义以便反斜杠字面保留。这是逐行处理文件的规范安全模式。
# 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数组
索引数组
Bash 数组是零索引的。@ 和 * 展开为所有元素——始终引用它们("${arr[@]}")以 正确处理带空格的元素。${#arr[@]} 给出计数。+= 追加。unset 在索引中留下间隙;要重新索引,使用 arr=("${arr[@]}")。使用 ${arr[@]:start:count} 切片返回子集。
# 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)。
# 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[@]}" 以便包含空格的元素作为单个项保留。在循环中使用 += 构建数组是累积结果的惯用方式。
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。
# 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 并将其余的向下移动。
# 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控制流与测试
If / Elif / Else
[ ] 是 POSIX 测试命令(可移植但有限);[[ ]] 是 Bash 的增强版本,支持模式匹配(== 带通配符)、正则表达式(=~)、&& / || 运算符,并且无需引用变量。在 Bash 脚本中优先使用 [[ ]]。始终在 [ ] 中引用变量以避免空值或包含空格的值导致语法错误。-f、-r、-d、-e 测试检查文件存在和属性。
# 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——绝不在 [ ] 中对整数使用 < >(它们会重定向!)。在 [[ ]] 和 (( )) 中您可以使用熟悉的 < > <= >= 运算符。记住这些运算符对于编写正确的条件语句至关重要。
# 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 equalCase 语句
case 是 Bash 中 switch 的等价物——它将值与 glob 模式匹配。| 分隔替代项。;; 结束分支(如 break)。*) 模式是默认项。模式支持通配符(*、?、[abc])但不支持完整正则表达式。对于分派单个值,case 比长 if-elif 链更干净,它是构建命令行子命令(start/stop/restart)的标准方式。
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 风格比较以及位运算符都可以工作。
# 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 语句。冒号 (:) 是无操作命令,对于空分支很有用。
# && 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循环与迭代
For 循环
for 循环迭代单词列表。大括号扩展 {1..5} 生成序列(带可选步长 {start..end..step})。C 风格 for ((init; cond; update)) 最适合数字计数器。用 *.txt 迭代文件时,未引用的 glob 安全展开,但如果没有文件匹配,您会得到字面值 '*.txt'——启用 shopt -s nullglob 以获得空列表。
# 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"
doneWhile 与 Until 循环
while 在条件成功(退出 0)时运行;until 运行直到成功——它们是相反的。'while read' 模式是逐行处理文件的规范方式;始终使用 'IFS= read -r' 以确保安全。注意管道到 while 循环会在子 shell 中运行它,因此变量更改不会持久——当您需要保留变量时使用进程替换 '< <(cmd)' 代替。
# 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"
doneBreak、Continue 与 Select
break 退出循环(break N 退出 N 个嵌套循环);continue 跳到下一次迭代。select 从列表创建交互式编号菜单——它循环直到被 break。这些控制语句在 for、while 和 until 循环中工作。break N 形式对于逃离深度嵌套循环至关重要,但应谨慎使 用,因为它会使代码更难理解。
# 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 产生空列表而不是字面模式。
# 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。
# 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函数
定义与调用函数
Bash 中的函数用 name() 或 function name 定义。用空格分隔的参数(无括号)按名称调用它们。在函数内部始终使用 'local' 声明变量以避免污染全局作用域——没有它,赋值会泄漏出去。Bash 函数不能直接返回值(return 是退出状态 0-255);要返回字符串,echo 它并用 $() 捕获。
# 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 "$@"。这种遮蔽是为什么函数是可重用的构建块。
# 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+——它允许函数按名称分配给调用者的变量。这是返回复杂数据的最干净方式。避免使用全局变量作为返回值,因为它使函数不可重入。
# 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);谨慎使用。阶乘示例显示了模式:局部变量 + 递归调用 + 算术。
# 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。
# 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)文本处理
grep — 模式搜索
grep 查找匹配模式的行。-i 忽略大小写,-v 反转,-n 显示行号,-r 递归,-E 使用扩展正则表达式(如 +、|、{})。-A/-B/-C 显示匹配周围的上下文行——对于理解错误非常有价值。在递归搜索期间使用 --include 过滤文件类型。如果找到匹配,grep 返回退出状态 0,使其在条件中很有用:if grep -q pattern file; then...
# 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 扩展。
# 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.txtawk — 列处理
awk 是用于列数据的小型编程语言。$1、$2... 是字段;$0 是整行;$NF 是最后一个字段。-F 设置输入分隔符;OFS 设置输出。BEGIN 在处理之前运行,END 在之后运行。NR 是记录(行)号;NF 是字段计数。awk 非常适合 CSV/TSV 处理、日志分析和生成报告——对于简单提取之外的任何内容,比 cut 强大得多。
# 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.txtcut、tr、sort 与 uniq
cut 是简单的字段/字符提取——快速但有限(不支持引用)。tr 转换字符(非常适合大小写转换或分隔符交换),-d 删除。sort 排序行(-n 数字,-r 反向,-k 字段)。uniq 只移除相邻的重复项,因此始终先通过 sort 管道。uniq -c 与 sort -rn 是频率分析的经典模式:'sort | uniq -c | sort -rn' 显示最常见的行。
# 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)至关重要。
# 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)文件与目录操作
find — 文件搜索
find 是最强大的文件搜索工具。-name 匹配 glob(使用 -iname 进行不区分大小写);-type 按 f/d/l 过滤;-mtime/-mmin 按修改时间过滤(- = 在之内,+ = 早于);-size 按大小过滤。-exec 对每个结果运行命令;{} 是文件名,\; 每个文件运行,+ 批处理它们。-delete 比 -exec rm 更安全地删除。始终先不带 -delete 测试 find!
# 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。
# 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 创建符号链接。
# 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 标志在压缩时保留原始文件。
# 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 之前始终排序输入,因为它需要排序的文件。
# 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进程管理与信号
后台作业与作业控制
追加 & 在后台运行命令,立即返回。jobs 列出活动作业;fg/bg 在前台和后台之间移动它们。Ctrl+Z 暂停前台作业(发送 SIGTSTP)。wait 阻塞直到后台作业完成——对于启动并行工作的脚本至关重要。disown 从 shell 的作业表中移除作业,使其在您注销后继续运行(与 nohup 不同,它适用于已在运行的作业)。
# 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 %1kill 与信号
kill 向进程发送信号。SIGTERM(默认)要求进程优雅退出——它可以清理。SIGKILL(-9)是强制和立即的;进程无法捕获或忽略它,因此它可能使资源处于不良状态。始终先尝试 SIGTERM,等待,然后仅在必要时使用 SIGKILL。killall/pkill 按名称终止。pkill -f 匹配完整命令行(更灵活)。使用 kill -l 列出所有信号名称。
# 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/nulltrap — 信号处理
trap 注册在脚本接收信号时运行的命令——对于清理至关重要。EXIT 伪信号在任何退出时触发(正常、错误或被杀死),使其非常适合移除临时文件。始终在创建它们清理的资源之前设置 trap。常见模式:trap cleanup EXIT INT TERM。trap '' SIGNAL 忽略它;trap - SIGNAL 恢复默认。这就是健壮脚本如何确保即使被中断也能清理。
# 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 查找资源占用者。
# 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 usersnohup、disown 与 tmux
nohup 使进程忽略 SIGHUP,使其在注销后存活——输出到 nohup.out。disown 对已在运行的后台作业实现相同效果。setsid 在新会话中启动进程,完全分离它。对于长时间运行的交互式工作,tmux 或 screen 更好:它们保持完整的终端会话存活,您可以稍后重新附加,因此即使是文本编辑器也能在断开连接后存活。这就是系统管理员管理远程服务器的方式。
# 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"脚本与高级主题
Shebang 与脚本结构
shebang(#!)告诉内核使用哪个解释器。#!/usr/bin/env bash 最可移植。结构良好的脚本以 'set -euo pipefail' 开始以确保安全,定义 usage(),并使用 getopts(用于短标志)或手动循环(用于长标志)解析参数。OPTIND 跟踪下一个参数;shift 跳过已解析的选项以便 $1 是第一个位置参数。这种结构使脚本健壮且用户友好。
#!/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)合法地返回非零,因此包装它们。
# 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 在第一个错误处停止并检查。
# 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 是更好的选择。
# 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 用于输入验证的最有用功能之一。
# 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"; figrep 深入
基本 grep 模式
grep 使用模式搜索文本。-i 忽略大小写;-w 匹配整个单词(防止在搜索 'error' 时匹配 'errors');-v 反转(非匹配行);-c 计数;-n 显示行号;-l 只列出有匹配的文件名;-h 在搜索多个文件时抑制文件名前缀。默认情况下,grep 使用基本正则表达式(BRE),其中元字符需要反斜杠转义。使用 -E 进行扩展正则表达式(更清晰的语法)或 -F 进行固定字符串(无正则表达式,对于字面搜索更快)。
# 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" *.loggrep 与正则表达式
grep -E(或 egrep)使用扩展正则表达式,语法更清晰:+、?、|、() 无需转义即可工作。^ 和 $ 锚定到行首/尾。[] 定义字符类;{} 指定重复。-P 启用 Perl 兼容正则表达式(PCRE),具有 \d、\w、前瞻等功能——但这是 GNU 特定的,不可移植。对于复杂正则表达式,考虑 ripgrep (rg),它更快,默认使用类似 PCRE 的语法。始终引用模式以防止 shell 解释特殊字符,如 $ 和 *。
# 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 digitsgrep 上下文与输出控制
上下文标志(-B、-A、-C)显示周围行,对于理解日志条目至关重要。-o 只输出匹配部分(对于提取 URL、数字等很有用)。--color 在终端中高亮匹配。-r 递归搜索(排除符号链接);-R 跟随符号链接。--include/--exclude/--exclude-dir 过滤要搜索的文件——对于大型代码库非常宝贵(始终排除 node_modules、.git、vendor)。-a 强制将二进制文件视为文本。对于代码搜索,ripgrep (rg) 是现代、更快的替代品,具有合理的默认值。
# 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 textgrep 与 stdin 及管道
grep 在管道中最强大。[n]ginx 技巧防止 grep 匹配自己的进程:括号使模式不匹配进程列表中的字面 'grep' 字符串。zgrep 搜索压缩文件而无需手动解压。pgrep 是专门的进程查找器(比 ps | grep 更好)。grep -rl 列出包含模式的文件;管道到 xargs grep 在这些文件中搜索另一个模式——一种常见的代码考古技术。对于交互式代码搜索,使用专为源代码设计的 ripgrep 或 ack。
# 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 检查的构建块。
# 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 exitsed 深入
sed 替换
sed(流编辑器)逐行转换文本。s 命令替换文本:s/pattern/replacement/flags。g(全局)替换每行所有出现;没有它,只替换第一个匹配。您可以将操作限制为特定行(按编号、范围或模式)。-i 就地编辑文件(危险——始终先不带 -i 测试,或使用 -i.bak 进行备份)。sed 独立处理每一行。分隔符不必是 /——当模式包含斜杠时(例如,文件路径)使用 s|old|new|g。
# 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 backupsed 与正则表达式及捕获组
sed -E(或 -r)使用扩展正则表达式,语法更清晰(括号/大括号前无反斜杠)。捕获组在替换中引用为 \1、\2 等。& 表示完整匹配。日期重格式化示例显示了其威力:分别捕获年、月、日并重新排列。多个命令可以用分号链接(s/.../.../;s/.../.../)。始终转义模式中的特殊字符(.、*、[ 等)并转义替换中的 /(或使用不同的分隔符)。
# 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 bracketssed 删除与打印
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 既是编辑器又是过滤器。
# 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 进行基于字段或多行逻辑。
# 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 holdsed 在管道与脚本中
sed 在文本转换的管道中表现出色。替换变量时,转义特殊字符(& 和 \)以防止解释。多个 -e 标志按顺序应用多个命令。-f 从文件读取命令(对于复杂脚本很有用)。CSV 到 TSV 转换显示了实际用途。对于脚本中的配置文件编辑,始终验证更改(sed 后 grep)并考虑使用专用工具(JSON 用 jq,YAML 用 yq)。sed 的优势是简单、快速、基于行的编辑——它是与 grep 和 awk 并列的 Unix 文本处理的主打工具。
# 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.csvawk 深入
awk 基础与字段
awk 自动将每行拆分为字段($1、$2、...、$NF)。-F 设置输入字段分隔符;OFS 设置输出分隔符。$0 是整行。NR(记录数)是行号;NF(字段数)是当前行的字段计数。awk 通过模式-动作对处理每一行:pattern { action }。如果没有模式,动作对每一行运行。如果没有动作,则打印该行。这使 awk 非常适合从 CSV、TSV、/etc/passwd 和日志文件中进行基于列的数据提取。
# 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.txtawk 模式与条件
awk 模式可以是正则表达式(/pattern/)、比较($3 > 100)、行号(NR == 5)或范围(/start/,/end/)。~ 和 !~ 将正则表达式应用于特定字段。条件可以用 &&、|| 和 ! 组合。这使 awk 成为强大的过滤器——比 grep 更具表现力,因为您可以按数字或字符串比较字段值。范围模式(/start/,/end/)打印两个标记之间的所有行(包含)。awk 每行评估条件;如果为真,则运行动作。没有动作时,默认为 {print}。
# 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 matchawk BEGIN/END 与变量
BEGIN 在读取任何输入之前运行(非常适合初始化、标题、变量设置)。END 在处理所有输入之后运行(非常适合摘要、总计)。用户变量不需要声明——它们默认为 0(数字)或空(字符串)。-v 将外部变量传递到 awk 中。这使 awk 成为用于数据处理的小型编程语言:sum、average、min、max、count——全部在一次遍历中。模式 'NR == 1 {max = $1}' 从第一行初始化 max,然后后续行更新它。这比多个 grep/sort/cut 管道高效得多。
# 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.txtawk 控制流
awk 有完整的控制流:if/else、for、while 和关联数组(键值)。这使其成为完整的数据处理语言。单词计数示例(words[$1]++)是经典的 awk 用例——计算列中每个值的出现次数,然后按频率排序。awk 中的数组是关联的(如字典),按字符串或数字索引。for (key in array) 迭代键(无序——通过 sort 管道)。awk 可以用单次高效遍历替换整个 cut、sort、uniq 和 wc 管道。对于复杂数据处理,awk 通常比链式 Unix 命令更清晰。
# 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 | headawk 实用示例
这些实用示例显示了 awk 的实际威力。IP 频率计数对于日志分析至关重要。按扩展名的文件大小摘要使用 split() 提取扩展名。按列值过滤 CSV 替换复杂的 grep/sed 管道。百分位计算演示了 awk 的数学能力(asorti 对数组索引排序)。重新格式化列(更改分隔符和选择字段)是常见的 ETL 任务。awk 是结构化文本处理的首选工具——当数据有列/字段时,awk 几乎总是正确的选择。对于 JSON,使用 jq;对于 CSV,使用 awk 或 csvkit。
# 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.csvfind 与 xargs
按名称与类型查找
find 按各种条件搜索文件系统。-name 匹配文件名(区分大小写);-iname 不区分大小写。-type f/d/l 按文件类型过滤。-path 匹配完整路径;-regex 使用完整路径正则表达式。-o(或)组合条件;使用 \( \) 进行分组。-maxdepth 限制递归深度(对于大型文件系统上的性能很重要)。find 输出路径;结合 -exec 或 xargs 对结果执行操作。始终引用模式以防止 shell glob 扩展。对于代码搜索,ripgrep (rg) 更快,但 find 对于文件系统操作更灵活。
# 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(或)组合。
# 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 每个文件提示确认。
# 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 -deletexargs 基础
xargs 将 stdin 转换为命令参数。它是 find 输出和不读取 stdin 的命令之间的桥梁。-n 限制每个命令的参数;-I {} 定义用于自定义位置的占位符。-0(与 find -print0 一起)正确处理带空格/换行符的文件名——始终使用这对以确保安全。-P 启用并行执行(非常适合 CPU 密集型任务,如图像转换)。-t(跟踪)在运行之前显示命令;-p 提示确认。当 -exec 太慢(每个文件一个进程)时,xargs 至关重要——xargs 高效地批处理参数。
# 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 -Ofind + xargs 模式
find + xargs 是用于批量文件操作的经典 Unix 模式。-print0 | xargs -0 是用于带空格或特殊字符的文件名的安全组合。批量 grep 模式比 -exec grep 快得多(一个 grep 进程 vs 多个)。并行 xargs(-P N)大幅加快 CPU 密集型任务,如音频/视频转换。归档模式(查找旧日志,tar 它们)是常见的日志轮换技术。始终使用 -print0/-0 以确保健壮性——没有它,带空格、引号或换行符的文件名会破坏管道。这种组合是 Unix 系统管理的基础。
# 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高级 Bash 技术
Here Document 与 Here String
Here document(<< DELIMITER)将多行文本作为 stdin 提供给命令——对于生成配置文件、SQL 查询或任何多行输入很有用。未引用的分隔符允许变量扩展;引用的('EOF')将内容视为字面值。Here string(<<<)将单个字符串作为 stdin 提供——对于简单情况比 echo | command 更干净。分隔符可以是任何单词(EOF、END、DONE);约定是大写。Here doc 对于生成文件或与交互式程序(mysql、psql、ssh)交互的脚本至关重要。缩进的分隔符(<<-)去除前导制表符以提高可读性。
# 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。
# 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 对于健壮脚本至关重要:没有它,临时文件累积,锁可能在失败时不释放。始终自己清理。
# 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 而不是全局。在生产脚本中始终使用严格模式。
# 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"' ERRcurl 与 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。
# 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信号与 Trap
trap 清理处理程序
trap 为信号注册处理程序。EXIT 是特殊的——它在 shell 因任何原因退出时触发,使其非常适合清理。始终在 trap 中清理锁文件、临时目录和子进程。要 trap 的常见信号:INT(Ctrl-C)、TERM(默认 kill)、HUP(终端关闭)、EXIT。
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 环境变量控制位置。
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 处理程序本身内部做繁重的工作——只设置标志。
#!/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-Ctimeout 与 watch
timeout 运行命令并在持续时间后杀死它(默认 SIGTERM)。退出代码 124 表示超时。使用 --kill-after 在进程 忽略 SIGTERM 时升级到 SIGKILL。与循环结合构建健康检查。watch 用于面向人类的周期性显示,而不是脚本。
# 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 记录失败的行号。
# 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函数与库
定义函数
函数将可重用命令分组。两种语法:name() {...} 和 function name {...}。函数内部的参数是 $1、$2 等——$0 仍然是脚本名。$@ 展开为所有参数(始终引用为 "$@" 以保留带空格的参数)。$# 是计数。
# 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 在递归 函数中至关重要。
# 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 执行——对于既是库又是可运行脚本的文件很方便。
# 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 变量——否则它们会在调用之间相互覆盖。
# 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 函数中解析标志和位置参数的惯用方式。
# 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
}数组与关联数组
索引数组
索引数组使用基于 0 的整数键。迭代时始终引用 "${arr[@]}" 以正确处理带空格的元素。${#arr[@]} 是计数。${!arr[@]} 给出索引(对于稀疏数组很有用)。unset 'arr[i]'(引用)移除元素。
# 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。
# 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 变量作用域问题。
# 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 数组最适合简单列表。
# 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 或真正的语言。
# 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字符串操作
参数扩展
参数扩展是 bash 最强大的字符串工具。# 和 ## 从前面去除(最短/最长匹配);% 和 %% 从末尾去除。/ 替换第一个,// 替换所有。这些避免生成子 shell——对于简单操作比通过 sed 管道快得多。
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。在规范化用户输入或构建标识符时,大小写转换很常见。
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 可以在分隔符和换行符之间转换。
# 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 是遗留的——对于新代码优先使用 [[ ]]。
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 matchprintf 格式化
printf 比 echo 更强大——支持 C 风格格式说明符(%s、%d、%f、%x)、宽度/精度和对齐。%()T 格式化时间而无需生成 date 命令。printf -v 将结果存储在变量中。printf '=%.0s' {1..40} 是重复字符 N 次的巧妙技巧。
# 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正则表达式
bash 正则表达式与 =~
=~ 使用 ERE(扩展正则表达式)。捕获组填充 BASH_REMATCH(索引 0 = 整个匹配,1+ = 组)。引用字符串但不引用正则表达式(引用正则表达式使其成为字面值)。对于不区分大小写的匹配,使用 shopt -s nocasematch。正则表达式是 POSIX ERE——没有 \d、\w;使用 [0-9]、[A-Za-z0-9_]。
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 nocasematchgrep 正则表达式模式
grep 默认使用 BRE(基本正则表达式),其中 +、?、|、() 需要反斜杠。-E 启用 ERE(更清晰的语法)。-P 启用 PCRE,带 \d、\w、\b、前瞻——最强大但可移植性较差(仅 GNU grep)。对于必须在 macOS/BSD 上运行的脚本,优先使用 -E。
# 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$' filesed 正则表达式替换
sed 默认使用 BRE(捕获组需要 \( \),反向引用 \1)。-E 切换到 ERE(更清晰的 () 和 +、?、|)。I 标志使替换不区分大小写(GNU sed)。当模式包含斜杠时使用不同的分隔符(s|...|...|)。[[:space:]] 对于空白是可移植的。
# 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 URLsawk 正则表达式
awk 使用 ERE。~ 运算符针对正则表达式测试字符串。/pat1/, /pat2/ 是范围模式(从 pat1 到 pat2 包含匹配)。gsub 进行就地全局替换。gawk 的带第三个参数的 match() 将组捕获到数组中——很方便但不能移植到 mawk/POSIX awk。
# 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)。
# 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调试
set 选项
set -e 在任何命令失败时退出(脚本必备)。set -u 捕获变量名中的拼写错误。set -o pipefail 使管道在任何部分失败时失败(没有它,只有最后一个命令的退出代码重要)。set -x 跟踪执行。组合为 -euo pipefail,这是'严格模式'——最安全的默认值。
#!/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 分开。
# 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 也在函数内部触发(否则在函数上下文中禁用)。
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++)); doneshellcheck linter
shellcheck 是 shell 脚本的事实上的 linter——捕获引用问题、未使用的变量、常见陷阱,并建议惯用法。在每个脚本上运行它。最常见的修复是 SC2086:引用变量以防止分词和 globbing。许多 CI 管道要求 shellcheck 通过。
# 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=灰色。
# 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安全
引用与注入
Bash 安全的 #1 规则:引用每个变量扩展。未引用的 $var 经历分词和 globbing,导致 bug 和注入。对于带动态参数的命令,使用数组(cmd=(ls "$dir"); "${cmd[@]}")而不是字符串。绝不要 eval 或 $() 用户输入到 shell 字符串中。
# 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 权限的配置文件。
# 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)比黑名单对于允许的操作更安全。
# 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 保持脚本对所有者私有。
# 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 让您在执行之前查看哈希——但更安全的模式是下载-然后验证-然后执行。
# 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")网络
curl 高级
curl 通过 URL 传输数据。-X 设置方法,-d 发送数据,-H 设置标头。-O 用远程文件名保存。-I 只获取标头。-L 跟随重定向。-u user:pass 用于认证。-v 用于详细。
# 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.comwget
wget 非交互式地下载文件。-m 镜像站点。-c 恢复中断的下载。-r 递归,-l 限制深度。-b 在后台运行。对于递归下载比 curl 更好。
# 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.zipssh 与 scp
ssh 连接到远程机器。-i 指定密钥文件。-L 创建本地端口转发。scp 通过 SSH 复制文件。-r 递归目录。使用 ssh-copy-id 安装密钥。配置 ~/.ssh/config 用于别名。
# 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 更快更详细。用于查找哪个进程使用端口或诊断连接问题。
# List listening ports
netstat -tlnp
ss -tlnp
# Show all connections
netstat -an
ss -tunap
# Show routing table
netstat -r
# Show interface stats
netstat -iping 与 traceroute
ping 测试可达性和延迟(-c 限制计数)。traceroute 显示到主机的路径。dig 查询 DNS 记录(A、MX、NS、TXT)。+short 给出简洁输出。用于网络诊断和 DNS 故障排除。
# 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.comShell 脚本深入
函数
函数将命令分组。local 创建函数作用域变量。$1、$2 是参数。return 设置退出状态(0-255)。用 $() 捕获输出。函数必须在使用之前定义。使用 source 从文件加载。
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 增强版,带正则表达式。
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 进行递归文件迭代。
# 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 拆分为数组。
# 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 开始脚本。
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文件操作深入
find 高级
find 按条件搜索文件。-type f/d 用于文件/目录。-size +1M 大于 1MB。-mtime -7 在 7 天内修改。-exec 对每个匹配运行命令。{} 是文件名;\; 结束命令。
# 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" -deletetar 与压缩
tar 组合文件;-z 用 gzip 压缩。-c 创建,-x 提取,-t 列出,-v 详细,-f 文件名。对于 .tar.bz2 使用 -j。zip/unzip 处理 ZIP 格式。使用 -C 提取到特定目录。
# 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.ziprsync
rsync 高效同步文件,只传输差异。-a 归档模式(保留属性),-v 详细,-z 压缩。--delete 移除源中不存在的文件。src/ 上的尾部斜杠很重要:有它,复制内容;没有它,复制目录。
# 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 更改所有权。
# 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 解析符号链接。
# 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/常见陷阱
未引用的变量
未引用的变量在空格上拆分和 glob 。$(ls) 在文件名包含空格时中断。使用 glob(*.txt)或 find -print0 与 read -d。始终引用变量:"$var"。使用 IFS= read -r 进行安全行读取。
# 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"
donecp vs mv
cp 复制,mv 移动。两者都可以在无警告的情况下覆盖。使用 -i 在覆盖之前提示。使用 -n 永不覆盖。mv 在同一文件系统上是原子的,对于锁和原子更新很有用。
# 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。
# 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.txtShebang
shebang(#!)指定解释器。#!/usr/bin/env bash 比 #!/bin/bash 更可移植。使用 /bin/sh 获得最大可移植性(避免 bashism)。始终用 chmod +x 使脚本可执行。
#!/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。在脚本中始终以有意义的状态代码退出。