在实际运维工作中Shell 脚本不是一门需要单独学习的“语言”而是把零散命令组织成可复用工具的方式。很多转行 Linux 运维的人刚开始会在命令行里逐条敲命令看起来也能完成操作但一旦遇到批量建用户、清理日志、巡检服务器、部署多台机器就会发现重复操作既慢又容易出错。Shell 脚本正是从这个阶段走向工程化的第一步。这篇文章按“理解概念 - 准备环境 - 编写脚本 - 调试排错 - 实战落地”的顺序展开。读者学完后能看懂常见运维脚本的结构能独立编写批量操作脚本能通过sh -x定位执行问题并能在定时任务中把脚本跑起来。整篇内容围绕 Linux 运维的日常场景设计不需要先学完整的编程语言只要会基础命令就可以开始。1. 先理解 Shell 脚本在运维里的位置1.1 Shell 脚本到底解决什么问题Linux 运维每天要面对大量重复操作例如备份配置文件、批量创建用户、清理过期日志、检查服务状态、同步多台服务器文件。手工执行这些命令有两个明显问题一是效率低二是一旦某条命令输错可能对生产环境造成直接影响。Shell 脚本的本质是把多条命令按照业务逻辑组织到一个文本文件里再由解释器逐行执行。脚本里可以包含变量、判断、循环、函数也可以调用 Linux 系统命令。这样可以做到批量操作一次完成。相同操作在多个环境保持一致。操作过程可以通过日志回溯。异常分支可以用判断语句处理。在真正进入代码之前要建立一个大前提Shell 脚本不是万能的。它擅长的是文件操作、命令编排、文本处理和系统管理不适合做复杂业务系统。运维场景里90% 的需求用 Shell 脚本就可以解决如果涉及大量字符串处理、复杂数据结构和并发调度再考虑 Python 或 Go。1.2 哪个 Shell 在执行你的脚本Linux 系统里常见的 Shell 有 bash、sh、zsh、dash 等。多数发行版默认登录 Shell 是 bash而/bin/sh在部分系统里会软链到 dash。两者语法大部分兼容但也存在差异例如数组、[[ ]]判断支持程度不一致。可以通过命令查看当前环境echo $SHELLbash --version写脚本时第一行通常是固定的解释器声明#!/bin/bash这行叫 shebang。它告诉系统执行脚本时使用哪个解释器。推荐写成#!/bin/bash而不是#!/bin/sh因为 bash 在判断条件、数组、正则匹配上的支持更完整也符合大多数运维脚本的写法习惯。1.3 学习环境与生产环境的差别学习 Shell 脚本时本地虚拟机或云主机可以随意折腾。建议准备一台 CentOS 或 Ubuntu 虚拟机创建一个普通用户用于练习遇到权限问题再切换到 root 或使用sudo。生产环境完全不同。生产服务器上脚本的目录权限、执行用户、依赖命令、日志落盘位置、错误处理都需要提前规划。下面这些原则越早形成越有利不要在 root 用户下长期操作脚本需要提权时明确写sudo或单独配置权限。脚本统一放置在/opt/scripts或/data/scripts不要散落在根目录和临时目录。脚本修改后先在测试环境跑通再复制到生产环境执行。凡是涉及删除、覆盖、重启服务的脚本先增加备份和回滚步骤。2. 开始写第一个脚本前要储备的命令2.1 写脚本并不等于背诵命令Shell 脚本的基础是 Linux 命令。常见命令不需要一次记住全部但有两类必须先掌握一类是文件与路径操作另一类是文本过滤和处理。为了节省时间下面按使用频率整理一份速查表练习时优先把这些命令用熟。命令典型用途常见示例ls列表文件ls -l /var/logcd切换目录cd /opt/scriptspwd查看当前目录pwdcp复制文件cp a.conf a.conf.bakmv移动或重命名mv old.log archive/mkdir创建目录mkdir -p /data/logsrm删除文件或目录rm -f /tmp/test.txtgrep按关键字过滤文本grep error app.logsed流式编辑文本sed -i s/old/new/g fileawk按列处理文本awk {print $1} filefind查找文件find /var/log -name *.logtar打包压缩tar -zcvf backup.tar.gz /datadf查看磁盘空间df -hfree查看内存free -mps查看进程ps -efchmod修改权限chmod x script.shid查看用户信息id username不要求一次记全。实际写脚本时遇到不确定的命令先man 命令或命令 --help比硬记更有效。但要达到能独立写脚本的程度grep、sed、awk、find这四个命令值得多花时间。2.2 理解命令执行结果和退出码在脚本里判断命令是否成功经常用到退出码。Linux 约定退出码为 0 表示成功非 0 表示失败。可以这样体会ls /etc/hosts echo 退出码: $?ls /no_such_path echo 退出码: $?在脚本中可以通过和||控制命令之间的逻辑id user01 echo 用户存在 || echo 用户不存在实际脚本里更推荐用正规的if判断而不是只靠和||串联因为可读性和可扩展性更好。3. 写出并运行第一个脚本理解执行原理3.1 创建脚本文件的完整过程用一个最简单的例子走通流程。先创建脚本目录再写一个打印系统时间的脚本mkdir -p /opt/scripts cd /opt/scriptsvim first.sh脚本内容#!/bin/bash echo Hello, Linux Ops echo 当前时间是: $(date %Y-%m-%d %H:%M:%S)这里的$(date %Y-%m-%d %H:%M:%S)是命令替换会先执行date命令再把日期字符串作为结果输出。3.2 三种运行方式效果不一样脚本可以三种方式执行区别如下bash first.shchmod x first.sh ./first.shsource first.sh运行方式是否需要执行权限是否新建子进程典型场景bash first.sh不需要是快速验证脚本./first.sh需要x权限是日常执行脚本source first.sh不需要否在当前 Shell 执行加载配置或环境变量如果脚本里只执行常规命令三种方式看起来差别不大。差异主要体现在环境变量和目录切换上。执行bash script.sh时脚本在子进程中运行脚本里export的变量不会影响当前终端而source会在当前 Shell 里执行所以脚本里改变的环境变量、切换的目录会保留。验证方式echo ABC123 test_env.sh echo export MY_VARhello test_env.sh bash test_env.sh echo $MY_VARsource test_env.sh echo $MY_VAR第一次执行后MY_VAR为空第二次source后能看到hello这个对比能帮助理解子进程与当前 Shell 的区别。3.3 权限问题Permission denied 是怎么来的直接执行./first.sh时如果看到-bash: ./first.sh: Permission denied说明当前用户没有脚本的执行权限。查看权限ls -l first.sh预期的-rw-r--r--中没有任何x。修复方式chmod x first.sh然后再次执行./first.sh这里要理解一个常见误区脚本文件需要有x权限才能用./方式执行但文件内容是否可读由r权限决定。如果去掉r权限bash 解释器也无法读取脚本内容执行时会报错。注意不要把脚本放在/tmp下长期使用。/tmp目录常见清理策略会清除文件权限也容易被其他用户读写。脚本和日志都要放到专用目录。3.4 变量、位置参数与输入脚本里使用变量有两种常见场景脚本内部赋值以及从外部传入参数。#!/bin/bash server_nameweb-01 log_dir/var/log/${server_name} echo 服务名: ${server_name} echo 日志目录: ${log_dir}在字符串中引用变量时推荐写成${var}。当变量后面紧跟字母或下划线时花括号可以避免歧义。位置参数示例#!/bin/bash echo 脚本名: $0 echo 第一个参数: $1 echo 第二个参数: $2 echo 参数数量: $#执行bash param.sh nginx prod输出脚本名: param.sh 第一个参数: nginx 第二个参数: prod 参数数量: 2交互式输入使用read#!/bin/bash read -p 请输入要备份的目录: backup_path echo 开始备份: ${backup_path}read会把输入内容存入变量backup_path。真实脚本里还需要判断目录是否存在、是否有读权限这些判断放在下一部分讲。4. 分支判断和循环脚本才有业务逻辑4.1 if 判断的三种常见场景脚本执行路径并不总是直线。文件是否存在、命令是否成功、数字是否达到阈值都需要用if处理。格式if [ 条件 ]; then 命令 elif [ 条件 ]; then 命令 else 命令 fi最常见的判断场景是文件类型和存在性#!/bin/bash if [ -f /etc/hosts ]; then echo /etc/hosts 存在 fi常用文件判断参数判断表达式含义-f 文件文件存在且是普通文件-d 目录目录存在-x 文件文件存在且有执行权限-r 文件文件存在且有读权限-e 文件文件或目录存在-z 字符串字符串为空-n 字符串字符串非空-gt数字大于-lt数字小于-eq数字相等注意[ ]内部的条件中括号两侧必须有空格。写成[$a -gt 1]会报错。数字比较示例#!/bin/bash disk_usage85 if [ $disk_usage -gt 80 ]; then echo 磁盘使用率已超过 80%需要关注 else echo 磁盘使用率正常 fi4.2 case 适合做多分支选择当需要根据同一个变量的不同取值执行不同命令时case比多个if更清晰。日常写服务启停脚本时很常用。#!/bin/bash case $1 in start) echo 启动服务 ;; stop) echo 停止服务 ;; restart) echo 重启服务 ;; *) echo 用法: $0 {start|stop|restart} exit 1 ;; esaccase的每个分支以双分号;;结束*用来匹配其他所有情况。实际生产环境中的服务启停脚本会在每个分支里执行对应的systemctl start/stop/restart命令并增加服务状态检查。4.3 for 循环批量处理数据批量操作是 Shell 脚本最大的优势之一。for循环可以遍历一组数据或文件列表。遍历固定列表#!/bin/bash for ip in 192.168.1.10 192.168.1.11 192.168.1.12; do echo 检查: ${ip} ping -c 1 -W 1 ${ip} /dev/null 21 echo ${ip} 可用 || echo ${ip} 不可用 done遍历当前目录下的.log文件#!/bin/bash for logfile in *.log; do echo 找到日志文件: ${logfile} done*.log会由 Shell 自动展开成匹配到的文件名。如果当前目录没有匹配文件*.log会原样保留为字符串此时循环会执行一次处理一个不存在的文件名。实际脚本里建议先判断文件是否存在。C 风格循环#!/bin/bash for (( i 1; i 5; i )); do echo 第 ${i} 次处理 done4.4 while 和 read 逐行读取文件配置文件批量处理时经常需要逐行读取文件内容。while read是最常用的方式。先准备一个用户列表文件echo -e user01\nuser02\nuser03 /tmp/users.txt再写脚本读取#!/bin/bash while read -r username; do echo 准备处理用户: ${username} done /tmp/users.txt-r参数防止反斜杠被转义。 /tmp/users.txt把文件重定向到while循环的标准输入。这样每读一行循环体就执行一次。实际脚本中通常会再加入空行判断和注释跳过逻辑避免因文件末尾有空行而产生多余处理。5. 实战一批量创建用户脚本5.1 需求拆解批量创建用户是面试和日常运维里都常出现的场景。需求可以设计成从users.txt读取用户名。跳过空行用户已存在时跳过。创建用户并设置初始密码。要求用户在首次登录后修改密码。输出处理结果方便检查。这个需求能覆盖前面提到的while循环、if判断、id命令和chpasswd命令。5.2 脚本完整实现创建用户列表cat /tmp/users.txt EOF zhangsan lisi wangwu zhangsan EOF脚本内容#!/bin/bash USER_FILE/tmp/users.txt INIT_PASSWORDChangeme2025 if [ ! -f ${USER_FILE} ]; then echo 用户列表文件不存在: ${USER_FILE} exit 1 fi while read -r username; do # 跳过空行 if [ -z ${username} ]; then continue fi # 检查用户是否已存在 if id ${username} /dev/null 21; then echo [跳过] 用户 ${username} 已存在 continue fi # 创建用户 useradd ${username} # 设置初始密码 echo ${username}:${INIT_PASSWORD} | chpasswd # 强制首次登录修改密码 chage -d 0 ${username} echo [完成] 用户 ${username} 创建成功 done ${USER_FILE}脚本说明id ${username} /dev/null 21把正常输出和错误输出都丢弃只关心命令是否成功。chpasswd从标准输入读取用户名:密码格式适合批量修改密码。chage -d 0把密码最后修改日期设为 0用户下次登录会被要求修改密码。5.3 验证脚本先做语法检查bash -n create_users.sh语法检查没有输出说明没有语法错误。然后执行chmod x create_users.sh ./create_users.sh预期输出包含用户创建完成的信息。验证用户是否真实创建id zhangsangrep zhangsan /etc/passwd再执行一次脚本观察重复用户是否被跳过。这个验证步骤很重要能确认脚本的幂等性。注意示例里的初始密码是演示用。生产环境不要直接在脚本里写死密码可以从外部文件、环境变量或临时交互输入获取密码复杂度要符合公司安全策略。5.4 脚本完善方向上面的脚本只做了最基础的功能。放到生产环境前至少还要补充用户名合法性校验避免创建包含特殊字符的用户。执行权限控制脚本设为750只允许管理员组读写执行。日志落盘把所有操作记录到/var/log/user_add.log。密码策略对接尽量使用公钥认证或统一认证平台替代固定密码。执行前生成用户列表备份方便出问题时回滚。6. 实战二日志清理脚本配合定时任务6.1 日志文件的问题业务系统运行时间越长日志目录占用的磁盘空间越大。常见场景是应用日志按天滚动但旧日志没有自动删除最终导致磁盘写满、服务异常。日志清理需求通常可以描述为删除N天前的*.log文件同时保留目录结构。清理类脚本的核心风险在于误删所以脚本设计要以安全为先。6.2 先用 find 确认要清理的文件find命令最适合按时间过滤文件。先手动执行find /var/log/myapp -type f -name *.log -mtime 30-mtime 30表示修改时间超过 30 天的文件。把命令执行结果先打印出来不要急着删除确认文件符合预期后再进入删除阶段。日志清理脚本#!/bin/bash LOG_DIR/var/log/myapp KEEP_DAYS30 if [ ! -d ${LOG_DIR} ]; then echo 日志目录不存在: ${LOG_DIR} exit 1 fi # 先列出将被处理的文件 echo 即将清理 ${LOG_DIR} 下 ${KEEP_DAYS} 天前的日志文件 find ${LOG_DIR} -type f -name *.log -mtime ${KEEP_DAYS} -print # 正式清理 find ${LOG_DIR} -type f -name *.log -mtime ${KEEP_DAYS} -delete echo 清理完成考虑到磁盘空间可能要立即释放也可以先压缩再删除#!/bin/bash LOG_DIR/var/log/myapp ARCHIVE_DAYS7 find ${LOG_DIR} -type f -name *.log -mtime ${ARCHIVE_DAYS} | while read -r logfile; do gzip ${logfile} done压缩后文件和原来的日志文件同名但后缀为.gz磁盘占用会明显下降。如果日志需要留档先压缩再迁移到对象存储或备份服务器是更合理的方案。6.3 危险操作的自我保护任何脚本里存在删除文件、清空文件、重启服务的操作时都应该增加保护性设计。下面几条很关键使用绝对路径不要依赖当前目录。删除前打印文件名便于确认。先用echo find ...或find ... -print演练。路径变量为空时直接退出防止rm -rf被意外执行成根目录操作。示例保护逻辑if [ -z ${LOG_DIR} ]; then echo LOG_DIR 为空拒绝执行 exit 1 fi注意find -delete和rm -rf一样具有破坏性。生产环境中建议先在一个测试目录模拟旧文件验证命令效果再进入真实目录执行。6.4 用 cron 定时执行脚本日志清理脚本只有定时运行才有价值。cron 是 Linux 内置的定时任务服务。编辑当前用户的定时任务crontab -e加入一行30 2 * * * /opt/scripts/clean_old_logs.sh /var/log/clean_old_logs.log 21这行表示每天凌晨 2 点 30 分执行脚本并把输出追加到日志文件。cron 时间的五段含义段位含义取值范围第一段分钟0-59第二段小时0-23第三段日期1-31第四段月份1-12第五段星期0-70 和 7 都表示周日常见示例时间表达式执行时间*/5 * * * *每 5 分钟0 * * * *每小时整点0 2 * * *每天凌晨 2 点0 3 * * 1每周一凌晨 3 点0 0 1 * *每月 1 日凌晨 0 点定时任务配置完成后先手动执行一次脚本确认输出再等待 cron 生效。不要写完定时任务就不管日志文件里的执行记录是验证定时任务是否正常运行的主要手段。7. 脚本运行不出来按这条链路排查7.1 先做语法检查再跟踪执行脚本运行报错时不要直接盯着一行代码猜。Shell 提供了两把调试工具bash -n script.sh只检查语法不执行。bash -x script.sh逐条打印每条命令和变量的展开结果。bash -x的输出里每行前面会出现号表示当前执行的是哪条命令。变量会被展开成实际值这样很容易发现变量为空、引号丢失、路径写错等问题。示例bash -x clean_old_logs.sh输出片段 LOG_DIR/var/log/myapp KEEP_DAYS30 [ ! -d /var/log/myapp ] echo 即将清理 /var/log/myapp 下 30 天前的日志文件看到[ ! -d ...这种输出说明是[ ! -d ... ]判断在调试模式下被展开。只要输出里的路径、变量值和预期一致基本就能定位问题范围。7.2 常见报错与处理办法报错现象常见原因检查方式处理建议command not found命令不在 PATH 中或拼写错误which 命令名使用绝对路径或修正命令名Permission denied脚本没有执行权限ls -l 脚本chmod x或改用bash 脚本No such file or directory路径不存在或脚本文件是 Windows 换行ls -l 路径检查路径使用file 脚本查看编码syntax error: unexpected end of fileif、for、case没有结束符查看脚本结尾确认fi、done、esac成对出现$\r: command not found脚本是 Windows 编辑保存的 CRLF 格式cat -A 脚本执行dos2unix 脚本或sed -i s/\r$// 脚本[: too many arguments变量没加引号空格导致参数展开过多bash -x查看展开结果变量统一使用${var}7.3 字符编码和换行符问题在 Windows 上编辑脚本后上传到 Linux最常见的坑是换行符。Windows 文本文件每行以\r\n结尾Linux 只识别\n多余的回车符会被当成命令的一部分。查看脚本真实内容cat -A test.sh如果每行结尾出现^M$说明文件是 CRLF 格式。修复方式sed -i s/\r$// test.sh也可以安装并使用dos2unixdos2unix test.sh查看脚本字符编码file -i test.sh正常输出中会看到charsetutf-8或charsetus-ascii等内容。脚本里出现中文注释但编码异常时运行可能不会直接报错但注释内容会乱码影响后续维护。建议统一使用 UTF-8 编码编写脚本。7.4 排查链路的固定顺序面对一个运行失败的脚本按下面顺序排查效率最高先检查脚本是不是 LF 换行排除 CRLF 问题。执行bash -n排除语法错误。执行bash -x定位第一条执行结果不符合预期的命令。检查涉及的文件和目录是否存在权限是否足够。检查变量是否为空、是否缺少引号。检查脚本依赖的命令是否已安装。检查脚本运行用户是否有权限。查看脚本写出的日志或输出文件。其中变量是否为空是最容易被忽略的一类问题。路径字符串一旦被命令拆分轻则报错重则操作范围扩大。因此脚本里所有变量都应使用${var}这种带双引号的写法。8. 把 Shell 脚本能力变成日常习惯8.1 先形成这几个好习惯写脚本和写普通命令不一样命令敲错只影响一次脚本写错会影响后续每次执行。下面这些习惯越早建立越好每个脚本开头写清注释说明用途、作者、修改时间。脚本文件统一命名使用.sh后缀放置到固定目录。脚本开头加入set -e时要注意set -e会在命令失败时立即退出避免错误继续向下执行但也可能导致某些预期内的非零退出码被当成致命错误。生产脚本要谨慎使用。涉及删除或覆盖的操作先备份。脚本输出要有明确提示方便人工确认和排查。推荐的基础脚本模板#!/bin/bash # 脚本用途: 说明这个脚本做什么 # 作者: 自己的名字 # 修改时间: 2025-01-01 set -e BASE_DIR/opt/scripts LOG_DIR/var/log/myapp if [ ! -d ${LOG_DIR} ]; then echo 日志目录不存在 exit 1 fi echo 脚本执行开始: $(date %Y-%m-%d %H:%M:%S) # 在这里编写业务逻辑 echo 脚本执行结束: $(date %Y-%m-%d %H:%M:%S)8.2 新人最常见的学习误区一是只看不写。Shell 脚本语法不复杂但每个坑都藏在执行细节里。只看别人写的脚本很难体会变量引号、路径展开、管道子 Shell 之间的问题。建议把本文中的脚本全部手动敲一遍再改参数、加判断、故意制造错误观察报错变化。二是一上来追求凑命令。看到别人脚本里用了复杂的sed和awk就想一步到位。实际学习路径应该是先能循环、判断、读文件、写日志再逐步优化命令链。awk和sed可以等基础流程熟练后再深入学习。三是忽略验证过程。脚本写完能运行一次还不够要验证重复执行、异常输入、权限不足、目录不存在这些边界情况。真正生产环境的故障往往发生在边界条件上。8.3 下一步可以往这几个方向扩展Shell 脚本学完之后建议按顺序延伸学sed、awk的常见用法解决文本处理问题。学crontab、systemd timer把脚本变成定时任务和守护服务。学find、xargs、tar组合使用提升批量文件处理能力。学变量替换和正则匹配提高对复杂文本的处理能力。学 Python 基础语法在 Shell 脚本不够用的时候用 Python 处理更复杂的自动化任务。从面试角度看Shell 脚本的问题通常围绕批量创建用户、日志清理、服务状态检查、MySQL 备份这类场景。把本文的批量创建用户和日志清理脚本彻底理解再动手改造成自己的版本比背大量题目更有用。Shell 脚本是 Linux 运维的入门第一课但也是一门“越用越熟练”的技能。关键在于尽早脱离照着命令敲的阶段把重复操作变成脚本再通过调试和定时任务让脚本自动运行起来。走到这一步才算真正进入运维自动化的起点。