导读

如果你用过 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"),厨师根据食材种类(nginxapachemysql)做出不同的菜(执行对应的代码块),最后用 ;; 标记这道菜做完。没在菜单上的就归入 * 默认项。


四、循环

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.0config.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 / whilefor x in list 遍历 / while [条件] 循环,别忘了改条件变量
函数func() { ... }local 局部变量、$@ 全部参数
错误处理set -e 遇错退、set -u 未定义报错、pipefail 管道故障传递
crontab0 8,20 * * * 每日两次、>> file 2>&1 保存日志

下一步建议

  • 练手:把上面系统体检脚本下载到本机跑一遍,看看实际输出
  • 进阶:了解 awksedxargs 这些命令行瑞士军刀,它们和 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 审查。脚本中没有任何隐藏的网络请求、数据外传或敏感操作——所有功能均为本地系统信息查询。