我看不少刚入行的朋友,对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里,双引号内的$、反引号、\符号仍会触发特殊含义,单引号则完全是“原样输出”。我这里给一个非常直观的对比:
# 双引号:$变量会被展开为值 price=10 echo "这件商品的价格是 $price 元" # 单引号:$变量不会展开,显示为字符串 echo '这件商品的价格是 $price 元'输出分别是:
这件商品的价格是 10 元 这件商品的价格是 $price 元所以在写脚本时,需要记住:如果你希望把变量的值、命令的执行结果拼进字符串,用双引号;如果你想输出一个包含大量特殊符号的文本(比如正则表达式、SQL语句),最好用单引号,省得一个个转义。
3.3 命令替换:让脚本“拿到命令的输出”
命令替换在脚本里极其常用,它会先执行里面的命令,再把这个命令的输出作为值赋给变量或嵌入字符串。
now=$(date) echo "现在时间是:$now"这种写法比先执行date命令,再把输出复制粘贴进脚本要可靠百倍。因为脚本是动态获取的值,不管你什么时候运行、跑了多少次,都能拿到当时的真实时间。
还有一种更老的写法是反引号:
now=`date`我不建议新写的脚本继续用反引号,一来嵌套困难,二来可读性差。$(...)既清晰又能嵌套,推荐统一使用。
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循环:
count=1 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和shellcheck
bash -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脚本的进步路径不是靠看,而是靠真实的问题驱动。一个人在电脑前,一遍一遍调试,把错误信息读懂,把一个报错解决掉,那种成就感会越来越强烈。下一篇我会沿着脚本进阶的路子继续,到时候咱们在更具体的实战场景里见。