1. 让Linux替你干活,从第一个Shell脚本开始
很多人一听到“Shell编程”第一反应是:这是不是要写一堆看不懂的符号?是不是得背很多命令?实际接触过之后你会发现,Shell就是你在Linux终端里敲的那些命令的集合,把它们按顺序写进一个文件,加上一点判断和循环,Linux就能自动完成重复性工作。想清楚这件事,10分钟足够你上手Shell编程的核心用法。
我最早接触到Shell自动化,是被一堆重复操作逼的。每次上线新环境都要手动执行几十条命令:检查端口、备份配置文件、重启服务、看日志。事情本身不难,但重复三遍以后就开始烦躁,重复十遍以后一定会出错——漏一条命令、写错一个路径,排查半天还未必能发现。后来我把这一套操作写成了脚本,之后就真的再也没手动敲过那一长串命令。
这篇文章的内容,就是一条完整的入门路径:
- Shell脚本文件长什么样、怎么创建和执行
- 变量、条件判断、循环这些核心语法怎么用
- 以“服务器体检”为例,写一个能直接跑起来的自动化脚本
- 怎么用crontab让脚本定时自动执行
- 脚本报错排查的常见套路和避坑经验
不管你是在学Linux的学生、刚转行做运维的新人,还是整天跟服务器打交道的开发者,这套东西学完就能用。不需要任何编程基础,你只需要能在终端里敲命令就够了。
2. 核心语法速览——10分钟必须拿下的知识点
10分钟时间很紧张,所以我只挑写自动化脚本最常用、最避不开的语法来讲。那些冷门复杂的写法,等你实际用到的时候再翻手册也不迟。
2.1 变量与参数传递:让脚本跟人对话
变量是Shell脚本的基础。Shell里给变量赋值要注意一个非常容易踩的坑:等号两边不能有空格。
# 正确写法 name="nginx" version=1.20 # 错误写法(会被当作命令执行) name = "nginx"用变量的时候,前面要加美元符号,建议用花括号包起来:${name}。非要问为什么的话,花括号是为了明确变量的边界,比如你想在变量后面拼接字符串:
name="nginx" echo "${name}_latest" # 输出:nginx_latest如果写成$name_latest,Shell会去找一个叫name_latest的变量,结果就是空的。
脚本想接收外部传入的参数,用位置变量就行:$1代表第一个参数,$2代表第二个,以此类推。$#代表参数个数,$@代表所有参数。这在实际使用中非常关键,因为你的脚本不应该写死,应该能被复用。
#!/bin/bash # 用法:./deploy.sh web_server 8080 app_name="$1" app_port="$2" echo "正在部署 ${app_name},端口 ${app_port}"注意:脚本里用到双引号的地方,我就一直用双引号把变量括起来。这能避免变量值里带空格时被拆成多个单词。这是个好习惯,写的时候可能觉得无所谓,等你处理带空格的路径时就明白它的价值了。
2.2 条件判断:让脚本学会做决定
自动化脚本不可能永远只按顺序执行,总要“看情况办”。if语句就是干这个的。
if [ -f /etc/nginx/nginx.conf ]; then echo "配置文件存在" else echo "配置文件不存在,需要初始化" fi这里的[ -f 路径 ]就是“判断这个路径是不是一个普通文件”。类似的常用判断还有:
| 判断写法 | 含义 |
|---|---|
[ -d 路径 ] | 路径是否为一个目录 |
[ -e 路径 ] | 路径是否存在 |
[ -z 字符串 ] | 字符串是否为空 |
[ "$a" = "$b" ] | 两个字符串是否相等 |
[ "$a" -gt 100 ] | 数字a是否大于100 |
我刚开始写的时候,经常搞混字符串比较和数字比较。字符串比较用=,数字比较用-eq、-ne、-gt、-lt。记混了不会报错,但结果就是不对。比如[ 10 -gt 9 ]在数字比较下成立,但[ 10 > 9 ]这种写法在Shell里不是数字大小比较,有可能得出错误结论。
另外还有一个经常跟[]混用的[[]],它是增强版测试,支持&&、||这种逻辑运算,写起来更直观:
if [[ -d /var/log && -w /var/log ]]; then echo "目录存在且有写权限" fi能用[[]]的场景我建议优先用它,代码可读性好很多。但注意[[]]是bash扩展语法,如果你用的是纯粹的sh环境,可能不支持。
2.3 循环:批量处理重复劳动
循环是自动化脚本的灵魂。你写5行命令处理一个文件,有了循环,你写5行就能处理500个文件。
for循环最常用:
for ip in 192.168.1.10 192.168.1.11 192.168.1.12; do ping -c 1 "$ip" > /dev/null 2>&1 && echo "$ip 通了" || echo "$ip 不通" done这段命令会依次ping三个地址。&&和||分别是“前一个命令成功了才执行后面”和“前一个命令失败了才执行后面”,是Shell里很精髓的写法,相当于把if压缩成一行。
处理文件列表时,通配符会帮你自动扩展:
for file in /var/log/*.log; do echo "日志文件:${file}" donewhile循环也有用武之地,尤其是读取文件内容的时候:
while read line; do echo "读取到:$line" done < /etc/hosts这行语法看着有点绕,但原理不复杂:<把文件内容喂给while,while每次读一行,执行完循环体再读下一行。我用这个方法处理过几千行的用户列表,效率很高。
2.4 函数与文本处理三件套:模块化你的脚本
脚本变长以后,把所有代码摊在一起会很难维护。函数可以把一块逻辑封装起来,需要一个功能就调用一次。
check_port() { local port="$1" if ss -tlnp 2>/dev/null | grep -q ":$port"; then echo "端口 ${port} 已被占用" return 1 else echo "端口 ${port} 空闲" return 0 fi } check_port 8080 check_port 3306函数里用local声明局部变量,避免和外部变量互相污染。return用来返回状态码,0表示成功,非0表示失败。
写自动化脚本离不开三个文本处理工具:grep、awk、sed。它们被合称为Linux文本处理三件套,也是在脚本里解析命令输出、提取信息、替换内容的核心工具。
# 提取当前CPU使用率 cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}') echo "CPU使用率:${cpu_usage}%" # 把配置文件里的端口从8080改成9090 sed -i 's/8080/9090/g' /etc/app/config.iniawk的默认动作是按空格把一行切成多列,$1就是第一列,$2是第二列。sed的s/旧值/新值/g表示全局替换。这三个工具配合起来,基本可以处理90%的文本处理需求。现在先混个脸熟,真正要用的时候再对照具体场景查细节,反而记得更牢。
3. 写一个能直接用的自动化脚本——服务器体检案例
前面的知识点就像是零件,现在把它们组装起来。这一节的目标很明确:写一个服务器体检脚本,一键采集CPU、内存、磁盘使用率和关键服务状态。这个脚本可以手工执行,也可以挂到定时任务里定期跑。
3.1 先理清思路:脚本要解决什么问题
写自动化脚本的一个原则是:先想清楚脚本的输出,再开始写代码。我见过的很多烂脚本,问题不在语法,而是根本不知道自己要输出什么、给谁看。
这个体检脚本的核心需求有三个:
- 采集系统核心指标:CPU使用率、内存使用率、磁盘使用率
- 检查关键服务状态:比如nginx、mysql、redis这些服务是否在运行
- 输出一份简洁清晰的报告,让人一眼看到有没有异常
确定了这三点,脚本的结构就出来了。整体分成三步:第一步定义检测函数,第二步执行检测,第三步汇总输出。检测逻辑抽成函数还有一个好处——你想检查一个新指标的时候,只需要加一个函数,不需要动主流程。
3.2 分步实现:从指标采集到状态检查
CPU使用率的获取方式不是唯一的。我用的是top命令在批处理模式下的输出,避开交互式界面:
cpu_late=$(top -bn1 2>/dev/null | grep "Cpu(s)" | awk -F' ' '{print $8}') cpu_usage=$(echo "100 - ${cpu_late}" | bc)这里有个关键点:top输出里的“id”列是空闲CPU百分比,100减去空闲值才是使用率。有些文章直接教人取第2列,实际不同Linux发行版输出格式有细微差别,直接抄很容易出错。所以我在提取的时候先打印出整行内容确认列位置,再去取数,花一分钟确认格式,能省好几小时排查时间。
内存使用率相对简单,直接读/proc/meminfo或者用free命令:
mem_total=$(free -m | awk '/^Mem:/ {print $2}') mem_used=$(free -m | awk '/^Mem:/ {print $3}') mem_percent=$((mem_used * 100 / mem_total)) echo "内存使用率:${mem_percent}%"注意这里用的是bash的算术展开$(( )),它只支持整数运算。所以先取兆字节为单位的总量和已用量,再乘100做整数除法,得到的就是整数百分比。如果要更精确的小数,就得借助bc计算器或者直接用awk来做浮点运算。
磁盘使用率的采集也常用df命令,然后对每个挂载分区逐行处理:
df -h | awk 'NR>1 {print $5, $6}' | while read use_percent mount_point; do echo "分区 ${mount_point} 使用率:${use_percent}" done这里的NR>1是 awk 内置变量,表示跳过第一行表头。按行读取,把第5列(使用率)和第6列(挂载点)取出来。
服务状态检查,我用的是systemctl is-active这个命令,它直接输出 active 或 inactive,非常干净:
check_service() { local service_name="$1" if systemctl is-active --quiet "$service_name"; then echo "${service_name}:运行中" return 0 else echo "${service_name}:未运行" return 1 fi }--quiet参数让命令不输出内容,只通过退出码传递状态,配合if判断最简洁。
3.3 输出格式:让结果一眼就看懂
人眼扫过一堆数字的时候,有对比才有重点。我的做法很简单:用颜色和提示语标记异常状态。终端色彩用ANSI转义序列控制,不难,颜色代码是这样的:
red="\\033[31m" green="\\033[32m" reset="\\033[0m" if [ "$disk_percent" -gt 85 ]; then echo -e "${red}磁盘告警:使用率 ${disk_percent}%${reset}" else echo -e "${green}磁盘良好:使用率 ${disk_percent}%${reset}" fi\\033[31m把后面输出的文字变成红色,\\033[0m恢复默认颜色。echo -e用于解析转义字符。
这里有一个细节值得说:告警阈值。磁盘使用率我默认设85%作为告警线,这不是拍脑袋定的,是参考了业界常用的容量预留策略——磁盘使用超过85%以后,系统性能会明显下降,也需要留足临时文件和日志写入的空间。千万不要把阈值设到100%再告警,那时候往往已经来不及处理了。内存的告警线建议设在90%,因为Linux会利用空闲内存做缓存,这个数值天然偏高,90%才算真正紧张。
3.4 完整脚本与逐段解读
把上面这些拼起来,一个可以直接复制运行的工具脚本就有了:
#!/bin/bash # 服务器体检脚本:采集CPU、内存、磁盘、服务状态 # 用法:./health_check.sh # ---------- 基础信息 ---------- host_name=$(hostname) cur_time=$(date "+%Y-%m-%d %H:%M:%S") # ---------- 颜色定义 ---------- red="\\033[31m" green="\\033[32m" yellow="\\033[33m" reset="\\033[0m" # ---------- 采集函数 ---------- get_cpu_usage() { # 提取空闲列,计算使用率 cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{printf "%s", $8}') cpu_use=$(echo "100 - ${cpu_idle}" | bc) echo "$cpu_use" } get_mem_percent() { mem_total=$(free -m | awk '/^Mem:/ {print $2}') mem_used=$(free -m | awk '/^Mem:/ {print $3}') echo $((mem_used * 100 / mem_total)) } check_service() { local svc="$1" if systemctl is-active --quiet "$svc" 2>/dev/null; then echo "运行中" else echo "未运行" fi } # ---------- 执行采集 ---------- cpu=$(get_cpu_usage) mem=$(get_mem_percent) nginx_status=$(check_service nginx) mysql_status=$(check_service mysql) # ---------- 汇总输出 ---------- echo "=================================" echo " 服务器体检报告:${host_name}" echo " 检查时间:${cur_time}" echo "=================================" echo "CPU使用率:${cpu}%" echo "内存使用率:${mem}%" disk_warning="" df -h | awk 'NR>1 {print $5, $6}' | while read use_percent mount_point; do echo "磁盘 ${mount_point}:${use_percent}" done echo "服务状态:" echo "nginx:${nginx_status}" echo "mysql:${mysql_status}"这个脚本没有处理所有边界情况,比如磁盘那一块的分区名可能带空格导致列错位,但在多数场景下是能跑的。先把能跑的脚本拿到手,再逐步优化,这个顺序比一开始就想写出完美脚本更加实际。跑起来以后,再考虑加微信/邮件告警、输出到日志文件、接入监控平台这些扩展功能。
注意:拿到脚本以后,先执行
chmod +x ./health_check.sh给它加执行权限,然后./health_check.sh运行。如果你看到 “Permission denied” 报错,几乎都是忘了这一步。
4. 让脚本自动跑起来——定时任务与执行细节
脚本写出来,人工执行只是第一步。自动化的关键在于:让它在你睡觉的时候自己跑。
4.1 crontab定时任务:五分钟搞定周期执行
Linux的定时任务标准工具是crontab。执行crontab -e打开当前用户的定时任务列表,按i进入编辑模式,写一行配置,按Esc再输入:wq保存退出。
crontab里每一行有五个时间字段,分别表示分钟、小时、日期、月份、星期,后面跟要执行的命令:
# 每天凌晨2点执行体检脚本 0 2 * * * /opt/scripts/health_check.sh >> /var/log/health_check.log 2>&1这行配置中,0 2 * * *的*表示“任意值”。组合一下就能实现各种频率:
| 时间配置 | 含义 |
|---|---|
*/10 * * * * | 每隔10分钟执行一次 |
0 * * * * | 每小时整点执行一次 |
30 8 * * 1-5 | 工作日上午8点30分执行 |
0 0 1,15 * * | 每月1号和15号零点执行 |
这里必须强调一个新手最容易踩的坑:crontab里的命令最好写绝对路径,脚本里的命令也一样。定时任务的环境跟你手动登录终端是不一样的,它不会加载你的登录Shell配置,PATH环境变量非常精简。我在crontab里写过top命令,手动执行没问题,定时任务就是不执行,查了半天时间成本极高,最后发现是/bin目录不在crontab的PATH里。解决方法就是把命令写成绝对路径,比如/usr/bin/top。
判断一个命令在哪,执行which top就能看到完整路径。如果你不确定某个命令的路径,就先在终端敲一下which 命令名,再把它写进脚本和crontab。
4.2 日志输出:脚本不能白跑
定时任务跑完以后,结果得有地方记录。刚才那行配置里的>> /var/log/health_check.log 2>&1就是干这个的。
>>:把stdout追加写入文件,而不是覆盖2>&1:把stderr(编号2)重定向到stdout(编号1)同一个位置
日志里既能看到正常输出,也能看到报错信息。这样设计的价值在于:脚本在凌晨2点跑挂了,第二天早上看一眼日志就能定位问题,不需要去猜。
注意:日志文件所在目录必须存在且有写权限,否则脚本的输出全部丢失,你连错误消息都看不到。先手动执行一遍脚本,确认输出路径没问题,再放crontab自动执行。
4.3 调试技巧:bash -x和set命令
脚本出问题,最笨也最有效的排查方法是逐行跟踪。
bash -x ./health_check.sh可以让脚本把执行过的每条命令和当前变量值打印出来。参数-x表示xtrace,打开执行跟踪。格式上每一行前面会加+号,看得非常清楚:
+ get_cpu_usage + cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{printf "%s", $8}') + cpu_use=$(echo "100 - 12.5" | bc) + echo 87.5这一下子就能看出来:变量有没有取到值、命令拼接是否正确、数值计算是否符合预期。
还有一种更细的调试方法,在脚本开头加上:
set -euo pipefailset -e遇到任何命令返回非零状态就立刻退出,set -u禁止使用未定义变量,set -o pipefail让管道命令组中的错误也能被捕捉。三行代码能让脚本的容错性提升一个级别。为什么重要?因为默认情况下Shell脚本即使某条命令失败了,后续命令还会继续执行。这在自动化场景里非常危险——一个导入失败可能导致后面所有操作都基于错误数据继续跑。
这里需要注意set -e有个副作用:当你手动写command || true这类结构时,true命令会兜底让退出码变成0,避免脚本被提前终止。如果你用了set -e而脚本里有很多“允许失败”的命令,记得给它们加|| true。
4.4 执行权限与相对路径的兼容问题
脚本运行时,请注意“你在哪里执行脚本”和“脚本在哪里被定义”是两个概念。我见过太多人写脚本这样写:
# 错误示例:依赖当前目录 script_dir="$(pwd)"如果你在/tmp目录执行/opt/scripts/backup.sh,脚本里的$(pwd)返回的是/tmp,不是脚本所在的/opt/scripts。正确做法是用脚本自己所在的目录:
script_dir="$(cd "$(dirname "$0")" && pwd)"这里的$0是脚本本身路径,dirname取出所在目录,cd进去再pwd就得到了绝对路径。之后脚本里的配置文件路径、日志路径都基于$script_dir来拼接,无论从哪里调用都一样可靠。这个写法是运维实战里积累出来的,一行代码解决一大类问题。
还有脚本第一行的奇特性。#!/bin/bash开头,它指定了执行这个脚本所使用的解释器。执行脚本时,系统会读取这个shebang行,然后调用对应的解释器来解释执行脚本内容。如果第一行写错成空格或放错位置,脚本要么无法执行,要么行为异常——不同Shell的语法差异足以让同样的脚本跑出不同结果。
5. 常见问题与排查技巧实录
写脚本多了以后,我总结出几个出现频率非常高的报错。遇到下面这些情况,大概率能在几分钟内解决,也省得你到处搜索问别人。
5.1 报错“Permission denied”
这个最典型,原因就是你没给脚本加执行权限。解决:
chmod +x ./脚本.sh另一种执行方式是bash ./脚本.sh,直接把脚本作为bash的参数运行,这样就不需要执行权限,只用读权限就行。两种方式实际效果几乎一样,唯一的区别是脚本是否有shebang行、是否依赖其他解释器。用bash执行时,第一行shebang会被当作注释忽略。
5.2 变量赋值报错“command not found”
如果你看到类似name: command not found的报错,十有八九是等号两边留了空格。
# 错误 name = "nginx" # 正确 name="nginx"这并不是刻意的设计,Shell语法里赋值和命令执行共用同一套解析逻辑。等号被空格拆开后,Shell认为name是一个要执行的命令,然后尝试在PATH里找它,找不到就报command not found。这种报错会让新手很迷茫,但记住“等号两边不能有空格”就能避开。
5.3 crontab任务不执行
定时任务不执行,原因集中在下面几个:
- 命令路径不完整:crontab环境里PATH是一组精简的目录,你手动命令能跑不代表定时任务能跑。排查方法是在脚本开头显式声明PATH,或在crontab里配置执行脚本的完整绝对路径。
- 脚本权限不足:执行位的权限缺失。给脚本chmod加权限。
- 时间格式错误:比如把
0 2写成2 0。系统不会报错,但执行时间就完全不是你预期的时间了。 - cron服务没有运行:很多精简系统默认没启动crond服务,先检查服务状态再排查其他原因。
排查方法:先crontab -l确认配置存在,再cat /var/log/cron或journalctl -u crond查看执行记录,把执行时间对准,往往很快就能发现问题。
5.4 脚本里引号不匹配导致行为异常
Shell里的引号有严格配对规则。单引号和双引号的区别是:单引号里的内容全部原样输出,$符号不会展开;双引号里的内容会做变量替换和命令替换。在拼接含引号的命令时,少写一个引号不仅会报语法错误,还有可能造成命令被意外执行。
一个常见的误用是嵌套引号:
# 想输出变量值,用了正确写法 echo "当前用户:${USER}" # 里面又想用单引号包一层,结果会出问题 echo '路径是:${pwd}'单引号里${pwd}不会被替换,而是原样输出。如果确认要展开变量再加引号,就统一用双引号。
5.5 排查万能组合命令
这里分享一个组合命令,脚本行为异常时能快速定位:
bash -x ./脚本.sh >> trace.log 2>&1把脚本每步执行的命令和变量值写到日志文件里,然后打开日志文件查看输出,定位在哪一步开始偏离预期。配合set -u的报错信息,几乎能解决所有脚本逻辑问题。
结合多年经验,脚本出问题的概率并不是语法本身,而是环境差异——PATH不全、目录不存在、文件格式是DOS换行符、编码不一样。深呼吸,继续查,总会找到的。
6. 实际使用中的一些体会
以上内容已经把Shell编程的核心语法、自动化方案和排查方法都过了一遍。最后聊聊我实际使用过程中的体会。
第一,Shell脚本要解决的是“频繁重复的操作”,不是“复杂的业务逻辑”。判断一个任务适不适合用Shell脚本,标准很简单:这个操作是不是每次都要敲同样几条命令?如果是,那就值得写成脚本。如果逻辑特别复杂,涉及大量数据处理、算法、数据结构,那还是Python更合适。Shell的优势在于它与Linux系统天然的亲密度——文件、进程、网络、服务,一条命令直接操作,不需要引入任何依赖。
第二,脚本的可读性比技巧炫技更重要。我看到不少人在脚本里用各种花哨的写法,一行命令嵌套七八层管道,自己写完第二天都看不懂。实用的原则是:宁可多写几行,也不能让人读不懂。变量名要表达意图,函数要短小,每个函数的职责要单一。维护半年后再来看,你会感谢那个写注释的自己。
第三,自动化脚本不是写完就结束。定期检查日志、关注脚本是否还在正确执行、机器环境变化后及时调整脚本——这些事跟写脚本一样重要。脚本挂掉有时候不是脚本自身的问题,而是依赖的服务路径变了、新系统默认目录改了、权限策略调整了。有次公司升级系统,把某个服务的二进制路径从/usr/bin挪到了/usr/local/bin,我的巡检脚本照样跑,但“检查服务状态”的功能就永远在误报。不是脚本有bug,而是环境变了。
最后一个小技巧:新人练手,可以从“把自己的日常操作写成脚本”开始。比如你每次都要去查日志目录会不会太大,就写一个日志清理脚本;每次都要输入一行复杂命令备份数据库,就封装成一个脚本。用起来顺手了,语法熟练程度也上来了,再去看高级的进阶内容,会发现一切水到渠成。Shell编程就是这样,不玄乎,就是让你在Linux世界里少做重复劳动,把省下来的时间用在学习新东西上。