ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Bash脚本入门指南:从Shell概念到实战脚本编写

Bash脚本入门指南:从Shell概念到实战脚本编写 我看不少刚入行的朋友对Linux或开发环境的第一道坎往往不在某个具体命令而是对bash本身没有概念。知道它是“终端里能输入命令的东西”但搞不清楚Shell、bash、脚本之间到底什么关系也不会写一个像样的脚本文件。这里我直接结合自己的使用经验和踩过的坑把bash脚本编写的入门思路、必要基础、常见误区以及一些可以直接上手抄的实例整理出来。这一篇先解决“最核心的那部分”——让你能自己动手写出、跑通第一个真正有用的脚本而不是停留在复制粘贴命令的水平。1. 先搞清楚bash是什么以及为什么你非学不可1.1 别把Shell、终端、bash当成同一回事很多新手会混淆三个概念终端Terminal、Shell、Bash。我见过有人把终端窗口称作bash也有人把bash理解成“一连串黑框命令”这些都是不准确的。终端Terminal是显示和输入的那个窗口程序。它本身不执行命令只负责显示字符、接收键盘输入。Shell壳是命令解释器跑在终端和操作系统内核之间负责把你输入的字符串翻译成系统调用。Bash是Shell的一种全称Bourne Again SHell是Linux发行版里最常用的默认Shell。简单类比一下终端是窗口柜台Shell是窗口背后的接线员bash就是其中最常见的那位接线员。你在终端里敲的每一条命令真正处理它的是bash。写bash脚本本质就是把这串要发给接线员的指令提前整理成一张工单按次序执行。1.2 为什么第一个脚本语言选bash而不是Python你可能会有疑问现在Python也很流行为什么还要专门学bash脚本我的观点是Python和bash的定位不同不该互相替代而该配合使用。bash天生就是操作系统的“本地人”启动服务、批量重命名、监控日志、备份文件、定时任务这些工作直接调用系统命令效率最高不需要额外装Python环境。bash零依赖几乎任何Linux服务器上都有bash写出来就能跑没有解释器版本不兼容的问题。bash擅长“把现成的命令串起来”而Python则更适合需要复杂数据结构、网络请求、数据处理逻辑的场景。所以合理的策略是操作系统层面的自动化用bash解决业务逻辑复杂的用Python。两个人各干各的互不抢饭碗。1.3 在Windows上也要了解bash的原因如果你平时用Windows开发可能会接触两道命令工具Git Bash和WSL。Git Bash提供一套模拟bash环境的工具让你在Windows里能使用大部分Linux命令比如ls、grep、awk。学习成本低适合日常文件操作和运行简单脚本。WSL适用于Linux的Windows子系统则是真正的Linux环境可以安装完整的发行版。适合需要原生Linux系统调用的场景。我的建议是入门阶段用任一工具都行重点是把bash命令的“感觉”找到。等你熟练了自然会知道自己该选哪条路。2. 动手写第一个脚本环境、文件权限与执行方式2.1 脚本文件是怎么被识别的Shebang行bash脚本本质上就是一个文本文件里面按行写命令在文件第一行加上Shebang也叫释伴行告诉系统“这个文件该用哪个解释器执行”。#!/bin/bash这行本身不是被执行的命令而是系统的“路标”标记了该脚本要用bash来运行。你也可以写成#!/usr/bin/env bash这种写法兼容性更好它会自动去环境变量里找bash的位置。需要注意一个小细节Shebang一定得是第一行前面不能有任何空行、空格或注释。否则系统会把它当成普通文件用当前Shell去执行有时候会导致诡异行为。2.2 写脚本前先检查编辑器编码问题Windows下用记事本或部分编辑器写脚本保存出来的文件默认是带BOM头Byte Order Mark的UTF-8。这个BOM头在Windows程序里无感但到Linux下bash解释器会把它当成一个字符导致第一行Shebang解析失败报错类似bad interpreter。我个人的习惯是统一用VS Code或任何你熟悉的代码编辑器把右下角编码明确设为UTF-8不带BOM再开始写脚本。Windows下写脚本的人十个里面至少有两三个栽过这个坑而且是脚本死活报错找不到原因的那种。2.3 为什么刚写完的脚本总是“权限不够”在Windows里写文本文件你可能习惯双击或直接打开。但在Linux下生成脚本文件时默认没有可执行权限。如果你直接执行会出现./hello.sh: Permission denied解决办法是给脚本加上执行权限chmod x hello.sh这个加号代表给文件增加add“可执行”权限。若是你想精确控制也可以写成chmod 755 hello.sh这是给所有用户读执行权限给属主写权限。如果你只想临时测试脚本不想改权限还有另一种方式用bash hello.sh直接调用bash解释器运行。这种方式下脚本文件不需要可执行权限相当于你主动告诉系统“帮我用bash去读这个文件”。2.4 你的第一个脚本逐步拆解我来带你写一个极简但用得上的脚本功能是输出当前时间、当前用户名和当前路径并把结果记录到日志文件里。先用编辑器创建一个文件myinfo.sh#!/bin/bash # 我的第一个bash脚本输出系统与用户信息 echo 当前时间$(date %Y-%m-%d %H:%M:%S) echo 当前用户$(whoami) echo 当前路径$(pwd) echo $(date %Y-%m-%d %H:%M:%S) 脚本执行完成 ~/myinfo.log然后按顺序执行chmod x myinfo.sh ./myinfo.sh如果一切顺利你会看到三行输出并且家目录下出现一个myinfo.log文件里面追加了一行记录。这里有两个知识点值得拆开讲$(...)叫命令替换含义是“先执行括号里的命令用它的输出来替代这一整段”。如果不加date命令就会原样显示为字符串而不是输出的时间。是追加符号意思是把内容追加到文件末尾而不是覆盖文件。如果要清空后写入用单个。3. 变量、引号与命令替换脚本的语法地基3.1 变量定义等号两边千万不能有空格bash脚本里的变量定义语法简单得让人意外但新手踩坑频率最高的也是这里name架构师注意等号两边一定不能有空格。写name 架构师会变成执行命令name参数和架构师几乎肯定会报“command not found”之类的错误。这是bash和大多数编程语言不一样的地方写多了C、Java的人最容易犯。使用变量时要在前面加$符号取它的值echo 你好$name更稳妥的写法是给变量加花括号echo 你好${name}欢迎使用bash为什么推荐加花括号因为如果写成$name好bash会理解成变量名是name好结果输出空白。加上花括号以后变量名边界一清二楚就能避免类似问题。3.2 单引号与双引号的差别90%新手意识不到的事在bash里双引号内的$、反引号、\符号仍会触发特殊含义单引号则完全是“原样输出”。我这里给一个非常直观的对比# 双引号$变量会被展开为值 price10 echo 这件商品的价格是 $price 元 # 单引号$变量不会展开显示为字符串 echo 这件商品的价格是 $price 元输出分别是这件商品的价格是 10 元 这件商品的价格是 $price 元所以在写脚本时需要记住如果你希望把变量的值、命令的执行结果拼进字符串用双引号如果你想输出一个包含大量特殊符号的文本比如正则表达式、SQL语句最好用单引号省得一个个转义。3.3 命令替换让脚本“拿到命令的输出”命令替换在脚本里极其常用它会先执行里面的命令再把这个命令的输出作为值赋给变量或嵌入字符串。now$(date) echo 现在时间是$now这种写法比先执行date命令再把输出复制粘贴进脚本要可靠百倍。因为脚本是动态获取的值不管你什么时候运行、跑了多少次都能拿到当时的真实时间。还有一种更老的写法是反引号nowdate我不建议新写的脚本继续用反引号一来嵌套困难二来可读性差。$(...)既清晰又能嵌套推荐统一使用。3.4 特殊变量脚本自己携带的隐式“参数包”写脚本时你还经常要接收外部传入的参数。bash自带一套隐藏变量用起来非常顺手变量含义$0脚本本身的名称包含调用路径$1第一个参数$2第二个参数以此类推$#参数总数$所有参数列表每个独立$*所有参数作为单个字符串$$当前脚本运行的进程ID比如你执行./backup.sh data /tmp脚本内部$1就是data$2就是/tmp$#是2。我建议脚本一开始就养成“核对参数”的习惯if [ $# -lt 2 ]; then echo 用法$0 源目录 目标目录 exit 1 fi这样比脚本运行到一半再发现少了参数要友好得多。4. 条件判断与循环让脚本学会“自己拿主意”4.1 if语句不是必须有else子句有搜索热词把“bash if语句必须有else子句吗”作为疑问这可能被其他编程语言的语法习惯影响。bash的if语句中else子句是可选的完全没必要时不必写。最常见的三种结构# 只有if if [ -f /etc/passwd ]; then echo 系统密码文件存在 fi # if else if [ -d /tmp ]; then echo 临时目录存在 else echo 临时目录不存在 fi # if elif else if [ -f /etc/os-release ]; then echo 这是一个现代Linux发行版 elif [ -f /etc/redhat-release ]; then echo 这是一个老式RedHat系统 else echo 无法判断系统类型 fi注意bash的if结构以fi结尾也就是if倒过来写这是bash和很多语言差异很大的地方我见过不少新手漏写fi导致语法错误。4.2 test命令与方括号的关系if [ ... ]里的[实际上是一个命令全称是test。所以[ -f file ]等价于test -f file。方括号里面每个参数之间必须有空格否则bash会把[-f当做一个命令名。常用的测试条件有表达式含义-f 文件判断是否为一个普通文件-d 目录判断是否为一个目录-e 路径是否存在文件或目录-z 字符串字符串长度为零-n 字符串字符串长度不为零-eq数值相等用于整数比较-ne数值不相等-lt数值小于-gt数值大于这里要特别提醒一个最坑的坑数字比较不要用或那是给字符串和重定向用的。在方括号里写[ 5 3 ]bash会把它当成“把3重定向到名为5的文件”结果永远为真且可能生成一个名为5的空文件。整数比较请用-gt、-lt、-eq这一套。4.3 for循环处理批量任务的一把好手for循环在bash里的写法非常灵活推荐掌握下面几种写法覆盖几乎全部场景。指定一组明确的值for file in a.txt b.txt c.txt; do echo 正在处理$file done生成数字序列for i in {1..10}; do echo 第 $i 次循环 done遍历目录下的文件名for file in /tmp/*.log; do if [ -f $file ]; then echo $file 是日志文件 fi done遍历命令结果for user in $(cut -d: -f1 /etc/passwd); do echo 系统用户$user donefor循环的结尾是done这和if必须以fi结尾一个道理千万别漏掉。4.4 while循环当条件满足时重复执行当你需要“直到某个条件不满足才停”时用while循环count1 while [ $count -le 5 ]; do echo 第 $count 次执行 count$((count 1)) done这里出现了一个新的运算符号$((...))它是“算术扩展”语法可以把里面的内容当作数学表达式计算。所以$((count 1))就是count加一。我还见过一个实用的场景读取文件每一行逐个处理while IFS read -r line; do echo 读取到$line done /etc/hostsIFS表示去掉行首行尾空白-r表示禁止反斜杠转义这是读取文件最安全、最推荐的姿势。5. 一个能直接用的小脚本批量备份目录理论知识讲完该有个完整的综合实例。我来写一个实用的备份脚本适合备份指定目录到目标目录并自动按日期生成压缩包。这个脚本是真实能跑的日常你完全可以改一改直接用。#!/bin/bash # 用途将源目录打包压缩到目标目录文件名带时间戳 # 用法./backup.sh 源目录 [目标目录] SOURCE_DIR$1 TARGET_DIR${2:-$HOME/backups} # 校验参数 if [ -z $SOURCE_DIR ]; then echo 错误请指定要备份的目录 echo 用法$0 源目录 [目标目录] exit 1 fi if [ ! -d $SOURCE_DIR ]; then echo 错误源目录 $SOURCE_DIR 不存在 exit 1 fi # 创建目标目录如果不存在 mkdir -p $TARGET_DIR # 生成备份文件名源目录名_日期_时间.tar.gz BASENAME$(basename $SOURCE_DIR) STAMP$(date %Y%m%d_%H%M%S) BACKUP_FILE$TARGET_DIR/${BASENAME}_${STAMP}.tar.gz # 执行打包 tar -czf $BACKUP_FILE $SOURCE_DIR if [ $? -eq 0 ]; then echo 备份成功$BACKUP_FILE echo 文件大小$(du -h $BACKUP_FILE | cut -f1) else echo 备份失败请检查tar命令输出 exit 1 fi这段代码里有几个值得留意的写法${2:-$HOME/backups}的意思是如果第二个参数没传就用默认值$HOME/backups。这是bash里很经典的缺省值语法你以后看别人的代码会频繁碰到。$(basename $SOURCE_DIR)是取旅程的最后一段路径比如/var/log/nginx会得到nginx这样文件名不会出现斜杠。$?是上一条命令的退出状态码0表示成功非0表示失败。判断成功与否用它最直接。运行方式chmod x backup.sh ./backup.sh /var/log/nginx执行后会在~/backups目录下生成类似nginx_20250115_103022.tar.gz的文件。我还建议你把这样的脚本挂到crontab里做定期备份比如每天凌晨3点执行0 3 * * * /home/用户名/backup.sh /var/log/nginx配合“备份成功后清理7天前备份”的逻辑就是一个完整的备份体系。6. 调试脚本的实用技巧别再用“肉眼找错”折磨自己6.1 用 -x 参数追踪脚本执行过程写脚本哪有不报错的关键是快速找到问题在哪一行。bash自带的调试能力比很多人想象中强。运行脚本时加-x参数bash -x backup.sh /var/log你会看到每一个命令执行前先把这个命令打印出来前缀带加号。这样能直观看到变量展开后的真实值、循环执行到第几次、哪个命令报错。如果脚本里某些关键步骤想单独追踪也可以在脚本内临时插入set -x开启和set x关闭set -x echo 这段会被详细追踪 set x echo 这段正常输出6.2 排除语法错误bash -n和shellcheckbash -n只检查语法不执行脚本。写了一大段脚本先跑一遍语法检查能提前发现很多粗心问题bash -n backup.sh如果没有任何输出就代表语法上通过了。但这不代表逻辑正确只能说明没有拼写错误、括号匹配完整。我更推荐的是装了shellcheck这个工具来检查它在几乎所有主流发行版和macOS都能安装。它的检查能力远不止语法会指出变量是否没加引号、该用[[ ]]却用了[ ]、哪些写法有歧义等。基本等于是给bash脚本配了一个私人Code Review。这里有一条实际的调试经验分享echo语句不光用来输出结果也是排查逻辑的利器。在关键变量后面加上echo 调试信息变量名$变量名能最快定位是哪一步出了问题。我见过不少同事调试半天找不到问题把几个关键变量打一通立刻看出来是参数顺序没对齐。6.3 set -e让脚本“失败即停”默认情况下bash脚本里某一条命令失败并不会让整个脚本终止后面的命令还是会继续执行。这在批量操作里很容易埋下隐患可能导致进程继续往下走做出一堆基于“失败前提”的错误操作。如果你希望脚本在任意命令失败时立刻退出可以在脚本开头加上set -e这样只要某条命令返回非零状态脚本就立刻终止。我建议所有正式运行的脚本都加上这个配置。不过也得注意set -e存在一些边界场景比如在if条件里执行的命令不受影响。所以它适合作为“兜底”而不是百分百依赖的措施关键命令还是自己判断$?更稳妥。7. 脚本风格与通用规范从“能跑”到“好维护”7.1 变量命名与脚本头部注释有人觉得脚本是一次性的随手写完就不管了。但实际工作中三年前的脚本可能还得跑到时候没有人记得里面写的是什么意思。所以脚本头部至少要留下几行注释#!/bin/bash # 功能备份指定目录并压缩 # 作者你的名字 # 更新时间2025-01-15 # 用法./backup.sh 源目录 [目标目录]变量命名方面我建议脚本级别的全局变量用全小写加下划线如source_dir常量用大写如MAX_RETRY。别用无意义的a、b、c命名一旦循环嵌套多了自己都会搞混。7.2 引号尽量加上别让变量“裸奔”在bash脚本中只要变量可能包含空格或其他特殊字符把变量用双引号包起来是习惯也是底线# 错误写法 cp $source_dir/*.log $target_dir # 正确写法 cp ${source_dir}/*.log $target_dir不加引号会把字符串按空格拆成多个词如果文件名里有空格直接导致文件找不到或者复制错对象。具体的教训我踩过太多次现在写脚本几乎见双引号就加。同理在判断语句里更要加# 如果不加引号且$name为空测试条件会变成 [ admin ]直接语法报错 if [ $name admin ]; then echo 欢迎管理员 fi7.3 用[[ ]]替代[ ]兼容性以外的好处在bash里更推荐使用[[ ]]双中括号来做条件判断。它比[ ]功能更丰富支持正则匹配~支持、||逻辑运算且即使变量为空也不会因字面解析而报错。if [[ $name admin ]]; then echo 欢迎管理员 fi if [[ $file *.log ]]; then echo 这是日志文件 fi关于兼容性这一块我要说明[[ ]]是bash内建的关键字而[ ]是POSIX标准。如果你要针对POSIX sh环境比如某些嵌入式系统、系统初始化脚本写那必须用[ ]如果脚本明确是bash环境那完全可以用[[ ]]。这个取舍没有绝对优劣按你所处的环境选择即可。7.4 函数封装重复逻辑让脚本不至于变成一坨脚本写长了反复执行的某段逻辑应该抽成函数不然每次修改都得改多处极易漏改。function log_msg { local msg$1 echo $(date %Y-%m-%d %H:%M:%S) $msg /var/log/myscript.log } # 使用 log_msg 开始备份 log_msg 备份完成这里值得注意的关键词是local它声明局部变量只在函数内部生效不会污染全局变量。我在写脚本时还有一个习惯所有函数放在文件顶部main逻辑放在底部。虽然bash不要求先声明后调用但这样阅读顺序最顺畅别人接手你的脚本会轻松很多。8. 常见报错与排查方向那些你一定会遇到的坑8.1 “command not found”的多重含义这是出现频率最高的报错但背后的原因不止一种命令真的不存在输入有拼写错误。命令存在但不在当前PATH路径里。比如你刚装了一个软件装到了/usr/local/bin但PATH没包含。脚本执行时用了sh而不是bash导致部分bash特性不识别。排查方法先用which 命令名看能不能找到再用echo $PATH确认路径最后确认脚本Shebang是否指向bash。8.2 “syntax error near unexpected token”这种报错大多是因为漏了fi或done、if和[之间少了空格、$(( ... ))括号不匹配。最快的定位方式是用bash -n做语法检查它会指出具体哪一行有问题。还有一个特殊情况Windows下编辑的脚本换行符是CRLF回车换行而Linux只认LF。CR会被bash当成命令的一部分进而报各种奇怪的语法错误。用sed -i s/\r$// 脚本名可以快速修复或者直接用VS Code右下角把换行符切换成LF。8.3 “bash: $\r: command not found”这个报错是CRLF换行符问题的“典型面孔”我一开始遇到时完全摸不着头脑。处理办法上面已经提到。平时写脚本时编辑器先统一改成LF能直接从根上避免这一系列问题。8.4 如何看这个脚本在服务器上能不能用跑脚本前先做几个体检用file 脚本名看脚本类型确认没有乱码、没有CRLF标记。用bash -n检查语法。用bash -x先小范围执行看实际行为是否符合预期。这几步做完基本能挡住九成低级错误。剩下的逻辑问题就要靠你对业务场景的理解了。9. 从这篇到底下一件该做的事这一篇侧重的是把bash从“一团模糊的命令行”变成一个你敢于动手写文件的东西。你学会了什么是bash、怎么创建并运行脚本、变量和引号、判断和循环、调试手段、规范习惯。接下来我建议你按下面的顺序继续往前走把文中的备份脚本改成适合自己目录结构的版本给日志、配置文件做备份试试。搞清楚find命令配合-exec批量操作文件这会在写更复杂脚本时频繁出现。掌握crontab定时任务把脚本变成自动运行的后台帮手。了解grep、awk、sed这三剑客在文本处理时几乎无可替代。bash脚本的进步路径不是靠看而是靠真实的问题驱动。一个人在电脑前一遍一遍调试把错误信息读懂把一个报错解决掉那种成就感会越来越强烈。下一篇我会沿着脚本进阶的路子继续到时候咱们在更具体的实战场景里见。
返回列表