ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Shell脚本工程化:模块化封装mkdir、cp、echo命令实践

Shell脚本工程化:模块化封装mkdir、cp、echo命令实践 1. 从“脚本小子”到“工程化”为什么我们需要模块化Shell命令如果你写过Shell脚本大概率有过这样的经历一个脚本文件从上到下几百行开头是变量定义中间是各种if-else和for循环夹杂着大量的echo、mkdir -p、cp -rf。每次修改都得小心翼翼地在代码海洋里寻找那个需要改动的地方。更头疼的是当另一个项目也需要类似的功能时你只能把整段代码复制过去然后祈祷它在新环境里不会出问题。这种“面条式”的代码是Shell脚本从一次性工具迈向可维护、可复用工程化代码的最大障碍。今天我们就来彻底解决这个问题。我们不满足于只会用echo打印日志、用mkdir创建目录、用cp复制文件。我们要做的是把这些最基础的命令封装成具有工程化思维的Shell函数模块。这不仅仅是把命令包进一个函数那么简单而是要赋予它们统一的日志输出、完善的错误处理、灵活的参数校验和优雅的默认行为。最终你将拥有一套属于自己的、像编程语言标准库一样可靠的Shell工具集。无论是部署脚本、备份任务还是日常运维你都可以像搭积木一样安全、高效地组合这些模块让脚本编写从“刀耕火种”进入“精耕细作”的时代。2. 模块化设计的核心思想告别“硬编码”拥抱“函数库”在深入代码之前我们必须先统一思想。模块化不是炫技而是为了解决实际开发中的痛点。其核心思想可以概括为三点抽象、隔离与复用。抽象意味着隐藏实现细节。调用者不需要关心mkdir命令的-p参数是如何避免“目录已存在”错误的他只需要调用make_dir /path/to/dir并确信目录会被创建好如果可能的话。我们将命令的常见选项、错误处理逻辑封装起来提供一个更简洁、更专注的接口。隔离是为了控制影响范围。在一个模块化的函数里如果日志格式需要从[INFO]改成(INFO)你只需要修改函数内部的echo语句所有调用它的脚本会自动生效。反之如果错误处理逻辑有缺陷你也只需要在一个地方修复它。这极大地降低了代码的耦合度提升了可维护性。复用是模块化的终极目标。今天在A项目里写好的log_info函数经过充分测试后可以直接复制到B、C、D项目中使用。你甚至可以把它打包成一个独立的utils.sh文件通过source命令引入到任何需要的脚本中实现“一次编写到处运行”。基于这些思想我们为即将封装的三个命令设定共同的设计目标统一的日志接口所有操作都通过标准的log_*函数输出信息便于集中控制日志级别如DEBUG, INFO, WARN, ERROR和输出目的地文件、终端、系统日志。严格的错误处理任何命令执行失败都应立即捕获记录详细的错误上下文时间、函数名、参数、错误信息并根据预设策略决定是退出脚本还是继续执行。友好的参数校验对输入参数进行有效性检查比如路径是否为空、是否为绝对路径、目标文件是否可读等在问题发生前给出清晰的提示。可配置的默认行为提供合理的默认选项如cp默认递归、mkdir默认创建父目录同时允许调用者通过参数覆盖这些默认行为。接下来我们就从最基础的日志模块开始搭建整个工具集的基石。3. 基石构建一个健壮、灵活的日志模块日志是脚本的“眼睛”没有好的日志调试就如同盲人摸象。我们首先要封装的不是echo而是一个超越echo的日志系统。3.1 定义日志级别与颜色单纯的echo “开始备份”信息量有限。我们需要区分信息的严重程度。通常我们定义以下几个级别#!/bin/bash # 文件名logging.sh # 定义日志级别常量 readonly LOG_LEVEL_DEBUG0 readonly LOG_LEVEL_INFO1 readonly LOG_LEVEL_WARN2 readonly LOG_LEVEL_ERROR3 # 设置脚本的默认日志级别低于此级别的日志将不输出 # 例如设置为 LOG_LEVEL_INFO则 DEBUG 日志不显示 readonly DEFAULT_LOG_LEVEL$LOG_LEVEL_INFO # 定义终端颜色代码增强可读性 readonly COLOR_DEBUG\033[36m # 青色 readonly COLOR_INFO\033[32m # 绿色 readonly COLOR_WARN\033[33m # 黄色 readonly COLOR_ERROR\033[31m # 红色 readonly COLOR_RESET\033[0m # 重置颜色这里有几个关键点使用readonly声明常量防止在脚本中被意外修改。颜色代码\033[36m是ANSI转义序列大多数现代终端都支持。它让不同级别的日志一目了然。DEFAULT_LOG_LEVEL是一个全局开关。在开发阶段可以设为LOG_LEVEL_DEBUG查看所有细节在生产环境则设为LOG_LEVEL_WARN或LOG_LEVEL_ERROR只关注重要信息。3.2 实现核心日志函数有了级别和颜色我们来实现核心的日志函数。这个函数是私有的不直接对外暴露由各个级别的包装函数调用。# 私有函数实际执行日志打印的函数 _log() { local level$1 local level_str$2 local color$3 shift 3 # 移除前三个参数剩下的所有参数都是要打印的消息 # 判断当前日志级别是否允许打印 if [[ $level -lt $DEFAULT_LOG_LEVEL ]]; then return 0 fi # 获取当前时间格式化为标准日志格式 local timestamp timestamp$(date %Y-%m-%d %H:%M:%S) # 获取调用日志函数的函数名用于追踪 local caller_info caller_info${FUNCNAME[2]:-main} # 跳过 _log 和 log_xxx 函数本身 # 格式化输出 # 示例[2023-10-27 14:30:15] [INFO] [main] 开始执行任务 echo -e ${color}[${timestamp}] [${level_str}] [${caller_info}] $*${COLOR_RESET} 2 }这个_log函数做了几件重要的事级别过滤通过比较传入的level和全局DEFAULT_LOG_LEVEL决定是否输出。这是实现动态日志级别的关键。丰富上下文自动添加了时间戳(timestamp)和调用者函数名(caller_info)。${FUNCNAME[2]}是一个Bash内置数组存储了函数调用栈。[2]表示向上追溯两层跳过_log和log_info这类包装函数找到真正的调用者。这在复杂的脚本调试中非常有用。输出到标准错误2将日志内容重定向到标准错误(stderr)。这是一个好习惯因为它允许你将脚本的正常输出如生成的数据、报告重定向到文件script.sh output.txt而日志信息依然显示在终端上互不干扰。注意echo -e参数用于解释反斜杠转义序列如颜色代码\033。在某些极简的Shell环境如dash它是/bin/sh在某些系统上的链接中-e可能不被支持。为了最大兼容性在生产脚本中有时会使用printf代替echo -e。但考虑到我们主要针对Bash环境且-e的兼容性问题在现代Linux发行版中已较少见这里仍使用echo -e以保持代码简洁。现在我们可以用这个私有函数来创建对外的、友好的日志接口了# 对外公开的日志函数 log_debug() { _log $LOG_LEVEL_DEBUG DEBUG $COLOR_DEBUG $; } log_info() { _log $LOG_LEVEL_INFO INFO $COLOR_INFO $; } log_warn() { _log $LOG_LEVEL_WARN WARN $COLOR_WARN $; } log_error() { _log $LOG_LEVEL_ERROR ERROR $COLOR_ERROR $; }使用起来非常简单直观source ./logging.sh log_info 应用程序启动成功。 log_debug 读取的配置文件路径是: $config_path log_warn 磁盘使用率超过80%请注意清理。 log_error 无法连接到数据库请检查网络和服务状态。输出效果类似于[2023-10-27 14:30:15] [INFO] [main] 应用程序启动成功。 [2023-10-27 14:30:15] [WARN] [check_disk] 磁盘使用率超过80%请注意清理。这个日志模块已经具备了生产级脚本的基础。接下来我们用它来武装我们的文件操作命令。4. 封装mkdir不仅仅是创建目录原生的mkdir命令很简单但缺乏我们想要的工程化特性没有成功/失败日志错误信息可能不直观尤其是涉及权限时默认不创建父目录有时会导致失败。我们的目标是创建一个更智能、更友好的make_dir函数。4.1 函数设计与参数处理首先我们分析mkdir的常用选项-p, --parents需要时创建父目录如果目录已存在也不报错。这几乎是99%场景下的默认需求。-v, --verbose为每个创建的目录打印一条信息。我们可以用更规范的日志代替。-m, --modeMODE设置目录权限如755。我们的封装函数make_dir应该支持这些核心功能并添加日志和错误处理。#!/bin/bash # 文件名file_utils.sh # 首先引入日志模块 source ./logging.sh # 创建目录 # 用法 make_dir [-p] [-m MODE] DIRECTORY... # 参数 # -p : 自动创建父目录默认行为 # -m : 设置目录权限例如 755, 700 # DIRECTORY : 一个或多个要创建的目录路径 make_dir() { local parents_flagtrue # 默认启用 -p 行为 local mode_flag local directories() # 使用 getopts 解析命令行风格的参数 # 这是一个比手动解析 $1, $2 更健壮的方法 while getopts :pm: opt; do case $opt in p) parents_flagtrue # -p 是默认值这里显式设置逻辑更清晰 ;; m) mode_flag$OPTARG # 简单的权限模式校验必须是3位数字且在合理范围内 if [[ ! $mode_flag ~ ^[0-7]{3}$ ]]; then log_error 无效的权限模式: $mode_flag。必须是一个三位八进制数如755。 return 1 fi ;; \?) log_error 无效选项: -$OPTARG return 1 ;; :) log_error 选项 -$OPTARG 需要一个参数。 return 1 ;; esac done shift $((OPTIND -1)) # 移除已处理的选项剩下的是目录参数 # 检查是否至少提供了一个目录参数 if [[ $# -eq 0 ]]; then log_error make_dir: 必须指定至少一个目录路径。 return 1 fi directories($) # 将剩余的参数赋值给数组 local mkdir_cmdmkdir local mkdir_args() # 构建 mkdir 命令参数 if [[ $parents_flag true ]]; then mkdir_args(-p) fi if [[ -n $mode_flag ]]; then mkdir_args(-m $mode_flag) fi # 执行创建操作 for dir in ${directories[]}; do # 参数校验目录路径不能为空 if [[ -z $dir ]]; then log_warn 忽略空的目录路径参数。 continue fi log_info 正在创建目录: $dir if $mkdir_cmd ${mkdir_args[]} $dir 2/dev/null; then log_info 目录创建成功: $dir # 可选检查并记录最终权限 if [[ -n $mode_flag ]]; then local actual_mode actual_mode$(stat -c %a $dir 2/dev/null || echo N/A) log_debug 目录 $dir 权限已设置为: $actual_mode fi else local error_code$? log_error 创建目录失败: $dir (退出码: $error_code) # 尝试给出更友好的错误提示 if [[ ! -w $(dirname $dir) ]]; then log_error 可能的原因父目录 $(dirname $dir) 没有写权限。 elif [[ -e $dir ! -d $dir ]]; then log_error 可能的原因路径 $dir 已存在但它不是一个目录。 fi return $error_code # 将错误码返回给调用者 fi done }4.2 关键实现细节与避坑指南这个函数虽然比直接调用mkdir复杂但每一个细节都有其价值使用getopts解析参数这是处理类命令行参数的标准、健壮的方式。它支持-p -m 755这样的组合也能正确处理-m后面必须跟参数的情况。手动解析$1,$2在参数复杂时极易出错。参数校验前置在真正执行命令前我们对权限模式$mode_flag进行了正则校验^[0-7]{3}$确保它是一个合法的三位八进制数。这避免了将-m 888这样的非法参数传给mkdir导致晦涩的错误。详细的错误诊断当mkdir失败时我们不仅记录错误码还尝试分析可能的原因。通过! -w $(dirname $dir)检查父目录是否可写通过-e $dir ! -d $dir检查路径是否被非目录文件占用。这些诊断信息能极大加速排错过程。静默错误与日志分级执行命令时使用了2/dev/null将mkdir自身的错误输出重定向到空设备。这是因为我们已经用log_error记录了更结构化的错误信息避免终端上出现重复、原始的报错。同时成功信息用log_info调试信息如实际权限用log_debug层次分明。实操心得在循环中创建多个目录时我们选择在遇到第一个错误时就return退出。这是一种“快速失败”的策略适用于目录创建具有依赖性的场景如先创建/a/b再创建/a/c。如果你的场景允许部分失败例如批量创建一批独立的缓存目录可以将return $error_code改为continue并记录下所有失败的目录最后再统一返回一个错误码。现在你可以这样使用它source ./file_utils.sh # 创建单个目录默认带-p make_dir /tmp/myapp/logs # 输出[INFO] 正在创建目录: /tmp/myapp/logs # [INFO] 目录创建成功: /tmp/myapp/logs # 创建带特定权限的目录 make_dir -m 750 /srv/myapp/secure_data # 输出[INFO] 正在创建目录: /srv/myapp/secure_data # [INFO] 目录创建成功: /srv/myapp/secure_data # [DEBUG] 目录 /srv/myapp/secure_data 权限已设置为: 750 # 批量创建目录 make_dir /backup/daily /backup/weekly /backup/monthly # 错误示例 make_dir -m 999 /invalid/path # 会触发权限校验错误 make_dir /root/system/dir # 如果没有sudo权限会触发权限错误并被诊断出来5. 封装cp安全、可控的文件复制cp命令比mkdir更复杂因为它涉及源和目标有覆盖风险并且有递归复制、保留属性等多种选项。我们的封装目标是安全第一体验第二。核心是避免意外覆盖重要文件并提供清晰的操作反馈。5.1 处理覆盖确认与备份原生cp的-i交互式选项并不适合脚本因为脚本无法响应终端提示。我们需要一个更脚本友好的方式要么强制不覆盖要么提供备份机制。# 复制文件或目录 # 用法 copy_file SOURCE... DESTINATION # 用法 copy_file [-r] [-b] SOURCE... DESTINATION # 参数 # -r : 递归复制目录默认对目录无效 # -b : 如果目标存在则自动备份添加 .bak 后缀 # SOURCE : 源文件或目录 # DESTINATION : 目标文件或目录 copy_file() { local recursive_flagfalse local backup_flagfalse local sources() local destination # 解析参数 while getopts :rb opt; do case $opt in r) recursive_flagtrue ;; b) backup_flagtrue ;; \?) log_error 无效选项: -$OPTARG return 1 ;; esac done shift $((OPTIND -1)) # 检查参数数量至少有一个源和一个目标 if [[ $# -lt 2 ]]; then log_error copy_file: 用法: copy_file [-r] [-b] SOURCE... DESTINATION return 1 fi # 最后一个参数是目标其余是源 destination${: -1} # 获取最后一个参数 sources(${:1:$#-1}) # 获取除最后一个外的所有参数 # 目标检查如果多个源目标必须是目录 if [[ ${#sources[]} -gt 1 ! -d $destination ]]; then # 即使目标不存在如果指定了多个源我们也假设用户想把它当目录 # 但为了安全我们要求目标必须以‘/’结尾或者我们尝试创建它 if [[ $destination ! */ ]]; then log_warn 指定了多个源文件但目标 $destination 不是目录且未以‘/’结尾。 log_warn 如果 $destination 已存在且是文件复制将失败。 # 这里不直接退出让 cp 命令自己报错但日志已给出警告。 fi fi local cp_cmdcp local cp_args() # 构建 cp 命令参数 if [[ $recursive_flag true ]]; then cp_args(-r) fi # 我们默认不使用 -i (交互式)而是用 -b 或逻辑判断来处理覆盖 if [[ $backup_flag true ]]; then cp_args(-b) # GNU cp 的 -b 会自动备份后缀默认为 ~ # 你也可以自定义后缀: cp -b -S .bak # 这里为了简单使用默认的 ~ 后缀 else # 如果不备份我们默认添加 -n (--no-clobber) 来禁止覆盖除非用户明确知道风险。 # 这是一个安全默认值。更激进的做法是强制用户使用 -f 或 -b。 cp_args(-n) log_debug 已启用安全模式 (-n)不会覆盖已存在的目标文件。 fi # 添加详细输出便于我们捕获日志 cp_args(-v) # 执行复制操作 for source_item in ${sources[]}; do # 检查源是否存在 if [[ ! -e $source_item ]]; then log_error 源路径不存在跳过: $source_item continue fi log_info 正在复制: $source_item - $destination # 执行复制并捕获输出和错误 local output if output$($cp_cmd ${cp_args[]} $source_item $destination 21); then # cp -v 会在成功时输出 ‘source’ - ‘destination’ # 我们可以解析这个输出或者直接记录成功 log_info 复制操作成功完成。 log_debug 命令输出: $output else local error_code$? log_error 复制失败: $source_item 到 $destination (退出码: $error_code) log_error 错误详情: $output # 常见错误分析 if [[ $error_code -eq 1 ]]; then log_error 可能的原因源文件不存在或无读权限或目标目录无写权限。 elif [[ $error_code -eq 126 ]]; then log_error 可能的原因源或目标是目录但未使用 -r 选项。 fi return $error_code fi done }5.2 安全策略与错误处理解析这个copy_file函数的核心是安全默认值策略默认禁止覆盖通过默认添加-n--no-clobber参数cp命令在目标文件存在时会静默跳过而不是覆盖。这防止了脚本意外覆盖重要配置文件或数据。如果用户确实需要覆盖他们应该明确使用-b备份或者未来可以扩展一个-f强制选项。备份机制-b选项是GNUcp的一个非常有用的功能。当目标文件存在时它会将旧文件重命名为filename~默认后缀。这提供了一个简单的版本回滚机制。在脚本中这比交互式询问要实用得多。递归复制显式声明递归复制目录必须通过-r选项显式开启。这避免了用户本想复制一个文件却因为源参数是个目录符号链接而意外复制了整个目录树。详细的错误捕获我们使用21将标准错误合并到标准输出并一起捕获到output变量中。这样无论是cp -v的正常输出还是真正的错误信息我们都能完整记录到日志中方便事后分析。一个重要的避坑点cp -v的输出格式在不同操作系统或cp版本中可能略有差异。上述代码假设-v输出是友好的。在生产环境中如果你需要更精确地判断单个文件是否复制成功可能需要放弃-v转而手动遍历源文件列表对每个文件执行cp并检查返回值。对于大多数情况当前的实现已经足够清晰和实用。使用示例source ./file_utils.sh # 安全复制单个文件目标存在则跳过 copy_file app.config app.config.backup # 输出[INFO] 正在复制: app.config - app.config.backup # [INFO] 复制操作成功完成。 # 递归复制整个目录并备份已存在的文件 copy_file -r -b /var/www/old_site /backups/ # 如果 /backups/old_site 已存在会被重命名为 /backups/old_site~ # 复制多个文件到一个目录 copy_file file1.txt file2.txt file3.txt /tmp/dest_dir/ # 错误示例尝试覆盖被 -n 阻止 copy_file new_data.txt existing_data.txt # 日志会显示跳过不会报错 # 错误示例复制目录不用 -r copy_file /etc/nginx/sites-available /tmp/ # 会触发错误码126并给出提示6. 封装echo超越简单的文本输出你可能会想echo已经很简单了有什么好封装的的确如果只是输出字符串echo足够了。但当我们把它放在脚本日志和交互的上下文中时我们可以赋予它更多责任格式化输出、条件输出、重定向到日志文件。我们的print_msg函数将成为一个多面手。6.1 实现分级、带格式的输出我们不重复造轮子而是让print_msg与之前构建的日志模块协同工作同时提供一些echo没有的便利功能。# 增强版信息输出函数 # 用法 print_msg [-l LEVEL] [-c COLOR] [-n] MESSAGE... # 参数 # -l LEVEL : 指定输出级别与日志级别对应 (DEBUG, INFO, WARN, ERROR)。指定后会受 DEFAULT_LOG_LEVEL 控制。 # -c COLOR : 指定颜色如 red, green, yellow, blue, cyan, magenta。仅当输出到终端时生效。 # -n : 不换行类似 echo -n。 # MESSAGE : 要输出的信息。 print_msg() { local level local color local newlinetrue local message # 解析参数 # 这里使用简单的循环因为 getopts 在混合普通参数时比较麻烦 while [[ $# -gt 0 ]]; do case $1 in -l|--level) if [[ -n $2 ]]; then level$(echo $2 | tr [:lower:] [:upper:]) # 转大写 shift 2 else log_error print_msg: 选项 -l 需要一个参数。 return 1 fi ;; -c|--color) if [[ -n $2 ]]; then case $2 in black) color\033[30m;; red) color\033[31m;; green) color\033[32m;; yellow) color\033[33m;; blue) color\033[34m;; magenta) color\033[35m;; cyan) color\033[36m;; white) color\033[37m;; *) log_warn print_msg: 不支持的颜色 $2将使用默认颜色。 color ;; esac shift 2 else log_error print_msg: 选项 -c 需要一个参数。 return 1 fi ;; -n|--no-newline) newlinefalse shift ;; *) # 第一个非选项参数开始剩下的都是消息内容 message$* break # 跳出循环处理消息 ;; esac done # 如果没有消息直接返回 if [[ -z $message ]]; then return 0 fi # 如果指定了级别则使用日志函数受日志级别控制 if [[ -n $level ]]; then case $level in DEBUG) log_debug $message ;; INFO) log_info $message ;; WARN) log_warn $message ;; ERROR) log_error $message ;; *) log_warn print_msg: 未知的日志级别 $level将作为普通信息输出。 # 降级为普通输出 ;; esac return 0 fi # 普通输出不受 DEFAULT_LOG_LEVEL 控制 local output_cmdecho local output_args() if [[ $newline false ]]; then output_args(-n) fi # 判断是否输出到终端决定是否使用颜色 if [[ -t 1 ]] [[ -n $color ]]; then message${color}${message}${COLOR_RESET} fi $output_cmd ${output_args[]} $message }6.2 灵活性与实用场景这个print_msg函数提供了多种输出方式作为日志的补充当你需要输出一些不属于严格日志流程但又需要带级别和颜色的提示信息时。print_msg -l INFO 开始执行数据迁移流程... print_msg -l WARN 请注意此操作将清空目标表。作为彩色终端输出工具在交互式脚本中美化用户提示。print_msg -c green ✓ 操作成功 print_msg -c red ✗ 发生错误 print_msg -c cyan 请输入您的选择 -n read user_input实现不换行输出用于创建进度条或等待动画。for i in {1..10}; do print_msg -c blue . -n sleep 0.5 done print_msg # 换行一个重要的技术细节[[ -t 1 ]]用于检查标准输出文件描述符1是否连接到一个终端。这是一个很好的实践它确保了当脚本输出被重定向到文件script.sh log.txt或管道script.sh | grep ...时颜色代码不会被写入文件从而避免文件里出现乱码。7. 模块的集成、测试与进阶技巧我们已经完成了三个核心命令的模块化封装。现在我们需要将它们组织起来形成一个完整的工具库并探讨如何在真实项目中应用和扩展。7.1 创建统一的工具库入口一个好的实践是创建一个主模块文件来统一导入和初始化所有子模块。#!/bin/bash # 文件名shell-utils.sh # Shell 脚本工具库主入口 # 获取脚本所在目录便于相对路径引用 UTILS_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) # 导入子模块 source $UTILS_DIR/logging.sh source $UTILS_DIR/file_utils.sh # 可以在这里定义一些全局配置或初始化函数 # 例如设置默认的日志级别基于环境变量 if [[ -n $SHELL_UTILS_LOG_LEVEL ]]; then case $SHELL_UTILS_LOG_LEVEL in DEBUG) DEFAULT_LOG_LEVEL$LOG_LEVEL_DEBUG ;; INFO) DEFAULT_LOG_LEVEL$LOG_LEVEL_INFO ;; WARN) DEFAULT_LOG_LEVEL$LOG_LEVEL_WARN ;; ERROR) DEFAULT_LOG_LEVEL$LOG_LEVEL_ERROR ;; *) log_warn 未知的 SHELL_UTILS_LOG_LEVEL: $SHELL_UTILS_LOG_LEVEL使用默认级别 INFO。 ;; esac log_debug 日志级别已通过环境变量设置为: $SHELL_UTILS_LOG_LEVEL fi # 提供一个简单的版本信息函数 utils_version() { echo Shell Utils Library v1.0.0 }在你的项目脚本中只需要一行代码即可引入所有功能#!/bin/bash # 你的业务脚本 source /path/to/shell-utils.sh log_info 工具库加载完毕。 make_dir -p /opt/myapp/{data,logs,config} copy_file -b default.config /opt/myapp/config/app.config print_msg -c green 环境初始化完成7.2 编写单元测试是的Shell脚本也可以测试模块化的好处之一是便于测试。我们可以为每个函数编写简单的测试脚本。#!/bin/bash # 文件名test_file_utils.sh source ./shell-utils.sh echo 测试 make_dir make_dir /tmp/test_dir_1 echo PASS: 创建目录成功 || echo FAIL: 创建目录失败 make_dir -m 755 /tmp/test_dir_2 echo PASS: 创建带权限目录成功 || echo FAIL: 创建带权限目录失败 make_dir /tmp/test_dir_1 2/dev/null echo PASS: 目录已存在时不报错 || echo FAIL: 目录已存在时报错 echo -e \n 测试 copy_file echo test content /tmp/source.txt copy_file /tmp/source.txt /tmp/dest.txt echo PASS: 复制文件成功 || echo FAIL: 复制文件失败 copy_file /tmp/source.txt /tmp/dest.txt 2/dev/null echo PASS: 目标存在时安全跳过 || echo FAIL: 目标存在时未正确处理 copy_file -b /tmp/source.txt /tmp/dest.txt ls -la /tmp/dest.txt* | grep -q .bak echo PASS: 备份功能生效 || echo FAIL: 备份功能未生效 echo -e \n 测试 print_msg print_msg -l INFO 这是一条INFO级别的测试信息。 print_msg -c red 这是一条红色错误信息模拟。 print_msg -n 不换行测试... echo [接上行] # 清理 rm -rf /tmp/test_dir_* /tmp/source.txt /tmp/dest.txt* echo -e \n测试完成。7.3 进阶技巧与扩展思路性能考量在循环中频繁调用make_dir或copy_file尤其是包含日志和错误检查会有性能开销。对于需要创建成千上万个目录的场景可以考虑批量操作。例如先收集所有要创建的目录路径然后通过xargs调用一次mkdir -p。我们的函数更适合用于关键路径的创建和需要严格错误处理的场景。更细粒度的错误处理目前的函数在错误时直接return。在某些场景下你可能希望收集所有错误最后统一报告。可以修改函数将错误信息追加到一个全局数组并设置一个全局错误标志函数本身返回成功最后在主流程中检查这个标志。支持更多命令和选项本文提供了三个最常用命令的封装思路。你可以用同样的模式去封装rm实现安全删除、移动到回收站、ln创建软硬链接、find封装复杂查询等。关键在于统一日志、统一错误处理、提供安全默认值。与环境集成让工具库感知运行环境。例如在log_info函数中可以检查一个环境变量SHELL_UTILS_LOG_FILE如果存在则将日志同时输出到该文件实现日志持久化。生成文档为你的函数编写简单的注释然后可以使用像shdoc这样的工具或自己写一个脚本来从脚本中提取注释生成API文档方便团队其他成员使用。从随手写下的几行echo和mkdir到如今这一套功能清晰、边界明确、安全可靠的模块化工具我们走过的路正是Shell脚本工程化的一个缩影。它带来的直接好处是代码更清晰、调试更轻松、复用更简单。但更深层的价值在于它迫使你以设计者的角度去思考每一个命令的边界和职责这种思维模式会潜移默化地提升你所有脚本的质量。下次当你再手指飞舞地敲下cp -r之前不妨先想想这个操作是否足够安全是否需要记录是否值得被封装成一个更可靠的copy_dir函数
返回列表