导读
如果你用过 Linux 或 Mac 的终端,一定接触过命令行的交互方式——你输一条命令,系统回一个结果。但如果要把几十条命令组合起来,按条件自动执行、循环遍历文件、甚至每天定时跑一次呢?这时候就需要 Shell 脚本。
本文从零开始,带你掌握 Shell 脚本最核心的六大知识板块:变量与引号、条件判断(if/case)、循环(for/while)、函数封装、参数传递、错误处理,最后用 crontab 定时任务把这些能力串联成一个完整的系统健康检查脚本。无论你是刚接触 Linux 的小白,还是想系统化补上 Shell 功底的开发者,这篇都能覆盖。
一、你的第一个 Shell 脚本
Shell 脚本本质上就是一个包含多条命令的纯文本文件。系统在读取第一行时,会判断用哪个解释器来执行它。
1.1 Hello World
新建 hello.sh,写入以下内容:
#!/bin/bash # 告诉系统用 bash 解释器执行(这叫 shebang)
echo "Hello, CleanResolver!" # 输出文字到终端
然后给它可执行权限并运行:
chmod +x hello.sh # 让文件变成"可执行程序"的样子
./hello.sh # 在当前目录下运行它
# 输出:Hello, CleanResolver!
白话解释:
chmod +x相当于给文件贴一张"可以执行"的标签;./表示"当前目录下的这个文件"。如果不加./,系统会在环境变量$PATH指定的系统目录里去找这个名字。
国内镜像提示:如果你在中国大陆下载 bash 相关教程,推荐 Runoob Shell 教程(直连)。国内打不开时用代理:ghfast.top 代理。
二、变量与引号
2.1 定义变量
Shell 中的变量就像快递柜的储物格编号——你把数据"存"进去,需要时再"取"出来。赋值时不加 $,取值时加 $。
name="CleanResolver" # 定义变量 name
version=1.0 # 数字也可以直接赋值(不用引号)
echo $name # 取值:CleanResolver
echo "${name}_v${version}" # 用大括号区分边界:CleanResolver_v1.0
重要规则:
- 等号
=两边不能有空格!name = "hello"是错误的写法- 变量名只能包含字母、数字和下划线,且不能以数字开头
2.2 单引号 vs 双引号(考试必考)
这是初学者最容易踩的坑。看下面两段代码的区别:
name="World"
echo 'Hello, $name!' # 单引号:原样输出 → 结果是 Hello, $name!
echo "Hello, $name!" # 双引号:展开变量 → 结果是 Hello, World!
| 对比项 | 单引号 '...' | 双引号 "..." |
|---|---|---|
| 变量展开 | 不展开($name 保持原样) | 展开($name 变成实际值) |
| 转义字符 | 不支持(\n 原样输出) | 支持(\n 变成换行) |
| 适用场景 | 要显示的字符串本身就含 $ 符号 | 需要嵌入变量值时 |
记忆口诀:单引号最严格(什么都不管),双引号通情达理(能认变量和转义)。
2.3 只读变量与删除变量
website="cleanresolver.com"
readonly website # 设为只读,之后不能再改
# website="new.com" # 取消注释试试:会报 "readonly variable" 错误
unset website # 删除变量(只读变量不能 unset)
echo "$website" # 输出空(已被删除)
2.4 预定义的特殊变量
| 变量 | 含义 | 示例 |
|---|---|---|
$0 | 脚本本身的名字 | ./hello.sh |
$1, $2 … | 第 1、第 2 个参数 | ./script.sh foo bar → $1=foo, $2=bar |
$@ | 所有参数(保留每个参数的独立性) | "foo" "bar"(两个独立参数) |
$# | 参数的个数 | 传了 3 个参数,$# = 3 |
$? | 上一个命令的退出码(0=成功,非 0=失败) | ls /nonexist ; echo $? → 2 |
$$ | 当前进程的 PID(进程 ID) | 12345 |
三、条件判断
3.1 if / elif / else / fi
最基础的分支结构:
age=18
if [ $age -lt 18 ]; then # -lt 表示 less than(小于)
echo "未成年"
elif [ $age -eq 18 ]; then # -eq 表示 equal(等于)
echo "刚好成年" # elif = else if
else # -gt 表示 greater than(大于)
echo "成年人"
fi # if 必须用 fi 结尾(反过来写,好记吧?)
常用测试运算符:
| 运算符 | 含义 | 示例 |
|---|---|---|
-eq | 等于 | [ $a -eq $b ] |
-ne | 不等于 | [ $a -ne $b ] |
-gt | 大于 | [ $a -gt $b ] |
-ge | 大于等于 | [ $a -ge $b ] |
-lt | 小于 | [ $a -lt $b ] |
-le | 小于等于 | [ $a -le $b ] |
文件/目录测试(运维中极常用):
file="/etc/passwd"
if [ -f "$file" ]; then # -f 判断是否是普通文件
echo "$file 是一个文件"
fi
if [ -d "/tmp" ]; then # -d 判断是否是目录
echo "/tmp 是一个目录"
fi
if [ -r "$file" ]; then # -r 判断是否可读
echo "$file 可读"
fi
3.2 case 语句(多路分支)
当分支超过 3 个时,if-elif-else 就不如 case 简洁了:
read -p "输入服务名称 [nginx/apache/mysql]: " service
case "$service" in
nginx) # 匹配 nginx
echo "Nginx 正在运行"
;; # 结束匹配项
apache) # 匹配 apache
echo "Apache 正在运行"
;;
mysql) # 匹配 mysql
echo "MySQL 正在运行"
;;
*) # * = 默认(其他所有情况)
echo "未知服务"
;;
esac # case 用 esac 结尾(case 倒过来写)
白话解释:
case就像餐厅菜单——你说出一个菜名("$service"),厨师根据食材种类(nginx、apache、mysql)做出不同的菜(执行对应的代码块),最后用;;标记这道菜做完。没在菜单上的就归入*默认项。
四、循环
4.1 for 循环
有两种写法:遍历列表和 C 语言风格。
# 写法 1:遍历列表(最常用)
fruits="apple banana cherry"
for fruit in $fruits; do
echo "我喜欢吃 $fruit"
done
# 输出:
# 我喜欢吃 apple
# 我喜欢吃 banana
# 我喜欢吃 cherry
# 写法 2:遍历命令输出
for logfile in $(ls /var/log/*.log 2>/dev/null); do
echo "日志文件:$logfile"
done
# 写法 3:C 语言风格(计数器)
for (( i=1; i<=5; i++ )); do
echo "计数:$i"
done
4.2 while 循环
适合"直到某个条件不再满足为止"的场景:
count=1
while [ $count -le 5 ]; do # count <= 5 时继续
echo "循环次数:$count"
(( count++ )) # 每次循环 count+1((( )) 算术运算)
done
# 经典实用场景:等待某个服务启动
# max_wait=30 # 最多等 30 秒
# elapsed=0
# while ! curl -fs http://localhost:8080/health >/dev/null 2>&1; do
# if [ $elapsed -ge $max_wait ]; then
# echo "超时!服务未响应"
# exit 1
# fi
# echo "等待中... 已等 ${elapsed}s"
# sleep 2
# (( elapsed += 2 ))
# done
# echo "服务已就绪!"
白话解释:
while循环就像一个闹钟——只要时间没到(条件为真),就不断重复干活;一旦闹钟响了(条件为假),循环就结束。注意要在循环体内修改条件变量,否则会变成死循环。
五、函数
函数就是把一段经常用到的代码封装成一个"名字",后面随时调用。
5.1 定义和调用
greet() { # 定义函数(不需要 return type)
local name="$1" # local 限定变量只在函数内有效
echo "你好,$name!欢迎来到干净解码。"
}
greet "小明" # 调用函数,传参数
# 输出:你好,小明!欢迎来到干净解码。
注意:调用函数时不需要加括号——
greet "小明"而不是greet("小明")。
5.2 参数传递
函数通过位置参数接收值:
add_two_numbers() {
local a=$1 # 第一个参数
local b=$2 # 第二个参数
echo "$a + $b = $(( a + b ))" # (( )) 是算术运算
}
add_two_numbers 3 5 # 传入 3 和 5
# 输出:3 + 5 = 8
# 用 $@ 处理"不确定多少个参数"的情况
print_all_args() {
local index=1
for arg in "$@"; do # "$@" 保留每个参数的完整性
echo "参数$index: $arg"
(( index++ ))
done
}
print_all_args "Hello" "World" "CleanResolver"
# 输出:
# 参数1: Hello
# 参数2: World
# 参数3: CleanResolver
5.3 返回值
Shell 函数返回的是退出码(0-255 的数字),不是 JavaScript 那样的返回值:
check_disk() {
local usage=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [ $usage -gt 90 ]; then
return 1 # >90% 磁盘占用,返回 1 表示异常
else
return 0 # ≤90%,返回 0 表示正常
fi
}
check_disk
result=$? # $? 拿到上一个命令的退出码
if [ $result -eq 0 ]; then
echo "磁盘空间充足"
else
echo "磁盘空间不足!请清理"
fi
白话解释:Shell 函数不像 Python 的
return value那样能返回任意值。它只能返回一个数字(0-255),约定俗成:0=成功/正常,非 0=失败/异常。如果你需要传递复杂数据,通常改用echo输出到标准输出,外面用$()捕获。
六、参数传递与错误处理
6.1 脚本层面的参数传递
脚本被调用时,参数会依次存入 $1、$2…… 整个脚本都可以访问:
#!/bin/bash
# script.sh — 演示各种参数用法
echo "脚本名称: $0"
echo "第1个参数: $1"
echo "第2个参数: $2"
echo "参数总数: $#"
echo "全部参数: $@"
echo "当前 PID: $$"
运行 ./script.sh foo bar baz,输出:
脚本名称: ./script.sh
第1个参数: foo
第2个参数: bar
参数总数: 3
全部参数: foo bar baz
当前 PID: 12345
6.2 getopts:解析带短横线的选项(如 -h、-v)
while getopts "h:v:f:" opt; do # h、v 是无参选项,f 需要跟值
case $opt in
h)
echo "帮助信息"; exit 0
;;
v)
version="$OPTARG" # -v 后面的值存在 OPTARG
echo "版本: $version"
;;
f)
filename="$OPTARG" # -f 后面的值是文件名
echo "文件: $filename"
;;
\?)
echo "未知选项: -$OPTARG" >&2 # 错误的选项写到 stderr
;;
esac
done
运行 ./script.sh -v 1.0 -f config.txt:
$OPTARG就是选项的值(1.0、config.txt)\?捕获非法选项
6.3 错误处理三板斧
(1) set -e:遇到错误立即退出
#!/bin/bash
set -e # 任何命令失败就立刻退出
echo "第一步"
ls /nonexistent_dir # 这条失败了,脚本直接终止
echo "这行不会执行" # set -e 生效后跳过
(2) set -u:使用未定义的变量时报错
#!/bin/bash
set -u # 关闭"变量可为空"的宽容模式
echo "用户名是:$username" # username 未定义 → 报错退出
(3) set -o pipefail:管道中任一命令失败即算失败
#!/bin/bash
set -eo pipefail # 同时启用出错退出 + 管道检测
grep "error" /var/log/syslog | sort -u | wc -l
# 没有 pipefail 时:即使 grep 找不到内容(返回 1),管道整体仍然返回最后一个命令的退出码(wc 返回 0)
# 有 pipefail 时:grep 的 1 会被管道继承,脚本因 set -e 而终止
白话解释:管道
|把多个命令串起来,像流水线。没有 pipefail 时,只有流水线的最后一环(wc)决定整条线是成功还是失败——前面的环节挂了也没人管。有了 pipefail 后,任何一环出问题都算全线失败。运维脚本中强烈建议加上。
七、管道与命令组合
管道符 | 是 Unix 哲学的核心——一个小工具做好一件事,多个小工具串联解决大问题。
# 统计 Nginx 访问日志中 IP 出现频率 Top 5
cat /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -5
# 逐段拆解:
# cat /var/log/nginx/access.log → 读取日志文件
# awk '{print $1}' → 提取每行第一个字段(IP 地址)
# sort → 排序(uniq 需要排序后的数据)
# uniq -c → 去重并统计每个 IP 的出现次数
# sort -rn → 按数字降序排列
# head -5 → 只看前 5 个
命令组合常用技巧:
# && 前一个成功才执行下一个(短路逻辑)
cd /tmp && mkdir cleanresolver && cd cleanresolver
# || 前一个失败才执行下一个
cp data.csv /backup/ || echo "备份失败,请手动操作" >&2
# ; 不管前一个成败都继续执行
rm temp.txt ; echo "清理完成"
# () 子 shell 执行(不影响当前环境)
(umask 077 && tar czf secret.tar.gz sensitive_data/)
# {} 当前 shell 执行(共享环境)
{ umask 077; tar czf secret.tar.gz sensitive_data; }
八、crontab 定时任务
写好的脚本不会自己跑——你需要告诉系统什么时候执行。crontab 就是 Linux 内置的定时器。
8.1 crontab 格式
┌───────────── 分钟(0-59)
│ ┌───────────── 小时(0-23)
│ │ ┌───────────── 日期(1-31)
│ │ │ ┌───────────── 月份(1-12)
│ │ │ │ ┌───────────── 星期(0-7,0和7都是周日)
│ │ │ │ │
│ │ │ │ │
* * * * * 要执行的命令
常用表达式示例:
| 表达式 | 含义 |
|---|---|
*/5 * * * * | 每 5 分钟执行一次 |
0 2 * * * | 每天凌晨 2 点执行 |
0 9 * * 1 | 每周一上午 9 点执行 |
0 0 1 * * | 每月 1 号凌晨 0 点执行 |
30 22 * * 1-5 | 周一至周五每晚 10:30 执行 |
8.2 编辑和管理
crontab -e # 用默认编辑器添加/修改定时任务
crontab -l # 列出当前所有定时任务
crontab -r # 删除所有定时任务(⚠️ 危险!不可逆)
国内用户提示:部分 Linux 发行版的默认编辑器可能是
nano(比 vim 更友好)。如果想切换为 vim:export EDITOR=vim再跑crontab -e。
8.3 实战:用 crontab 配合我们的体检脚本
假设我们已经写好了系统体检脚本 /root/scripts/system-report.sh,让它每天早上 8 点和晚上 8 点自动生成报告:
# 编辑 crontab
crontab -e
# 添加这两行:
0 8,20 * * * /root/scripts/system-report.sh >> /var/log/sys-health.log 2>&1
>> /var/log/sys-health.log 2>&1 的含义:
>>把标准输出追加到日志文件2>&1把标准错误也合并到同一个文件- 这样无论脚本成功还是报错,都有记录可查
云间提醒:如果你的服务器在海外(比如 Cloudflare Tunnel 背后的节点),定时任务跑得稳;但国内有些虚拟主机可能不允许 crontab。这种情况下可以考虑用第三方监控平台的 Webhook(如 云间 API 中转站 支持的各类回调通知)来做替代方案。
九、完整实战:系统体检报告脚本
前面讲了所有核心语法,现在把它们串在一起——写一个真正的系统体检脚本。这个脚本覆盖了变量、函数、条件判断、循环、参数传递、错误处理,可以直接在生产环境中使用。
9.1 完整源码(.sh 版)
下面是你可以直接复制使用的完整脚本。每一行都有注释,保证小白也能看懂:
#!/usr/bin/env bash
# ============================================================
# Shell 脚本编程实战 —— 系统体检报告 一键脚本(macOS / Linux / WSL)
# 公开源码,欢迎审查 —— 不放心可先复制给 AI 判断
# 覆盖语法:变量/引号、条件判断(if/case)、循环(for/while)、
# 函数、参数传递($@/$#)、错误处理(set -eo pipefail)
# ============================================================
set -euo pipefail # 遇错退出 + 未定义变量报错 + 管道故障传递
# ---------- 颜色日志函数(终端彩色输出) ----------
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m' # NC = No Color,重置终端颜色
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
step() { echo -e "${CYAN}[STEP]${NC} $1"; }
# ---------- 变量与引号演示 ----------
REPORT_DATE=$(date +"%Y-%m-%d %H:%M:%S") # 用双引号确保日期含空格时不被截断
HOSTNAME=$(hostname)
UPTIME_STR=$(uptime -p 2>/dev/null || uptime | sed 's/.*up\s*//' | sed 's/,.*load.*//')
# ---------- 函数:检测发行版 ----------
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "$PRETTY_NAME"
elif command -v sw_vers >/dev/null 2>&1; then
sw_vers -productVersion
else
echo "Unknown OS ($(uname -s) $(uname -r))"
fi
}
# ---------- 函数:检测磁盘使用情况 ----------
check_disk() {
step "检查磁盘使用情况..."
df -h / 2>/dev/null | awk 'NR==2 {printf " 总计: %s 已用: %s 可用: %s 使用率: %s\n", $2, $3, $4, $5}'
# 检查根分区是否超过 90%(用 -gt 数值比较)
local usage
usage=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%')
if [ "${usage:-0}" -gt 90 ]; then
warn "根分区使用率 ${usage}%,超过 90% 警戒线!"
else
info "根分区使用率 ${usage}%,健康"
fi
}
# ---------- 函数:检测内存 ----------
check_memory() {
step "检查内存使用情况..."
free -h 2>/dev/null | awk '/^Mem:/ {printf " 总计: %s 已用: %s 可用: %s 使用率: %.1f%%\n", $2, $3, $7, ($3/$2)*100}'
# 循环演示:遍历所有内存交换分区
local swap_count
swap_count=$(free -m 2>/dev/null | awk '/^Swap:/ {print $2}')
if [ "${swap_count:-0}" -gt 0 ]; then
info "检测到交换分区 (${swap_count}MB),请关注 Swap 使用率以防物理内存不足"
else
info "无交换分区"
fi
}
# ---------- 函数:检测登录用户 ----------
check_users() {
step "检查当前登录用户数..."
local user_count
user_count=$(who 2>/dev/null | wc -l | tr -d ' ')
echo " 当前在线用户:$user_count 人"
if [ "$user_count" -gt 5 ]; then
warn "在线用户过多($user_count),可能有未授权的远程登录"
fi
# 循环遍历当前用户
if [ "$user_count" -gt 0 ]; then
echo " 用户列表:"
while IFS= read -r line; do
local user_name
user_name=$(echo "$line" | awk '{print $1}')
local user_tty
user_tty=$(echo "$line" | awk '{print $2}')
echo " - $user_name (TTY: $user_tty)"
done < <(who 2>/dev/null)
fi
}
# ---------- 函数:检测网络连通性 ----------
check_network() {
step "检查网络连通性..."
local dns_servers
dns_servers=$(grep nameserver /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -3)
if [ -n "$dns_servers" ]; then
echo " DNS 服务器:"
echo "$dns_servers" | while read -r dns; do
echo " - $dns"
done
fi
# 循环测试多个目标(演示 for 循环)
local targets=("8.8.8.8" "1.1.1.1" "223.5.5.5") # Google DNS + Cloudflare DNS + 阿里 DNS
for target in "${targets[@]}"; do
if ping -c 1 -W 2 "$target" >/dev/null 2>&1; then
info " $target : 连通"
else
warn " $target : 不通(可能被防火墙拦截)"
fi
done
}
# ---------- 函数:演示条件判断(case 语句) ----------
classify_system_load() {
local load_avg
load_avg=$(awk '{print $1}' /proc/loadavg 2>/dev/null)
if [ -z "$load_avg" ]; then
echo " (无法获取负载信息)"
return
fi
# 提取整数部分进行比较
local load_int
load_int=${load_avg%%.*}
case "$load_int" in
0) echo " 系统负载:空闲($load_avg)" ;;
[1-3]) echo " 系统负载:正常($load_avg)" ;;
*) warn " 系统负载偏高:$load_avg(建议检查高 CPU 进程)" ;;
esac
}
# ---------- 函数:主流程汇总 ----------
generate_report() {
local net_mode="${1:-auto}" # 参数传递:auto/domestic/overseas,默认 auto
echo "============================================"
echo " 系统体检报告"
echo " 时间:$REPORT_DATE"
echo " 主机:$HOSTNAME"
echo " 系统:$(detect_os)"
echo " 运行时长:$UPTIME_STR"
echo "============================================"
echo ""
check_disk
check_memory
check_users
check_network
classify_system_load
echo ""
echo "============================================"
# 演示 $@ 全部参数
if [ $# -gt 0 ]; then
echo "传入的参数:$@"
fi
echo "============================================"
info "体检完成!如需定期检查,可加入 crontab 定时任务:"
echo " crontab -e 添加:"
echo " 0 8,20 * * * /path/to/system-report.sh >> /var/log/sys-health.log 2>&1"
echo ""
echo " (详细版建议每周日早 8 点跑一次,cron 表达式:0 8 * * 0)"
}
# ---------- 入口 ----------
main() {
generate_report "$@"
}
main "$@"
9.2 Windows PowerShell 版
Windows 用户可以通过 WSL(Windows Subsystem for Linux) 直接运行上面的 Bash 脚本。如果你习惯 PowerShell,这里是等价版本——同样覆盖变量、条件判断、循环、函数和错误处理:
# ============================================================
# Shell 脚本编程实战 —— 系统体检报告 一键脚本(Windows PowerShell)
# 公开源码,欢迎审查 -- 不放心可先复制给 AI 判断
# 说明:PowerShell 版在 Windows 原生环境下运行,无需 WSL
# ============================================================
$ErrorActionPreference = "Stop" # 遇到错误立即停止(等同于 set -e)
# ---------- 颜色日志函数 ----------
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green }
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Err { Write-Host "[ERROR] $args" -ForegroundColor Red }
function Write-Step { Write-Host "[STEP] $args" -ForegroundColor Cyan }
# ---------- 函数:检测操作系统 ----------
function Get-OsInfo {
$os = Get-CimInstance Win32_OperatingSystem
return "$($os.Caption) $($os.Version)"
}
# ---------- 函数:检测磁盘 ----------
function Check-Disk {
Write-Step "检查磁盘使用情况..."
$disks = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"
$usedPercent = [math]::Round(($disks.Size - $disks.FreeSpace) / $disks.Size * 100, 1)
$totalGB = [math]::Round($disks.Size / 1GB, 1)
$freeGB = [math]::Round($disks.FreeSpace / 1GB, 1)
$usedGB = $totalGB - $freeGB
Write-Output " 总计: ${totalGB}GB 已用: ${usedGB}GB 可用: ${freeGB}GB 使用率: ${usedPercent}%"
if ($usedPercent -gt 90) {
Write-Warn "C盘使用率 ${usedPercent}%,超过 90% 警戒线!"
} else {
Write-Info "C盘使用率 ${usedPercent}%,健康"
}
}
# ---------- 函数:检测内存 ----------
function Check-Memory {
Write-Step "检查内存使用情况..."
$cs = Get-CimInstance Win32_ComputerSystem
$os = Get-CimInstance Win32_OperatingSystem
$totalGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
$freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
$usedGB = $totalGB - $freeGB
$usedPercent = [math]::Round(($usedGB / $totalGB) * 100, 1)
Write-Output " 总计: ${totalGB}GB 已用: ${usedGB}GB 可用: ${freeGB}GB 使用率: ${usedPercent}%"
}
# ---------- 函数:检测在线用户 ----------
function Check-Users {
Write-Step "检查当前会话数..."
$sessions = Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" | Select-Object -Property ProcessId, CommandLine -Unique
$sessionCount = $sessions.Count
Write-Output " 当前会话:$sessionCount"
if ($sessionCount -gt 5) {
Write-Warn "会话数较多($sessionCount),可能有多个用户远程连接"
}
}
# ---------- 函数:检测网络 ----------
function Check-Network {
Write-Step "检查网络连通性..."
$targets = @("8.8.8.8", "1.1.1.1", "223.5.5.5") # Google / Cloudflare / 阿里 DNS
foreach ($target in $targets) {
try {
$ping = Test-Connection -ComputerName $target -Count 1 -TimeoutSeconds 2 -ErrorAction Stop
Write-Info " $target : 连通 (RTT: ${ping.ResponseTime}ms)"
} catch {
Write-Warn " $target : 不通(可能被防火墙拦截)"
}
}
}
# ---------- 函数:生成报告 ----------
function Generate-Report {
param(
[string]$NetMode = "auto" # 参数传递:auto/domestic/overseas
)
$reportDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$computerName = $env:COMPUTERNAME
Write-Host "============================================"
Write-Host " 系统体检报告"
Write-Host " 时间:$reportDate"
Write-Host " 主机:$computerName"
Write-Host " 系统:$(Get-OsInfo)"
Write-Host "============================================"
Write-Host ""
Check-Disk
Check-Memory
Check-Users
Check-Network
Write-Host ""
Write-Host "============================================"
# 演示全部参数
if ($args.Count -gt 0) {
Write-Output "传入的参数:$($args -join ' ')"
}
Write-Host "============================================"
Write-Info "体检完成!"
Write-Host ""
Write-Host " 如果你想定期自动执行(相当于 Linux 的 crontab),可以在 Windows 任务计划程序中设置:"
Write-Host " 1. 打开「任务计划程序」(taskschd.msc)"
Write-Host " 2. 创建基本任务 → 触发器选「每天 8:00」→ 操作选「启动程序」"
Write-Host " 3. 程序填写 powershell.exe,参数填写:"
Write-Host " -File `"C:\scripts\system-report.ps1`""
}
# ---------- 入口 ----------
Generate-Report -NetMode $args[0]
9.3 crontab 定时执行示例
想让体检脚本每天早晚自动跑?在 Linux 上加一行 crontab:
crontab -e
# 每天早上 8 点和晚上 8 点执行,输出追加到日志
0 8,20 * * * /path/to/system-report.sh >> /var/log/sys-health.log 2>&1
# 每周日凌晨 3 点跑一次完整版(含邮件通知,如果你有配置的话)
0 3 * * 0 /path/to/system-report.sh -verbose >> /var/log/sys-health-weekly.log 2>&1
白话解释:
crontab就是 Linux 的闹钟。0 8,20 * * *的意思是"每分钟的第 0 分,8 点和 20 点,每天都跑"。>>把输出保存到日志文件,2>&1把错误也一起记录——这样哪天系统出问题了,翻日志就能追溯。
十、小结与进阶方向
今天我们一起走过了 Shell 脚本的核心版图:
| 知识点 | 关键要点 |
|---|---|
| 变量与引号 | $var 取值、${var} 保边界、单引号不展开 / 双引号展开 |
| if / case | -eq -gt -lt 数值比较、-f -d -r 文件测试 |
| for / while | for x in list 遍历 / while [条件] 循环,别忘了改条件变量 |
| 函数 | func() { ... }、local 局部变量、$@ 全部参数 |
| 错误处理 | set -e 遇错退、set -u 未定义报错、pipefail 管道故障传递 |
| crontab | 0 8,20 * * * 每日两次、>> file 2>&1 保存日志 |
下一步建议:
- 练手:把上面系统体检脚本下载到本机跑一遍,看看实际输出
- 进阶:了解
awk、sed、xargs这些命令行瑞士军刀,它们和 Shell 脚本是绝配 - 自动化:学会写 CI/CD pipeline 脚本(GitHub Actions / GitLab CI),把手动部署变全自动
最后再提一句——如果你觉得在本机折腾 Shell 脚本不如直接在云端调 API 方便,云间 API 中转站(cloudzone-api.cyou)提供了 OpenAI 兼容 + Anthropic 兼容的统一接口,国内直连、0.05x 起价,对想要快速上手又不想管基础设施的同学来说是个不错的选择。当然,Shell 基本功还是要扎实的,毕竟服务器出了问题总得 SSH 上去看一眼对吧?
附录:双版本完整源码下载
不方便阅读长文的同学,可以直接复制下方完整源码,新建文本文档粘贴后改后缀为 .sh 或 .ps1 运行。
也可从以下链接下载(经 CF 边缘缓存,全球加速):
Shell(bash)— 完整源码
#!/usr/bin/env bash
# ============================================================
# Shell 脚本编程实战 —— 系统体检报告 一键脚本(macOS / Linux / WSL)
# 公开源码,欢迎审查 —— 不放心可先复制给 AI 判断
# 覆盖语法:变量/引号、条件判断(if/case)、循环(for/while)、
# 函数、参数传递($@/$#)、错误处理(set -eo pipefail)
# ============================================================
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'
CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
step() { echo -e "${CYAN}[STEP]${NC} $1"; }
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "$PRETTY_NAME"
elif command -v sw_vers >/dev/null 2>&1; then
sw_vers -productVersion
else
echo "Unknown OS ($(uname -s) $(uname -r))"
fi
}
check_disk() {
step "检查磁盘使用情况..."
df -h / 2>/dev/null | awk 'NR==2 {printf " 总计: %s 已用: %s 可用: %s 使用率: %s\n", $2, $3, $4, $5}'
local usage
usage=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%')
if [ "${usage:-0}" -gt 90 ]; then
warn "根分区使用率 ${usage}%,超过 90% 警戒线!"
else
info "根分区使用率 ${usage}%,健康"
fi
}
check_memory() {
step "检查内存使用情况..."
free -h 2>/dev/null | awk '/^Mem:/ {printf " 总计: %s 已用: %s 可用: %s 使用率: %.1f%%\n", $2, $3, $7, ($3/$2)*100}'
local swap_count
swap_count=$(free -m 2>/dev/null | awk '/^Swap:/ {print $2}')
if [ "${swap_count:-0}" -gt 0 ]; then
info "检测到交换分区 (${swap_count}MB),请关注 Swap 使用率"
else
info "无交换分区"
fi
}
check_users() {
step "检查当前登录用户数..."
local user_count
user_count=$(who 2>/dev/null | wc -l | tr -d ' ')
echo " 当前在线用户:$user_count 人"
if [ "$user_count" -gt 5 ]; then
warn "在线用户过多($user_count),可能有未授权远程登录"
fi
if [ "$user_count" -gt 0 ]; then
echo " 用户列表:"
while IFS= read -r line; do
local uname tty
uname=$(echo "$line" | awk '{print $1}')
tty=$(echo "$line" | awk '{print $2}')
echo " - $uname (TTY: $tty)"
done < <(who 2>/dev/null)
fi
}
check_network() {
step "检查网络连通性..."
local dns_servers
dns_servers=$(grep nameserver /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -3)
if [ -n "$dns_servers" ]; then
echo " DNS 服务器:"
echo "$dns_servers" | while read -r dns; do
echo " - $dns"
done
fi
local targets=("8.8.8.8" "1.1.1.1" "223.5.5.5")
for target in "${targets[@]}"; do
if ping -c 1 -W 2 "$target" >/dev/null 2>&1; then
info " $target : 连通"
else
warn " $target : 不通(可能被防火墙拦截)"
fi
done
}
classify_load() {
local load_avg
load_avg=$(awk '{print $1}' /proc/loadavg 2>/dev/null)
if [ -z "$load_avg" ]; then
echo " (无法获取负载信息)"; return
fi
local load_int=${load_avg%%.*}
case "$load_int" in
0) echo " 系统负载:空闲($load_avg)" ;;
[1-3]) echo " 系统负载:正常($load_avg)" ;;
*) warn " 系统负载偏高:$load_avg(建议检查高 CPU 进程)" ;;
esac
}
generate_report() {
local net_mode="${1:-auto}"
local REPORT_DATE UPTIME_STR HOSTNAME
REPORT_DATE=$(date +"%Y-%m-%d %H:%M:%S")
HOSTNAME=$(hostname)
UPTIME_STR=$(uptime -p 2>/dev/null || uptime | sed 's/.*up\s*//' | sed 's/,.*load.*//')
echo "============================================"
echo " 系统体检报告"
echo " 时间:$REPORT_DATE"
echo " 主机:$HOSTNAME"
echo " 系统:$(detect_os)"
echo " 运行时长:$UPTIME_STR"
echo "============================================"
echo ""
check_disk
check_memory
check_users
check_network
classify_load
echo ""
echo "============================================"
if [ $# -gt 0 ]; then
echo "传入的参数:$@"
fi
echo "============================================"
info "体检完成!如需定期检查,可加入 crontab 定时任务:"
echo " 0 8,20 * * * /path/to/system-report.sh >> /var/log/sys-health.log 2>&1"
}
generate_report "$@"
PowerShell — 完整源码
# ============================================================
# Shell 脚本编程实战 —— 系统体检报告 一键脚本(Windows PowerShell)
# 公开源码,欢迎审查 -- 不放心可先复制给 AI 判断
# 说明:PowerShell 版在 Windows 原生环境下运行,无需 WSL
# ============================================================
$ErrorActionPreference = "Stop"
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green }
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Err { Write-Host "[ERROR] $args" -ForegroundColor Red }
function Write-Step { Write-Host "[STEP] $args" -ForegroundColor Cyan }
function Get-OsInfo {
$os = Get-CimInstance Win32_OperatingSystem
return "$($os.Caption) $($os.Version)"
}
function Check-Disk {
Write-Step "检查磁盘使用情况..."
$disks = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"
$usedPercent = [math]::Round(($disks.Size - $disks.FreeSpace) / $disks.Size * 100, 1)
$totalGB = [math]::Round($disks.Size / 1GB, 1)
$freeGB = [math]::Round($disks.FreeSpace / 1GB, 1)
$usedGB = $totalGB - $freeGB
Write-Output " 总计: ${totalGB}GB 已用: ${usedGB}GB 可用: ${freeGB}GB 使用率: ${usedPercent}%"
if ($usedPercent -gt 90) {
Write-Warn "C盘使用率 ${usedPercent}%,超过 90% 警戒线!"
} else {
Write-Info "C盘使用率 ${usedPercent}%,健康"
}
}
function Check-Memory {
Write-Step "检查内存使用情况..."
$cs = Get-CimInstance Win32_ComputerSystem
$os = Get-CimInstance Win32_OperatingSystem
$totalGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
$freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
$usedGB = $totalGB - $freeGB
$usedPercent = [math]::Round(($usedGB / $totalGB) * 100, 1)
Write-Output " 总计: ${totalGB}GB 已用: ${usedGB}GB 可用: ${freeGB}GB 使用率: ${usedPercent}%"
}
function Check-Users {
Write-Step "检查当前会话数..."
$sessions = Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" | Select-Object -Property ProcessId, CommandLine -Unique
$sessionCount = $sessions.Count
Write-Output " 当前会话:$sessionCount"
if ($sessionCount -gt 5) {
Write-Warn "会话数较多($sessionCount),可能有多个用户远程连接"
}
}
function Check-Network {
Write-Step "检查网络连通性..."
$targets = @("8.8.8.8", "1.1.1.1", "223.5.5.5")
foreach ($target in $targets) {
try {
$ping = Test-Connection -ComputerName $target -Count 1 -TimeoutSeconds 2 -ErrorAction Stop
Write-Info " $target : 连通 (RTT: ${$ping.ResponseTime}ms)"
} catch {
Write-Warn " $target : 不通(可能被防火墙拦截)"
}
}
}
function Generate-Report {
param([string]$NetMode = "auto")
$reportDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$computerName = $env:COMPUTERNAME
Write-Host "============================================"
Write-Host " 系统体检报告"
Write-Host " 时间:$reportDate"
Write-Host " 主机:$computerName"
Write-Host " 系统:$(Get-OsInfo)"
Write-Host "============================================"
Write-Host ""
Check-Disk; Check-Memory; Check-Users; Check-Network
Write-Host ""
Write-Host "============================================"
if ($args.Count -gt 0) {
Write-Output "传入的参数:$($args -join ' ')"
}
Write-Host "============================================"
Write-Info "体检完成!可在 Windows 任务计划程序中设置定时执行。"
}
Generate-Report -NetMode $args[0]
透明声明:以上脚本公开源码,欢迎复制给任何 AI 审查。脚本中没有任何隐藏的网络请求、数据外传或敏感操作——所有功能均为本地系统信息查询。
请完成验证后查看评论区