Shell脚本入门到实战:自动化运维与文本处理核心技巧
1. 从“黑窗口”到自动化利器:为什么你需要学习Shell脚本?
如果你每天的工作都离不开那个黑色的终端窗口,或者你经常需要重复执行一系列枯燥的命令,比如批量重命名文件、定时备份数据、或者检查一堆服务器的状态,那么Shell脚本就是你必须掌握的技能。它不是什么高深莫测的黑魔法,而是将你手动敲入的命令,按照逻辑顺序写进一个文本文件里,然后让计算机自动、反复地去执行。想象一下,你每天上班第一件事是登录五台服务器,查看日志、检查磁盘空间、重启某个服务。手动操作可能需要15分钟,还容易出错。而一个几十行的脚本,可能只需要几秒钟,并且每次执行的结果都一模一样。这就是Shell脚本最直接的价值:将重复劳动自动化,将复杂操作流程化。
很多人觉得Shell脚本是运维工程师的专属,其实不然。对于开发者来说,用它来编译项目、运行测试、打包部署;对于数据分析师,用它来清洗和整理原始数据文件;甚至对于普通电脑用户,用它来管理照片、整理文档,都大有裨益。它的学习曲线相对平缓,因为你不需要先学习一门完整的编程语言语法,你只需要知道怎么把命令组合起来。你之前零星使用过的那些命令,比如ls,cp,grep,就是构建脚本的砖瓦。学习Shell脚本,本质上是在学习如何更聪明、更高效地使用命令行,是对你已有知识的一次系统化升级和威力放大。
我最初学习Shell脚本,就是因为被一次手工处理上千个数据文件的任务搞到崩溃。从那以后,我几乎养成了一个习惯:任何需要重复超过三次的操作,我都会想一想:“能不能写个脚本?” 这不仅节省了时间,更重要的是,它把操作过程固化了下来,变成了可分享、可复查、可改进的资产。接下来,我会带你从最基础的概念开始,一步步拆解Shell脚本的核心要素,并分享那些只有踩过坑才知道的实操技巧。
2. Shell脚本的核心骨架与执行原理
2.1 第一行“魔法注释”:#!/bin/bash
创建一个脚本文件,比如叫myscript.sh,用文本编辑器打开,第一行我们几乎总是会写:
#!/bin/bash这行被称为“Shebang”或“Hashbang”。它不是注释,而是告诉系统,当直接运行这个脚本文件时,应该用哪个解释器来执行文件内的命令。/bin/bash指定了使用Bash这个Shell。为什么是Bash?因为它是在Linux和macOS上最通用、功能最强大的Shell。虽然还有/bin/sh(通常是Bash的简化模式或Dash),但为了使用Bash丰富的特性(如数组、更强大的条件判断),我们通常明确指定它。
注意:在某些极简的Docker镜像或嵌入式系统中,可能只有
/bin/sh。如果你的脚本要在这种环境下运行,要么使用更通用的#!/bin/sh,并在编写时严格遵循POSIX标准,避免使用Bash特有语法;要么在构建镜像时确保安装bash。
2.2 如何让一个文本文件变成可执行程序?
写完脚本后,你直接输入./myscript.sh,很可能会收到“Permission denied”的错误。这是因为新建的文件默认没有执行权限。在Unix/Linux哲学中,权限是需要明确赋予的。你需要执行:
chmod +x myscript.sh这条命令给文件myscript.sh的所有者、组和其他用户都添加了执行(x)权限。chmod是“change mode”的缩写,+x就是增加执行权。之后,你就可以通过./myscript.sh来运行它了。这里的./表示当前目录,是必需的,因为出于安全考虑,系统通常不会将当前目录加入命令搜索路径(PATH)。
2.3 脚本执行的三种方式及其本质区别
作为可执行文件运行:
./myscript.sh这是最标准的方式。系统读取Shebang行,找到/bin/bash,然后启动一个新的Bash子进程,由这个子进程来执行脚本中的命令。脚本中的变量、函数等都在这个子Shell中生效,执行完毕后,子Shell退出,其内部状态不会影响父Shell(即你当前的终端)。使用解释器直接运行:
bash myscript.sh这种方式显式指定了解释器bash。此时,脚本文件的第一行#!/bin/bash会被当作普通注释忽略。效果和第一种方式类似,也是在新子Shell中运行。在当前Shell中运行(Source):
. myscript.sh或source myscript.sh这种方式有本质不同。它不会启动子Shell,而是让当前Shell直接读取并执行脚本文件中的命令。这意味着:- 脚本中设置的环境变量会在当前Shell中生效。
- 脚本中定义的函数可以在当前Shell中直接调用。
- 脚本中使用
cd命令会改变当前Shell的工作目录。 这种方式通常用于加载配置(如.bashrc),或者在调试脚本时,需要让变量留在当前环境进行查看。
理解这些区别至关重要。比如,你写了一个脚本用来设置项目环境变量,如果你用./setup.sh运行,脚本结束变量就消失了;你必须用source setup.sh才能让设置对当前终端生效。这是一个常见的“坑”。
3. 变量、字符串与引号:Shell编程的基石与陷阱
3.1 变量的定义、使用与作用域
在Bash中定义变量很简单,但有一些严格的规则:
name="John Doe" # 定义变量,等号两边不能有空格! age=30使用变量时,需要在变量名前加美元符号$:
echo $name echo Hello, $name!为了明确变量名的边界,避免歧义,我们常用花括号将变量名括起来:${name}。这在变量与其他字符连接时非常有用:
echo "Hello, ${name}!" # 清晰,推荐 echo "I am ${age}years old" # 如果没有{},Shell会尝试寻找名为`ageyears`的变量作用域:默认情况下,在脚本中定义的变量是全局的,在整个脚本内都有效。在函数内部,如果你修改了一个全局变量,修改会生效。如果你在函数内想创建一个局部变量,需要使用local关键字:
function myfunc() { local local_var="I'm local" global_var="I'm global" }执行函数后,local_var在函数外无法访问,而global_var则可以在脚本任何地方访问。
3.2 引号的艺术:单引号、双引号与反引号
引号是Shell脚本中最容易混淆的部分之一,也是很多bug的来源。
单引号
':强引用。引号内的所有字符都保持字面意义,变量和命令替换都不会发生。echo '$name is $age' # 输出: $name is $age双引号
":弱引用。引号内的大部分字符保持字面意义,但变量替换和命令替换会正常进行。echo "$name is $age" # 输出: John Doe is 30反引号
`和$():命令替换。它们用于执行一条命令,并将其输出结果替换到当前位置。$()是更现代、更推荐的方式,因为它更清晰,且可以嵌套。current_date=`date` # 旧式,不推荐嵌套 current_date=$(date) # 新式,清晰且可嵌套,如:$(echo $(date)) echo "Today is $current_date"
一个黄金法则:除非你有明确理由需要使用单引号(比如需要原样输出$符号),否则在定义包含变量的字符串时,总是使用双引号。这可以避免很多因单词分割和通配符扩展导致的意外错误。
file_path="/path with spaces/my file.txt" # 错误示范: rm $file_path # 这会被解析为 rm /path with spaces/my file.txt,试图删除四个不存在的文件,可能引发灾难! # 正确示范: rm "$file_path" # 引号保证了变量值作为一个整体参数传递3.3 环境变量、位置参数与特殊变量
环境变量:像
PATH,HOME,USER这些由Shell或系统预设的变量,可以被所有子进程继承。在脚本中,你可以直接读取它们,也可以用export命令将自定义变量提升为环境变量,供子进程使用。echo "My home directory is $HOME" export MY_TOOL_PATH="/opt/mytool" # 之后,在脚本中启动的子进程就能读取到MY_TOOL_PATH了位置参数:当通过命令行向脚本传递参数时,这些参数可以通过
$1,$2,$3...来访问。$0代表脚本本身的名称。# 假设脚本调用:./myscript.sh arg1 arg2 echo "Script name: $0" echo "First arg: $1" echo "Second arg: $2"特殊变量:
$#:传递给脚本的参数个数。$@:所有位置参数的列表,每个参数作为独立的单词(通常配合循环使用)。$*:所有位置参数,但被合并成一个单独的字符串。$?:上一条命令的退出状态码。0通常表示成功,非0表示失败。这是进行条件判断的关键。$$:当前Shell进程的进程ID(PID)。$!:最后一个在后台运行的进程的PID。
4. 流程控制:让脚本学会判断与循环
4.1 条件判断:if/else/fi 与 test命令
Shell脚本使用if、then、else、elif、fi来进行条件分支。判断的核心是一个“条件测试”,它实际上是一条命令,根据其退出状态码($?)是否为0来判断真假。
有两种常用的测试语法:
test命令及其简写[ ]:if test -f "/path/to/file"; then echo "File exists." fi # 等价于更常见的方括号写法(注意括号内的空格是必须的!) if [ -f "/path/to/file" ]; then echo "File exists." fi更强大的
[[ ]]:这是Bash的扩展语法,比[ ]更安全、功能更强(支持正则匹配=~,且字符串比较更直观)。if [[ -f "/path/to/file" ]]; then echo "File exists." fi if [[ "$name" == "John" ]]; then # 使用==进行字符串比较,比[ ]的=更直观 echo "Hello, John!" fi
常见的测试运算符:
| 类别 | 运算符 | 含义 | 示例 |
|---|---|---|---|
| 文件测试 | -e file | 文件/目录是否存在 | [ -e /tmp ] |
-f file | 是否为普通文件 | [ -f /etc/passwd ] | |
-d file | 是否为目录 | [ -d /home ] | |
-r file | 文件是否可读 | [ -r data.txt ] | |
-w file | 文件是否可写 | [ -w log.txt ] | |
-x file | 文件是否可执行 | [ -x myscript.sh ] | |
| 字符串测试 | -z str | 字符串长度是否为0 | [ -z "$var" ] |
-n str | 字符串长度是否非0 | [ -n "$var" ] | |
str1 = str2 | 字符串相等 | [ "$a" = "$b" ] | |
str1 != str2 | 字符串不等 | [ "$a" != "$b" ] | |
| 算术比较 | num1 -eq num2 | 等于 (equal) | [ $a -eq 10 ] |
num1 -ne num2 | 不等于 (not equal) | [ $a -ne 0 ] | |
num1 -gt num2 | 大于 (greater than) | [ $a -gt $b ] | |
num1 -lt num2 | 小于 (less than) | [ $a -lt 100 ] | |
num1 -ge num2 | 大于等于 | [ $a -ge 1 ] | |
num1 -le num2 | 小于等于 | [ $a -le 10 ] | |
| 逻辑组合 | ! expr | 非 | [ ! -f file ] |
expr1 -a expr2 | 与 (and) | [ -f file -a -r file ] | |
expr1 -o expr2 | 或 (or) | [ -z "$var" -o "$var" = "yes" ] | |
| (推荐) | && | 逻辑与 (命令执行成功) | [[ -f file && -r file ]] |
| ` | ` |
实操心得:对于字符串判断,务必先检查变量是否为空,或者始终将变量放在双引号内。
[ -n $var ]在var为空时,会变成[ -n ],这会被解释为测试字符串“-n”是否非空,结果为真,导致逻辑错误。正确的写法是[ -n "$var" ]。这也是我更推荐使用[[ ]]的原因之一,它对未定义变量的处理稍宽容一些。
4.2 循环结构:for、while 与 until
循环是自动化的核心。
for循环:常用于遍历一个已知的列表。# 遍历值列表 for fruit in apple banana orange; do echo "I like $fruit" done # 遍历命令输出结果(例如文件列表) for file in *.txt; do echo "Processing $file" # 可以对每个.txt文件进行操作 done # C语言风格的for循环(用于数字序列) for (( i=1; i<=5; i++ )); do echo "Iteration $i" donewhile循环:当条件为真时,持续循环。count=1 while [ $count -le 5 ]; do # 使用[ ]测试 echo "Count is $count" ((count++)) # 使用双括号进行算术运算 done # 逐行读取文件内容(非常实用的模式) while IFS= read -r line; do echo "Line: $line" done < "input.txt" # 输入重定向到循环这里
IFS=是为了防止行首行尾的空白被修剪,-r选项防止反斜杠被解释。until循环:与while相反,当条件为假时持续循环,直到条件为真。相对较少使用。count=1 until [ $count -gt 5 ]; do echo "Count is $count" ((count++)) done
4.3 分支选择:case 语句
当需要根据一个变量的多种可能值进行分支时,case语句比一堆if/elif更清晰。
fruit="banana" case $fruit in apple) echo "It's an apple." ;; banana | mango) # 可以匹配多个模式,用|分隔 echo "It's a tropical fruit." ;; orange) echo "It's an orange." ;; *) # 默认情况,匹配任何值 echo "Unknown fruit." ;; esac每个分支以;;结束,代表break。
5. 函数、输入输出与脚本调试
5.1 函数的定义与调用:封装可重用逻辑
函数让你可以把一段常用的代码块封装起来,提高脚本的模块化和可读性。
# 定义函数 greet_user() { local name=$1 # 使用局部变量 local hour=$(date +%H) if [ $hour -lt 12 ]; then echo "Good morning, $name!" elif [ $hour -lt 18 ]; then echo "Good afternoon, $name!" else echo "Good evening, $name!" fi } # 调用函数,并传递参数 greet_user "Alice"函数内通过$1,$2...来访问传递的参数。使用local定义变量可以避免污染全局命名空间。函数也可以有返回值,但严格来说,Shell函数返回的是退出状态码(0成功,非0失败)。如果需要返回一个字符串值,通常的做法是用echo输出,然后在调用处用命令替换捕获:
get_full_path() { local filename=$1 echo "$(pwd)/$filename" } my_path=$(get_full_path "data.txt")5.2 与用户交互:read命令与菜单
让脚本变得交互式很简单,使用read命令。
echo -n "Please enter your name: " # -n 表示不换行 read username echo "Hello, $username!" # 读取多个值 echo "Enter your first and last name:" read firstname lastname echo "First: $firstname, Last: $lastname" # 静默读取(用于密码) read -s -p "Enter your password: " password echo # 换行 # -s 表示不显示输入,-p 允许直接指定提示符 # 带超时的读取 if read -t 10 -p "You have 10 seconds to respond: " answer; then echo "You said: $answer" else echo "Time out!" fi5.3 脚本的输入输出重定向
这是Shell脚本强大功能之一,它允许你控制命令的数据来源和去向。
>和>>:输出重定向。>覆盖文件,>>追加到文件。echo "Hello" > output.txt # 创建或覆盖output.txt date >> output.txt # 将日期追加到output.txt末尾<:输入重定向。将文件内容作为命令的输入。wc -l < input.txt # 统计input.txt的行数2>和&>:标准错误重定向。命令的输出分为标准输出(stdout, 文件描述符1)和标准错误(stderr, 文件描述符2)。ls non_existent_file 2> error.log # 将错误信息重定向到error.log ls *.txt &> all_output.log # 将stdout和stderr都重定向到同一个文件管道
|:将一个命令的标准输出作为下一个命令的标准输入。ps aux | grep nginx | head -5 # 列出进程,过滤出nginx相关的前5条
5.4 调试技巧:让错误无处遁形
写脚本难免出错,掌握调试方法能极大提升效率。
启用调试模式:
bash -x script.sh:在运行脚本时,打印出每一行被执行的命令及其扩展后的参数。这是最强大的调试工具。- 在脚本内部第一行(Shebang之后)添加
set -x,同样可以启用调试。用set +x关闭。
启用严格模式:在脚本开头加上以下两行,是好习惯。
#!/bin/bash set -euo pipefailset -e:一旦任何命令执行失败(返回非0状态),脚本立即退出。set -u:遇到未定义的变量时,报错并退出。set -o pipefail:管道命令中,只要有一个命令失败,整个管道就视为失败。 这能防止错误像雪球一样滚大,让你尽早发现并定位问题。
打印调试信息:在关键位置使用
echo或printf输出变量状态。echo "DEBUG: The value of var is: $var" >&2 # 重定向到标准错误,不影响正常输出流使用 shellcheck:这是一个静态分析工具,可以检查Shell脚本中的语法错误、不规范的写法以及潜在的bug。强烈建议在编写脚本后使用
shellcheck your_script.sh进行检查,它能发现很多肉眼难以察觉的问题。
6. 实战进阶:文本处理三剑客与综合案例
Shell脚本的强大,一半来自于Shell本身,另一半则来自于Unix系统下那些小巧而强悍的文本处理工具,其中最著名的就是grep、sed、awk,常被称为“三剑客”。
6.1 grep:全局正则表达式打印,用于搜索过滤
grep用于在文件或输入流中搜索匹配特定模式的行。
- 基本搜索:
grep "pattern" file.txt - 忽略大小写:
grep -i "error" log.txt - 显示行号:
grep -n "TODO" source_code.py - 反向匹配:
grep -v "^#" config.conf(显示所有不以#开头的行,常用于查看有效配置) - 递归搜索:
grep -r "function_name" ./src/(在当前目录及子目录中搜索) - 使用扩展正则表达式:
grep -E "error|warning|fatal" log.txt(匹配error或warning或fatal)
6.2 sed:流编辑器,用于文本替换与编辑
sed以行为单位,对文本进行替换、删除、插入等操作,尤其擅长批量替换。
- 基本替换:
sed 's/old/new/' file.txt(将每行第一个old替换为new) - 全局替换:
sed 's/old/new/g' file.txt(将每行所有的old替换为new) - 直接修改原文件:
sed -i 's/foo/bar/g' file.txt(-i选项直接修改文件,务必先备份或测试!) - 删除行:
sed '/^#/d' file.txt(删除所有以#开头的注释行) - 打印特定行:
sed -n '10,20p' file.txt(只打印文件的第10到20行)
6.3 awk:强大的文本分析工具,也是一门编程语言
awk将输入行分割成字段(默认以空格或制表符分隔),然后对字段进行处理。它功能极其强大。
- 打印特定列:
awk '{print $1, $3}' data.txt(打印第1列和第3列) - 指定分隔符:
awk -F',' '{print $2}' csvfile.csv(使用逗号,作为分隔符) - 条件过滤:
awk '$3 > 100 {print $1, $3}' data.txt(打印第3列大于100的行的第1、3列) - 内置变量:
NF(字段数),NR(当前行号),$0(整行内容)。awk '{print NR ": " $0}' file.txt # 给每一行加上行号 awk 'NF > 5' file.txt # 打印字段数大于5的行(常用于过滤空行或简单行) - 进行计算:
awk '{sum += $1} END {print sum}' numbers.txt(计算第一列的总和)
6.4 综合实战案例:服务器日志分析脚本
假设我们有一个Nginx的访问日志文件access.log,格式类似:
192.168.1.1 - - [10/May/2024:15:30:01 +0800] "GET /index.html HTTP/1.1" 200 1234我们需要写一个脚本analyze_log.sh,实现以下功能:
- 统计总访问次数。
- 找出访问量最高的前5个IP地址。
- 统计所有非200状态码的请求及其数量。
#!/bin/bash # analyze_log.sh - 简易Nginx日志分析脚本 set -euo pipefail LOG_FILE="${1:-access.log}" # 使用第一个参数作为日志文件,默认为access.log if [[ ! -f "$LOG_FILE" ]]; then echo "错误:日志文件 '$LOG_FILE' 不存在。" >&2 exit 1 fi echo "===== 日志分析报告 =====" echo "分析文件: $LOG_FILE" echo "生成时间: $(date)" echo "=========================" echo # 1. 统计总访问次数 total_requests=$(wc -l < "$LOG_FILE") echo "1. 总访问次数: $total_requests" echo # 2. 找出访问量最高的前5个IP (假设IP在第一列) echo "2. 访问量TOP 5的IP地址:" awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -5 echo # 3. 统计非200状态码的请求 (假设状态码在第9列) echo "3. 非200状态码请求统计:" awk '$9 != 200 {print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn echo echo "分析完成。"脚本解析与技巧:
- 参数处理:
${1:-access.log}是一种参数默认值写法。如果执行脚本时提供了第一个参数(如./analyze_log.sh my.log),则LOG_FILE为my.log;如果没提供,则默认为access.log。 - 文件存在性检查:在操作文件前,先检查它是否存在,是好习惯。
- 核心管道分析:
awk '{print $1}':提取第一列(IP地址)。sort:对IP进行排序,这是uniq -c(统计相邻重复行出现次数)的前提。uniq -c:统计每个IP出现的次数,输出格式如“5 192.168.1.1”。sort -rn:按数字(-n)逆序(-r)排序,即按访问次数从高到低排。head -5:取前5行。
- 错误输出重定向:
>&2将错误信息输出到标准错误,方便与正常分析结果区分。
你可以通过运行chmod +x analyze_log.sh和./analyze_log.sh来使用这个脚本。这个案例融合了变量、参数、条件判断、管道和文本处理三剑客,是一个典型的Shell脚本应用场景。
7. 避坑指南与最佳实践
结合我多年的经验,下面这些“坑”和最佳实践,能让你的脚本更健壮、更专业。
7.1 必须养成的安全习惯
- 总是给变量加双引号:这是防止空白字符和路径名扩展导致错误的最重要规则。
rm "$file"永远比rm $file安全。 - 使用
set -euo pipefail:在脚本开头加上这行,能让脚本在遇到错误、未定义变量或管道失败时立即停止,而不是继续执行并产生更糟糕的结果。 - 对输入进行验证:特别是当脚本需要处理用户输入或外部参数时。检查文件是否存在、参数数量是否正确、输入格式是否合法。
- 慎用
rm -rf:在变量和路径拼接时,尤其要小心。可以考虑先使用rm -rf的“模拟”模式,或者先echo出要删除的命令,确认无误后再执行。# 危险! dir_to_clean="/tmp/important" rm -rf "$dir_to_clean/*" # 如果变量意外为空,命令变成 `rm -rf /*` 将是灾难! # 稍好的做法 [[ -n "$dir_to_clean" ]] && rm -rf "${dir_to_clean:?}/"* # 使用`:?`运算符,如果变量为空或未设置,会报错退出。
7.2 提升脚本的可读性与可维护性
- 添加注释:在复杂的逻辑块、函数定义、重要的变量声明处添加注释,说明其目的。但避免对显而易见的代码进行注释。
- 使用有意义的变量名:
backup_dir比bd好,max_retry_count比mrc好。 - 定义函数:将重复的代码或独立的逻辑块封装成函数。一个函数最好只做一件事。
- 使用
local变量:在函数内部始终使用local声明变量,避免副作用。 - 统一的代码风格:缩进(通常用2或4个空格)、括号位置保持统一。虽然Shell对缩进不敏感,但良好的格式对阅读者至关重要。
7.3 性能与可移植性考量
- 避免在循环中使用管道调用外部命令:特别是在处理大量数据时。例如,在循环内反复调用
grep或awk可能很慢。尽量将数据一次性通过管道传递给这些工具处理。 - 使用
$(command)代替反引号:$()更清晰,且支持嵌套。 - 注意可移植性:如果你的脚本需要在不同的Unix-like系统(如Linux, macOS, BSD)上运行,要小心使用某些Bash特有的特性(如
[[ ]],=~)。对于需要高度可移植的脚本,坚持使用POSIX标准的[ ]和test命令。 - 处理带有特殊字符的文件名:使用
find命令的-print0和xargs的-0选项,或者使用while IFS= read -r -d ''循环来处理可能包含空格、换行符的文件名。
7.4 一个健壮的脚本模板
最后,分享一个我常用的、相对健壮的脚本开头模板,它集成了很多最佳实践:
#!/usr/bin/env bash # 脚本名称:robust_script_template.sh # 描述:一个健壮的Shell脚本模板 # 用法:./script_template.sh [选项] <参数> set -euo pipefail # 启用严格模式 # 初始化变量 readonly SCRIPT_NAME=$(basename "$0") readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) # 获取脚本所在目录 VERSION="1.0.0" VERBOSE=false # 颜色定义(用于输出,非必需) readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly NC='\033[0m' # No Color # 函数定义 usage() { cat <<EOF Usage: $SCRIPT_NAME [OPTIONS] <input_file> Options: -h, --help Display this help message and exit. -v, --verbose Enable verbose output. -V, --version Display version information. Example: $SCRIPT_NAME --verbose data.txt EOF } log_info() { echo -e "${GREEN}[INFO]${NC} $*" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2 } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2 } # 主逻辑 main() { local input_file="" # 解析命令行参数 while [[ $# -gt 0 ]]; do case $1 in -h|--help) usage exit 0 ;; -v|--verbose) VERBOSE=true shift ;; -V|--version) echo "$SCRIPT_NAME version $VERSION" exit 0 ;; -*) log_error "Unknown option: $1" usage exit 1 ;; *) input_file="$1" shift ;; esac done # 参数验证 if [[ -z "$input_file" ]]; then log_error "Input file is required." usage exit 1 fi if [[ ! -f "$input_file" ]]; then log_error "Input file '$input_file' does not exist or is not a regular file." exit 1 fi $VERBOSE && log_info "Starting processing of '$input_file'..." # 这里是你的脚本核心业务逻辑 process_file "$input_file" $VERBOSE && log_info "Processing completed successfully." } # 具体的处理函数 process_file() { local file="$1" log_info "Processing file: $file" # ... 实现具体的处理逻辑 ... } # 脚本入口 main "$@"这个模板包含了严格的错误处理、清晰的用法说明、命令行参数解析、日志函数和基本的代码结构。从这样一个坚实的基础开始编写脚本,能帮你避开很多初级错误,也让你的脚本看起来更专业、更可靠。记住,Shell脚本的终极目标不是炫技,而是可靠、清晰、高效地解决问题。