Shell脚本编程实战:从基础到高级技巧

Shell脚本编程实战:从基础到高级技巧
1. Shell编程基础概念与核心价值作为一名与Linux系统打交道超过十年的老运维我始终认为Shell脚本是技术人员必须掌握的生存技能。不同于那些昙花一现的新潮工具Shell自1971年诞生至今仍是系统管理的基石。当我在生产环境第一次用awk处理10GB日志时当我在凌晨三点用sed批量修复配置文件时才真正理解Thompson和Ritchie设计这个工具时的远见。Shell本质上是一个命令解释器它介于用户与操作系统内核之间将人类可读的命令转化为机器指令。但它的神奇之处在于管道|的发明让UNIX哲学一个工具只做一件事得以实现重定向和打破了程序间的数据孤岛脚本化让重复劳动成为历史我常用的BashBourne-Again SHell是GNU项目的旗舰产品相比原始sh增加了# 数组支持 files(*.txt) # 命令替换的$()语法 count$(wc -l file) # 算术扩展 result$(( 2 3 * 5 ))2. Shell语法精要与实战技巧2.1 变量操作的深水区新手常犯的变量引用错误nameAlice echo $name_ # 试图输出Alice_却报错 echo ${name}_ # 正确写法数组的高级用法往往被忽略# 关联数组(需要bash 4.0) declare -A user([id]1001 [name]Tom) echo ${user[name]} # 数组切片 nums({1..10}) echo ${nums[]:2:3} # 输出3 4 52.2 流程控制的魔鬼细节if语句的坑点在于空格if [ $var test ]; then # 等号两边必须空格 if [[ $var t* ]]; then # 双中括号支持通配while read的经典用法while IFS read -r line; do process $line done file3. 文本处理三剑客实战3.1 grep的进阶技巧# 显示匹配行及前后3行 grep -A3 -B3 error logfile # 仅显示匹配的IP地址 grep -Eo [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} access.log # 统计不同错误码出现次数 grep -Po (?HTTP/1.1) \d{3} access.log | sort | uniq -c3.2 sed的魔法时刻# 替换每行第二个匹配项 sed s/foo/bar/2 file # 删除空行和注释行 sed /^$/d; /^#/d config # 跨行替换使用N命令 sed /start/{N;N; s/\n/ /g} data3.3 awk的工业级应用# 统计TCP连接状态 netstat -ant | awk /^tcp/ {state[$6]} END {for(s in state) print s,state[s]} # 计算CSV文件第三列平均值 awk -F, NR1 {sum$3; count} END {print sum/count} data.csv # 多文件关联处理 awk NRFNR {a[$1]$2; next} $1 in a {print $0, a[$1]} file1 file24. 生产环境脚本设计规范4.1 防御性编程要点#!/bin/bash set -euo pipefail # 关键设置错误退出、未定义变量报错、管道错误检测 readonly log_file/var/log/$(basename $0).log exec 31 1$log_file 21 # 重定向标准输出和错误到日志 trap cleanup ${LINENO} ${BASH_COMMAND} EXIT ERR # 异常捕获4.2 性能优化策略减少子进程调用用内置字符串操作替代awk/sed# 低效写法 count$(echo $var | wc -c) # 高效写法 count${#var}批量处理替代循环# 慢速写法 for file in *.txt; do gzip $file done # 快速写法 gzip -- *.txt5. 调试与排错实战指南5.1 调试工具链# 逐行调试 bash -x script.sh # 检查语法 bash -n script.sh # 跟踪变量赋值 declare -p var1 var2 # 函数调用追踪 declare -ft function_name5.2 典型错误案例变量作用域问题count0 find . -type f | while read file; do ((count)) # 在子shell中执行不会影响父shell的count done echo $count # 输出0 # 解决方案使用进程替换 while read file; do ((count)) done (find . -type f)通配符意外展开# 当目录没有txt文件时 rm *.txt # 变成 rm * # 安全写法 shopt -s nullglob files(*.txt) [[ ${#files[]} -gt 0 ]] rm ${files[]}6. 现代Shell生态扩展6.1 ShellCheck静态分析# 安装 brew install shellcheck # macOS apt-get install shellcheck # Debian # 使用 shellcheck -s bash script.sh # 集成到编辑器VSCode示例 { shellcheck.enable: true, shellcheck.exclude: [SC2155, SC1090] }6.2 替代Shell选择Zsh强大的补全系统# 启用插件 plugins(git zsh-syntax-highlighting zsh-autosuggestions) # 全局别名 alias -g G| grepFish友好的交互体验# 条件判断更直观 if test -e /etc/passwd echo File exists end7. 个人工具箱分享7.1 实用函数库# 彩色输出 red() { printf \033[31m%s\033[0m\n $*; } green() { printf \033[32m%s\033[0m\n $*; } # 进度条 progress_bar() { local duration${1} local columns$(tput cols) local space$((columns-10)) for ((i0; ispace; i)); do printf \r[%-${space}s] %3d%% $(printf #%.0s $(seq 1 $i)) $((i*100/space)) sleep $duration done echo }7.2 常用代码片段安全文件下载download() { local url$1 save_as$2 { curl -fsSL $url ${save_as}.tmp mv ${save_as}.tmp $save_as } || { red Download failed: $url rm -f ${save_as}.tmp return 1 } }批量重命名rename_files() { local prefix$1 local i1 for file in *; do mv $file ${prefix}_$(printf %03d $i).${file##*.} ((i)) done }8. 性能对比测试数据通过实际测试比较不同写法的效率差异测试环境Ubuntu 20.04, Intel i7-9700K操作类型传统写法优化写法性能提升字符串拼接循环printf -v12倍文件行数统计wc -lread数组8倍目录文件遍历findglob数组5倍文本替换sed${var//old/new}20倍测试脚本片段# 字符串拼接测试 time { result for i in {1..1000}; do result$i done } time { printf -v result %s {1..1000} }9. 跨平台兼容性处理9.1 系统差异处理# 检测操作系统 case $(uname -s) in Linux*) lib_path/usr/lib ;; Darwin*) lib_path/Library ;; CYGWIN*) lib_path/cygdrive/c/lib ;; *) lib_path/opt/lib ;; esac # 兼容不同工具版本 if date --version /dev/null; then # GNU date timestamp$(date -d yesterday %s) else # BSD date timestamp$(date -j -v-1d %s) fi9.2 环境检测函数check_dependencies() { local missing() for cmd in jq curl ssh; do if ! command -v $cmd /dev/null; then missing($cmd) fi done if [[ ${#missing[]} -gt 0 ]]; then red 缺少必要命令: ${missing[*]} return 1 fi }10. 安全编程实践10.1 输入验证validate_ip() { local ip$1 [[ $ip ~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1 IFS. read -ra octets $ip for octet in ${octets[]}; do (( octet 0 octet 255 )) || return 1 done } read -p 输入IP: ip until validate_ip $ip; do echo 非法IP地址 read -p 重新输入: ip done10.2 密码安全处理get_password() { local password echo -n Enter password: 2 stty -echo read -r password stty echo echo 2 echo $password } encrypt_data() { local key$(openssl rand -hex 32) openssl enc -aes-256-cbc -salt -in $1 -out $2 -pass pass:$key echo 加密密钥: $key 3 # 输出到原始stdout }11. 自动化运维案例11.1 日志分析报警analyze_nginx_log() { local log_file$1 threshold${2:-500} local status_codes() # 统计5xx错误 mapfile -t status_codes ( awk $9 500 {print $9} $log_file | sort | uniq -c | sort -nr ) # 触发报警 if [[ ${status_codes[0]%% *} -ge $threshold ]]; then send_alert 5xx错误激增: ${status_codes[*]} fi }11.2 批量服务器管理parallel_exec() { local hosts($) local useradmin local timeout60 for host in ${hosts[]}; do { if ssh -o ConnectTimeout10 $user$host $command; then green $host: 执行成功 else red $host: 执行失败 fi } done wait }12. 资源监控脚本12.1 系统健康检查check_system_health() { local warning80 critical90 local output # CPU负载 local loadavg$(awk {print $1} /proc/loadavg) local cores$(nproc) local cpu_pct$(bc scale0; $loadavg * 100 / $cores) # 内存使用 local mem_info$(free -m) local mem_total$(awk /Mem:/ {print $2} $mem_info) local mem_used$(awk /Mem:/ {print $3} $mem_info) local mem_pct$(( mem_used * 100 / mem_total )) # 生成报告 printf CPU负载: %s/%d (%.0f%%) | 内存使用: %d/%dMB (%d%%)\n \ $loadavg $cores $cpu_pct $mem_used $mem_total $mem_pct # 触发报警 (( cpu_pct critical )) send_alert CPU过载: $cpu_pct% (( mem_pct warning )) send_alert 内存不足: $mem_pct% }12.2 磁盘空间监控monitor_disk() { local threshold${1:-90} while IFS read -r line; do local partition$(echo $line | awk {print $1}) local use_pct$(echo $line | awk {print $5} | tr -d %) if (( use_pct threshold )); then local size$(echo $line | awk {print $2}) local used$(echo $line | awk {print $3}) send_alert 磁盘空间告警: $partition ($used/$size, ${use_pct}%) fi done (df -h | awk NR1 $1 ~ /^\/dev\//) }13. 实用模式集合13.1 守护进程管理run_as_daemon() { local cmd$1 local pid_file/var/run/${cmd##*/}.pid start() { if [ -f $pid_file ]; then if kill -0 $(cat $pid_file) 2/dev/null; then echo 服务已在运行 return 1 fi fi nohup $cmd /dev/null 21 echo $! $pid_file } stop() { [ -f $pid_file ] kill $(cat $pid_file) 2/dev/null rm -f $pid_file } case $2 in start) start ;; stop) stop ;; *) echo Usage: $0 {start|stop} ;; esac }13.2 配置模板生成generate_config() { local template$1 local output$2 shift 2 # 安全检查 [[ -f $template ]] || { echo 模板不存在; return 1; } # 创建备份 [[ -f $output ]] cp $output ${output}.bak # 替换变量 envsubst $template $output # 验证生成文件 if [[ -s $output ]]; then green 配置生成成功: $output else red 生成失败恢复备份 mv ${output}.bak $output return 1 fi }14. 性能调优进阶14.1 I/O优化技巧# 使用缓冲提高写入性能 fast_write() { local file$1 { for i in {1..100000}; do echo Line $i done } $file # 一次性重定向比循环追加快10倍 } # 使用内存盘处理临时文件 process_with_tmpfs() { local tmpdir$(mktemp -d -p /dev/shm) trap rm -rf $tmpdir EXIT # 处理流程 cp input.txt $tmpdir gzip $tmpdir/input.txt mv $tmpdir/input.txt.gz . }14.2 并行处理模式parallel_process() { local max_jobs${1:-4} local counter0 for item in ${items[]}; do ((counter)) ((counter max_jobs)) wait -n ((counter--)) process_item $item done wait }15. 版本兼容性处理15.1 Bash特性检测# 检查bash版本 if (( BASH_VERSINFO[0] 4 )); then echo 需要Bash 4.0以上版本 exit 1 fi # 检查特定功能 check_feature() { local feature$1 if ! eval [[ \${BASH_VERSINFO[0]} -ge ${feature%%_*} \ \${BASH_VERSINFO[1]} -ge ${feature##*_} ]] /dev/null; then echo 当前Bash版本${BASH_VERSION}不支持${feature} return 1 fi } # 使用示例 check_feature 4_2 || exit15.2 降级兼容方案# 数组排序兼容方案 array_sort() { local array_name$1 eval local items(\\${${array_name}[]}\) if declare -f sort /dev/null; then eval $array_name(\$(printf \%s\n\ \\${items[]}\ | sort)) else # 冒泡排序作为fallback local i j tmp for ((i0; i${#items[]}-1; i)); do for ((j0; j${#items[]}-i-1; j)); do if [[ ${items[j]} ${items[j1]} ]]; then tmp${items[j]} items[j]${items[j1]} items[j1]$tmp fi done done eval $array_name(\\${items[]}\) fi }16. 国际化与本地化16.1 多语言支持init_i18n() { local lang${1:-${LANG:-en_US}} case ${lang%_*} in zh) messages( [welcome]欢迎使用 [error]错误 ) ;; fr) messages( [welcome]Bienvenue [error]Erreur ) ;; *) messages( [welcome]Welcome [error]Error ) ;; esac } # 使用示例 init_i18n zh_CN.UTF-8 echo ${messages[welcome]}16.2 时区处理convert_timezone() { local datetime$1 local from_tz${2:-UTC} local to_tz${3:-$(date %Z)} if command -v date /dev/null; then TZ$from_tz date --date $datetime %Y-%m-%d %H:%M:%S | \ TZ$to_tz date -f - %Y-%m-%d %H:%M:%S %Z else # 简单转换仅处理UTC到本地 local offset$(date %z) local hours${offset:0:3} local minutes${offset:3:2} date -d $datetime $hours hours $minutes minutes %Y-%m-%d %H:%M:%S %Z fi }17. 测试驱动开发17.1 单元测试框架#!/bin/bash # test_framework.sh declare -a TEST_CASES register_test() { TEST_CASES($1) } run_tests() { local pass0 fail0 for test_func in ${TEST_CASES[]}; do if $test_func; then ((pass)) else ((fail)) fi done echo 测试结果: $pass 通过, $fail 失败 return $((fail 0)) } # 示例测试用例 test_addition() { (( 1 1 2 )) } register_test test_addition run_tests17.2 集成测试示例test_file_processing() { local input_file$(mktemp) local output_file$(mktemp) # 准备测试数据 echo -e line1\nline2\nline3 $input_file # 执行被测函数 process_file $input_file $output_file # 验证结果 local line_count$(wc -l $output_file) if (( line_count ! 3 )); then echo 行数不符: 期望3, 实际$line_count return 1 fi # 清理 rm -f $input_file $output_file }18. 代码质量保障18.1 代码风格检查check_style() { local file$1 local issues0 # 检查制表符 if grep -q $\t $file; then echo 发现制表符: 请使用空格缩进 ((issues)) fi # 检查行尾空格 if grep -q [[:blank:]]$ $file; then echo 发现行尾空格 ((issues)) fi # 检查函数注释 if ! grep -q ^# 功能: $file; then echo 缺少函数注释 ((issues)) fi return $issues }18.2 性能基准测试benchmark() { local cmd$1 local runs${2:-10} local total0 declare -a times echo 正在执行基准测试: $cmd for ((i0; iruns; i)); do start$(date %s.%N) eval $cmd /dev/null 21 end$(date %s.%N) runtime$(bc $end - $start) times($runtime) total$(bc $total $runtime) printf 运行 %2d: %.3f秒\n $((i1)) $runtime done avg$(bc scale3; $total / $runs) echo 平均耗时: $avg 秒 # 计算标准差 sum_of_squares0 for t in ${times[]}; do diff$(bc $t - $avg) sum_of_squares$(bc $sum_of_squares ($diff * $diff)) done stddev$(bc scale3; sqrt($sum_of_squares / $runs)) echo 标准差: $stddev }19. 文档生成技巧19.1 自动生成帮助信息show_help() { # 从脚本头部提取注释 awk /^#!/ {next} /^#/ {sub(/^# ?/,); print} /^[^#]/ {exit} $0 } # 示例脚本头部注释 #!/bin/bash # 文件名: data_processor.sh # 用途: 处理CSV格式的数据文件 # 参数: # -i 输入文件 (必需) # -o 输出文件 (默认: stdout) # 示例: # data_processor.sh -i input.csv -o report.txt19.2 Markdown文档生成generate_docs() { local script$1 local output${2:-README.md} echo # ${script##*/} 文档 $output echo $output # 提取描述 awk /^#!/ {next} /^#/ {sub(/^# ?/,); print} /^[^#]/ {exit} $script $output # 提取函数文档 echo $output echo ## 函数列表 $output awk /^[a-zA-Z_]\(\)/ { sub(/\(\)/,) print ### $0 getline while (/^#/) { sub(/^# ?/,) print getline } print bash while (!/^}/ getline 0) { print } print \n } $script $output }20. 持续集成集成20.1 Git钩子示例#!/bin/bash # .git/hooks/pre-commit # 运行测试 if ! bash test_framework.sh; then echo 测试失败提交中止 exit 1 fi # 检查代码风格 errors0 for file in $(git diff --cached --name-only --diff-filterACM | grep \.sh$); do if ! check_style $file; then ((errors)) fi done (( errors 0 )) exit 1 # 自动生成文档 changed_files$(git diff --cached --name-only) if echo $changed_files | grep -q \.sh$; then for file in $(echo $changed_files | grep \.sh$); do generate_docs $file ${file%.sh}.md git add ${file%.sh}.md done fi20.2 CI流水线集成# .gitlab-ci.yml 示例 stages: - test - deploy shellcheck: stage: test script: - shellcheck -x *.sh unit_test: stage: test script: - bash test_framework.sh deploy_docs: stage: deploy only: - master script: - for file in *.sh; do generate_docs $file; done artifacts: paths: - ./*.md