很多刚开始接触 Linux 的朋友,尤其是想转行运维的读者,都有类似的困惑:看到别人在终端里敲几行命令就能自动完成一堆任务,自己却还在一个个手动输入;写了几个命令,重启服务器后又得重新来一遍。其实这些问题的答案都指向同一个方向——Shell 脚本。
这篇文章我会从零开始,把 Shell 脚本涉及的背景概念、环境准备、核心语法、完整实战案例和常见排查思路一次讲清楚。内容尽量做到保姆级,既有最小示例,也有能直接用到工作中的系统巡检脚本。如果你正准备转行运维,或者刚入职做服务器维护,这篇文章值得收藏起来慢慢看。
1. 背景与核心概念
1.1 Shell 是什么?Shell 脚本又是什么?
先来解决最基础的问题。
Shell 中文可以翻译为“壳”,它是用户与 Linux 内核之间的桥梁。你在终端里敲入一条命令,Shell 负责解释这条命令,然后交给内核去执行,最后把结果返回给你。
常见 Shell 有:
- bash:Linux 默认最多的 Shell,功能全面,也是本文使用的 Shell。
- sh:最古老的 Shell,很多脚本第一行写的都是
#!/bin/sh。 - zsh:macOS 默认 Shell,交互体验好。
- csh / tcsh / ksh:其他常见 Shell,日常使用较少。
Shell 脚本,简单理解就是把多条 Linux 命令按照一定的逻辑写进一个文件里,然后交给 Shell 去逐行解释执行。脚本可以包含变量、条件判断、循环、函数、管道、重定向等编程语言特性。
用一句话概括:Shell 是解释命令的工具,Shell 脚本是用这种工具语言编写的程序。
1.2 Shell 脚本解决了什么问题
运维工作中,大量任务都是重复性的,比如:
- 每天检查服务器 CPU、内存、磁盘使用率。
- 定期备份数据库和关键配置文件。
- 批量创建用户、批量分发公钥。
- 定时清理过期日志。
- 监控某个进程是否存活,宕掉后自动拉起。
这些任务如果全部手动执行,效率低且容易遗漏。而写成 Shell 脚本后,配合 crontab 定时任务,就能实现自动化运维,这也是为什么 Shell 脚本是运维工程师的必备技能。
1.3 Shell 脚本与 Linux 命令的区别
新手容易混淆“命令”和“脚本”这两个概念。
| 区别项 | Linux 命令 | Shell 脚本 |
|---|---|---|
| 存放位置 | 系统自带或软件安装产生 | 用户自己创建的文件 |
| 执行方式 | 输入即执行 | 需要赋予执行权限或使用 bash 执行 |
| 逻辑处理 | 单条命令功能 | 支持 if/for/while 等编程逻辑 |
| 复用性 | 命令执行完即结束 | 可以反复执行,传给不同参数 |
简单说,命令是脚本的零件,脚本是命令的组合。学习 Shell 脚本的前提是你已经掌握了一些常用命令,例如grep、awk、sed、df、free、ps等。如果这些命令还不太熟,建议先反复练习 Linux 基础命令,再回来学习脚本。
2. 环境准备与版本说明
2.1 操作系统要求
Shell 脚本本身不依赖复杂的运行环境,只要你的机器是 Linux 或者安装了终端模拟器的 macOS,基本都可以直接学习。
本文所有示例基于以下环境:
- 操作系统:CentOS 7 / Ubuntu 20.04(均可)
- Shell 版本:bash 4.x
- 编辑器:vim / vscode 均可
如果你使用的是 Windows,建议安装虚拟机或者使用 WSL(Windows Subsystem for Linux),不要在 Windows 命令提示符下直接运行 Shell 脚本,因为语法体系完全不同。
2.2 查看当前 Shell 类型
登录 Linux 后,先来确认当前使用的 Shell:
echo $SHELL输出示例:
/bin/bash看到/bin/bash,说明当前 Shell 是 bash。也可以查看系统支持哪些 Shell:
cat /etc/shells输出示例:
/bin/sh /bin/bash /usr/bin/bash /bin/rbash /usr/bin/rbash2.3 创建一个测试目录
为了方便后续练习,我们创建一个专门的工作目录:
mkdir -p ~/shell_lab cd ~/shell_lab后续所有脚本都放在这个目录中。如果你用的是 root 用户,路径就是/root/shell_lab。
2.4 编辑器选择
写脚本没必要用太复杂的 IDE,vim足够了。如果你对 vim 不熟,也可以使用:
yum install -y vim # CentOS apt install -y vim # Ubuntu或者直接用 vscode 远程连接服务器编辑,方式不限,关键是能保存为纯文本文件即可。
3. 核心语法与配置拆解
这一部分我们拆解 Shell 脚本最常用的几个知识点:变量、条件判断、循环、函数、重定向与管道。每一块都会给出最小示例,并解释关键点。
3.1 变量:脚本的存储单元
变量是 Shell 脚本中最基础的组成部分。它的作用是把值保存下来,后续可以直接引用。
#!/bin/bash # 变量定义:等号两侧不能有空格 name="csdn" version="1.0" echo "欢迎阅读 ${name} 的 Shell 教程" echo "当前脚本版本: $version"输出:
欢迎阅读 csdn 的 Shell 教程 当前脚本版本: 1.0关键点:
- 变量名和等号之间不能有空格。
- 引用变量时,可以使用
$变量名或${变量名}。 - 推荐使用
${变量名},尤其在变量后面紧跟其他字符时,花括号可以明确变量边界。 - 字符串建议加双引号,避免空格导致意外拆分。
除了用户自定义变量,Shell 还有位置变量和预定义变量:
#!/bin/bash echo "脚本名称: $0" echo "第一个参数: $1" echo "第二个参数: $2" echo "参数个数: $#" echo "所有参数: $@"执行:
bash test.sh hello world输出:
脚本名称: test.sh 第一个参数: hello 第二个参数: world 参数个数: 2 所有参数: hello world这类变量在写批量处理脚本时非常有用,可以根据用户传入的参数动态执行任务。
3.2 条件判断:让脚本拥有决策能力
Shell 脚本之所以能完成复杂任务,离不开判断语法。最常用的就是if和case。
if 语法示例:
#!/bin/bash score=85 if [ $score -ge 90 ]; then echo "优秀" elif [ $score -ge 60 ]; then echo "及格" else echo "不及格" fi这里需要特别注意:
[和]两侧必须留空格。- 条件表达式中的
-ge表示大于等于,常用比较符还有-eq(等于)、-ne(不等于)、-gt(大于)、-lt(小于)、-le(小于等于)。 if必须以fi结尾。
文件判断是运维场景中最高频的,示例:
#!/bin/bash if [ -f /etc/passwd ]; then echo "/etc/passwd 文件存在" else echo "文件不存在" fi常用文件判断参数:
| 参数 | 含义 |
|---|---|
| -f | 是否为普通文件 |
| -d | 是否为目录 |
| -e | 文件或目录是否存在 |
| -r | -w |
| -s | 文件是否存在且不为空 |
case 语法示例:
#!/bin/bash case $1 in start) echo "服务启动中..." ;; stop) echo "服务停止中..." ;; restart) echo "服务重启中..." ;; *) echo "用法: $0 {start|stop|restart}" ;; esac这种写法常用于服务管理脚本,比if的嵌套更清晰。
3.3 循环:重复任务的克星
运维中经常需要对一批文件、一批服务器、一批用户做相同操作,这时候循环就派上用场了。
for 循环示例:
#!/bin/bash # 遍历列表 for ip in 192.168.1.10 192.168.1.11 192.168.1.12; do echo "正在检查服务器: $ip" done#!/bin/bash # 遍历数字范围 for i in {1..5}; do echo "第 $i 次执行" done#!/bin/bash # C 语言风格的 for 循环 for ((i=1; i<=10; i++)); do echo "当前数值: $i" donewhile 循环示例:
#!/bin/bash count=1 while [ $count -le 5 ]; do echo "count = $count" count=$((count + 1)) done需要特别提醒的是,$((count + 1))是 Shell 算术运算语法,不能写成count = count + 1,否则会报错。
3.4 函数:复用代码块
一个稍大规模的脚本,如果全部指令堆在一起,后期维护会非常痛苦。把重复使用的逻辑封装成函数,是推荐的工程习惯。
#!/bin/bash # 定义函数 check_disk() { echo "===== 磁盘使用情况 =====" df -h } check_memory() { echo "===== 内存使用情况 =====" free -m } # 调用函数 check_disk check_memory函数也可以接收参数:
#!/bin/bash log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1" } log_info "系统巡检开始" log_info "磁盘检查完成"注意:Shell 函数的参数在函数体内部使用$1、$2引用,调用时直接把参数跟在函数名后面即可。
3.5 重定向与管道:数据处理的重要工具
重定向和管道不是语法,但它们是 Shell 的灵魂。
重定向常用符号:
>:将标准输出写入文件(覆盖)>>:将标准输出追加到文件2>:将错误输出写入文件&>:将标准输出和错误输出都写入文件<:从文件读取输入
# 标准输出写入文件 echo "hello shell" > output.txt # 标准错误输出单独保存 ls /nonexistent 2> error.log # 正确日志和错误日志分开保存 find /etc -name "*.conf" > conf_list.txt 2> find_error.log管道示例:
# 查看当前系统进程数 ps -ef | wc -l # 查看 Nginx 进程 ps -ef | grep nginx | grep -v grep # 查看磁盘使用率最高的分区 df -h | awk '{print $5, $6}' | sort -n | tail -3管道符|的作用是把左侧命令的输出作为右侧命令的输入,多个命令可以串联使用。这也是 Shell 最强大、最美妙的地方。
4. 完整实战案例:编写一个系统巡检脚本
前面讲了这么多基础语法,接下来我们把这些内容拼装成一个完整的实战案例。这个案例直接面向运维场景:一键收集 Linux 服务器关键运行状态并输出报告。
4.1 需求分析
我们希望实现以下功能:
- 查看系统主机名、内核版本、运行时间。
- 查看 CPU 负载情况。
- 查看内存使用情况。
- 查看磁盘使用情况。
- 检测指定服务(如 nginx)是否在运行。
- 将结果输出到文件,并附加时间戳。
4.2 创建项目结构
mkdir -p ~/shell_lab/system_check cd ~/shell_lab/system_check touch system_check.sh chmod +x system_check.shchmod +x的作用是给脚本添加可执行权限,这样可以直接./system_check.sh运行,否则只能使用bash system_check.sh。
4.3 编写完整脚本
文件路径:~/shell_lab/system_check/system_check.sh
#!/bin/bash # 描述: 一键系统巡检脚本 # 用法: ./system_check.sh [service_name] # 作者: csdn # 设置输出文件名,包含当前时间戳 REPORT_FILE="system_report_$(date '+%Y%m%d_%H%M%S').txt" # 函数: 输出分隔线 print_line() { echo "==========================================" >> "$REPORT_FILE" } # 函数: 写入信息头 write_header() { echo "" >> "$REPORT_FILE" print_line echo "$1" >> "$REPORT_FILE" print_line } # 开始生成报告 echo "系统巡检报告生成中..." # 写入报告标题 echo "Linux 系统巡检报告" > "$REPORT_FILE" echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')" >> "$REPORT_FILE" # 1. 系统基本信息 write_header "系统基本信息" echo "主机名: $(hostname)" >> "$REPORT_FILE" echo "内核版本: $(uname -r)" >> "$REPORT_FILE" echo "操作系统: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'=' -f2 | tr -d '\"')" >> "$REPORT_FILE" echo "运行时间: $(uptime -p)" >> "$REPORT_FILE" # 2. CPU 负载 write_header "CPU 负载情况" uptime >> "$REPORT_FILE" echo "" >> "$REPORT_FILE" echo "CPU 核心数: $(nproc)" >> "$REPORT_FILE" # 3. 内存使用情况 write_header "内存使用情况 (单位: MB)" free -m >> "$REPORT_FILE" # 4. 磁盘使用情况 write_header "磁盘使用情况" df -h >> "$REPORT_FILE" # 5. 指定服务状态检测 SERVICE_NAME="${1:-nginx}" write_header "服务状态检测: $SERVICE_NAME" if pgrep -x "$SERVICE_NAME" > /dev/null; then echo "服务 $SERVICE_NAME 正在运行中" >> "$REPORT_FILE" else echo "服务 $SERVICE_NAME 未运行" >> "$REPORT_FILE" fi # 6. 内存占用 Top 5 进程 write_header "内存占用 Top 5 进程" ps aux --sort=-%mem | head -6 >> "$REPORT_FILE" # 完成提示 echo "" echo "系统巡检完成,报告已保存到: $REPORT_FILE"4.4 运行与验证
./system_check.sh nginx如果脚本没有可执行权限,会报错Permission denied。此时可以:
chmod +x system_check.sh ./system_check.sh nginx或者直接用 bash 解释器运行:
bash system_check.sh nginx运行结束后,当前目录下会多出一个类似system_report_20250101_120000.txt的文件,使用cat查看内容:
cat system_report_20250101_120000.txt预期输出结构如下:
Linux 系统巡检报告 生成时间: 2025-01-01 12:00:00 ========================================== 系统基本信息 ========================================== 主机名: localhost 内核版本: 3.10.0-1160.el7.x86_64 操作系统: CentOS Linux 7 (Core) 运行时间: up 3 days, 2 hours, 15 minutes ========================================== CPU 负载情况 ========================================== 12:00:01 up 3 days, 2:15, 2 users, load average: 0.01, 0.05, 0.03 CPU 核心数: 2 ========================================== 内存使用情况 (单位: MB) ========================================== total used free shared buff/cache available Mem: 1837 617 446 8 773 1039 Swap: 0 0 0 ========================================== 磁盘使用情况 ========================================== Filesystem Size Used Avail Use% Mounted on /dev/vda1 40G 12G 27G 31% / ========================================== 服务状态检测: nginx ========================================== 服务 nginx 未运行 ========================================== 内存占用 Top 5 进程 ========================================== USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.2 42988 3612 ? Ss Jan01 0:07 /usr/lib/systemd/systemd ...4.5 结合 crontab 实现定时巡检
上面的脚本已经可以手动运行。实际生产环境中,我们往往需要每天自动执行,然后把报告保存下来,方便日后查询。用 crontab 实现定时执行:
crontab -e在打开的编辑器中添加下面一行(每天凌晨 2 点执行巡检):
0 2 * * * /root/shell_lab/system_check/system_check.sh nginx >> /root/shell_lab/system_check/cron.log 2>&1保存后,使用crontab -l可以确认定时任务已生效:
crontab -l补充说明:2>&1的作用是把标准错误输出也重定向到日志文件中,避免脚本报错时没有任何记录。
5. 常见问题与排查思路
Shell 脚本报错不直观,对新手不太友好。下面列几个出现频率极高的问题,以及对应的排查思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
./test.sh: Permission denied | 脚本文件没有执行权限 | chmod +x test.sh或改用bash test.sh |
bad interpreter: /bin/bash^M: no such file or directory | 脚本是在 Windows 上编辑的,文件包含 CRLF 换行符 | 使用sed -i 's/\r$//' test.sh转换格式 |
[ : command not found | [与]两侧缺少空格,或 if 语句中少了空格 | 检查if [ $var -eq 1 ]; then的写法 |
syntax error near unexpected token 'fi' | if 语句没有正确闭合,或者关键字写错 | 检查是否缺少fi、done等结束关键字 |
| 变量值一直为空 | 变量名拼写不一致,或等号两侧有空格 | Echo 输出变量检查,确认变量名与定义完全一致 |
5.1 脚本文件编码问题
如果你在 Windows 上编辑脚本,然后上传到 Linux 执行,经常遇到换行符不兼容的问题。可以在 Linux 上使用file命令检查:
file test.sh如果输出中有CRLF,说明需要转换:
sed -i 's/\r$//' test.sh转换后再次执行,问题通常就会消失。
5.2 排查脚本执行过程
如果脚本执行结果不符合预期,推荐使用bash -x来调试。它可以逐行显示命令执行过程,帮助定位出错位置:
bash -x system_check.sh输出中带有+前缀的每一行就是脚本实际执行的命令。通过观察变量当时的值,基本能定位到问题所在。
5.3 变量名注意大小写
Linux 环境变量区分大小写,$Name、$NAME、$name是三个完全不同的变量。排查问题时先确认拼写。
6. 最佳实践与工程建议
学完语法、写完脚本,下一步要考虑的就是怎么写得更规范、更可靠。以下是 Shell 脚本工程化的一些建议,也是面试和实际工作中经常考察的点。
6.1 脚本头部声明
每个脚本第一行都应该指定解释器:
#!/bin/bash虽然有时候不写也能运行,但加上它是一个好习惯。系统会明确使用 bash 解释执行,避免因默认 Shell 不同导致兼容问题。
6.2 变量引用加花括号
统一使用${变量名}而不是$变量名,例如:
echo "报告存放于 ${REPORT_DIR}/backup_${DATE}.tar.gz"如果不加花括号,碰上$REPORT_DIR_2025这样的写法,Shell 会尝试寻找名为REPORT_DIR_2025的变量,导致取值为空。
6.3 字符串变量加双引号
引用变量时建议加上双引号,尤其是文件路径和参数中可能包含空格或特殊字符时:
# 推荐 echo "欢迎, ${username}" # 不推荐 echo 欢迎, $username反例在变量内容为空时可能会看到奇怪的报错,加上双引号能减少这类问题。
6.4 脚本开头添加检查和退出机制
比较健壮的脚本会在关键命令执行失败后主动退出,避免后续命令带着错误状态继续执行:
#!/bin/bash set -e echo "步骤1: 备份数据库" mysqldump -u root test_db > /backup/test_db.sql echo "步骤2: 删除过期备份" find /backup -name "*.sql" -mtime +7 -deleteset -e的含义是:只要脚本中任何一条命令返回非零状态码,立即退出脚本。注意不要在不允许退出的临时场景使用,否则可能跳过预期逻辑。
6.5 日志记录
生产环境中的脚本一定要有日志。日志至少应该包含时间、等级、操作内容:
#!/bin/bash LOG_FILE="/var/log/my_script.log" log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" } log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" }出问题时,运维人员第一时间翻日志,而不是重新跑一遍脚本看现象。
6.6 敏感信息不要硬编码
脚本中涉及数据库密码、API Token 等敏感信息时,不要直接写成明文。建议使用环境变量:
#!/bin/bash DB_PASSWORD="${DB_PASSWORD:-default_password}" mysql -u root -p"${DB_PASSWORD}" -e "SHOW DATABASES;"或者将敏感信息放入单独的配置文件中,并设置严格的文件权限。不要把生产密码提交到代码仓库,这是运维事故的高发源头。
6.7 静态检查工具 ShellCheck
写了几百行脚本之后,肉眼检查很容易遗漏问题。推荐使用 ShellCheck 工具做静态检查。
安装:
# CentOS yum install -y shellcheck # Ubuntu apt install -y shellcheck使用:
shellcheck system_check.shShellCheck 会输出代码中潜在的语法问题、变量引用问题,以及明显不安全的写法,非常适合排查隐藏 bug。
6.8 注意脚本兼容性
如果你的脚本可能在多个 Linux 发行版上运行,尽量避免使用某个发行版专属的命令选项。比如:
- CentOS 上使用
yum,Ubuntu 上使用apt。 - GNU 版本
grep、sed与 BSD 版本存在细微差异。
可以在脚本中先检测系统类型,再执行对应命令:
#!/bin/bash if [ -f /etc/redhat-release ]; then echo "CentOS / RHEL 系统" yum install -y vim elif [ -f /etc/lsb-release ]; then echo "Ubuntu / Debian 系统" apt install -y vim else echo "未知系统,请手动安装" fi7. 总结:怎么练才能真正掌握 Shell 脚本
Shell 脚本这门技能,最大的特点是上手快,进阶慢。语法本身不复杂,变量、循环、判断、函数,一天就可以学会。但真正拉开差距的,是遇到实际问题时能否快速用脚本解决,以及写出来的脚本是否健壮、可维护、可复用。
如果你现在刚入门,建议按下面的路径练习:
- 先把 Linux 最常用的命令练熟,比如
ls、cd、grep、awk、sed、find、df、free、ps、netstat。 - 手动完成 3 到 5 个简单脚本,包括变量处理、条件判断、循环输出。
- 把日常工作里重复的操作抽出来,写成脚本,哪怕最开始脚本只有 10 行。
- 学会使用
bash -x调试脚本,遇到报错不要慌,逐步定位。 - 阅读系统自带脚本,比如
/etc/init.d/下的服务管理脚本,学习别人的写法。
Shell 脚本真正的价值不是炫技,而是把运维工作中重复、琐碎、易出错的操作,变成稳定可靠的一键执行。坚持写一段时间,你会发现自己的效率提升非常明显。
如果这篇文章对你有帮助,可以收藏备用。下一步你可以继续学习:
- 定时任务 crontab 的进阶用法。
- awk、sed、grep 三剑客的深入实战。
- Shell 脚本调用云厂商 API 实现自动化。
- Ansible、Python 脚本与 Shell 脚本的分工与配合。
动手写今天第一个脚本吧,十分钟之后,你就已经是写过 Shell 脚本的人了。