Linux Shell 脚本进阶
大约 19 分钟约 5608 字
Linux Shell 脚本进阶
简介
Shell 脚本进阶涵盖函数、数组、字符串处理、文件操作和正则表达式。掌握高级脚本技巧,有助于编写自动化运维脚本和数据处理工具。Shell 脚本是 Linux 运维工程师最核心的技能之一,从简单的批量重命名到复杂的自动化部署流水线,Shell 脚本都能胜任。本文将从函数封装、数组处理、字符串高级操作、流程控制、信号处理、调试技巧、性能优化等多个维度深入讲解 Shell 脚本进阶知识。
特点
实现
函数与参数处理
#!/bin/bash
set -euo pipefail
# 函数定义
log() {
local level="$1"; shift
local msg="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $msg"
}
# 带返回值的函数
check_port() {
local port="$1"
if ss -tlnp | grep -q ":${port} "; then
return 0 # 端口被占用
else
return 1
fi
}
# 使用
log INFO "Starting deployment"
if check_port 8080; then
log WARN "Port 8080 is in use"
fi
# 参数处理(getopts)
usage() { echo "Usage: $0 [-e ENV] [-p PORT] APP_NAME"; exit 1; }
env="dev"; port=8080
while getopts "e:p:h" opt; do
case $opt in
e) env="$OPTARG" ;;
p) port="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND-1))
app="${1:?Application name required}"
log INFO "Deploying $app to $env on port $port"函数进阶用法
#!/bin/bash
set -euo pipefail
# ====== 函数递归 ======
# 计算目录下所有文件的总大小(递归方式)
calc_dir_size() {
local dir="$1"
local total=0
for item in "$dir"/*; do
[ -e "$item" ] || continue
if [ -d "$item" ]; then
local sub_size=$(calc_dir_size "$item")
total=$((total + sub_size))
elif [ -f "$item" ]; then
local size=$(stat -c%s "$item" 2>/dev/null || echo 0)
total=$((total + size))
fi
done
echo "$total"
}
# 使用示例
total_bytes=$(calc_dir_size "/var/log")
echo "Total size: $((total_bytes / 1024)) KB"
# ====== 通过全局变量返回多个值 ======
get_system_info() {
SYS_INFO_OS=$(cat /etc/os-release 2>/dev/null | grep '^NAME=' | cut -d'"' -f2)
SYS_INFO_KERNEL=$(uname -r)
SYS_INFO_UPTIME=$(uptime -p 2>/dev/null || uptime | sed 's/.*up/up/')
SYS_INFO_CPUS=$(nproc)
SYS_INFO_MEM=$(free -h | awk '/Mem:/{print $2}')
}
get_system_info
echo "OS: $SYS_INFO_OS"
echo "Kernel: $SYS_INFO_KERNEL"
echo "Uptime: $SYS_INFO_UPTIME"
echo "CPUs: $SYS_INFO_CPUS"
echo "Memory: $SYS_INFO_MEM"
# ====== 函数作为回调使用 ======
process_with_callback() {
local file="$1"
local callback="$2"
if [ -f "$file" ]; then
$callback "$file"
else
echo "File not found: $file"
fi
}
my_callback() {
echo "Processing: $1"
wc -l "$1"
}
process_with_callback "/etc/hosts" my_callback
# ====== 命名空间模拟 ======
# 使用函数前缀模拟命名空间
db::connect() {
echo "Connecting to $DB_HOST:$DB_PORT as $DB_USER"
}
db::query() {
local sql="$1"
echo "Executing: $sql"
mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" -e "$sql" "$DB_NAME"
}
db::disconnect() {
echo "Disconnected from database"
}
# 使用命名空间函数
export DB_HOST="127.0.0.1"
export DB_PORT="3306"
export DB_USER="root"
export DB_PASS="secret"
export DB_NAME="mydb"
db::connect
db::query "SELECT COUNT(*) FROM users;"
db::disconnect数组操作详解
#!/bin/bash
set -euo pipefail
# ====== 基础数组操作 ======
# 声明数组
fruits=("apple" "banana" "cherry" "date" "elderberry")
# 访问元素(从 0 开始)
echo "First: ${fruits[0]}" # apple
echo "Last: ${fruits[-1]}" # elderberry
echo "All: ${fruits[@]}" # 所有元素
echo "Count: ${#fruits[@]}" # 5
# 追加元素
fruits+=("fig" "grape")
echo "After append: ${#fruits[@]}" # 7
# 删除元素
unset 'fruits[1]' # 删除 banana
echo "After delete: ${fruits[@]}"
# 切片操作
echo "Slice [1:3]: ${fruits[@]:1:3}" # 从索引 1 开始取 3 个
# 替换元素
fruits[0]="apricot"
echo "After replace: ${fruits[0]}"
# ====== 关联数组(字典) ======
declare -A server_config
server_config[web1]="192.168.1.10"
server_config[web2]="192.168.1.11"
server_config[db1]="192.168.1.20"
server_config[db2]="192.168.1.21"
# 遍历关联数组
echo "=== Server List ==="
for key in "${!server_config[@]}"; do
echo "$key -> ${server_config[$key]}"
done
# 检查 key 是否存在
if [[ -v server_config[web1] ]]; then
echo "web1 exists: ${server_config[web1]}"
fi
# 获取所有 key 和 value
all_keys=("${!server_config[@]}")
all_values=("${server_config[@]}")
echo "Keys: ${all_keys[*]}"
echo "Values: ${all_values[*]}"
# ====== 数组与文件读取 ======
# 将文件内容读入数组(每行一个元素)
mapfile -t lines < /etc/hosts
echo "Hosts file has ${#lines[@]} lines"
echo "First line: ${lines[0]}"
# 使用 readarray(mapfile 的别名)
readarray -t processes < <(ps aux)
echo "Total processes: ${#processes[@]}"
# ====== 数组排序与去重 ======
# 排序
numbers=(5 3 8 1 9 2 7 4 6)
IFS=$'\n' sorted=($(sort -n <<< "${numbers[*]}")); unset IFS
echo "Sorted: ${sorted[*]}"
# 去重
dup_arr=(1 2 3 2 4 5 3 6 1)
IFS=$'\n' unique=($(echo "${dup_arr[*]}" | tr ' ' '\n' | sort -u)); unset IFS
echo "Unique: ${unique[*]}"
# ====== 二维数组模拟 ======
# Bash 不支持原生二维数组,使用关联数组模拟
declare -A matrix
matrix[0,0]="A1"; matrix[0,1]="A2"; matrix[0,2]="A3"
matrix[1,0]="B1"; matrix[1,1]="B2"; matrix[1,2]="B3"
for row in 0 1; do
for col in 0 1 2; do
echo -n "${matrix[$row,$col]} "
done
echo
done字符串高级处理
#!/bin/bash
set -euo pipefail
# ====== 参数扩展 ======
str="Hello,World,Bash,Scripting"
# 基础操作
echo "Length: ${#str}" # 字符串长度
echo "Upper: ${str^^}" # 转大写
echo "Lower: ${str,,}" # 转小写
echo "Capitalize: ${str^}" # 首字母大写
# 截取子串
echo "Substring: ${str:6:5}" # 从位置 6 开始取 5 个字符 -> World
echo "From end: ${str: -9}" # 最后 9 个字符
# 替换
file_path="/home/user/docs/file.txt"
echo "Basename: ${file_path##*/}" # file.txt(最长匹配删除前缀)
echo "Dirname: ${file_path%/*}" # /home/user/docs(最短匹配删除后缀)
echo "Ext: ${file_path##*.}" # txt
# 全局替换与首次替换
text="foo-bar-foo-baz"
echo "Replace first: ${text/foo/FOO}" # FOO-bar-foo-baz
echo "Replace all: ${text//foo/FOO}" # FOO-bar-FOO-baz
# 删除前缀/后缀
version="v1.2.3-release"
echo "Remove prefix: ${version#v}" # 1.2.3-release
echo "Remove suffix: ${version%-release}" # v1.2.3
# ====== 默认值与替换 ======
# 如果变量未设置或为空,使用默认值
echo "${UNSET_VAR:-default_value}" # default_value
echo "${UNSET_VAR:=assigned_value}" # 同时赋值给变量
echo "UNSET_VAR is now: $UNSET_VAR"
# 如果变量已设置且非空,使用替代值
SET_VAR="hello"
echo "${SET_VAR:+REPLACED}" # REPLACED
echo "${EMPTY_VAR:+REPLACED}" # (空)
# 如果变量未设置,报错退出
# ${MISSING_VAR:?Error: MISSING_VAR is required}
# ====== 字符串分割 ======
csv_data="name,age,city,job"
IFS=',' read -ra fields <<< "$csv_data"
for i in "${!fields[@]}"; do
echo "Field[$i]: ${fields[$i]}"
done
# 按行分割
multi_line="line1
line2
line3"
while IFS= read -r line; do
echo "Line: $line"
done <<< "$multi_line"
# ====== 字符串测试与模式匹配 ======
filename="report-2024-01.pdf"
# 正则匹配
if [[ $filename =~ ^report-[0-9]{4}-[0-9]{2}\.pdf$ ]]; then
echo "Valid report filename"
fi
# 通配符匹配
case "$filename" in
*.pdf) echo "PDF file" ;;
*.doc*) echo "Word document" ;;
*.txt) echo "Text file" ;;
*) echo "Unknown type" ;;
esac
# 检查字符串是否包含子串
if [[ "hello world" == *"world"* ]]; then
echo "Contains 'world'"
fi
# ====== printf 格式化输出 ======
name="Alice"; age=30; score=95.678
printf "Name: %-10s Age: %3d Score: %.2f\n" "$name" "$age" "$score"
# 生成对齐的表格
printf "%-20s %-10s %-15s\n" "SERVER" "STATUS" "RESPONSE_TIME"
printf "%-20s %-10s %-15s\n" "-------------------" "----------" "---------------"
printf "%-20s %-10s %-15s\n" "web-server-01" "OK" "45ms"
printf "%-20s %-10s %-15s\n" "db-server-01" "OK" "12ms"
printf "%-20s %-10s %-15s\n" "cache-server-01" "WARN" "230ms"流程控制进阶
#!/bin/bash
set -euo pipefail
# ====== select 菜单 ======
PS3="Please select an option: "
options=("Check Disk Usage" "Check Memory" "Check Network" "View Processes" "Quit")
select opt in "${options[@]}"; do
case $REPLY in
1) df -h ;;
2) free -h ;;
3) ip addr show | grep "inet " ;;
4) ps aux --sort=-%mem | head -10 ;;
5) break ;;
*) echo "Invalid option: $REPLY" ;;
esac
done
# ====== 嵌套循环与 break/continue ======
# 查找多个目录中最大的文件
search_dirs=("/var/log" "/tmp" "/etc")
for dir in "${search_dirs[@]}"; do
[ -d "$dir" ] || continue
echo "=== Scanning $dir ==="
max_size=0
max_file=""
while IFS= read -r -d '' file; do
size=$(stat -c%s "$file" 2>/dev/null || echo 0)
if [ "$size" -gt "$max_size" ]; then
max_size=$size
max_file="$file"
fi
done < <(find "$dir" -type f -print0 2>/dev/null)
if [ -n "$max_file" ]; then
echo "Largest: $max_file ($((max_size / 1024)) KB)"
fi
done
# ====== 循环中的管道陷阱 ======
# 错误示例:管道创建子 Shell,变量修改不会传递回来
echo "a b c d" | read -r first rest
echo "First: $first" # 空!因为管道在子 Shell 中执行
# 正确写法:使用进程替换
read -r first rest < <(echo "a b c d")
echo "First: $first" # a
# 或使用 here-string
read -r first rest <<< "a b c d"
echo "First: $first" # a
# ====== 条件判断进阶 ======
# 文件类型判断
check_file() {
local file="$1"
if [ ! -e "$file" ]; then
echo "$file: does not exist"
return 1
fi
[ -f "$file" ] && echo "$file: regular file"
[ -d "$file" ] && echo "$file: directory"
[ -L "$file" ] && echo "$file: symbolic link"
[ -r "$file" ] && echo "$file: readable"
[ -w "$file" ] && echo "$file: writable"
[ -x "$file" ] && echo "$file: executable"
[ -s "$file" ] && echo "$file: not empty (size: $(stat -c%s "$file") bytes)"
}
# 数字比较
compare_numbers() {
local a="$1" b="$2"
if (( a > b )); then
echo "$a is greater than $b"
elif (( a == b )); then
echo "$a equals $b"
else
echo "$a is less than $b"
fi
}
# 多条件组合
if [ -f "/etc/nginx/nginx.conf" ] && [ -r "/etc/nginx/nginx.conf" ]; then
echo "Nginx config exists and is readable"
fi
# 使用 [[ ]] 进行模式匹配
if [[ "$USER" == root || "$UID" -eq 0 ]]; then
echo "Running as root"
fi文件处理与批量操作
# 批量处理日志文件
process_logs() {
local log_dir="${1:-/var/log/app}"
local output="${2:-report.txt}"
echo "=== Log Report $(date) ===" > "$output"
# 统计各日志文件行数
for logfile in "$log_dir"/*.log; do
[ -f "$logfile" ] || continue
local lines=$(wc -l < "$logfile")
local errors=$(grep -c "ERROR" "$logfile" 2>/dev/null || echo 0)
echo "$(basename "$logfile"): $lines lines, $errors errors" >> "$output"
done
# 提取最近 1 小时的错误
echo -e "\n=== Recent Errors ===" >> "$output"
find "$log_dir" -name "*.log" -mmin -60 -exec grep "ERROR" {} + >> "$output" 2>/dev/null
}
# 并行处理
parallel_process() {
local max_procs=4
local running=0
for item in "$@"; do
process_item "$item" &
running=$((running + 1))
if [ $running -ge $max_procs ]; then
wait -n
running=$((running - 1))
fi
done
wait
}文件处理进阶技巧
#!/bin/bash
set -euo pipefail
# ====== 安全的文件操作 ======
# 原子性文件写入(防止写入中断导致文件损坏)
safe_write() {
local target="$1"
local content="$2"
local tmp_file
tmp_file=$(mktemp "$(dirname "$target")/.XXXXXX")
echo "$content" > "$tmp_file"
sync
mv -f "$tmp_file" "$target"
}
# 安全的文件备份
backup_file() {
local file="$1"
local backup_dir="${2:-/backup}"
local timestamp
timestamp=$(date '+%Y%m%d_%H%M%S')
local backup_name="${file##*/}.${timestamp}.bak"
mkdir -p "$backup_dir"
cp -a "$file" "${backup_dir}/${backup_name}"
echo "Backed up to: ${backup_dir}/${backup_name}"
}
# ====== 配置文件操作 ======
# 读取 INI 风格配置文件
parse_ini() {
local ini_file="$1"
local section=""
while IFS= read -r line; do
# 去除首尾空格
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# 跳过空行和注释
[[ -z "$line" || "$line" == \#* ]] && continue
# 解析 section
if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
section="${BASH_REMATCH[1]}"
continue
fi
# 解析 key=value
if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
echo "${section:+$section.}$key=$value"
fi
done < "$ini_file"
}
# 使用示例(假设有 config.ini)
# parse_ini config.ini
# 修改配置文件中的值
config_set() {
local file="$1"
local key="$2"
local value="$3"
if grep -q "^${key}=" "$file" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${value}|" "$file"
else
echo "${key}=${value}" >> "$file"
fi
}
# 获取配置值
config_get() {
local file="$1"
local key="$2"
local default="${3:-}"
grep "^${key}=" "$file" 2>/dev/null | cut -d'=' -f2- || echo "$default"
}
# ====== CSV 文件处理 ======
# 读取 CSV(简单版,不含逗号在字段中的情况)
read_csv() {
local csv_file="$1"
local delimiter="${2:-,}"
local header=1
while IFS="$delimiter" read -r line; do
if [ $header -eq 1 ]; then
echo "HEADER: $line"
header=0
continue
fi
# 处理数据行
IFS="$delimiter" read -ra cols <<< "$line"
echo "Row: col1=${cols[0]}, col2=${cols[1]}, col3=${cols[2]}"
done < "$csv_file"
}
# ====== 批量文件重命名 ======
batch_rename() {
local dir="$1"
local pattern="$2"
local replacement="$3"
local dry_run="${4:-false}"
local count=0
for file in "$dir"/*$pattern*; do
[ -e "$file" ] || continue
local new_name
new_name=$(echo "$(basename "$file")" | sed "s/$pattern/$replacement/g")
local new_path="$(dirname "$file")/$new_name"
if [ "$dry_run" = "true" ]; then
echo "[DRY RUN] $file -> $new_path"
else
mv -n "$file" "$new_path"
echo "Renamed: $file -> $new_path"
fi
count=$((count + 1))
done
echo "Total: $count files"
}
# 使用示例(先试运行)
# batch_rename "/data/files" ".txt" ".log" "true"
# batch_rename "/data/files" ".txt" ".log"信号处理与模板
#!/bin/bash
# 标准脚本模板
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP_DIR=""
cleanup() {
log INFO "Cleaning up..."
[ -d "$TMP_DIR" ] && rm -rf "$TMP_DIR"
exit 0
}
trap cleanup EXIT INT TERM
TMP_DIR=$(mktemp -d)
log INFO "Working in $TMP_DIR"
# 带超时的命令
run_with_timeout() {
local timeout_sec="$1"; shift
timeout "$timeout_sec" "$@"
}
# 重试逻辑
retry() {
local max_attempts="$1"; shift
local attempt=1
while [ $attempt -le $max_attempts ]; do
if "$@"; then return 0; fi
log WARN "Attempt $attempt failed, retrying in 5s..."
sleep 5
attempt=$((attempt + 1))
done
log ERROR "All $max_attempts attempts failed"
return 1
}
# 使用
retry 3 curl -sf http://health-endpoint/ || exit 1信号处理进阶
#!/bin/bash
set -euo pipefail
# ====== 完整的信号处理框架 ======
#!/bin/bash
set -euo pipefail
# 全局状态
SCRIPT_NAME=$(basename "$0")
PID_FILE="/tmp/${SCRIPT_NAME%.sh}.pid"
LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"
TMP_DIR=""
VERBOSE=0
DRY_RUN=0
# 日志函数
log() {
local level="$1"; shift
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local message="[$timestamp] [$level] $*"
echo "$message" >> "$LOG_FILE" 2>/dev/null || true
if [ "$VERBOSE" -eq 1 ] || [[ "$level" == "ERROR" || "$level" == "WARN" ]]; then
echo "$message" >&2
fi
}
# 创建 PID 文件防止重复运行
create_pid_file() {
if [ -f "$PID_FILE" ]; then
local old_pid
old_pid=$(cat "$PID_FILE")
if kill -0 "$old_pid" 2>/dev/null; then
log ERROR "Another instance is running (PID: $old_pid)"
exit 1
fi
log WARN "Stale PID file found, removing"
rm -f "$PID_FILE"
fi
echo $$ > "$PID_FILE"
log INFO "Started with PID $$"
}
# 清理函数
cleanup() {
local exit_code=$?
log INFO "Cleaning up (exit code: $exit_code)..."
# 停止后台进程
jobs -p | xargs -r kill 2>/dev/null || true
wait 2>/dev/null || true
# 清理临时目录
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
rm -rf "$TMP_DIR"
log INFO "Removed temp directory: $TMP_DIR"
fi
# 删除 PID 文件
rm -f "$PID_FILE"
log INFO "Cleanup complete"
exit $exit_code
}
# 注册信号处理
trap cleanup EXIT
trap 'log WARN "Received SIGINT (Ctrl+C), shutting down gracefully..."; exit 130' INT
trap 'log WARN "Received SIGTERM, shutting down gracefully..."; exit 143' TERM
trap 'log ERROR "Received SIGHUP"; exit 129' HUP
# ====== 不同信号的处理 ======
# SIGUSR1/SIGUSR2 自定义信号
trap 'log INFO "Received SIGUSR1 - reloading config..."; reload_config' USR1
trap 'log INFO "Received SIGUSR2 - rotating logs..."; rotate_logs' USR2
reload_config() {
log INFO "Reloading configuration..."
# 重新加载配置文件
if [ -f "/etc/myapp/config.conf" ]; then
source /etc/myapp/config.conf
log INFO "Configuration reloaded"
fi
}
rotate_logs() {
local max_logs=5
for i in $(seq $((max_logs - 1)) -1 1); do
local prev=$((i - 1))
[ -f "${LOG_FILE}.${prev}" ] && mv "${LOG_FILE}.${prev}" "${LOG_FILE}.${i}"
done
[ -f "$LOG_FILE" ] && mv "$LOG_FILE" "${LOG_FILE}.0"
log INFO "Logs rotated"
}
# ====== 守护进程模式 ======
run_as_daemon() {
local interval="${1:-60}"
log INFO "Entering daemon mode (interval: ${interval}s)"
while true; do
main_loop
sleep "$interval"
done
}
main_loop() {
log INFO "Running main loop..."
# 在这里放主要逻辑
}
# 初始化
create_pid_file
TMP_DIR=$(mktemp -d)
# 根据参数决定运行模式
if [ "${1:-}" == "--daemon" ]; then
run_as_daemon "${2:-60}"
else
main_loop
fi正则表达式与文本处理
#!/bin/bash
set -euo pipefail
# ====== Bash 内置正则 ======
# 使用 =~ 进行正则匹配
ip="192.168.1.100"
if [[ $ip =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
echo "Valid IP: ${BASH_REMATCH[0]}"
echo "Octets: ${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]}"
fi
# 验证邮箱格式
email="user@example.com"
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email: $email"
fi
# 提取 URL 中的域名
url="https://www.example.com:8080/path/to/page?query=value"
if [[ $url =~ https?://([^/:]+) ]]; then
echo "Domain: ${BASH_REMATCH[1]}"
fi
# ====== grep 高级用法 ======
# 递归搜索,排除目录
grep -r "TODO" --include="*.py" --exclude-dir={".git","venv","__pycache__"} .
# 使用 Perl 兼容正则
grep -P '(?<=password:)\s*\S+' config.txt
# 只输出匹配部分
grep -oP '\d+\.\d+\.\d+\.\d+' access.log
# 统计匹配数
grep -c "ERROR" /var/log/app.log
# 上下文行
grep -B2 -A5 "Exception" /var/log/app.log # 前 2 行后 5 行
# ====== sed 高级用法 ======
# 多行替换
sed -i '/<VirtualHost/,/<\/VirtualHost>/c\
<VirtualHost *:80>\
ServerName example.com\
DocumentRoot /var/www/html\
</VirtualHost>' /etc/httpd/conf/httpd.conf
# 使用不同分隔符(当路径包含 / 时)
sed -i 's|/old/path|/new/path|g' config.txt
# 只替换第 N 次匹配
sed -i 's/old/new/2' file.txt # 只替换每行第 2 次
# 删除空行和注释
sed -i '/^$/d; /^#/d' config_clean.txt
# 在匹配行后追加内容
sed -i '/^export PATH=/a\export CUSTOM_PATH=/opt/bin' ~/.bashrc
# ====== awk 高级用法 ======
# 多字段处理
awk -F',' 'NR>1 {sum+=$3; count++} END {print "Average:", sum/count}' data.csv
# 格式化输出
df -h | awk 'NR>1 {printf "%-20s %8s %8s %6s\n", $1, $3, $4, $5}'
# 多文件关联
awk -F',' 'FNR==NR{a[$1]=$2; next} {print $0, a[$1]}' users.txt orders.txt
# 统计 access.log 中各 IP 的访问次数并排序
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn | head -20
# 提取时间范围内的日志
awk '$0 >= "2024-01-15 10:00:00" && $0 <= "2024-01-15 12:00:00"' /var/log/app.log调试技巧
#!/bin/bash
set -euo pipefail
# ====== 调试模式 ======
# 方式 1:bash -x(逐行追踪)
# bash -x script.sh
# 方式 2:在脚本中开启
set -x # 开启调试
# ... 调试的代码 ...
set +x # 关闭调试
# 方式 3:只调试特定函数
debug_func() {
{ set -x; complex_operation; } 2>&1 | tee /tmp/debug.log
}
# ====== 自定义调试函数 ======
DEBUG=0
debug() {
[ "$DEBUG" -eq 1 ] || return 0
local func_name="${FUNCNAME[1]:-main}"
local line_no="${BASH_LINENO[0]:-?}"
local msg="[$func_name:$line_no] $*"
echo "[DEBUG] $msg" >&2
}
# 使用示例
DEBUG=1
my_func() {
debug "Entering function with args: $*"
local result=$((1 + 1))
debug "Computed result: $result"
echo "$result"
}
my_func "hello" "world"
# ====== ShellCheck 静态分析 ======
# 安装
# yum install -y shellcheck
# 或
# dnf install -y shellcheck
# 使用
# shellcheck script.sh
# 常见 ShellCheck 警告及修复:
# SC2086 - Double quote to prevent globbing and word splitting
# 修复: "$variable" 而不是 $variable
# SC2034 - variable appears unused
# 修复: 删除未使用的变量或添加 _ 前缀
# SC2004 - $/${} is unnecessary on arithmetic variables
# 修复: $((var)) 而不是 $(($var))
# ====== 性能分析 ======
# 测量脚本执行时间
time bash script.sh
# 更详细的时间分析
start_time=$(date +%s.%N)
# ... 要测量的代码 ...
end_time=$(date +%s.%N)
elapsed=$(echo "$end_time - $start_time" | bc)
echo "Elapsed: $elapsed seconds"
# 使用内置的 SECONDS 变量
SECONDS=0
# ... 代码 ...
echo "Took $SECONDS seconds"
# ====== 日志追踪 ======
# 在关键位置添加调用栈信息
trace_caller() {
echo "Called from: ${FUNCNAME[1]} (line ${BASH_LINENO[0]})"
}
# 执行跟踪
exec > >(tee -a "$LOG_FILE") 2>&1
# 将所有 stdout 和 stderr 同时输出到终端和日志文件性能优化
#!/bin/bash
set -euo pipefail
# ====== 避免在循环中调用外部命令 ======
# 慢:每次循环都调用 date
for i in $(seq 1 1000); do
ts=$(date +%s)
done
# 快:使用内置变量
start=$(date +%s)
for i in $(seq 1 1000); do
ts=$start
done
# ====== 使用内置字符串操作替代外部命令 ======
# 慢
basename=$(echo "$path" | sed 's|.*/||')
# 快
basename="${path##*/}"
# ====== 批量操作替代逐条操作 ======
# 慢:逐条查找
for file in *.log; do
grep "ERROR" "$file"
done
# 快:一次查找
grep "ERROR" *.log
# ====== 使用 mktemp 和文件描述符 ======
# 使用 process substitution 避免临时文件
diff <(sort file1.txt) <(sort file2.txt)
# 使用文件描述符
exec 3<>/tmp/myfile # 打开文件描述符 3 用于读写
echo "hello" >&3
read -r line <&3
exec 3>&- # 关闭文件描述符 3
# ====== 大文件处理 ======
# 使用 dd 进行块操作
dd if=input.txt of=output.txt bs=1M count=100 # 只读取前 100MB
# 流式处理大文件(不加载到内存)
while IFS= read -r line; do
process_line "$line"
done < huge_file.txt
# ====== 并行处理 ======
# 使用 xargs 并行
find /data -name "*.log" -print0 | xargs -0 -P4 -I{} gzip {}
# 使用 GNU parallel(如果安装了)
# find /data -name "*.log" | parallel -j4 gzip {}
# 纯 Bash 并行控制
parallel_exec() {
local max_jobs="${1:-4}"
shift
local job_count=0
for cmd in "$@"; do
eval "$cmd" &
job_count=$((job_count + 1))
if [ "$job_count" -ge "$max_jobs" ]; then
wait -n 2>/dev/null || wait
job_count=$((job_count - 1))
fi
done
wait
}
# 使用示例
parallel_exec 4 \
"process_dir /data/dir1" \
"process_dir /data/dir2" \
"process_dir /data/dir3" \
"process_dir /data/dir4"实战脚本:服务管理工具
#!/bin/bash
# service_manager.sh - 简易服务管理工具
set -euo pipefail
APP_NAME="myapp"
APP_HOME="/opt/$APP_NAME"
PID_FILE="$APP_HOME/$APP_NAME.pid"
LOG_FILE="$APP_HOME/logs/$APP_NAME.log"
CONF_FILE="$APP_HOME/config/application.yml"
JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"
JAR_FILE="$APP_HOME/lib/$APP_NAME.jar"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
print_color() {
local color="$1"; shift
echo -e "${color}$*${NC}"
}
# 检查 Java 环境
check_java() {
if ! command -v java &>/dev/null; then
print_color "$RED" "ERROR: Java not found in PATH"
exit 1
fi
echo "Java version: $(java -version 2>&1 | head -1)"
}
# 检查服务状态
status() {
if [ -f "$PID_FILE" ]; then
local pid
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
print_color "$GREEN" "$APP_NAME is running (PID: $pid)"
# 显示内存使用
local mem
mem=$(ps -o rss= -p "$pid" 2>/dev/null | awk '{printf "%.1f MB", $1/1024}')
echo "Memory usage: $mem"
return 0
else
print_color "$YELLOW" "$APP_NAME is not running (stale PID file)"
return 1
fi
else
print_color "$YELLOW" "$APP_NAME is not running"
return 1
fi
}
# 启动服务
start() {
if status &>/dev/null; then
print_color "$YELLOW" "$APP_NAME is already running"
return 0
fi
check_java
print_color "$GREEN" "Starting $APP_NAME..."
mkdir -p "$(dirname "$LOG_FILE")"
nohup java $JAVA_OPTS -jar "$JAR_FILE" \
--spring.config.location="$CONF_FILE" \
>> "$LOG_FILE" 2>&1 &
local pid=$!
echo "$pid" > "$PID_FILE"
# 等待启动
local max_wait=30
local waited=0
while [ $waited -lt $max_wait ]; do
if ! kill -0 "$pid" 2>/dev/null; then
print_color "$RED" "Failed to start $APP_NAME. Check log: $LOG_FILE"
tail -20 "$LOG_FILE"
rm -f "$PID_FILE"
return 1
fi
if curl -sf http://localhost:8080/actuator/health &>/dev/null; then
print_color "$GREEN" "$APP_NAME started successfully (PID: $pid)"
return 0
fi
sleep 1
waited=$((waited + 1))
done
print_color "$YELLOW" "$APP_NAME may still be starting... check $LOG_FILE"
}
# 停止服务
stop() {
if ! status &>/dev/null; then
print_color "$YELLOW" "$APP_NAME is not running"
rm -f "$PID_FILE"
return 0
fi
local pid
pid=$(cat "$PID_FILE")
print_color "$GREEN" "Stopping $APP_NAME (PID: $pid)..."
kill "$pid" 2>/dev/null
# 等待优雅关闭
local max_wait=30
local waited=0
while [ $waited -lt $max_wait ]; do
if ! kill -0 "$pid" 2>/dev/null; then
print_color "$GREEN" "$APP_NAME stopped"
rm -f "$PID_FILE"
return 0
fi
sleep 1
waited=$((waited + 1))
done
# 强制终止
print_color "$YELLOW" "Force killing $APP_NAME..."
kill -9 "$pid" 2>/dev/null
rm -f "$PID_FILE"
print_color "$GREEN" "$APP_NAME force stopped"
}
# 重启服务
restart() {
stop
sleep 2
start
}
# 查看日志
logs() {
local lines="${1:-50}"
tail -f "$LOG_FILE" -n "$lines"
}
# 主入口
case "${1:-}" in
start) start ;;
stop) stop ;;
restart) restart ;;
status) status ;;
logs) logs "${2:-50}" ;;
*)
echo "Usage: $0 {start|stop|restart|status|logs [lines]}"
exit 1
;;
esac优点
缺点
总结
Shell 脚本进阶通过函数、参数处理和信号捕获编写健壮的运维脚本。set -euo pipefail 防止静默失败。trap 确保清理临时资源。复杂逻辑建议使用 Python 替代。在实际工作中,Shell 脚本最适合用于系统管理、自动化部署、日志分析等场景。对于需要复杂数据结构或高级算法的任务,建议使用 Python、Go 等语言。
关键知识点
- set -euo pipefail 在错误、未定义变量和管道失败时退出。
- local 限制变量作用域在函数内。
- trap 捕获 EXIT/INT/TERM 信号执行清理逻辑。
$'...'语法支持转义字符,例如换行\n。${var:-default}提供默认值,${var:=default}同时赋值。${var#pattern}最短前缀匹配删除,${var##pattern}最长前缀匹配删除。${var%pattern}最短后缀匹配删除,${var%%pattern}最长后缀匹配删除。- mapfile/readarray 可以将文件内容读入数组。
- declare -A 声明关联数组(字典)。
- [[ ]] 支持正则匹配和模式匹配,比 [ ] 更安全。
- BASH_REMATCH 数组存储正则匹配的分组结果。
项目落地视角
- 为部署、备份和健康检查编写标准脚本。
- 使用 getopts 处理脚本参数。
- 所有脚本添加 trap 清理逻辑。
- 建立脚本模板库,统一编码规范。
- 所有脚本纳入版本控制,变更需 Code Review。
- 关键脚本添加 ShellCheck 静态检查到 CI 流程。
常见误区
- 忘记引号包裹变量导致空格问题。
- 不使用 set -euo pipefail 导致错误静默通过。
- 在循环中创建大量子进程影响性能。
- 管道创建子 Shell,导致变量修改丢失。
- 使用
$(( ))时忘记变量不需要$前缀。 - echo -e 在某些 Shell 中行为不一致,优先使用 printf。
- 在 heredoc 中忘记引用结束标记导致变量展开。
进阶路线
- 学习 ShellCheck 静态分析工具。
- 研究 Bash 补全脚本编写。
- 复杂任务迁移到 Python。
- 学习 expect 脚本处理交互式命令。
- 研究 Bash 4+ 的新特性(关联数组、mapfile 等)。
- 学习 AWK 和 SED 的高级用法,减少对 Python 的依赖。
适用场景
- 自动化部署和运维任务。
- 日志分析和报表生成。
- 批量服务器操作。
- 系统初始化和配置管理。
- CI/CD 流水线中的构建和部署步骤。
- 监控告警和自动恢复。
落地建议
- 使用 ShellCheck 检查脚本质量。
- 建立脚本模板(头部注释、set -euo、trap)。
- 关键脚本添加日志和错误通知。
- 编写脚本时遵循 Shell Style Guide。
- 对所有脚本编写单元测试(使用 bats 框架)。
- 将常用函数抽取为共享库,通过 source 引入。
排错清单
- bash -x script.sh 逐步调试。
- 检查变量是否用引号包裹。
- 确认 shebang 是否正确。
- 使用 shellcheck script.sh 进行静态检查。
- 检查脚本是否有执行权限(chmod +x)。
- 检查换行符是否为 Unix 格式(dos2unix)。
- 使用 bash -n script.sh 检查语法错误(不执行)。
复盘问题
- 什么场景下应该用 Python 替代 Shell 脚本?
- 脚本的安全审计应该检查哪些内容?
- 如何保证脚本在不同 Linux 发行版上兼容?
- 如何为 Shell 脚本编写单元测试?
- 如何实现脚本的优雅降级和容错?
