
刚接触Linux那会儿我最大的愿望就是找到一份“命令大全”之类的文档把所有命令背下来。后来真把常用命令背得滚瓜烂熟才发现自己依然干不了什么复杂的活——文件批量处理、日志分析、定时任务、一键部署哪一个单靠手动敲命令都得把人累死。直到我正儿八经开始写Shell脚本才感觉从“会敲命令”到了“会用电脑”的质变。这篇东西就是给那些还在“背命令”阶段挣扎的朋友写的我会直接拿我实际用过的脚本举例从变量、条件、循环、函数讲到调试和排错尽量让你看完能直接动手改造自己的日常工作。Shell脚本的本质很简单把多条命令写进一个文本文件然后按顺序执行。但真正让它强大的是变量、条件判断、循环、函数这些编程结构以及和Linux各种命令的深度配合。这篇文章适合完全零基础的小白也适合那些学过一点编程但没系统写过Shell的人。我尽量少扯理论多放能跑、能改、能用的实战片段。1. 内容整体设计与思路拆解1.1 为什么选择Shell而不是Python或Perl很多新人问我既然要写脚本为什么不直接用Python功能更强大语法也更现代我承认Python在某些场景下确实更好但Shell在几个方面几乎是不可替代的它不需要额外安装解释器任何Linux发行版和macOS都自带Shell而Python在某些精简环境里还得现装。它天生是命令的胶水你写Shell脚本本质上是在编排Linux命令管道、重定向、文件描述符这些概念天然就是Shell的核心换个语言反而要额外封装一层。系统管理和运维场景默认就是Shell开机启动、定时任务、容器Entrypoint、CI/CD流水线只要你去配这些东西就绕不开Shell语法。所以我的建议很直接Python要学但Shell必须会。这两个不冲突Shell负责快速、轻量、贴近系统的场景Python负责复杂逻辑和数据处理。1.2 设计一个Shell脚本的几个核心原则写了几百个脚本之后我总结了几条自己一直遵循的原则这里直接分享给你一个脚本只干一件事。把多个功能硬塞到一个脚本里换来的是调试困难、复用性差。我宁可写三个小脚本也不写一个“全能”大脚本。可读性优先于“炫技”。用if写清楚的分支逻辑别非要用 ||把代码压成一行除非那行确实简单且不会误导人。脚本要安全、要防呆。比如变量引用尽量加双引号后面我会详细讲为什么删除操作前先输出日志批量操作前先演练一遍。做好日志记录。很多脚本刚写出来没问题跑了一个月才开始出毛病没有日志你连从哪里排查都不知道。1.3 从零基础到实战的学习路径设计这套学习路径我是按自己当年摸索的经验规划的分四个阶段阶段一看懂代码。不急着写先看懂一个现成的脚本里每一行在干什么。这阶段解决的是“语法恐惧”让你意识到Shell其实没什么神秘。阶段二改代码。把别人的脚本拿来改点参数、加个判断、换个路径让脚本变成适合你的样子。阶段三独立写小工具。遇到重复劳动尝试写个几行的脚本来替代手动操作。阶段四工程化规范。用函数、变量统一管理、参数校验、异常处理把脚本当成正式项目来维护。下一节我就按这个路径从最基础的运行原理开始讲。2. 核心语法拆解与实操要点2.1 Shell脚本的执行方式与运行原理Shell脚本本质就是一个包含若干命令的文本文件。比如最简单的#!/bin/bash echo Hello World第一行的#!叫 shebang它告诉系统这个文件需要用/bin/bash这个解释器来解析。你也可以替换成#!/bin/sh指向系统默认sh、#!/usr/bin/env bash通过env查找bash路径这种方式可移植性更好。执行这个脚本有几种方式# 方式1显式用bash执行 bash hello.sh # 方式2给脚本加上执行权限后直接执行 chmod x hello.sh ./hello.sh # 方式3用source或点号执行 source hello.sh . hello.sh这里有个关键区别前两种方式会开启一个子Shell来运行脚本第三种方式是在当前Shell进程里直接运行。这意味着如果你在脚本里cd到某个目录方式1和方式2执行完后你的当前目录不会变而方式3会直接改变你的当前目录。这特性在写一些环境配置脚本时很实用。还有一点新手经常踩坑用./hello.sh执行时文件必须要有执行权限并且第一行必须有正确的shebang。如果你在Windows上用的换行符CRLF还会遇到诡异的-bash: ./hello.sh: /bin/bash^M: bad interpreter错误这时候用sed -i s/\r$// hello.sh清理一下就好。2.2 变量与字符串处理被忽略的引号问题Shell里的变量不需要提前声明直接赋值就行namezhangsan age26 echo $name is $age years old注意两点等号两边不能有空格变量引用尽量用$var或${var}其中${var}这种写法在处理变量与字符串拼接时更安全。比如prefixdata echo ${prefix}_2024.zip如果你写成$prefix_2024.zipShell会认为变量名是prefix_2024压根取不到值。这种隐晦的错误十分坑人排查时还不容易想到。引号问题是新手重灾区我见过很多同事因为引号问题删错文件。看下面这个对比filemy important file.txt # 不带引号会被拆成三个参数 rm $file # 带双引号作为一个整体参数 rm $file # 单引号和双引号的差别在于双引号会做变量展开单引号不会 echo $HOME # 输出 /home/user echo $HOME # 输出 $HOME**我的准则是涉及变量引用的、包含空格或特殊字符的一律加双引号。**只有在确实需要单词拆分或通配符展开时才不加引号。如果你写过Python可以类比成“显式优于隐式”。2.3 条件判断与退出码Linux里每条命令执行后都会返回一个退出码0表示成功非0表示失败。Shell的if判断本质上“命令执行成功与否”。if grep -q error /var/log/app.log; then echo 日志中存在error else echo 日志中不存在error fi这个写法新手往往看不习惯因为没有“等于”之类的比较符号。理解它其实很简单grep -q error 文件这就是一条命令执行成功找到了匹配就进入then分支没找到返回非0就进else分支。至于数字比较和字符串比较写法差异很大放一起对比一下# 数字比较 if [ $num -gt 10 ]; then echo num大于10 fi # 字符串比较 if [ $str hello ]; then echo 字符串相等 fi # 文件判断 if [ -f /etc/passwd ]; then echo 文件存在且是普通文件 fi # 新版语法推荐使用 if [[ $num -gt 10 ]]; then echo num大于10 fi以前老教程喜欢用单中括号[ ]但[ ]是外部命令test的语法糖里面有很多坑比如变量为空时报错、正则不支持等。强烈建议直接用双中括号[[ ]]它是bash内置的关键字支持更丰富的表达式而且没有那些历史包袱。上面这个写法在双中括号里也一样有效下面的示例我就统一用[[ ]]了。2.4 循环与批量处理实战循环是脚本自动化最提效的部分。for循环和while循环是最常用的直接看实际例子。批量重命名文件是一个几乎人人都遇到过的需求。比如要把所有.txt文件改成.md#!/bin/bash for file in *.txt; do mv $file ${file%.txt}.md done这里${file%.txt}是Shell的“去掉后缀”语法%表示从右往左匹配并删除最短匹配。我看网上的热搜词里也有“linux用shell重命名文件”这就是最简单的答案。批量重命名更复杂的场景推荐先echo出来看结果确认无误后再动真格的。遍历文件内容也是一类常事。比如要分析一个日志文件把每一行的行号和长度打印出来#!/bin/bash count1 while IFS read -r line; do echo $count: ${#line} count$((count 1)) done app.log这里IFS和-r是两个防止“行首尾空格被吃掉”和“反斜杠被转义”的保险丝。${#line}是取字符串长度的语法。 app.log做输入重定向把文件内容喂给while循环。我见过很多人写cat app.log | while read line这种写法确实能用但存在“管道会另起一个子Shell循环内赋的值在外面取不到”的坑。所以直接用重定向更稳。2.5 函数与脚本结构规范脚本一旦超过几十行函数就是必须的。函数的好处不只是“省重复代码”更是“把逻辑分层”——一个脚本的逻辑从一堆平铺的代码变成“读取配置→检查环境→执行任务→输出结果”这种清晰的结构。#!/bin/bash # 日志函数统一输出格式 log_info() { echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $* } log_error() { echo [$(date %Y-%m-%d %H:%M:%S)] [ERROR] $* 2 } check_env() { if ! command -v $1 /dev/null; then log_error 找不到命令: $1 exit 1 fi } main() { check_env curl check_env jq log_info 环境检查通过 # ... 其他业务逻辑 } # 只有直接执行时才调用main被source时不会乱跑 if [[ ${BASH_SOURCE[0]} $0 ]]; then main $ ficommand -v是用来检查命令是否存在的比which更可靠。BASH_SOURCE判断是防止别人source你的脚本时把副作用带出来。2表示把输出重定向到标准错误。写函数时还有个经验函数一定要短小一眼能看完。一个函数干一件事超过20行就考虑拆开。我在实际维护中深刻体会到短小的函数几乎不需要注释而大函数写再多的注释都救不回来。3. 实操过程与核心环节实现3.1 从零写一个真实可用的脚本批量日志分析与告警理论讲再多不如来一个完整的实战。我在这里分享一个我实际用来处理Nginx日志的脚本功能是统计访问量Top 5的IP、状态码分布并在500错误超过阈值时发出告警。先看看目标日志的一行长什么样192.168.1.23 - - [05/Jan/2025:14:23:11 0800] GET /api/user/info HTTP/1.1 200 1024 - Mozilla/5.0思路是先定义一个日志文件变量然后分别用awk、sort、uniq组合来统计最后用grep -c统计500错误数量超过阈值就发邮件这里用mailx你可以根据自己的环境换成钉钉、企业微信或者server酱的webhook。#!/bin/bash # desc: Nginx访问日志分析告警脚本 # usage: ./log_analyze.sh /var/log/nginx/access.log LOG_FILE${1:-/var/log/nginx/access.log} ERROR_THRESHOLD100 ALERT_TOopsexample.com # 检查文件是否存在 if [[ ! -f $LOG_FILE ]]; then echo [ERROR] 日志文件不存在: $LOG_FILE 2 exit 1 fi echo 访问量Top 5 IP awk {print $1} $LOG_FILE | sort | uniq -c | sort -rn | head -5 echo echo 状态码分布 awk {print $9} $LOG_FILE | sort | uniq -c | sort -rn echo error_count$(grep -c 5[0-9][0-9] $LOG_FILE) echo 5xx错误数量: $error_count if [[ $error_count -gt $ERROR_THRESHOLD ]]; then echo 触发告警阈值发送通知... # 这里调用报警工具mailx、curl 等 # echo Nginx 5xx错误数超过阈值: $error_count | mailx -s [告警] Nginx错误数超限 $ALERT_TO fi逐个拆解一下关键命令awk {print $1}提取每行第一个空格分隔的字段即IP$9是HTTP状态码。sort | uniq -c | sort -rn是先排序让相同IP排在一起uniq -c统计每项的连续出现次数再按次数倒序排列。head -5取前5个。grep -c 5[0-9][0-9] 匹配引号后面的第一个数字为5的三位数状态码注意我在前后都留了空格避免误匹配到URL里的字段。这个脚本写完后直接chmod x然后执行就能看结果。如果你想把其中某段逻辑抽出来做成一个函数比如stat_top_ip() { ... }后续可以复用形成自己的工具库。3.2 实战案例批量创建用户并初始化环境服务器上新来了一批同事要给每个人创建系统用户、设置初始化密码、建立工作目录这手工做不仅慢还容易出错。用脚本批量做就要把“可复制性”发挥到极致。假设用户列表存在一个文本文件里每行一个用户名可以先手动整理一份zhangsan lisi wangwu用户的初始化密码有个简单的生成规则用户名固定后缀比如Zs123456。注意这只是示例生产环境千万不要用这种弱密码规则建议用openssl rand -base64 12生成随机密码。#!/bin/bash # desc: 批量创建用户脚本 # usage: ./batch_create_user.sh userlist.txt USER_LIST${1:?用法: $0 userlist.txt} if [[ ! -f $USER_LIST ]]; then echo [ERROR] 用户列表文件不存在: $USER_LIST 2 exit 1 fi # 先检查是否有root权限 if [[ $(id -u) -ne 0 ]]; then echo [ERROR] 必须用root运行此脚本 2 exit 1 fi while IFS read -r username; do # 跳过空行和以#开头的注释行 [[ -z $username || $username \#* ]] continue if id $username /dev/null; then echo [SKIP] 用户 $username 已存在 continue fi useradd -m -s /bin/bash $username echo $username:Init123456 | chpasswd chage -d 0 $username # 强制首次登录修改密码 mkdir -p /home/$username/workspace chown -R $username:$username /home/$username echo [OK] 用户 $username 创建完成 done $USER_LIST echo 全部用户处理完毕这个脚本里有几个要点值得学习${1:?用法: $0 userlist.txt}如果第一个参数没传Shell会直接报错退出比手动if判断参数更简洁。id -u获取当前用户ID0是root用来做权限校验。[[ $username \#* ]]中的反斜杠是为了转义#防止被当成注释。chage -d 0是强制用户下次登录修改密码这是安全基线要求。这种脚本第一次跑完以后就再也不用担心漏建用户了。后面如果想扩展到批量删除用户、批量设置SSH密钥改起来也只是在这个框架里加逻辑。3.3 用Shell脚本实现定时备份与清理策略服务器上最刚需的自动化是什么我投“定时备份日志清理”一票。这个场景几乎每个Linux服务器都需要而且脚本本身不算复杂特别适合拿来当首个生产环境脚本练手。我的需求是这样的每晚凌晨2点备份指定目录下的数据压缩后存到备份目录。备份文件按日期命名。只保留最近7天的备份超过的直接删除避免磁盘被撑爆。#!/bin/bash # desc: 数据目录定时备份与清理脚本 # usage: 配合crontab使用建议放在 /usr/local/bin/backup_data.sh SOURCE_DIR/data/www BACKUP_BASE/data/backup KEEP_DAYS7 DATE$(date %Y%m%d_%H%M%S) BACKUP_FILE${BACKUP_BASE}/www_${DATE}.tar.gz LOG_FILE/var/log/backup.log log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* $LOG_FILE } # 创建备份根目录 mkdir -p $BACKUP_BASE log 开始备份: $SOURCE_DIR # 用tar压缩排除缓存目录和临时文件减少无用的体积 tar -czf $BACKUP_FILE \ --exclude$SOURCE_DIR/cache \ --exclude$SOURCE_DIR/tmp \ $SOURCE_DIR # 检查上一条命令是否成功失败了要立刻报错退出 if [[ $? -ne 0 ]]; then log [ERROR] 备份失败 exit 1 fi log 备份完成: $BACKUP_FILE # 清理超过保留天数的旧备份文件 find $BACKUP_BASE -name www_*.tar.gz -type f -mtime $KEEP_DAYS -delete log 已清理 ${KEEP_DAYS} 天前的旧备份然后添加定时任务修改当前用户的crontabcrontab -e # 加入这一行每天早上2点执行 0 2 * * * /usr/local/bin/backup_data.sh这个脚本里面tar -czf的--exclude选项放在源目录之前这个顺序很重要如果放错了位置排除规则可能不生效。find ... -mtime 7是按“文件的修改时间距离今天超过7天”来匹配-delete直接删除匹配文件。我从这个脚本里学到的最大教训是备份脚本一定要测试“备份文件能不能恢复”。好多人的备份脚本写了几年了、日志也看不出问题结果真到要恢复数据的时候才发现压缩包是坏的。所以后来我都会在脚本里加上“生成备份后立即解压校验一下”的步骤# 校验备份包完整性 tar -tzf $BACKUP_FILE /dev/null if [[ $? -eq 0 ]]; then log 备份包校验通过 else log [ERROR] 备份包校验失败 rm -f $BACKUP_FILE exit 1 fi不要嫌这一步多余生产环境数据恢复这件事上所有的过度谨慎都不过分。3.4 处理用户输入与参数解析脚本写多了以后你一定会有“这个脚本需要传几个参数但我想让参数更灵活”的需求。手动判断$1、$2的写法简单粗暴但到了一定复杂程度就需要用一种统一的方式解析参数。用getopts解析选项是Shell内置的成熟方案。比如写一个带-f指定文件和-v开启verbose模式的脚本#!/bin/bash # desc: 带短参数解析的示例脚本 # usage: ./demo.sh -f filename -v VERBOSEfalse FILE usage() { echo 用法: $0 [-f 文件名] [-v] echo -f 指定要处理的文件 echo -v 开启详细输出模式 exit 1 } while getopts f:v opt; do case $opt in f) FILE$OPTARG ;; v) VERBOSEtrue ;; *) usage ;; esac done # 必须的文件路径参数校验 if [[ -z $FILE ]]; then echo [ERROR] 必须指定 -f 参数 2 usage fi if [[ $VERBOSE true ]]; then echo 详细模式开启 fi echo 处理文件: $FILEgetopts字符串里的f:表示选项f后面必须跟一个参数v表示是开关型选项。这个写法虽然看起来比$1繁琐但它让你不用关心参数的顺序也不需要自己处理-f和后面值的配对关系。等脚本参数超过三个的时候你会感谢自己花时间学了getopts。4. 常见问题与排查技巧实录4.1 “脚本执行报错”快速排查思路我收到过最多的求助就是“脚本跑起来报错了”。说实话很多报错问题一眼就能看出原因这里整理一张速查表错误现象常见原因解决方法bad interpreter脚本换行符是CRLFWindows编辑过sed -i s/\r$// script.shcommand not found变量引用时没加双引号导致被拆分#!/bin/bash写错检查shebang变量加双引号Permission denied没有执行权限chmod x script.shNo such file or directoryshebang路径不对比如#!/bin/bash但bash在/usr/bin/local/bin/bashwhich bash确认路径变量赋值异常等号两边出现空格比如name xx去掉空格命令行参数带空格被拆开变量没加双引号全部改为$var排查命令错误最核心的思路是用bash -x来调试。-x选项会打印每一条实际执行的命令能清晰地看到变量展开成了什么、哪一步出错bash -x my_script.sh还有一种方式是在脚本开头加入set -x # 开启调试输出执行时打印每一条命令 set -e # 任何命令返回非0状态时立即退出防止错误被继续放大 set -u # 使用了未定义变量时直接报错避免写错变量名导致静默错误当然直接在正式脚本里加set -e要谨慎因为有些命令“非0退出”是可预期的比如grep没匹配到加上set -e后脚本会直接中断。我个人的习惯是开发脚本时用set -eux让问题暴露稳定运行后去掉-x保留-e和-u如果确实没有“预期内非0”的情况。4.2 变量值为空和空格导致的经典失误这是一个几乎所有老手都踩过、但新手还不容易意识到的坑。看这个场景#!/bin/bash DIR/path/to/dir cd $DIR rm -rf *如果变量DIR因为某些原因是空的这行命令实际执行的是cd rm -rf *cd没带参数会把你带到$HOME然后rm -rf *就会把你家目录下的文件全部删掉。这就是为什么我一直强调凡是展开变量的地方要么加双引号要么用set -u来拦截未定义变量。上面这段即使加了set -u也拦不住因为它只是“变量值正常但内容包含空格”的情况。比如DIR/path/with space/my dir cd $DIR不加引号时cd会收到两个参数/path/with和space/my直接报错或者顽固地进入错误目录。处理这类问题最省心的方案还是那句cd $DIR4.3 子Shell副作用为什么循环里的变量在外面取不到值下面这段脚本很多新手都写过#!/bin/bash count0 cat data.txt | while read line; do count$((count 1)) done echo 总行数: $count # 你以为会输出总行数结果输出0原因就是管道右边的while循环是在一个子Shell里执行的循环里对count的修改影响不到当前Shell的变量。解决方法有两个用进程替换替代管道while read line; do count$((count 1)) done (cat data.txt)或者用前面的输入重定向方式直接读文件while read line; do count$((count 1)) done data.txt理解了“管道会开子Shell”这件事很多所谓“灵异事件”就都说得通了。类似的还有在循环里cd、在函数里修改全局变量但外部看不出变化等。4.4 反引号与算术运算要避开的坑老教程里经常出现反引号包命令的写法todaydate %F反引号和$()功能类似但反引号有几个历史遗留问题嵌套时非常难写、可读性差、在部分Shell里对反斜杠的处理有隐患。所以我统一建议用$()today$(date %F)如果$()里的命令本身需要转义嵌套使用时推荐$(command1 $(command2))这种写法层级关系一眼就能看清。算术运算也类似很多人喜欢用$((expr))这个没什么问题但稍微注意一下不要和整数整除搞混。比如$((5/2))结果是2而不是2.5Shell的算术都是整数运算。要算浮点得借助bc或awkawk BEGIN {printf %.2f\n, 5/2}4.5 Shell常见的坑高频踩雷复盘写到这里我把平时见过最多的几个“老坑”集中列一下如果你是新手建议直接抄进笔记忘记给变量加双引号。这是万坑之源文件路径带空格、参数为空、通配符误展开都是它的锅。rm -rf后面跟变量时要格外小心。尤其是变量可能为空的场景建议在执行前先做一层防护判断或者用find ... -delete替代。在Windows下编辑的脚本直接上传运行。CRLF换行会带来各种奇奇怪怪的报错在服务器上先file看一下文件类型就能发现端倪。if [ $var x ]里变量为空时报错。单中括号在变量为空时会变成[ x ]语法直接错误用双中括号则没有这个问题。脚本里执行cd后脚本结束没有恢复环境。如果你的脚本会被其他脚本调用或者你自己在命令行里source它这个副作用会带到当前环境。写公用脚本最好用子Shell包一层(cd /path 命令)。把注释里的中文写成中文全角冒号。看起来没毛病某些旧的Shell环境或sh模式下会报语法错误英文标点最稳妥。sh script.sh和./script.sh可能不是同一个解释器。有些系统sh指向的是dash语法和bash有区别用#!/bin/bash标注清楚或用bash script.sh执行。4.6 调试工具与调试心态除了bash -x我常用的还有这几个bash -n script.sh只做语法检查不执行。适合在运行前快速排除低级语法问题。shellcheck静态分析工具它能检查出很多你注意不到的隐患比如未加引号的变量、不兼容POSIX的写法、潜在的命令注入风险。这是我最推荐新手从一开始就用起来的工具。set -euxo pipefail整套调试组合。pipefail让管道中任何一条命令失败都会让整个管道的返回值为非0避免“最后一层成功了前面错了但没发现”。我的调试习惯是先shellcheck扫一遍再bash -x跑一遍通过后把调试组合关掉或只用set -e最后再模拟边界情况比如文件不存在、权限不足、参数为空测一遍。这套流程下来基本能过滤掉九成以上的问题。5. 让脚本更健壮工程化思维提升5.1 参数校验与错误处理设计正式交给别人用的脚本输入校验绝对不能省略。不要觉得“我自己用参数肯定没问题”往往就是自己的随手一敲出了问题才是最难排查的。我的习惯是必选参数缺失时直接输出用法并退出非0状态码。文件类参数用-f、-d、-r、-w判断存在性与权限。对于危险操作比如删除、覆盖、批量修改加一个-y/--yes确认参数或交互式read确认避免手滑酿成事故。默认把所有可能失败的命令尤其是tar、mysqldump、cp、mv等的返回值检查一遍。#!/bin/bash # 参数和错误处理示例 set -euo pipefail SCRIPT_NAME$(basename $0) usage() { echo 用法: $SCRIPT_NAME -i 输入文件 -o 输出文件 [-f] echo -i 必选指定输入文件 echo -o 可选指定输出文件默认使用 stdout echo -f 可选强制覆盖输出文件 exit 1 } INPUT OUTPUT FORCEfalse while getopts i:o:f opt; do case $opt in i) INPUT$OPTARG ;; o) OUTPUT$OPTARG ;; f) FORCEtrue ;; *) usage ;; esac done # 校验必选参数 [[ -n $INPUT ]] || usage # 校验输入文件是否存在且可读 [[ -r $INPUT ]] || { echo [ERROR] 输入文件不可读: $INPUT 2; exit 1; } # 处理输出文件 if [[ -n $OUTPUT ]]; then if [[ -e $OUTPUT $FORCE ! true ]]; then echo [ERROR] 输出文件已存在使用 -f 强制覆盖 2 exit 1 fi echo 处理 $INPUT 并写入 $OUTPUT else echo 处理 $INPUT 并输出到标准输出 fi这个脚本的结构是一个“输入-输出-选项”的最小完整范式。以后不管写什么工具套这个骨架再往里面填业务逻辑就行。5.2 日志、颜色与可读性生产环境用的脚本日志和可读性直接影响维护效率。我的日志输出有两套普通日志写文件或标准输出错误日志同时输出到标准错误。如果需要人眼识别等级可以给关键信息加上颜色但保存到文件时要去掉颜色码否则日志文件里全是\033[31m这种垃圾字符。#!/bin/bash # 带颜色的日志函数TTY输出时带色重定向自动取消 if [[ -t 1 ]]; then COLOR_RESET\033[0m COLOR_RED\033[31m COLOR_GREEN\033[32m COLOR_YELLOW\033[33m else COLOR_RESET COLOR_RED COLOR_GREEN COLOR_YELLOW fi log_info() { echo -e ${COLOR_GREEN}[INFO]${COLOR_RESET} $*; } log_warn() { echo -e ${COLOR_YELLOW}[WARN]${COLOR_RESET} $* 2; } log_error() { echo -e ${COLOR_RED}[ERROR]${COLOR_RESET} $* 2; }[[ -t 1 ]]判断标准输出是否是终端从而决定是否启用颜色码。这个技巧在调试和日志审计时非常实用。5.3 定时任务与脚本运行的注意事项脚本写好后丢到crontab里跑也有很多门道crontab环境变量比交互式Shell少很多。如果你在crontab里运行脚本发现command not found多半是PATH没有包含对应目录。稳妥做法是在脚本开头显式定义PATH。#!/bin/bash export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bincrontab里的脚本一定要把标准输出和错误重定向到日志文件不然你根本不知道它是什么时候失败的0 2 * * * /usr/local/bin/backup_data.sh /var/log/backup.log 21脚本里建议使用绝对路径。因为在crontab环境下相对路径很可能指向的是/目录而不是你的当前目录这会导致“明明手动跑得好好的定时任务却找不到文件”的怪事。5.4 面试中的高频Shell问题给自己一次查漏补缺搜索热词里“shell面试题”排得很靠前说明Shell也是各类运维开发和测试岗位面试的常客。我把面试里经常出现的几类问题归类一下这些问题也正是检验你对Shell掌握程度的试金石变量作用域与子Shell问题写出一个在管道里赋值但管道外访问不到的示例并给出解决方案。$?的陷阱echo $?和echo $?中间如果插了别的命令退出码就不是上一条的值了。文本处理组合统计nginx日志里Top 10 IP、统计某个接口的响应码分布这是awksortuniq的经典组合必须熟练到肌肉记忆。[ ]与[[ ]]的区别除了语法差异外[[ ]]还支持正则匹配~、模式匹配很多逻辑写起来会自然很多。如何安全删除七天前的日志find /var/log -name *.log -mtime 7 -delete并说明7的含义是“超过7天”7表示正好7天。写一个死循环监控进程并自动拉起比如while true; do ...; sleep 30; done或者用nohup包一层。这些问题其实没有一个考察的是“背答案”全在考你日常积累。多写多练面试时自然会从你嘴里流出那些细节。6. 工具选型与执行效率提升6.1 编辑器与终端的选择工欲善其事必先利其器。Shell脚本写起来轻巧但对编辑器和终端还是有一定要求的。我的组合是编辑器VS Code Shell Check插件。语法高亮、括号匹配、静态检查、一键格式化对新手极其友好。如果你常年在服务器上改脚本vim的基本操作也得会至少做到打开、编辑、保存、搜索、替换。终端macOS 推荐 iTerm2Linux 推荐 Terminator 或 TilixWindows 下用 Windows Terminal WSL2。多标签、分屏、搜索是底线。版本管理脚本写多了最好纳入Git管理。尤其是那些改动频繁的生产脚本没有版本控制等于踩着钢丝跳舞。6.2 常用调试与静态检查工具前面反复提到的shellcheck是我最想让你记住的工具。安装很简单# Ubuntu / Debian apt install shellcheck # CentOS / RHEL yum install shellcheck # macOS brew install shellcheck用起来更简单shellcheck my_script.sh它不止提示错误还会给出修改建议和理由。我经常给新人说你写完脚本先过一遍shellcheck它能帮你省下80%的踩坑时间。有些问题是运行时报错你才能发现的shellcheck在运行前就扫出来了。6.3 从“会写脚本”到“维护脚本”的转变有些朋友问过我我已经会写几十行的小工具了接下来怎么提高我的建议是不断给自己提需求。比如给脚本加上参数解析、加上退出码处理、加上日志轮转、加上彩色输出、加上并发控制、加上锁文件防止重复执行。说到锁文件这是生产环境一个很常见的需求。crontab里脚本跑太久下一个任务又到了结果两个任务同时操作同一个文件就会出问题。解决办法是加一个简单的文件锁#!/bin/bash LOCK_FILE/tmp/my_script.lock if [[ -e $LOCK_FILE ]]; then echo [ERROR] 已有脚本实例在运行退出 exit 1 fi touch $LOCK_FILE trap rm -f $LOCK_FILE EXIT # ... 正常业务逻辑trap是Shell里一个很强大的内置命令它可以在脚本退出、收到中断信号时执行指定的清理动作。上面用trap ... EXIT确保无论脚本是正常结束还是异常退出锁文件都会被删掉。这个模式在备份、批量任务、定时脚本里非常常用。还有一个提升是“并发控制”。Shell本身支持后台运行和并行用启动任务、用wait等待全部完成。比如要并发压缩多个目录#!/bin/bash for dir in /data/project_*; do tar -czf ${dir}.tar.gz $dir done wait echo 所有目录压缩完成wait会等待所有后台任务结束。这个简单的并行技巧能让批量压缩从串行的几分钟降到十几秒。从我自己的经历来说Shell脚本的进步没有捷径。写小工具解决眼前的问题遇到不懂的语法先查文档用一阵子shellcheck成习惯过几个月回头看自己前半年的脚本一定会发现一堆可以优化的地方那种“原来我可以写得更好”的觉察感其实就是你在进阶了。