Shell 脚本模板
在日常的开发工作中,我们经常需要编写一些脚本,来完成一些自动化工作,同时还能保存一些环境使用的配置。可以在一段时间后,仍然能保证快速地回忆和使用。
但每一个脚本有可能需要传递命令行参数,如果为每一个脚本都撰写一套命令行解析,会比较麻烦,浪费时间。但不进行命令行参数解析,又会导致后续使用不便,一段时间后就不记得怎么使用了。
模板
先给出模板代码。这个脚本模板采用模块化设计,每个模块都有独立的函数,方便管理和调用。每一个功能由单独的选项来调用,方便记忆和使用。
可以在一个脚本中整合多个功能,利用统一的命令行参数处理框架,不需要为单个功能编写脚本单独处理参数。
1#!/usr/bin/env bash2#!/usr/bin/env bash3#==================================================================4# ██╗ ██╗ ██╗███╗ ██╗██████╗ ██████╗ █████╗5# ██║ ╚██╗ ██╔╝████╗ ██║██╔══██╗██╔══██╗██╔══██╗6# ██║ ╚████╔╝ ██╔██╗ ██║██║ ██║██████╔╝███████║7# ██║ ╚██╔╝ ██║╚██╗██║██║ ██║██╔══██╗██╔══██║8# ███████╗██║ ██║ ╚████║██████╔╝██║ ██║██║ ██║9# ╚══════╝╚═╝ ╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝10#==================================================================11#12# 文件名称: 脚本模板.sh13# 文件描述: 一个通用的 Shell 脚本模板,提供命令行参数处理、日志输出等基础功能14#15# 作者: Lyndra <lyndra@hysling.top>197 collapsed lines
16# 版本: 1.0.017# 创建日期: 2025-02-2818# 最后修改: 2025-02-2819#20# 使用许可: GPLv2 or other licenses21# Copyright (c) 2025 Lyndra22#23#==================================================================24
25set -eo pipefail26
27# 颜色配置28RED='\033[0;31m'29GREEN='\033[0;32m'30YELLOW='\033[1;33m'31NC='\033[0m'32
33# 命令配置,格式为:[命令名]="描述::处理函数::选项声明1::选项声明2::..."34declare -A COMMAND_MAP=(35 ["install"]="安装软件包::handle_install::-p,--package:指定包名(必须)"36 ["remove"]="移除软件包::handle_remove::-p,--package:指定包名(必须)"37 ["backup"]="执行备份::handle_backup::-s,--src:源目录(必须)::-d,--dest:目标目录(默认:/backups)"38)39
40# 错误处理函数41panic() {42 echo -e "${RED}[错误]${NC} $1" >&243 exit 144}45
46# 日志输出函数47log_info() {48 echo -e "${GREEN}[信息]${NC} $1" >&249}50
51log_warning() {52 echo -e "${YELLOW}[警告]${NC} $1" >&253}54
55log_success() {56 echo -e "${GREEN}[成功]${NC} $1" >&257}58
59##############################################60# 通用的帮助信息生成函数 #61##############################################62
63show_command_help() {64 local cmd="$1"65 local info="${COMMAND_MAP[$cmd]}"66
67 # 1) 把所有 "::" 替换成一个不会在脚本中出现的分隔符,比如 '|'68 info="${info//::/|}"69
70 # 2) 这时才能安全地用 IFS='|' 来做分割71 local desc handler options_str72 IFS="|" read -r desc handler options_str <<< "$info"73
74 echo -e "${YELLOW}命令: ${GREEN}$cmd${NC}"75 echo -e "${YELLOW}描述: ${NC}$desc"76 echo -e "${YELLOW}选项:${NC}"77
78 # 3) 选项声明里可能有多段「::」分隔,继续替换并拆分79 options_str="${options_str//::/|}"80 IFS="|" read -ra opt_lines <<< "$options_str"81
82 for opt_line in "${opt_lines[@]}"; do83 [[ -z "$opt_line" ]] && continue84
85 # opt_line 类似 "-p,--package:指定包名(必须)"86 # 用 ":" 分割出(短+长选项)和描述87 IFS=":" read -r flags desc <<< "$opt_line"88 printf " ${GREEN}%-20s${NC} %s\n" "$flags" "$desc"89 done90}91
92# 主帮助信息93show_help() {94 echo -e "${YELLOW}使用方法:${NC}"95 echo " $0 [命令] [选项]"96 echo97 echo -e "${YELLOW}可用命令:${NC}"98 for cmd in "${!COMMAND_MAP[@]}"; do99 local desc=${COMMAND_MAP[$cmd]%%::*}100 printf " ${GREEN}%-15s${NC} %s\n" "$cmd" "$desc"101 done102 echo -e "\n使用 ${YELLOW}$0 命令 --help${NC} 查看具体命令帮助"103}104
105##############################################106# 命令处理函数 #107##############################################108
109handle_install() {110 local package=""111 while [[ $# -gt 0 ]]; do112 case $1 in113 -p|--package)114 package="$2"115 shift 2116 ;;117 -h|--help)118 show_command_help "install"119 exit 0120 ;;121 *)122 panic "未知选项: $1"123 ;;124 esac125 done126 [[ -z "$package" ]] && panic "必须指定包名"127 log_success "正在安装 ${YELLOW}$package${NC}"128}129
130handle_remove() {131 local package=""132 while [[ $# -gt 0 ]]; do133 case $1 in134 -p|--package)135 package="$2"136 shift 2137 ;;138 -h|--help)139 show_command_help "remove"140 exit 0141 ;;142 *)143 panic "未知选项: $1"144 ;;145 esac146 done147 [[ -z "$package" ]] && panic "必须指定包名"148 log_success "正在移除 ${YELLOW}$package${NC}"149}150
151handle_backup() {152 local src="" dest="/backups"153 while [[ $# -gt 0 ]]; do154 case $1 in155 -s|--src)156 src="$2"157 shift 2158 ;;159 -d|--dest)160 dest="$2"161 shift 2162 ;;163 -h|--help)164 show_command_help "backup"165 exit 0166 ;;167 *)168 panic "未知选项: $1"169 ;;170 esac171 done172 [[ -z "$src" ]] && panic "必须指定源目录"173 log_info "从 ${YELLOW}$src${NC} 备份到 ${YELLOW}$dest${NC}"174
175 # 示范如何安全处理包含空格的文件名176 log_info "搜索文件..."177 local count=0178
179 while IFS= read -r -d '' file; do180 ((count++))181 log_info "处理文件: ${YELLOW}$file${NC}"182 # 这里放实际的处理逻辑183 done < <(find "$src" -type f -print0)184
185 log_success "共处理了 ${YELLOW}$count${NC} 个文件"186}187
188##############################################189# 主逻辑 #190##############################################191
192main() {193 [[ $# -eq 0 ]] || [[ "$1" == "--help" ]] && show_help && exit 0194
195 local cmd="$1"196 shift197
198 [[ -n "${COMMAND_MAP[$cmd]}" ]] || panic "未知命令: $cmd"199
200 # 同样地,这里也要避免直接 IFS="::" 分割201 # 先用 '|' 替换 "::" ,再做一次分割即可202 local info="${COMMAND_MAP[$cmd]}"203 info="${info//::/|}"204
205 local desc handler options_str206 IFS="|" read -r desc handler options_str <<< "$info"207
208 # 调用对应的处理函数209 $handler "$@"210}211
212main "$@"
使用方式
在命令行中输入 ./Shell_script_template.sh
即可看到帮助信息。命令行参数分为命令、选项。
命令是脚本的功能,比如 install
、remove
、backup
等。
选项是命令支持的参数,比如 -p,--package
表示包名。每一个选项可以传入多种不同类型的输入参数,由该选项自主解析。
例如:
1./Shell_script_template.sh install -p package_name
基于模板开发脚本
基于这个脚本模板开发新脚本时,只需要在 COMMAND_MAP
中添加新的命令,并实现对应的处理函数即可。
COMMAND_MAP
的命令配置格式为:[命令名]="描述::处理函数::选项声明1::选项声明2::..."
。其中 选项声明
的格式为:-短选项,--长选项:描述
。
参数的解析由每一个选项的处理函数来完成。可以处理多个参数,或者不处理参数。
处理多参数选项
对于需要接收多个参数的选项,可以通过数组来收集参数。下面是一个示例:
1# 在 COMMAND_MAP 中添加新命令2declare -A COMMAND_MAP=(3 # ... 其他命令 ...4 ["compile"]="编译源文件::handle_compile::-s,--src:源文件列表::-d,--debug:启用调试"5)6
7# 处理函数示例8handle_compile() {9 local -a source_files=() # 定义数组保存源文件10 local debug=false11
12 while [[ $# -gt 0 ]]; do13 case $1 in14 -s|--src)15 shift # 移除 --src 选项36 collapsed lines
16 # 收集所有不以 - 开头的参数作为源文件17 while [[ $# -gt 0 && ! $1 =~ ^- ]]; do18 source_files+=("$1")19 shift20 done21 ;;22 -d|--debug)23 debug=true24 shift25 ;;26 -h|--help)27 show_command_help "compile"28 exit 029 ;;30 *)31 panic "未知选项: $1"32 ;;33 esac34 done35
36 # 检查是否提供了源文件37 [[ ${#source_files[@]} -eq 0 ]] && panic "必须指定至少一个源文件"38
39 # 显示编译信息40 log_info "准备编译以下文件:"41 for src in "${source_files[@]}"; do42 log_info "- ${YELLOW}$src${NC}"43 done44
45 if [[ $debug == true ]]; then46 log_info "调试模式已启用"47 fi48
49 # 这里添加实际的编译逻辑50 log_success "编译完成"51}
使用示例:
1# 编译多个源文件2./build.sh compile --src file1.c file2.c file3.c3
4# 启用调试模式编译5./build.sh compile --src main.c utils.c --debug6
7# 查看帮助8./build.sh compile --help
关键点说明:
- 使用数组
local -a source_files=()
来存储多个参数 - 通过
while
循环收集连续的非选项参数 - 使用
! $1 =~ ^-
判断参数是否以-
开头来区分选项和参数 - 使用
${#source_files[@]}
获取数组长度 - 使用
for
循环遍历数组中的所有参数
说明:
在 bash 中,符号 =~
表示通过正则表达式匹配。$1 =~ ^-
表示 $1
参数通过正则表达式 ^-
匹配,并返回匹配结果。若 $1
参数以 -
开头,则返回 true
,否则返回 false
。