一开始是照着这么一张练习清单挨个写的,从统计目录文件数开始,一天一个,最后一个再把它们攒成一个菜单:

一、统计目录文件数量并排序前 10
思路是用 find 把文件全捞出来,再用 awk 按 / 切分出目录,最后统计次数排序。不过在写脚本之前,得先看看 find 出来的东西长什么样。
在根目录执行,先取前 3 行看看每一列都是什么:
# 在根目录执行
# 输出所有子目录,按照 / 分隔,查看得到前 3 行的每列
# 并且查看最后一列文件名 ($NF)
find . -type f | head -3 | awk -F / '{print "$1=" $1 " $2=" $2 " $3=" $3 " $4=" $4 " $5=" $5}'
root@VM-0-12-ubuntu:/# find . -type f | head -3 | awk -F / '{print "$1="$1 " $2="$2 " $3="$3 " $4="$4 " $NF="$NF}'
$1=. $2=tmp $3=itcast_clean.sql $4= $NF=itcast_clean.sql
$1=. $2=tmp $3=test.sql $4= $NF=test.sql
$1=. $2=tmp $3=disable_rt_runtime_share.log $4= $NF=disable_rt_runtime_share.log
# 去掉文件名(最后一列)并指定输出分隔符为 /
find -type f|awk -F / -v OFS=/ '{$NF=""; print $0}'|head -3
# $0表示 当前行 的全部初始内容(1 2 3都是输出指定列)
# 数组统计每个目录出现的次数
find . -type f | awk -F / -v OFS=/ '{$NF=""; count[$0]++} END {for(d in count) print count[d], d}'
awk 后面单引号里的东西就相当于一段代码,每一块 {} 就是一段,对 awk 处理过的内容做操作,只是横着写了一行而已,想竖着格式化写也行,但日常都是横着写。
为什么统计目录出现的次数就等于文件数量?因为 find -type f 输出的每一条都是一个文件路径,多一个文件,它所在的目录就多出现一次。
脚本 file_cnt.sh:
root@VM-0-12-ubuntu:~/scripts# vim file_cnt.sh
#! /bin/bash
# 找出执行脚本目录,子目录文件最多的目录
# 用法:./count_files.sh [目录] 不指定目录则统计当前目录
if [ $# -eq 1 ]
then
FILE_DIR=$1
echo "获取到的目录:$FILE_DIR"
else
echo "只能输入一个目录喵"
exit 1
fi
cd $FILE_DIR || exit 1
# 这里上了一层保险
# 但是相对滴,执行脚本也要写绝对路径不然下面的find找不到脚本
find "$FILE_DIR" -type f|awk -F / -v OFS=/ '{$NF=""; count[$0]++} END {for(dir in count) print count[dir],dir}' | sort -nr|head -10
注释里写的“不指定目录则统计当前目录”其实没实现,参数不等于 1 就直接退出了,想支持的话自己给 FILE_DIR 加个默认值就行。
执行结果,路径里那两个斜杠是因为 OFS=/ 加上最后一列被清空,多出来一个:
root@VM-0-12-ubuntu:~/scripts# bash /root/scripts/file_cnt.sh /
获取到的目录:/
8823//usr/src/linux-headers-5.15.0-160-generic/include/config/
3060//var/lib/dpkg/info/
1985//usr/local/qcloud/YunJing/YDFlow/pocs/vul/
1437//usr/local/qcloud/python/lib/python3.7/test/__pycache__/
1329//usr/src/linux-headers-5.15.0-160/include/linux/
1121//usr/share/man/man1/
945//usr/bin/
893//usr/share/man/man3/
822//data/safeline/resources/postgres/data/base/16384/
795//usr/share/man/man8/
root@VM-0-12-ubuntu:/# source /root/scripts/file_cnt.sh /var
获取到的目录:/var
3060//var/lib/dpkg/info/
703//var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/594/fs/usr/lib/mysqlsh/lib/python3.13/site-packages/oci/core/models/
686//var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/594/fs/usr/lib/mysqlsh/lib/python3.13/site-packages/oci/database_management/models/
623//var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/602/fs/usr/lib64/python2.7/
601//var/lib/docker/rootfs/overlayfs/5f89ee9839b9474dfb17cbfe2e6016c67f59de2c2f40180922529055ef09a774/var/lib/dpkg/info/
601//var/lib/docker/rootfs/overlayfs/09d76763c36e61d7ab1f42d835d318a8d065e692e0438df6a844926c9563e65c/var/lib/dpkg/info/
598//var/lib/docker/rootfs/overlayfs/e1a7f0150fed235f6502d1eabee5009f0f424e3f842946261338be2c52ce0d7a/var/lib/dpkg/info/
594//var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/610/fs/usr/lib/mysqlsh/lib/python3.9/site-packages/oci/data_integration/models/
594//var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/594/fs/usr/lib/mysqlsh/lib/python3.13/site-packages/oci/data_integration/models/
554//var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/594/fs/usr/lib/mysqlsh/lib/python3.13/site-packages/oci/database/models/
root@VM-0-12-ubuntu:/var#
二、统计 nginx 日志里出现最多的 10 个 IP
先看一眼日志格式,第一列就是 IP:
root@VM-0-12-ubuntu:/var/log/nginx# cat access.log|head
220.196.160.146 - - [09/Sep/2026:00:02:19 +0800] "GET / HTTP/1.1" 200 2298 "-" "Mozilla/5.0 (Linux; Android 11; RMX2117) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
43.152.72.247 - - [09/Sep/2026:00:16:02 +0800] "GET / HTTP/1.1" 200 0 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
220.196.160.61 - - [09/Sep/2026:00:17:45 +0800] "GET / HTTP/1.1" 200 2298 "-" "Mozilla/5.0 (Linux; Android 11; RMX2117) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
220.196.160.61 - - [09/Sep/2026:00:17:45 +0800] "GET /taffy.png HTTP/1.1" 200 29570 "http://senrenbanka.art/" "Mozilla/5.0 (Linux; Android 11; RMX2117) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
220.196.160.61 - - [09/Sep/2026:00:17:45 +0800] "GET /mudan.png HTTP/1.1" 200 75413 "http://senrenbanka.art/" "Mozilla/5.0 (Linux; Android 11; RMX2117) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
220.196.160.61 - - [09/Sep/2026:00:17:45 +0800] "GET /leimi.png HTTP/1.1" 200 261374 "http://senrenbanka.art/" "Mozilla/5.0 (Linux; Android 11; RMX2117) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
27.148.196.62 - - [09/Sep/2026:00:21:52 +0800] "GET / HTTP/1.1" 200 2298 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
43.159.36.180 - - [09/Sep/2026:00:29:35 +0800] "GET / HTTP/1.1" 200 10 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
104.23.223.64 - - [09/Sep/2026:00:31:05 +0800] "GET /wp-admin/install.php?step=1 HTTP/1.1" 404 0 "-" "http://senrenbanka.art/wp-admin/install.php?step=1"
180.101.245.250 - - [09/Sep/2026:00:32:40 +0800] "GET / HTTP/1.1" 200 2298 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
第一版直接把日志路径写死在脚本里:
vim nginx_ip.sh
# 获取Nginx访问日志中出现次数最多的10个IP
NGINX_LOG="/var/log/nginx/access.log"
echo "当前统计$NGINX_LOG中的IP"
cat $NGINX_LOG | awk '{count[$1]++} END {for(ip in count) print count[ip],ip}'|sort -nr | head -10
改成从参数传进来:
# 获取Nginx访问日志中出现次数最多的10个IP
NGINX_LOG=$1
if [ $# -eq 1 ]; then
echo "当前统计$NGINX_LOG中的IP"
cat $NGINX_LOG | awk '{count[$1]++} END {for(ip in count) print count[ip],ip}'|sort -nr | head -10
else
echo "只能输入一个目录喵"
exit 1 # 这个可以不写,如果执行错误了会退出ssh,因为给到了当前shell执行
fi
root@VM-0-12-ubuntu:/# source /root/scripts/nginx_ip.sh /var/log/nginx/access.log
当前统计/var/log/nginx/access.log中的IP
51 193.32.204.199
28 154.17.162.2
27 192.210.234.226
24 46.151.182.62
14 45.91.64.7
11 16.5.0.236
9 220.196.160.61
8 165.22.77.54
6 66.132.195.72
6 45.194.92.67
再优化一版,干脆用 read 交互着输入,顺便判断一下文件在不在:
# 获取Nginx访问日志中出现次数最多的10个IP
read -p "输入日志绝对路径:" NGINX_LOG
# 判断文件存在,存在为真
if [ -f "$NGINX_LOG" ]; then
echo "当前统计$NGINX_LOG中的IP"
cat $NGINX_LOG | awk '{count[$1]++} END {for(ip in count) print count[ip],ip}'|sort -nr | head -10
else
echo "该路径不存在"
fi
root@VM-0-12-ubuntu:~# source /root/scripts/nginx_ip.sh
输入日志绝对路径:/var/log/nginx/access.log
当前统计/var/log/nginx/access.log中的IP
51 193.32.204.199
32 154.17.162.2
27 192.210.234.226
24 46.151.182.62
14 45.91.64.7
11 16.5.0.236
9 220.196.160.61
8 165.22.77.54
6 66.132.195.72
6 45.194.92.67
root@VM-0-12-ubuntu:~# source /root/scripts/nginx_ip.sh
输入日志绝对路径:/add/d
该路径不存在
三、日志自动清理脚本
先备份重要日志,这个脚本负责自动清理 /var/log 下超过七天的日志压缩包(注意是压缩包,不是正在写的日志文件)。
想要的效果
不递归子目录,要清掉的就是图里标红的那批 log.数字.gz 文件:

顺手把要求也记一下,日志修改时间超过 7 天就清掉,最后报一句释放了多少空间:

涉及规律数字,肯定得用正则。
构建过程
#! /bin/bash
# 清理输入路径下log.数字.gz的压缩包日志文件
read -p "输入要清理日志压缩包的目录" LOG_DIR
if [ -d "$LOG_DIR" ]; then
echo "正在清理"
sleep 2
else
echo "目录不存在"
fi
cd "$LOG_DIR" || echo "对该目录没有读权限"
rm *log.[0-9]*.gz
echo "清理完毕"
不建议用 rm,rm 不支持复杂的正则,这种活交给 find -delete:

# .*经常和 -regex配合使用,因为是匹配完整路径
find "$LOG_DIR" -type f -regextype posix-extended -regex ".*\.log\.[0-9]+(\.gz)?$" -delete
find 这几个核心参数记住就行:
-type f:只找文件-regex:用正则匹配完整路径- 必须先写
-regextype,再写-regex -regextype posix-extended:让正则支持+、?、|-delete:删除找到的文件
-name 和 -regex 的区别也顺便记一下,一个只匹配文件名、用通配符,一个匹配完整路径、用正则:

最终脚本:
#! /bin/bash
# 清理输入路径下log.数字.gz的压缩包日志文件
read -p "输入要清理日志压缩包的目录" LOG_DIR
if [ -d "$LOG_DIR" ]; then
echo "正在清理"
sleep 2
else
echo "目录不存在"
fi
# {}和代码直接必须有空格,可以去掉看看,高亮会变暗
cd "$LOG_DIR" || { echo "对该目录没有读权限"; exit 1; } # 末尾要有结束符、空格也可以
# 如果没有cd这里就写绝对路径
find . -type f -regextype posix-extended -regex ".*\.log\.[0-9]+(\.gz)?$" -delete
echo "清理完毕"
root@VM-0-12-ubuntu:~/scripts# bash clear_var_log.sh
输入要清理日志压缩包的目录/var/log
正在清理
清理完毕
如果报错就重新创建文件试试。
清完之后长这样,log.数字.gz 都没了:

如果还想把清理掉的文件名打印出来,加上 -print 就行:
find . -type f -regextype posix-extended -regex \
".*\.log\.[0-9]+(\.gz)?$" -print -delete
四、磁盘监控告警
磁盘主要看物理磁盘 /dev 开头的,最要紧的是使用率那一列:

#! /bin/bash
# 如果磁盘只有一个,就直接赋值
DEV_USED=$(df -h | grep '^/dev/' | awk '{print $5}'| tr -d '%')
if [ "$DEV_USED" -gt 80 ]; then
echo "磁盘空间不足(使用率:$DEV_USED%)"
else
echo "磁盘空间使用正常($DEV_USED%)"
fi
tr 是命令名,全称 translate(转换/翻译);-d 是参数,全称 delete(删除);'%' 就是要删掉的字符。
顺便记一个清空文件的办法:
> [文件名]
优化
还可以加上 date,以及磁盘有多个的时候用循环一个个判断:读两列,while read 一次赋两个变量。
# 如果磁盘有多个可以配合循环使用
# 这时就需要加入磁盘名,读两列,while赋值两个变量
df -h | grep '^/dev/' | awk '{print $1, $5}'| tr -d '%' | while read DEV DEV_USED; do
if [ "$DEV_USED" -gt 80 ]; then
echo "磁盘$DEV内存不足($DEV_USED%)"
else
echo "磁盘$DEV空间使用正常($DEV_USED%)"
sleep 2
fi
done
# echo -e也可以解析\n,不然会当作字符输出
df -h | grep '^/dev/' | awk '{print $1, $5}'| tr -d '%' | while read DEV DEV_USED; do
if [ "$DEV_USED" -gt 80 ]; then
echo -e "磁盘$DEV内存不足($DEV_USED%)\n"
else
echo -e "磁盘$DEV空间使用正常($DEV_USED%)\n"
sleep 2
fi
done
数值比较的几个符号:
-gt大于-lt小于-eq等于-ne不等于-ge大于等于-le小于等于
五、服务存活检测和自动重启
$(...) 的意思是:先执行括号里的命令,然后把命令的输出返回。
#! /bin/bash
# 服务探测重启脚本,重启失败打印报错信息
SERVICE_NAME=$1
SERVICE_STATUS=$(systemctl is-active "$SERVICE_NAME")
if [ "$SERVICE_STATUS" = "active" ]; then
echo -e "$SERVICE_NAME服务正常运行\n"
sleep 1
else
echo -e "$SERVICE_NAME未启动\n"
sleep 1
RETURN=$(systemctl restart "$SERVICE_NAME" 2>&1)
if [ $? -ne "0" ]; then
echo -e "$RETURN\n"
else
echo -e "服务重启成功\n"
fi
fi
# 值得区分的是,mysqld是进程名,服务名是mysql
root@VM-0-12-ubuntu:~/scripts# bash service_status.sh mysqld
mysqld未启动
Failed to restart mysqld.service: Unit mysqld.service not found.
root@VM-0-12-ubuntu:~/scripts# bash service_status.sh mysql
mysql服务正常运行
root@VM-0-12-ubuntu:~/scripts# systemctl is-active mysql
active
root@VM-0-12-ubuntu:~/scripts# systemctl is-active mysqld
inactive
这里有个坑值得说一下:systemctl restart 成功的时候什么都不输出,失败才会往错误输出(stderr)里打印报错。所以 RETURN=$(systemctl restart "$SERVICE_NAME" 2>&1) 里那个 2>&1 不能省,它的意思是把错误输出重定向到标准输出,这样 $() 才能把报错信息也一起装进袋子里。不加的话,RETURN 永远是空的,报错直接喷到屏幕上了。

关于重定向和标准输出、错误输出,常用的就这几个写法:
| 写法 | 作用 |
|---|---|
> file | 标准输出 → file |
2> file | 标准错误 → file |
>> file | 标准输出追加 → file |
2>> file | 标准错误追加 → file |
&> file | 标准输出 + 标准错误 → file |
> file 2>&1 | 同上(更通用) |
2>&1 | 错误输出合并到标准输出 |
> /dev/null | 丢弃标准输出 |
2> /dev/null | 丢弃标准错误 |
&> /dev/null | 全部丢弃 |
六、文件备份脚本
这个脚本要干这几件事:
- 把指定目录打包压缩成
.tar.gz - 文件名带时间戳(
backup_2026-09-12.tar.gz) - 备份文件存到指定目录
- 自动删除 7 天前的旧备份
- 每次操作记录日志
#! /bin/bash
# 传入指定目录打包
mkdir -p /backup/
read -p "传入一个目录(绝对路径)" DIR
DATE=$(date +%Y%m%d_%H%M%S) # date格式化,右边无空格,左边空一格
if [ -d "$DIR" ]; then
echo "备份目录:$DIR"
sleep 1
# 1.记录1(追加写入,必须加echo,否则会当成命令执行)
echo "[$DATE] 开始备份$DIR" >> /backup/operate.log
cd /backup/ || { echo "未找到备份目录"; exit 1; }
tar -czf ${DATE}.tar.gz "$DIR"
# 检查tar是否成功
if [ $? -ne "0" ]; then
echo "备份失败"
exit 1
fi
# 记录2
echo "[$DATE] 备份成功 ${DATE}.tar.gz" >> /backup/operate.log
# 如果不指定普通文件,会把文件目录、软连接、设备文件都找出
find /backup -type f -name "202*_*.tar.gz" -mtime +7 -delete && echo "7天前的文件已删除"
# 记录3
echo "[$DATE] 清理了7天前的文件" >> /backup/operate.log
else
echo "传入的不是一个目录"
exit 1 # 写了退出就不能用source再当前shell执行,会退出ssh,bash就行
fi
-mtime 是 modification time(修改时间)的缩写,后面那个 +7 是它的参数,几个写法的区别:

${} 是为了明确变量的边界,不加的话 shell 会顺着往后读变量名:

每一步都记日志,出了问题才能回头查:

tar 打包会把开头的 / 去掉
# 1. 打包命令
tar -czf backup.tar.gz /tmp/test_data
# 2. 查看压缩包里的内容
tar -tzf backup.tar.gz
你会看到输出是:
tmp/test_data/a.txt
tmp/test_data/b.txt
开头的 / 没了,它变成了 tmp/test_data。这时候解压:
tar -xzf backup.tar.gz
它不会去覆盖系统的 /tmp/test_data,而是在当前所在的目录下新建一个 tmp 目录放进去。备份包不在当前目录的话,把路径写全就行:
tar -xzf /backup/20260912_123456.tar.gz
想指定解压到哪个位置,加一个 -C:
tar -xzf /backup/20260912_123456.tar.gz -C /
这样它才会解压回 /tmp/test_data。
真想保留绝对路径,就加一个大写的 -P 参数:
tar -czPf backup.tar.gz /tmp/test_data
警告:千万别在生产环境随便用 -P,解压的时候它会直接按绝对路径覆盖原来的文件。
跑一遍看看:
root@VM-0-12-ubuntu:~/scripts# bash bak_file.sh
传入一个目录(绝对路径)/root/scripts
备份目录:/root/scripts
tar: Removing leading `/' from member names
7天前的文件已删除
备份目录和日志都在:
root@VM-0-12-ubuntu:~/scripts# cd /backup
root@VM-0-12-ubuntu:/backup# l
20260912_222922.tar.gz operate.log
root@VM-0-12-ubuntu:/backup# cat operate.log
[20260912_222922] 开始备份/root/scripts
[20260912_222922] 备份成功 20260912_222922.tar.gz
[20260912_222922] 清理了7天前的文件
root@VM-0-12-ubuntu:/backup# tar -ztf 20260912_222922.tar.gz
root/scripts/
root/scripts/bak_file.sh
root/scripts/dev_monitor.sh
root/scripts/clear_var_log.sh
root/scripts/nginx_ip.sh
root/scripts/service_status.sh
root/scripts/file_cnt.sh