1. 为什么必须学 Shell:一个运维工程师每天都在用的技能
很多刚开始接触 Linux 的同学都会有这样的困惑:Linux 命令我会敲不少,cd、ls、cp、rm 都挺熟练,为什么还要专门学 Shell 脚本?
这个问题的答案,在真实的生产环境中非常清晰。
假设你管理着 50 台 Linux 服务器,每天要做的事情包括:检查磁盘空间、清理过期日志、备份数据库文件、定时同步配置。如果你一台一台登录,逐条命令去敲,每天光重复操作可能就要耗费两三个小时。而如果你把这些操作写成 Shell 脚本,配合 crontab 定时任务,所有工作会在每天凌晨自动完成,早上打开电脑只需要看一份运行日志即可。
Shell 脚本就是这样一个“把命令变成程序”的工具。它不是一门新的编程语言,而是把 Linux 命令按逻辑组织起来,配合变量、循环、判断、函数等编程能力,让服务器自动完成重复性工作。
除了提高效率,Shell 脚本还是自动化运维的基石。现在企业招聘运维工程师和 DevOps 工程师,Shell 几乎是必考技能。不管是写自动化部署脚本、监控脚本、日志采集脚本,还是编写容器启动入口脚本,都离不开 Shell。即便是开发岗位,掌握 Shell 也能让你在处理构建、发布、排查线上问题时更从容。
从 CSDN 后台和招聘平台的热搜数据也能看出,shell脚本入门、linux常用命令教程、自动化运维 shell这类关键词常年处于高位。这说明 Shell 不是过时的技术,而是 Linux 生态中最基础、最持久的能力之一。
本文将会从零开始,不假设你有任何编程基础,逐步讲清楚 Shell 脚本的核心语法、常用命令组合、完整实战案例和常见坑点。读完并动手练习后,你就能独立编写自己的第一个自动化运维脚本。
2. 环境准备:开始前的几个必要检查
Shell 脚本的学习和运行,必须有一个 Linux 环境。这里不要求你一定购买云服务器,本地虚拟机、Windows 下的 WSL(Windows Subsystem for Linux)都可以。
2.1 选择操作系统与 Shell 版本
目前市面上主流的 Linux 发行版有两大家族:
| 家族 | 代表系统 | 常用软件包管理 | 常见环境 |
|---|---|---|---|
| Debian 系 | Ubuntu、Debian、Deepin | apt | 开发测试、个人学习 |
| RedHat 系 | CentOS、Rocky Linux、Alibaba Cloud Linux | yum / dnf | 企业服务器、生产环境 |
不同发行版之间,命令的基本语法一致,区别主要在软件包管理器和默认目录结构上。本文中的 Shell 语法和大部分命令在各发行版上是通用、可以直接运行的。
Shell 本身也有多种实现,常见的有:
bash(Bourne Again Shell):Linux 默认 Shell,功能最全面,也是本文主讲的版本。sh(Bourne Shell):早期的 Shell,在一些脚本中会看到#!/bin/sh。zsh:macOS 默认 Shell,兼容 bash,交互体验更好。dash:Debian 系系统中/bin/sh的实际指向,体积小、执行快,但缺少一些 bash 特性。
版本需要根据你的项目实际情况调整。如果你用的是 CentOS 7 或 Ubuntu 18.04 以上的系统,默认 bash 版本基本都在 4.x 以上,学习本文内容没有任何障碍。
2.2 检查当前环境
打开终端,执行以下命令查看当前 Shell 和版本:
echo $SHELL bash --version输出示意:
/bin/bash GNU bash, 版本 4.2.46(2)-release (x86_64-redhat-linux-gnu)如果你的系统没有 bash,可以使用系统的软件包管理器安装:
# Debian/Ubuntu sudo apt update && sudo apt install -y bash # CentOS/RHEL sudo yum install -y bash2.3 创建实验目录与第一个脚本
为了接下来的学习,建议单独建立一个目录,避免测试文件散落各处:
mkdir -p ~/shell-lab && cd ~/shell-lab然后创建一个最简单的脚本文件:
vim hello.sh输入以下内容:
#!/bin/bash echo "Hello, Shell!"保存退出后,有两种执行方式:
# 方式一:直接指定解释器执行,不要求脚本有执行权限 bash hello.sh # 方式二:给脚本添加执行权限后,直接运行 chmod +x hello.sh ./hello.sh运行结果:
Hello, Shell!这里需要注意:脚本第一行的#!/bin/bash称为 shebang,它告诉系统应该用哪个解释器来执行这个脚本。虽然写不写有时候都能运行,但建议每个脚本都写上,这样可以避免很多莫名其妙的兼容性问题。
3. Shell 核心语法拆解:从变量到函数
学 Shell 脚本,不需要死记硬背“知识点大全”,更有效的方式是按“我需要表达什么逻辑”去掌握对应的语法要素。下面我们把 Shell 编程需要的基础语法按功能逐一拆解。
3.1 变量:命名、赋值、引用与特殊变量
Shell 变量和编程语言中的变量类似,用来临时存储数据。定义一个变量的语法非常简单:
name="zhangsan" num=100使用变量时,在变量名前加$符号即可:
echo $name echo "欢迎 $name 登录系统"注意一个高频误区:等号两边不能有空格。name = "zhangsan"是错误的写法,会被 Shell 解析成执行一个叫作name的命令,然后传入=和"zhangsan"参数。
变量名的规范是:由字母、数字、下划线组成,不能以数字开头。赋值内容如果包含空格,需要用双引号或单引号包裹:
full_name="Zhang San"双引号和单引号是有区别的:
- 双引号内的
$变量名会被解析成变量值。 - 单引号内的内容会原样输出,不解析任何变量。
name="zhangsan" echo "hello, $name" # 输出 hello, zhangsan echo 'hello, $name' # 输出 hello, $name除了自定义变量,Shell 还内置了一批位置变量和特殊变量,在编写自动化脚本时非常常用:
| 变量 | 含义 |
|---|---|
$0 | 当前脚本的文件名 |
$1、$2... | 脚本的第 1 个、第 2 个参数 |
$# | 参数的个数 |
$@ | 所有参数列表(每个参数独立) |
$* | 所有参数列表(整体作为单个字符串) |
$? | 上一条命令的退出状态码,0 表示成功 |
$$ | 当前 Shell 进程的 PID |
写一个args.sh来验证特殊变量:
#!/bin/bash echo "脚本名称: $0" echo "第一个参数: $1" echo "参数个数: $#" echo "所有参数: $@" echo "上条命令退出码: $?"运行:
bash args.sh hello world输出:
脚本名称: args.sh 第一个参数: hello 参数个数: 2 所有参数: hello world 上条命令退出码: 0另外,Shell 中还有shift这个常用命令,它可以把参数列表左移一位,也就是丢弃掉当前的$1,原来的$2变成$1。这个命令在编写需要循环处理大量参数的脚本时非常实用,后面实战案例中会用到。
3.2 字符串处理:截取、替换、判断长度
在自动化脚本中,字符串处理是最频繁的操作之一,例如处理日志文件路径、解析配置项、拼接日期等。
字符串截取的基本语法:
str="hello-shell-world" echo ${str:0:5} # 从第 0 个字符开始截取 5 个字符:hello echo ${str:6} # 从第 6 个字符截取到末尾:shell-world echo ${#str} # 字符串长度:17字符串替换:
echo ${str/-/_} # 替换第一个匹配:hello_shell-world echo ${str//-/} # 替换全部匹配:helloshellworld字符串判断是否为空:
if [ -z "$str" ]; then echo "str 为空" else echo "str 不为空" fi在真实场景中,判断变量是否为空的场景非常常见,比如检查用户传参是否完整。建议养成习惯:变量在比较时加上双引号,例如[ -z "$var" ]而不是[ -z $var ],这样可以避免变量内容为空或包含空格时产生语法错误。
3.3 条件判断:if、case 与 test
条件判断是让脚本拥有“智能逻辑”的基础。Shell 中最常见的是if语句,配合[ ]或test命令进行判断。
基本结构:
#!/bin/bash score=$1 if [ $score -gt 90 ]; then echo "优秀" elif [ $score -gt 60 ]; then echo "及格" else echo "不及格" fi注意[ ]的左右括号和里面的表达式之间必须有空格,否则会报语法错误。为了方便记忆,可以把[看作test命令的别名。
文件相关的判断是脚本中最高频的操作:
[ -f "file.txt" ] # 判断是否为普通文件 [ -d "dir" ] # 判断是否为目录 [ -e "path" ] # 判断路径是否存在 [ -r "file.txt" ] # 判断是否可读 [ -w "file.txt" ] # 判断是否可写 [ -x "file.sh" ] # 判断是否可执行数值判断和字符串判断用的运算符也容易混淆:
| 判断类型 | 运算符 | 示例 |
|---|---|---|
| 数值相等 | -eq | [ $a -eq $b ] |
| 数值不等 | -ne | [ $a -ne $b ] |
| 数值大于 | -gt | [ $a -gt $b ] |
| 数值小于 | -lt | [ $a -lt $b ] |
| 字符串相等 | =或== | [ "$s1" = "$s2" ] |
| 字符串不等 | != | [ "$s1" != "$s2" ] |
| 字符串为空 | -z | [ -z "$s1" ] |
| 字符串非空 | -n | [ -n "$s1" ] |
&&表示“并且”,||表示“或者”。下面这个写法在脚本中很常见:
if [ -z "$1" ] || [ -z "$2" ]; then echo "请传入两个参数" exit 1 fi当分支情况比较多时,case语句可读性比一连串的if-elif更好:
#!/bin/bash case $1 in start) echo "启动服务" ;; stop) echo "停止服务" ;; restart|reload) echo "重启服务" ;; *) echo "用法: $0 {start|stop|restart}" exit 1 ;; esac*)是默认分支,对应其他所有未匹配的情况。每个分支结束时用两个分号;;收尾,最后用esac(case 反写)结束整个语句。
3.4 for 循环、while 循环与实战场景
循环是脚本最提效的语法之一。比如批量处理日志文件、批量创建用户、批量重命名文件,靠的是循环。
for循环最常见的有三种写法。
写法一:遍历列表
for ip in 192.168.1.1 192.168.1.2 192.168.1.3; do echo "Ping $ip" ping -c 1 $ip > /dev/null 2>&1 && echo "$ip 可达" || echo "$ip 不可达" done写法二:使用seq生成数字序列
for i in $(seq 1 5); do echo "第 $i 次执行" done写法三:C 语言风格
for ((i=1; i<=5; i++)); do echo "number: $i" donewhile循环同样常用,重点是“当满足条件时持续执行”。例如持续检查某个进程是否存在:
#!/bin/bash counter=0 while [ $counter -lt 5 ]; do echo "counter=$counter" counter=$((counter + 1)) done这里$((...))是 Shell 的算术运算语法,也可以简写为((counter++))。
处理参数列表时,for配合$@是一种标准模式:
#!/bin/bash for arg in "$@"; do echo "处理参数: $arg" done再结合前面说的shift命令,可以在while循环中逐步消费参数:
#!/bin/bash while [ $# -gt 0 ]; do echo "当前参数: $1" shift done3.5 函数:封装复用逻辑
当一个脚本开始变得复杂,就应该把重复的逻辑封装成函数。Shell 函数定义很灵活,但有几个规范建议务必遵守。
函数定义的推荐写法是写清函数名和空括号,函数体用花括号包住:
#!/bin/bash # 定义一个日志函数 log_info() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $1" } log_error() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $1" } # 业务函数 check_disk() { local max=80 local usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%') if [ "$usage" -gt "$max" ]; then log_error "根分区使用率已达 $usage%" return 1 else log_info "根分区使用率正常:$usage%" return 0 fi } log_info "开始检查磁盘空间" check_disk if [ $? -eq 0 ]; then echo "磁盘状态正常" else echo "需要处理磁盘空间" fi关于函数的几个关键点:
local关键字用于声明局部变量。如果不加local,函数里定义的变量都是全局的,外部也可能被误改。- 函数通过
return返回状态码,范围是 0-255,0 表示成功,非 0 表示失败。如果要返回字符串结果,可以用echo输出,然后用$(函数名)捕获。 - 函数可以先定义再调用,但一定要确保调用时函数已经被定义,不能像 C 语言那样声明前置。
3.6 常用符号与命令组合的坑
Shell 中有几个容易混淆的低频符号,在阅读他人脚本时经常遇到:
| 符号 | 含义 | 示例 |
|---|---|---|
; | 分号,命令分隔符,不关心前面命令是否成功 | cd /tmp; ls |
&& | 只有前一条命令成功才执行后一条 | mkdir test && cd test |
| ` | ` | |
& | 后台执行命令 | ./start.sh & |
> | 重定向输出到文件,覆盖 | echo 123 > a.log |
>> | 重定向输出到文件,追加 | echo 123 >> a.log |
2>&1 | 将标准错误重定向到标准输出 | command > a.log 2>&1 |
: | Shell 内置的空操作命令,永远返回 0 | while :; do echo ok; done |
特别提一下:这个命令,它不显眼但很实用。前面热词中也出现了: 这是一个 shell 内置的空操作命令。它常用于:
- 充当死循环的条件:
while : - 占位,避免脚本结构不完整:
if [ -z "$1" ]; then :; else echo "有参数"; fi - 调试时临时注释掉某些逻辑,又不想破坏结构。
4. 自动化运维实战:三个可直接使用的脚本案例
本章我们进入实战环节。下面三个脚本是我认为 Shell 入门阶段最值得写一遍的案例,分别覆盖系统信息采集、批量文件处理和日志清理。案例难度从简单到中等,代码可以直接复制到自己的环境中运行。
4.1 系统信息采集脚本
运维工作第一步往往是了解机器状态:磁盘用了多少、内存还够不够、当前负载如何、系统已经运行多久。写一个脚本把这些信息一次性采集出来,是很好的练手项目。
创建sys_info.sh:
#!/bin/bash # 描述: 采集 Linux 系统关键信息 # 用法: bash sys_info.sh echo "======== 系统基本信息 ========" echo "主机名: $(hostname)" echo "操作系统: $(cat /etc/os-release | grep PRETTY_NAME | cut -d '=' -f2 | tr -d '"')" echo "内核版本: $(uname -r)" echo "运行时间: $(uptime -p)" echo "" echo "======== 硬件资源使用情况 ========" echo "CPU 核心数: $(nproc)" echo "内存使用情况:" free -h | awk 'NR==1 || NR==2' echo "" echo "根分区磁盘使用情况:" df -h / | awk 'NR==1 || NR==2' echo "" echo "======== 网络信息 ========" # 获取本机主要 IP(排除回环地址) ip -o -4 addr show | awk '{print $2, $4}' | grep -v '^lo ' echo "" echo "======== 关键进程检查 ========" for pkg in sshd crond; do if pgrep -x "$pkg" > /dev/null 2>&1; then echo "$pkg 运行中" else echo "$pkg 未运行" fi done运行方式:
bash sys_info.sh运行结果类似:
======== 系统基本信息 ======== 主机名: vm-master-01 操作系统: "CentOS Linux 7 (Core)" 内核版本: 3.10.0-1160.el7.x86_64 运行时间: up 25 days, 3 hours ======== 硬件资源使用情况 ======== CPU 核心数: 2 内存使用情况: total used free shared buff/cache available Mem: 1.8G 1.2G 104M 8.5M 535M 459M ... ======== 网络信息 ======== eth0 192.168.1.10/24 ======== 关键进程检查 ======== sshd 运行中 crond 未运行这个脚本用到了两个非常重要的小工具:awk和grep。awk 'NR==1 || NR==2'表示只输出第 1 行和第 2 行,awk '{print $2, $4}'表示取每行的第 2 和第 4 列。grep -v表示排除匹配行。这些文本处理命令在 Shell 脚本中与循环、管道搭配使用,能解决绝大多数日志与输出解析问题。
4.2 批量重命名文件脚本
批量重命名文件是高频需求。比如批量把*.txt改为带日期前缀,把.log改成.bak,或者把文件名中的空格替换成下划线。用 Shell 写一个通用一点的批量重命名脚本,是一个很不错的实战练习。
创建rename_files.sh:
#!/bin/bash # 描述: 批量重命名当前目录下的文件 # 用法: bash rename_files.sh 旧关键字 新关键字 if [ $# -ne 2 ]; then echo "用法: $0 旧关键字 新关键字" echo "示例: $0 .log .bak" exit 1 fi old_word="$1" new_word="$2" count=0 for file in *"$old_word"*; do # 防止目录下没有匹配文件时,* 被原样输出 if [ -e "$file" ]; then new_name="${file//$old_word/$new_word}" mv "$file" "$new_name" echo "已重命名: $file -> $new_name" count=$((count + 1)) fi done echo "共重命名 $count 个文件"测试一下:
touch a.log b.log c.txt bash rename_files.sh .log .bak输出:
已重命名: a.log -> a.bak 已重命名: b.log -> b.bak 共重命名 2 个文件这个脚本有两个细节值得注意:
第一,for file in *"$old_word"*中的星号是通配符,Shell 会把它展开成匹配的文件列表。如果目录下没有匹配文件,循环变量会直接拿到一个不存在的路径*.log。因此循环体内先做[ -e "$file" ]判断,是健壮性的关键。
第二,${file//$old_word/$new_word}是前面语法部分讲过的“全局替换”,注意替换内容中如果包含/或特殊字符,写法需要调整。这个脚本适用于关键字中不包含斜杠的简单场景。
4.3 日志清理与磁盘告警脚本
日志清理是最典型的自动化运维场景。应用日志会不断增长,如果不清理,最终会耗尽磁盘空间。下面这个脚本完成两件事:
- 找出指定目录下 N 天前的日志文件并删除。
- 检查磁盘使用率,超过阈值则输出告警。
创建clean_logs.sh:
#!/bin/bash # 描述: 清理指定目录下 N 天前的日志文件,同时检查磁盘空间 # 用法: bash clean_logs.sh /var/log/myapp 7 log_dir="$1" days="$2" threshold=85 # 参数检查 if [ -z "$log_dir" ] || [ -z "$days" ]; then echo "用法: $0 日志目录 保留天数" exit 1 fi if [ ! -d "$log_dir" ]; then echo "错误: $log_dir 目录不存在" exit 1 fi echo "开始清理 $log_dir 下 $days 天前的日志文件..." # 使用 find 命令找到并删除旧日志 deleted=$(find "$log_dir" -name "*.log" -type f -mtime +$days | wc -l) find "$log_dir" -name "*.log" -type f -mtime +$days -delete echo "已删除 $deleted 个日志文件" # 检查磁盘使用率 disk_usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%') echo "当前根分区使用率: $disk_usage%" if [ "$disk_usage" -ge "$threshold" ]; then echo "警告: 磁盘使用率已超过阈值 $threshold%,请及时处理!" else echo "磁盘使用率正常。" fi运行示例:
bash clean_logs.sh /tmp/logs 7预期输出:
开始清理 /tmp/logs 下 7 天前的日志文件... 已删除 5 个日志文件 当前根分区使用率: 68% 磁盘使用率正常。这里find命令是核心:
-name "*.log"指定文件名模式。-type f限定为普通文件,避免误删目录。-mtime +$days表示文件修改时间在 N 天前。-delete直接删除匹配文件。
在生产环境中,建议先去掉-delete,只打印出匹配的文件列表,人工确认后再真正执行删除。这也是所有删除类脚本的通用安全经验:先验证,再删除;先备份,再变更。
将这个脚本加入 crontab,就可以实现每天自动清理:
crontab -e添加一行:
0 3 * * * /bin/bash /root/scripts/clean_logs.sh /var/log/myapp 7 >> /var/log/clean_logs.log 2>&1这行配置表示每天凌晨 3 点执行一次清理,脚本输出追加到日志文件中,方便观察执行情况。
5. 常见问题与排查思路
Shell 脚本在运行时报错,新手最容易无从下手。下面把最常见的几类问题汇总成表,并给出排查思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
./test.sh: 权限不够 | 脚本文件没有可执行权限 | 执行chmod +x test.sh,或用bash test.sh运行 |
bad interpreter: /bin/bash^M | 在 Windows 上编辑脚本,文件包含回车符 | 使用dos2unix test.sh转换,或换用 Linux 编辑器 |
[: missing]'` | if 判断里[ ]两侧没有加空格 | 确保写成[ "$var" = "value" ] |
command not found | 可能拼写错误,或环境变量 PATH 未包含命令路径 | 检查命令名是=还是空格赋值,检查是否用了中文标点 |
变量赋值写成了name = "xx" | 等号两边不能有空格 | 改成name="xx" |
| 脚本没有输出任何内容 | 可能脚本没有执行权限,或 shebang 写错 | 用bash -x script.sh调试运行查看执行过程 |
循环时*被当成普通字符 | 目录下没有匹配的文件 | 循环体内先加[ -e "$file" ]判断 |
$?获取不到正确退出码 | 中间执行了 echo 等命令,覆盖了退出码 | 把退出码先存到变量:ret=$? |
| 日期或变量在单引号内不解析 | 用了单引号包裹 | 改成双引号"$var" |
syntax error near unexpected token | 脚本语法错误,或者 if / for 结构不完整 | 使用bash -n script.sh检查语法 |
除了上面的问题,这里额外推荐两个调试技巧。
第一个是bash -x,逐行跟踪脚本的执行过程,常被称为 Shell 脚本调试的“金钥匙”:
bash -x test.sh它会打印出每条命令及其展开后的真实内容。比如echo $name实际展开为echo zhangsan,一眼就能看出变量值是否符合预期。
第二个是bash -n,只做语法检查,不实际执行,适合在执行前快速排查:
bash -n test.sh如果没有语法错误,命令不会有任何输出。如果有错误,会直接指出出错行号。
新手遇到脚本行为不符预期时,不要猜,先运行bash -x看执行过程,再按上面表格对照排查,绝大多数问题都能定位。
6. Shell 编程最佳实践与工程建议
Shell 脚本入门容易,但写出“可维护、可排错、可上线”的脚本并不简单。以下经验是在实际运维和开发过程中逐渐总结出来的,希望你能从最初就养成这些习惯。
6.1 每个脚本都写注释和使用说明
脚本是给别人看的,也是给三个月后的自己看的。建议开头固定写清:脚本功能、使用方式、作者或维护日期。例如:
#!/bin/bash ########################################################### # 脚本名称: backup_data.sh # 功能: 备份 MySQL 指定数据库到 /data/backup 并压缩 # 用法: bash backup_data.sh <数据库名> # 作者: your_name # 更新: 2025-06-01 ###########################################################写注释的成本很低,但在半年后排查问题时价值巨大。
6.2 脚本开头设置严谨选项
在脚本开头加入下面几个选项,可以显著增强脚本健壮性:
set -e # 遇到任何命令返回非 0 状态时立即退出脚本 set -u # 使用未定义变量时直接报错,避免变量名拼写错误 set -o pipefail # 管道命令中,只要有一个命令失败,整个管道返回失败但这三个选项不一定是所有脚本的黄金标准。set -e在部分循环和判断场景下会“误伤”脚本,比如你本来希望某个命令失败后继续执行后续逻辑。建议在编写脚本时理解每个选项的意义,按需组合。更稳妥的做法是,在确实需要忽略某条命令失败时,显式追加|| true:
command_that_may_fail || true6.3 使用变量而不是重复硬编码
硬编码路径和参数会大大降低脚本的可移植性。例如,不要直接在脚本里写:
rm -rf /var/log/myapp/*.log而是定义成变量:
LOG_DIR=/var/log/myapp find "$LOG_DIR" -name "*.log" -type f -delete这样后续只需要维护变量即可,也能防止误操作写错目录。
6.4 删除和变更类命令要加保护
涉及rm、mv、覆盖写入、数据库变更等危险操作时,遵循以下原则:
- 先在测试环境执行,确认效果。
- 删除前先打印将删除的文件列表,人工确认后再加
-delete或rm。 - 重要数据先备份或移动到一个回收目录。
- 生产环境脚本不要裸写
rm -rf,至少用变量、路径判断和日志记录保护。
比如可以在清理脚本里加上一行“防呆”判断:
if [ "$log_dir" = "/" ] || [ "$log_dir" = "/data" ]; then echo "危险路径,拒绝执行" exit 1 fi6.5 日志记录比 echo 更重要
脚本在 crontab 中自动化运行后,没有人盯着屏幕看输出。所以关键操作必须写日志。统一的日志格式建议:
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> /var/log/my_script.log }调用时:
log "开始执行备份任务" log "备份完成,文件大小: $(du -sh backup.tar.gz | awk '{print $1}')"6.6 注意跨平台差异
- 不要在脚本中用
\r或 Windows 记事本编辑脚本。 /bin/bash和/bin/sh的差异要清楚,如果脚本用到 bash 特性,shebang 写#!/bin/bash。- 调用命令时尽量用绝对路径或确保 PATH 中包含相应目录,尤其是在 crontab 环境中,PATH 可能与登录 Shell 不一样。可以在脚本开头显式设置:
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin6.7 合理使用退出码
脚本的不同失败场景,建议返回不同的退出码。运维监控脚本可以通过$?判断失败类别:
exit 0 # 成功 exit 1 # 一般性错误 exit 2 # 参数错误这在后续接入监控平台、自动化调度平台时非常有用。
7. 总结与进阶路线
走到这里,你已经掌握了 Shell 脚本从零到实战的核心知识。现在回顾一下,你应该已经具备以下能力:
- 理解 Shell 脚本的作用和适用场景,能区分不同 Shell 的差异。
- 熟练写出变量、字符串处理、条件判断、循环、函数等脚本基本结构。
- 掌握
$?、$#、$@、shift等特殊变量,能处理脚本参数。 - 能综合使用
find、awk、grep、crontab等命令完成自动化运维任务。 - 对常见脚本报错有清晰的排查思路,知道用
bash -x和bash -n调试脚本。 - 具备编写安全、可维护脚本的工程意识。
接下来的进阶方向,可以根据你的实际需求选择:
- 学习文本处理三剑客的深入用法:
grep、sed、awk。它们能让你的 Shell 脚本处理复杂文本时效率翻倍。 - 学习如何用 Shell 封装复杂的部署流程,配合
expect处理交互式输入,配合rsync做增量同步。 - 学习 Shell 与其他工具结合,例如调用 Python 脚本处理复杂数据、调用 Docker 命令管理容器生命周期。
- 如果从事云原生或平台研发,还要了解容器镜像的入口脚本通常也是 Shell 写的,Shell 是打通“系统命令”和“上层应用”的桥梁。
最终想说的是:Shell 脚本这门技术,入门门槛不高,但上限很高。判断你是否真正掌握它的标准,不是背了多少语法,而是遇到重复性工作时,你是否能下意识地想到“这个能用脚本自动跑掉”。带着这个思路去写脚本,你的进步会非常快。建议现在就打开终端,从第一个Hello, Shell!开始,把一个实际工作中的重复操作写成脚本,跑通它。脚本跑通的那一刻,你对 Shell 的理解就真正扎实了。