☰
25个Linux Bash脚本实例:从文件操作到系统监控的自动化实战
2026/10/2 8:53:53 网站建设 项目流程

说到Linux运维和开发,bash脚本几乎是绕不开的一道坎。很多朋友一听到“脚本”两个字就觉得是程序员才需要学的,其实完全不是这么回事。日常批量改文件名、压缩备份、看磁盘空间、定时执行某个命令,这些重复劳动写成一个bash脚本,一次写好,后面敲两下键盘就能搞定。我自己的工作流里,脚本帮我省下的时间,累计起来足够刷好几部剧了。

这篇文章把25个最常见的Linux bash脚本例子拿出来逐个拆解,覆盖文件操作、文本处理、系统监控、备份网络这些日常最常用的场景。每个例子都给你可以直接复制运行的代码,再配上我实际操作中的踩坑记录。不管你是刚接触Linux的小白,还是已经写了几年脚本但没系统整理过的人,这25个例子都能拿来即用,或者当做一个“随时查阅的枕头书”。

1. 为什么要用bash脚本:先搞清楚它解决什么问题

1.1 脚本不是程序员的专属

有些人一听到bash脚本就头疼,觉得语法奇怪、看起来像天书。换个角度看,bash脚本其实就是把你在终端里一条一条敲的命令,提前写进一个文件里。终端上会敲ls、cd、cp,就会写脚本,只是多了一层流程控制和变量而已。

我见过很多做运维、做测试、甚至做数据分析的朋友,他们不写Java也不写Python,但靠几个bash脚本把日常工作自动化得明明白白。比如每天上班第一件事是检查服务器磁盘,以前是登录服务器敲df -h再盯着看半天,后来写成脚本,自动把超过80%的分区用红字标出来。这就是脚本最有价值的地方:把重复劳动交给机器,把判断逻辑沉淀成可复用的资产。

1.2 学完这25个例子你能得到什么

这25个例子覆盖了bash脚本最核心的知识点:变量与参数、条件判断、循环、函数、数组、文本处理、系统信息获取。学完后你会掌握三种能力:

  • 看得懂别人写的脚本,拿到新环境不至于抓瞎
  • 能把日常重复的命令组合打包成一个可执行的脚本,加参数、做判断
  • 遇到脚本报错,知道用哪些调试手段快速定位问题

这比背一堆命令要有用得多。命令是砖头,脚本是把砖头砌成墙的方式。

2. 环境准备与最基础语法:5分钟把地基打牢

2.1 第一个脚本怎么写:shebang、执行权限、运行方式

先别急着看25个例子,花5分钟把最基础的三个概念弄清楚,后面会顺畅很多。

第一个脚本内容如下:

#!/bin/bash echo "hello world"

第一行#!/bin/bash叫做shebang,它告诉系统用哪个解释器来执行这个脚本。虽然也有人写成#!/bin/sh,但我在生产环境里一律推荐写#!/bin/bash,因为bash功能更全,语法兼容性更好,避免在数组、[[ ]]这些高级特性上翻车。

保存脚本后,需要给它执行权限:

chmod +x hello.sh

然后可以这样运行:

./hello.sh

这里有一个新手特别容易踩的坑:直接输hello.sh会提示command not found。因为系统只在PATH里找命令,当前目录通常不在PATH里。所以要么加上./,要么把脚本放到/usr/local/bin这类目录下。我个人习惯是本地测试用./,正式部署时放到/usr/local/bin并去掉.sh后缀,用起来跟系统命令一样。

2.2 变量、注释、退出码三个必须懂的概念

变量是脚本的血肉。定义变量很简单:

name="张三" echo "你好,$name"

注意等号两边不能有空格,写name = "张三"会报错。取值用$name或者${name},推荐用带花括号的写法,避免变量名边界问题。比如${name}_backup,如果写成$name_backup,系统会去找name_backup这个变量,结果就是空。

注释用#开头,只注释到行尾。脚本里注释多写几行,三个月后回头看会感谢自己。

退出码是脚本和调用方沟通的语言。一条命令执行成功返回0,失败返回非0。脚本里可以用exit 1主动退出并带上错误码,配合后面的逻辑判断,能写出更健壮的脚本。判断上一条命令是否成功,最直接的就是$?:

ls /tmp if [ $? -eq 0 ]; then echo "成功" else echo "失败" fi

3. 25个常见bash脚本实例:分四类逐个拆解

3.1 文件与目录操作(例子1-7)

文件操作是bash脚本最常干的活。每天面对服务器上的日志、归档、临时文件,用好这几个例子能省下大量时间。

例子1:输出和日志

#!/bin/bash echo "开始执行任务" echo "错误信息" >&2

>&2是把内容输出到标准错误。这点很多新手忽略,但在脚本结合cron定时任务时很重要,因为标准输出和标准错误可以分别写到不同日志里,排查问题的时候一眼定位。

例子2:创建带时间戳的目录

#!/bin/bash backup_dir="/data/backup/$(date +%Y%m%d_%H%M%S)" mkdir -p "$backup_dir" echo "备份目录已创建:$backup_dir"

date +%Y%m%d_%H%M%S生成形如20250614_153001的时间戳,mkdir -p表示父目录不存在时自动创建。

例子3:遍历目录下的所有txt文件

#!/bin/bash for file in /data/logs/*.txt; do echo "找到文件:$file" done

如果/data/logs/*.txt一个都没匹配到,bash会把通配符原样传给循环,$file就会变成/data/logs/*.txt这个字符串。稳妥的做法是加一层判断:

for file in /data/logs/*.txt; do [ -e "$file" ] || continue echo "找到文件:$file" done

例子4:批量重命名备份

#!/bin/bash for file in *.conf; do cp "$file" "${file}.bak.$(date +%Y%m%d)" done

把当前目录所有.conf文件复制一份带日期的备份。这里花括号${file}是必需的,如果不写,变量名会跟后面的.bak连在一起解析,结果完全不对。

例子5:删除指定天数前的临时文件

#!/bin/bash find /tmp -type f -name "*.tmp" -mtime +7 -exec rm {} \;

mtime +7表示修改时间超过7天。-exec rm {} \;是find的标准写法,也可以用-delete代替:find /tmp -type f -name "*.tmp" -mtime +7 -delete,简洁很多。但是在删除之前,先加-print预览一遍是个好习惯。

例子6:统计目录下文件数量

#!/bin/bash count=0 for file in /var/log/*.log; do [ -f "$file" ] && count=$((count+1)) done echo "日志文件数量:$count"

更高效的方式是ls -1 /var/log/*.log | wc -l,但遇到目录名带空格的情况会出错,所以循环遍历加-f判断更稳。

例子7:递归查看目录结构

#!/bin/bash find . -type d | while read dir; do echo "目录:$dir" done

这个例子展示了while read配合管道处理逐行输出的套路,后续会在文本处理里频繁出现。

3.2 文本处理与字符串(例子8-13)

bash本身处理字符串能力不算强,但配合grep、sed、awk这些命令,整个系统都成了你的文本处理工具。

例子8:查找日志中的错误行

#!/bin/bash grep -i "error" /var/log/app.log | awk '{print $1, $2, $NF}'

awk '{print $1, $2, $NF}'打印第一列、第二列和最后一列,常用于提取时间、级别和关键信息。这里的$NF是awk内置变量,表示字段数量,也即最后一列。

例子9:提取特定字段

#!/bin/bash line="2025-06-14 15:30:01 ERROR 连接超时" # 以空格为分隔符,取第4列 echo "$line" | cut -d' ' -f4

cut -d' ' -f4指定空格为分隔符,取第4个字段。注意-d' '要传一个空格,容易手滑写成-d '',那样是错的。

例子10:字符串替换

#!/bin/bash str="hello_world_2025" # 替换第一个下划线 echo "${str/_/-}" # 替换所有下划线 echo "${str//_/-}"

${var/old/new}替换第一个,${var//old/new}替换所有。这个语法在你写批量处理脚本时非常好用,不用为了替换一个字符串去启动sed。

例子11:判断字符串是否为空

#!/bin/bash input="$1" if [ -z "$input" ]; then echo "第一个参数为空" exit 1 fi echo "参数内容:$input"

-z判断字符串长度是否为0。这里也推荐用[[ ]]代替[ ],功能更强大,比如模式匹配:

if [[ "$input" == *.log ]]; then echo "以.log结尾" fi

例子12:循环读取文件每一行

#!/bin/bash while IFS= read -r line; do echo "读取到:$line" done < /etc/hosts

IFS=表示不去掉行首行尾的空格,-r表示反斜杠不转义。这两个细节如果不注意,遇到带有特殊字符的配置文件时,行内容会悄悄变化。我自己写过很多读取列表的脚本,这个写法是唯一在各种环境都稳定工作的。

例子13:统计单词出现次数

#!/bin/bash cat /tmp/words.txt | tr ' ' '\n' | sort | uniq -c | sort -nr | head -5

这个组合命令经常被用来快速统计日志里某个词的出现频率。tr ' ' '\n'把空格换成换行,sort排序,uniq -c统计,最后sort -nr按数字降序。别背,理解每个工具的职责后,你随时能自己拼出类似管道。

3.3 系统信息与监控(例子14-19)

在服务器上做巡检、告警,bash脚本是最轻量的方案,不需要装agent,一个脚本加一条cron任务就能跑。

例子14:动态获取当前日期和时间

#!/bin/bash today=$(date +%F) time_now=$(date +%T) echo "今天:$today,当前时间:$time_now"

%F等价于%Y-%m-%d,%T等价于%H:%M:%S。注意date +%F返回的是字符串,想把它作为数字比较,比如比较哪天的目录新旧,要转成时间戳:

date1=$(date -d "2025-06-01" +%s) date2=$(date -d "2025-06-14" +%s) if [ "$date1" -lt "$date2" ]; then echo "6月1号更早" fi

例子15:获取系统负载并判断

#!/bin/bash load=$(uptime | awk -F'load average:' '{print $2}' | cut -d',' -f1) echo "当前1分钟负载:$load" threshold=2.0 if [ $(echo "$load > $threshold" | bc) -eq 1 ]; then echo "负载过高,注意检查" fi

bash不支持浮点运算,echo "$load > $threshold" | bc让bc做浮点比较,返回1为真。这个点新手很容易卡住。

例子16:磁盘使用率告警

#!/bin/bash df -h | grep -vE '^Filesystem' | while read line; do usage=$(echo "$line" | awk '{print $5}' | tr -d '%') if [ "$usage" -ge 85 ]; then echo "$line" | awk '{print "分区"$NF"使用率:"$5" 容量:"$2}' fi done

这条命令会遍历所有挂载点,找出使用率大于等于85%的分区。grep -vE '^Filesystem'把表头行过滤掉,tr -d '%'去掉百分号,然后进行数值比较。

例子17:实时监控内存使用

#!/bin/bash mem_total=$(free -m | awk 'NR==2{print $2}') mem_used=$(free -m | awk 'NR==2{print $3}') mem_rate=$((mem_used * 100 / mem_total)) echo "内存使用率:${mem_rate}%"

free -m第二行对应Mem,awk 'NR==2{...}'是取第二行。整数运算用$(( )),注意乘除的顺序,$((mem_used * 100 / mem_total))如果不加括号,bash会按从左到右算,可能得到错误精度。

例子18:检查关键进程是否存活

#!/bin/bash if pgrep -x "nginx" > /dev/null; then echo "nginx运行中" else echo "nginx未运行,尝试启动" systemctl start nginx fi

pgrep -x是精确匹配进程名。脚本里判断进程时,不要用ps -ef | grep nginx | grep -v grep这种老方法,容易误匹配,pgrep更可靠。

例子19:获取本机IP地址

#!/bin/bash ip_addr=$(hostname -I | awk '{print $1}') echo "本机IP:$ip_addr"

hostname -I会输出所有IP,取第一个通常是主IP。如果你的机器有多网卡,用ip -4 addr show eth0 | grep inet | awk '{print $2}' | cut -d'/' -f1更精准。

3.4 备份、网络与用户交互(例子20-25)

例子20:带日期打包备份目录

#!/bin/bash backup_dir="/data/backup" source_dir="/var/www/html" tar -czf "$backup_dir/html_$(date +%Y%m%d_%H%M%S).tar.gz" -C /var/www html

注意-C /var/www html的写法,先切换到/var/www,再打包html目录,这样解压时不会把多级目录结构带出来。如果是直接tar -czf xxx.tar.gz /var/www/html,那么解压后路径会变成var/www/html,后面写恢复脚本时会很痛苦。

例子21:检查网站是否在线

#!/bin/bash url="https://example.com" if curl -s --head --max-time 10 "$url" | grep "HTTP/"; then echo "网站可以访问" else echo "网站异常" fi

--max-time 10限制10秒超时,避免curl卡死。只取响应头比下载整个页面轻量得多,检查可用性完全够用。

例子22:批量ping检测主机

#!/bin/bash for host in 192.168.1.1 192.168.1.2 192.168.1.3; do if ping -c 1 -W 1 "$host" > /dev/null; then echo "$host 在线" else echo "$host 离线" fi done

-c 1发1个包,-W 1等待1秒。内网环境探测主机非常实用,把IP列表换成文件逐行读取,就是一套小型监控工具。

例子23:读取用户输入做交互菜单

#!/bin/bash echo "请选择操作:" echo "1) 备份" echo "2) 清理临时文件" echo "3) 退出" read -p "输入数字:" choice case $choice in 1) echo "执行备份" ;; 2) echo "执行清理" ;; 3) echo "再见" exit 0 ;; *) echo "无效输入" exit 1 ;; esac

read -p带提示符读取用户输入。case语法注意每个分支以)开头,以;;结束,*)是兜底分支。

例子24:脚本自动创建用户

#!/bin/bash username="devuser" if id "$username" &>/dev/null; then echo "用户已存在" else useradd -m -s /bin/bash "$username" echo "已创建用户 $username" fi

id "$username" &>/dev/null把成功和失败的输出都丢弃,只关心返回码。为脚本用户名设置默认值,再用参数覆盖,是很常见的模式。

例子25:检查脚本是否有root权限

#!/bin/bash if [ "$(id -u)" -ne 0 ]; then echo "请用root运行此脚本" exit 1 fi echo "当前是root,继续执行"

id -u返回用户ID,root是0。这个检查放在脚本开头能避免一堆半路报错。

4. 运行脚本时的常见问题与排查工具

4.1 chmod后还是报Permission denied?八成是路径问题

我自己被这个问题坑过很多次,所以先拿出来说。脚本明明加了执行权限,但运行./myscript.sh还是提示Permission denied,有这么几种可能:

  • 脚本所在的文件系统以noexec挂载,比如某些/home或/tmp。检查方式:mount | grep noexec
  • 脚本的第一行(shebang)指向的解释器路径不对,比如写了#!/bin/bash但系统里bash不在/bin而在/usr/bin。用which bash确认一下
  • 文件没有正确的执行权限,ls -l看看是不是-rw-r--r--,如果是,chmod +x即可

解决路径问题还有个通用办法:不用直接执行,改用bash script.sh。这样不需要执行权限,只要有读权限就能跑。调试阶段我经常这么干,省得反复chmod。

4.2 bash -x、set -e这些调试开关怎么用

脚本运行不如预期时,第一件事不是盯着代码看,而是用调试模式跑一遍:

bash -x script.sh

-x会在每条命令执行前把命令打印到标准错误,前面带+号。配合PS4变量自定义前缀,可以把行号打出来:

PS4='+${BASH_SOURCE}:${LINENO}: ' bash -x script.sh

这样能瞬间定位到具体行。另外几个常用开关:

  • set -e:脚本中任何一条命令失败,立刻退出脚本,避免带病继续执行
  • set -u:使用未定义的变量直接报错,而不是当空字符串处理
  • set -x:和在脚本内部开bash -x等效
  • set -o pipefail:管道命令中只要一个命令失败,整个管道返回失败

我通常写脚本时,正式环境会加set -euo pipefail三件套,但在调试写一半的脚本时会暂时去掉,因为可能一条命令没准备好,整个脚本全挂。生产脚本必须有,这是底线。

4.3 if语句的常见翻车现场

bash的if语句经常有人翻车,比如这样:

if [ $var = "yes" ]; then echo "相等" fi

如果$var是空的,命令展开后变成[ = "yes" ],直接报语法错误。解决方案是给变量加双引号:

if [ "$var" = "yes" ]; then

还有比较数字用-eq,比较字符串用=,这是两个体系,混用会报错。更推荐用[[ ]]替代[ ],双中括号是bash关键字,行为更稳定:

if [[ $var == "yes" ]]; then echo "相等" fi

==在[[ ]]里也能正常用。而且[[ ]]支持正则、模式匹配,越来越成为主流写法。注意[[ ]]不是所有sh都支持,但既然用了bash,就不需要纠结这一点。

5. 让脚本更健壮:学完例子后进阶的3个习惯

5.1 引用参数防空格分裂

看过很多脚本因为没加引号导致文件路径带空格时代码崩溃。比如复制文件:

cp $source $backup

如果$source的值是/data/my file.txt,bash会把路径拆成两个词,结果复制失败。几乎所有涉及变量的场景,加双引号都是更安全的:

cp "$source" "$backup"

而$@代表所有参数时,建议用"$@"而不是$*,前者能保留每个参数的边界。我用一个真实例子说明:假设文件名是a b.txt,脚本里用"$@"传给cat能正确读取,用$*就会爆炸。

5.2 函数化减少重复

25个例子全是平铺直叙,但实际生产脚本超过50行后,最好把逻辑拆成函数。这是我推荐的骨架:

#!/bin/bash log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } check_disk() { local threshold="$1" df -h | awk -v t="$threshold" 'NR>1 {gsub("%","",$5); if($5+0 >= t+0) print $0}' } main() { log "开始巡检" check_disk 85 log "巡检结束" } main "$@"

函数的好处是每个模块可以单独复用到别的脚本里。local声明局部变量防止变量污染。脚本从上到下先定义函数,最后统一在main里调用,结构一目了然。

5.3 日志和错误处理

脚本写完能跑只是及格,上线跑起来不出幺蛾子才叫合格。我给生产脚本统一加三样东西:

第一,日志函数,统一记录到指定文件:

log() { echo "$(date '+%F %T') $*" >> /var/log/myscript.log }

第二,关键步骤的错误检查:

if ! cp "$source" "$backup"; then log "备份复制失败" exit 1 fi

第三,临时文件去重。如果你用了$$生成PID做临时文件名,记得在脚本结束用trap清理:

tmpfile="/tmp/myscript_$$.tmp" trap "rm -f $tmpfile" EXIT

trap ... EXIT保证脚本正常退出或异常退出都会执行清理,临时垃圾文件是脚本最常见的残留物。

6. 写在最后的一些实在话

写了这么多年脚本,最深的体会是:bash脚本的门槛不在于语法难,而在于细心。一个空格、一个引号、一个换行符,都可能让脚本行为完全不同。上面这些例子都是我从真实需求里抽出来的,你可以直接复制运行,改一改路径和参数就能用。但在部署到生产环境前,务必先在测试目录跑一遍,加上set -euo pipefail,再用bash -x跟踪一遍逻辑。

最后分享一个我自己坚持的习惯:脚本一定要写版本注释。在文件头部加# Version: 1.0和# Last modified: 2025-06-14,当哪天发现脚本行为不对,能快速追溯到改动历史。别嫌麻烦,等脚本变成几十行、上百行,被同事改了三轮之后,你会感谢这几行注释。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询