当前位置: 首页 > news >正文

Shell脚本实战:从基础语法到自动化运维脚本编写

Shell 脚本是 Linux 自动化运维里最基础也最高频的技能。很多新手把lscd当成 shell 的全部,但遇到批量改文件、清理日志、定时备份、服务异常拉起这些任务时,才发现自己只会手动一条条敲命令,敲完还容易漏掉某一步。这篇文章默认读者没有写过脚本,从 shell 是什么开始讲,一路写到可以放到生产环境跑的基础运维脚本,中间会解释为什么要这样写,以及脚本报错时应该怎么查。

文章会围绕一条学习主线展开:先用最小脚本理解执行流程,再掌握变量、条件、循环、函数这些骨架语法,然后通过日志清理、批量重命名、进程守护三个实战案例把语法串起来,最后处理最常见的调试和排错问题。每段都会给出可直接运行的命令和代码,读者可以用自己的 Linux 机器或虚拟机跟着做一遍。学完之后,至少能把日常重复性的运维操作改成脚本执行,并能理解别人写的常见运维脚本。

1. 先搞清楚 Shell 到底是什么,以及自动化运维为什么依赖它

1.1 Shell 不是“黑窗口”,而是一层命令解释器

在 Linux 系统里,Shell 是一个位于用户和内核之间的程序。用户每输入一行命令,Shell 负责解析命令、查找可执行程序、传递参数,并把结果返回给终端。之所以叫“Shell”,是因为它像外层壳一样包裹着内核,提供交互界面。常见的 Shell 包括 Bash、sh、Zsh、Dash 等,它们在语法细节上有差异,但在 Linux 上最通用的还是 Bash,很多发行版默认也使用 Bash。

Shell 类型常见位置特点
bash/bin/bashLinux 默认 shell,功能丰富,适合编写运维脚本
sh/bin/sh兼容性较好,很多系统上实际由 dash 提供
zsh/bin/zsh交互体验好,兼容 bash 大部分语法,常用于开发环境
dash/bin/dash轻量、启动快,通常用于系统初始化脚本

写脚本的时候,应该在文件第一行声明用哪个解释器执行,例如#!/usr/bin/env bash。这个声明叫 shebang。这里不是普通注释,而是告诉系统:如果这个脚本有执行权限并被直接运行,就使用/usr/bin/env找到的bash程序来执行它。使用env而不是写死/bin/bash,可以兼容 bash 安装在不同路径的情况。

1.2 自动化运维里的 Shell 价值:把重复动作变成可复用流程

自动化的本质不是“写一段没人看得懂的复杂命令”,而是把频繁操作固化成可重复执行、可记录、可排错的流程。日常运维中,Shell 特别适合处理三类问题:批量操作(对多台服务器或多文件做统一处理)、定时任务(每天凌晨清理日志、备份数据)、异常恢复(进程挂了自动拉起,磁盘满了发告警)。这些场景命令本身不难,难的是把边界情况处理好,比如文件不存在、权限不足、目录为空、服务名变化。

Shell 脚本正是用程序逻辑把命令串起来的胶水。它可以调用系统自带的findgrepawksed等工具,也能调用自己编写的二进制程序。如果你需要解析 JSON,可以用jq;需要处理复杂文本,可以用awksed;需要做更大的配置管理,可以上 Ansible 或 SaltStack。但无论上什么工具,最终落到底层仍然离不开命令和脚本思维。把 Shell 学扎实,后续理解自动化运维工具会轻松很多。

2. 搭建一个能安心练手的环境,再确认 Bash 版本和脚本执行权限

2.1 学习环境和生产环境怎么准备

入门阶段不需要生产机器,最少只需要一台有 Linux 环境的主机。常见做法:虚拟机装 Ubuntu、CentOS Stream、Debian;Windows 上开启 WSL;也可以购买一台低配云服务器。生产环境建议使用发行版官方维护的版本,并提前确认 Bash 版本,因为新版 Bash 对[[ ]]、数组、字符串处理支持更完整。

先把环境信息确认一遍,避免后面脚本跑出莫名其妙的问题。执行下面几条命令:

uname -a cat /etc/os-release bash --version echo "$SHELL"

uname -a查看内核版本和架构;cat /etc/os-release查看发行版名称和版本;bash --version确认 Bash 版本;echo "$SHELL"查看当前登录用户的默认 shell。如果默认 shell 不是 bash,可以手动执行bash进入,但这只影响当前终端,不影响系统其他用户。

2.2 脚本权限、执行方式和常见的 Permission denied

先创建一个练习目录和第一个脚本:

mkdir -p ~/shell-lab cd ~/shell-lab cat > hello.sh <<'EOF' #!/usr/bin/env bash echo "hello, shell" EOF

这里使用cat和 heredoc 把内容写入hello.sh<<'EOF'中的单引号表示内容里的变量不会被立即展开,保持原样写入文件。

先用bash直接执行,这条命令不要求脚本有执行权限:

bash hello.sh

如果直接改成./hello.sh执行,大概率会提示Permission denied。因为直接执行脚本时,内核需要检查文件是否有执行权限。这时需要设置执行位:

chmod +x hello.sh ./hello.sh
执行方式是否要求文件可执行执行环境推荐场景
bash hello.sh不需要新启动 bash 子进程快速测试,不关心文件权限
./hello.sh需要按 shebang 指定的解释器执行正式发布脚本,路径固定
source hello.sh. hello.sh不需要当前 shell 进程内执行加载配置、修改当前环境变量

直接执行脚本时会创建一个新的 Bash 子进程,脚本里定义的变量不会影响当前终端。使用source时,脚本是在当前进程里执行,所以变量会保留下来,这也是为什么修改~/.bashrc后要用source ~/.bashrc让配置立即生效。

注意:./hello.sh启动后会创建一个新的 Bash 进程;source hello.sh则是在当前进程里执行。前者适合隔离,后者适合改环境变量。

3. 从变量、条件判断到循环,把 Shell 脚本的骨架搭起来

3.1 变量、引号和命令替换

变量是脚本最基本的数据容器。Shell 变量名区分大小写,等号两侧不能有空格,否则会被当作命令而不是赋值。

#!/usr/bin/env bash name="alice" age=18 echo "${name} is ${age}" today=$(date +%F) echo "today is ${today}"

双引号里的变量会展开,单引号里的内容会原样输出。为了避免文件路径或字符串中包含空格导致被拆成多个参数,建议所有变量都用"${变量}"的形式引用。命令替换使用$(...)代替老式的反引号,因为$(...)可读性更好,也支持嵌套。

特殊变量含义
$0脚本名
$1,$2, ...第 1、第 2 个位置参数
$#位置参数的个数
$@所有参数,双引号引用时每个参数独立
$?上一条命令的退出码,0 表示成功
$$当前脚本进程 PID

带参数使用的例子:

#!/usr/bin/env bash echo "脚本名: $0" echo "第一个参数: $1" echo "参数数量: $#"

如果是接收多个文件,推荐用"$@"而不是$@,这样参数中的空格和特殊字符不会被打散。

3.2 条件判断:if、test 和 [[ ]]

Shell 的if不是按照“布尔表达式”来理解,而是按照“命令退出码”来理解。命令返回 0 表示真,返回非 0 表示假。因此可以把greptest等命令直接作为判断条件。

常用文件测试表达式:

表达式含义
-e 文件文件或目录存在
-f 文件存在且是普通文件
-d 文件存在且是目录
-r 文件存在且可读
-w 文件存在且可写
-x 文件存在且可执行

字符串和数值判断也有固定写法:=判断字符串相等,!=判断不等,-z判断字符串为空,-n判断非空;数值比较使用-eq-ne-gt-ge-lt-le

#!/usr/bin/env bash config="/etc/nginx/nginx.conf" if [ -f "${config}" ]; then echo "配置文件存在" else echo "配置文件不存在" fi if [ "$(id -u)" -eq 0 ]; then echo "当前是 root" else echo "当前不是 root" fi

这里要注意[本身是一个命令,后面必须有空格,条件表达式和]之间也必须用空格隔开。另一个常见问题是[ $var = "x" ]$var为空时会展开成[ = "x" ],直接报语法错误。所以变量必须加双引号。

在 Bash 中,推荐使用[[ ]]替代[ ][[ ]]是 Bash 内置的关键字,不需要额外空格也能正常工作,支持&&||、正则匹配=~,而且变量为空时不会报错。但要注意[[ ]]是 Bash 扩展,如果脚本声明的是#!/bin/sh,那么不能保证所有系统都支持。建议脚本统一写#!/usr/bin/env bash

3.3 for/while 循环和函数封装

循环用于处理重复任务。for遍历一个列表,while在条件为真时反复执行。

#!/usr/bin/env bash for i in 1 2 3 4 5; do echo "number: ${i}" done for file in *.txt; do echo "find: ${file}" done

第一个循环输出 1 到 5。第二个循环会把当前目录下所有.txt文件名展开并逐个赋值给file。这里有一个隐藏问题:如果目录下没有任何.txt文件,Shell 默认不会把*.txt当作空列表,而是把字面的*.txt当作一个普通字符串。这样脚本可能进入预期之外的逻辑。可以用nullglob选项规避:

#!/usr/bin/env bash shopt -s nullglob for file in *.txt; do echo "find: ${file}" done shopt -u nullglob

shopt -s nullglob之后,没有匹配时列表为空,循环不会执行。用完再关掉,避免影响脚本其他地方的通配符行为。

函数用来封装一段可复用的逻辑:

#!/usr/bin/env bash log_info() { echo "[INFO] $(date '+%F %T') $*" } log_info "start backup" log_info "end backup"

函数体内可以直接使用$1$2作为自己的参数,也可以使用$*表示所有参数。函数里的变量默认是全局的,如果只想在函数内部使用,用local声明:

#!/usr/bin/env bash process_file() { local target="$1" echo "处理文件: ${target}" }

3.4 用 set -euo pipefail 提前发现错误

写了多条命令的脚本,如果某条命令失败,默认 Shell 会继续执行后面的命令。这在自动化场景很容易放大故障。比如删除文件的命令失败了,脚本却继续执行后续的打包命令,最后得到一个不完整的结果。使用set -e可以让脚本在遇到非零退出码时立即退出。

#!/usr/bin/env bash set -euo pipefail
  • set -e:命令失败时立即退出。
  • set -u:把未定义变量当作错误处理。
  • set -o pipefail:管道中最后一个非零退出码作为整个管道的退出码。

比如grep找不到匹配时返回 1,如果脚本里有grep pattern file这样直接执行的命令,加了set -e后脚本会退出。但在if条件中的命令例外,因为if本身需要根据退出码分支,不会直接退出。

注意:set -e只能拦截“未处理”的非零退出码,管道中的失败需要set -o pipefail配合,函数返回值也要注意用return显式传递。

4. 用三个运维实战案例把语法串起来

4.1 案例一:清理过期日志文件

日志清理是运维最常见的需求。最简单的思路是:找出指定目录下修改时间超过 N 天的.log文件,先打印列表,确认无误后删除。

#!/usr/bin/env bash set -euo pipefail log_dir="/var/log/myapp" keep_days=30 find "${log_dir}" -type f -name "*.log" -mtime +${keep_days} -print

这条命令会列出log_dir下修改时间超过 30 天的日志文件。-mtime +30表示修改时间大于 30 天,-30表示 30 天以内,不带符号表示正好等于 30 天,容易写混。先-print打印,确认列表正确,再把-print改成-delete

find "${log_dir}" -type f -name "*.log" -mtime +${keep_days} -delete

生产环境执行删除之前,建议先加一个 dry-run 模式,通过环境变量或参数控制是否真正删除。

#!/usr/bin/env bash set -euo pipefail log_dir="${1:-/var/log/myapp}" keep_days=30 dry_run="${DRY_RUN:-1}" if [ "${dry_run}" = "1" ]; then find "${log_dir}" -type f -name "*.log" -mtime +${keep_days} -print else find "${log_dir}" -type f -name "*.log" -mtime +${keep_days} -delete fi

默认DRY_RUN=1,只打印会删除哪些文件。确认无误后,用DRY_RUN=0 ./clean_log.sh真正执行。这样能很大程度避免误删。真正部署前还要确认日志目录是否存在、是否有备份策略,以及脚本被 cron 调用时的 PATH。

4.2 案例二:批量重命名文件

批量重命名是另一个高频需求。假设一个目录里有一批report_2025.txt文件,需要把.txt改成.bak,可以用for循环加参数扩展实现。

#!/usr/bin/env bash set -euo pipefail shopt -s nullglob for file in *.txt; do mv -v "${file}" "${file%.txt}.bak" done

${file%.txt}是参数扩展,表示从文件名右侧删除匹配的.txt,然后追加.bak%删除最短匹配,%%删除最长匹配。-v参数让mv打印每一步操作,方便观察。安全做法是先把mv改成echo mv试运行,确认后去掉echo再执行。

如果文件名中包含空格,这里必须给变量加双引号。如果文件超过数百个,还要注意命令长度限制,通常用find + xargsrename工具来扩展。

4.3 案例三:监控服务进程并在异常时自动拉起

服务进程的保护通常由 systemd 或 supervisor 这类进程管理器完成,但在没有这些工具的简化环境,可以用 Shell 写一个轻量看护脚本。核心思路:用pgrep检查进程是否存在,不存在就启动。

#!/usr/bin/env bash set -euo pipefail service_bin="/usr/sbin/nginx" if pgrep -x "nginx" > /dev/null 2>&1; then echo "[OK] nginx is running" else echo "[WARN] nginx is down, try to start" if [ -x "${service_bin}" ]; then "${service_bin}" else echo "[ERROR] ${service_bin} not executable" >&2 exit 1 fi fi

pgrep -x "nginx"要求进程名精确匹配,避免把nginx-foo也算进来。> /dev/null 2>&1表示丢弃标准输出和错误输出,我们只关心退出码。实际部署中,nginx启动后会有 master 和 worker 多个进程,pgrep -x nginx可能匹配到子进程,具体要看部署方式。如果有 systemd,更推荐用systemctl is-active nginx判断。

如果担心脚本反复拉起失败进程,可以引入失败次数记录。

#!/usr/bin/env bash set -euo pipefail fail_file="/tmp/nginx_guard_fail_count" max_fail=3 if ! pgrep -x "nginx" > /dev/null 2>&1; then count=0 if [ -f "${fail_file}" ]; then count=$(cat "${fail_file}") fi count=$((count + 1)) echo "${count}" > "${fail_file}" if [ "${count}" -ge "${max_fail}" ]; then echo "[ERROR] nginx failed too many times" >&2 exit 1 fi systemctl start nginx echo "[WARN] nginx restarted, count=${count}" else rm -f "${fail_file}" fi

这里用本地文件记录失败次数,一旦连续失败超过max_fail次,脚本就退出并输出错误。生产环境更应该把日志输出到文件,并把告警接入钉钉、企业微信或其他通知渠道。

4.4 用 crontab 让脚本按计划运行

脚本写好之后,借助 crontab 做定时执行。例如每天凌晨 3 点清理日志:

crontab -e

加入一行:

0 3 * * * /opt/scripts/clean_log.sh >> /var/log/clean_log.log 2>&1

cron 的字段依次是:分、时、日、月、周。这条规则表示每天 3 点 0 分执行。后面必须有输出重定向,否则 cron 出错时很难看到日志。Cron 环境里的 PATH 通常比较小,脚本内部要尽量使用绝对路径,或者在脚本开头显式设置 PATH。

#!/usr/bin/env bash export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

5. 脚本跑不通时,按这条链路排查

5.1 常见报错和根因速查表

报错或现象常见原因排查方式处理建议
command not found命令未安装,或 PATH 不完整执行which 命令type 命令安装命令,或在脚本开头设置 PATH
Permission denied脚本没有执行权限ls -l script.shchmod +x script.sh
syntax error: unexpected end of fileif/for 缺少fi/done,引号未闭合编辑器语法高亮,检查成对关键字补齐关键字或引号
bad interpreter: /bin/bash: no such file or directory脚本第一行解释器路径不对,或文件有 CRLF 换行head -1 scriptfile scriptcat -A script修改 shebang,执行dos2unix
变量值被拆成多个单词变量没有加双引号bash -x script.sh观察实际执行命令所有变量用"${var}"包裹

排查顺序建议:先看输入,包括文件路径、参数、环境变量;再看文件权限和解释器;然后看依赖命令是否存在;最后用bash -x逐行跟踪。不要刚看到报错就去改代码,先确认当前脚本到底是在什么环境、什么用户下执行的。

5.2 用 bash -x 观察每一步执行

最有效的调试方法是让 Bash 打印每条命令展开后的结果:

bash -x clean_log.sh

输出中每行前面有+号,表示该行命令展开后的实际内容。如果看到变量为空,说明变量赋值或参数传递有问题;如果看到某条命令报错但没有退出,则需要检查是否设置了set -e,以及是否在管道中执行。

也可以在脚本内临时加set -x,调试完删掉。使用echo输出关键变量是笨办法但很直接。注意不要在大量循环里频繁echo,避免输出淹没日志。

for file in *.txt; do echo "DEBUG file=${file}" # 具体处理逻辑 done

查完变量,再看退出码。在命令后面执行echo $?0表示成功,非 0 表示失败。注意管道后的$?默认是最后一个命令的退出码,所以配合set -o pipefail更可靠。

5.3 引用、空格、换行符和通配符的坑

Shell 脚本最常见的错误发生在“展开”阶段。不写双引号会把带空格的路径拆成多个参数,if [ $var = "x" ]在变量为空时会变成if [ = "x" ],直接报错。解决方案是始终给变量加双引号:[ "${var}" = "x" ],或者使用[[ ]]

Windows 下编辑过脚本后传到 Linux,可能出现bad interpreter$'\r'之类的问题,因为文件末尾多了\r。检查方式:

file hello.sh cat -A hello.sh

如果看到^M$,说明文件是 CRLF 换行。转换方式:

dos2unix hello.sh # 或者用 sed sed -i 's/\r$//' hello.sh

通配符*在没有匹配时会保留原样,配合shopt -s nullglob可以避免。如果文件名以-开头,命令会误认为选项,需要使用--分隔,例如rm -- -file.txt

6. Shell 脚本的工程化实践与检查清单

6.1 写一个可维护的脚本骨架

一个适合长期维护的脚本,至少应该包含:解释器声明、用途说明、set -euo pipefail、变量定义、主流程、日志函数、退出码。参考骨架如下:

#!/usr/bin/env bash # # 脚本名称: backup_log.sh # 用途: 备份并清理指定目录的日志文件 # 使用: ./backup_log.sh /var/log/myapp 30 # set -euo pipefail log_info() { echo "[$(date '+%F %T')] $*" } die() { echo "[ERROR] $*" >&2 exit 1 } log_dir="${1:-}" keep_days="${2:-30}" if [ -z "${log_dir}" ]; then die "用法: $0 <log_dir> [keep_days]" fi if [ ! -d "${log_dir}" ]; then die "目录不存在: ${log_dir}" fi log_info "开始清理: ${log_dir}, 保留 ${keep_days} 天" find "${log_dir}" -type f -name "*.log" -mtime +"${keep_days}" -print log_info "执行完成" exit 0

这个骨架没有直接删除文件,适合作为模板扩展。die函数把错误信息输出到标准错误,并返回非零退出码。正式使用前要确认find-mtime参数、日志文件命名规则、是否有历史备份等。

6.2 安全和性能建议:引用、eval、临时文件、凭据

脚本越接近生产,越要注意安全和副作用。

不要使用eval拼接用户输入,这很容易引发命令注入。如果非用不可,要仔细校验输入。其次,变量必须加双引号,尤其是把用户输入作为文件路径或参数时。第三,临时文件要用mktemp创建,而不是使用固定路径,并配置trap清理:

tmp_file=$(mktemp) trap 'rm -f "${tmp_file}"' EXIT

敏感信息如数据库密码,不应该硬编码在脚本里,可以读取权限受限的配置文件,或使用密钥管理工具。脚本给他人使用前检查是否有chmod 777、是否包含明文密码。

性能方面,避免在循环里重复调用外部命令。例如统计文件行数,不要对每个文件都启动一次wc,先收集文件列表,再用xargsawk统一处理。Shell 适合做调度和胶水,复杂数据处理交给awksedjq或 Python。

6.3 发布前检查清单和下一步学习方向

下面这份检查清单可以直接用于脚本上线前走查:

  • 脚本开头是否有#!/usr/bin/env bash
  • 是否设置set -euo pipefail,并明确已知例外。
  • 使用到的命令是否都已安装,PATH 是否完整。
  • 所有变量是否都加了双引号。
  • 是否处理了空目录、文件不存在、权限不足等异常情况。
  • 删除、移动、覆盖等破坏性操作是否有 dry-run 或人工确认。
  • 是否打印了可读日志,是否记录退出码。
  • 定时任务是否把标准输出和错误输出重定向到日志。
  • 脚本是否包含明文密码或敏感信息。
  • 是否在测试环境执行过,是否验证过失败恢复。

学完基础语法,可以继续深入:掌握正则表达式和awksed处理文本;用jq解析 JSON;用findxargs构建复杂文件操作;了解 systemd 的 Timer 代替 crontab;再往后可以理解 Ansible、SaltStack 这类自动化工具,你会发现它们底层大量依赖 Shell 命令,但把状态管理和幂等性封装好了。Shell 是进入自动化运维领域性价比最高的第一门语言,先把这里面的变量、条件、循环、函数和调试方法磨熟,后面学任何运维工具都会更快。

http://www.cnnetsun.cn/news/4319558.html

相关文章:

  • 老CPU无SSE4.2?用Wine和替代方案让微信在Linux上跑起来
  • 从零开始学Java:一份面向初学者的学习路线图
  • 高级 RAG 架构演进:GraphRAG、自适应检索与多模态检索实战
  • 基于STM32的智能控温水杯设计:硬件、PID与低功耗全解析
  • 相机标定原理与实操:从张正友标定法到OpenCV畸变校正
  • 广工809信号与系统考研:从章节到得分点的高效复习法
  • GDScript Lambda表达式:从匿名函数到高阶回调的Godot实战指南
  • 游戏角色语音整理实战:从切片、识别到本地检索API全流程
  • STM32入门实战:OLED贪吃蛇与摇杆控制完整教程
  • Replit 全面解析:从在线 IDE 到云开发与一键部署平台
  • ffmpeg+Demucs+Whisper:现场音乐素材人声分离与字幕生成实践
  • STM32小车电机驱动实战:L298N接线与PWM调速全解析
  • RAG技术实战:从原理到代码,构建企业知识库问答系统
  • 信号与系统考研公式不用死记:理解三大变换,构建公式调用链
  • 海尔舒适风Pro 3匹立式柜机深度评测:选购、安装与验收全攻略
  • 夜鹰EA策略包解析:夜间图表形态与摆动交易实战指南
  • 192、【Agent】【OpenCode】TuiThreadCommand handler:从参数到 Worker 就绪
  • Java面试前需要系统梳理的五个核心知识点
  • 深入浅出TinyML 23:代表性数据集和量化感知训练分别解决什么问题?
  • 本地大模型跑不快?MacBook Pro 推理性能瓶颈与优化实践
  • 长时程AI任务评测:顶尖模型仅达人类27.3%的原因与实现
  • 苹果生态私密通讯与工作空间实战:Xcode构建、同步与排错指南
  • Claude Code 科研实战:安装配置、模型接入与数据科学全流程
  • Codex安全实践指南:从安装配置到运行监控的完整防护
  • 工厂排班临时调整怎么选考勤系统?6家厂家横向对比
  • 吴恩达Vibe Coding教程:从大模型入门到AI自动写代码实战
  • 24南昌大学811信号与系统真题解析:高频考点与备考策略
  • 零基础转行软件测试:从知识体系到项目实战的完整攻略
  • Maya下载安装保姆级教程:零基础到成功运行全流程
  • 基于SpringBoot的甜品商城购物网站的设计开发:技术栈、背景意义与核心代码