刚接触 Linux 的时候,我第一个想搞明白的事情就是“怎么把一串命令存下来,下次一键执行”。那时候看同事在终端里敲几下就自动完成一堆部署操作,羡慕得不行。后来才知道,那不是什么黑魔法,就是 Shell 脚本,说白了就是把命令按顺序写进一个文件里,然后让 Bash 帮你批量执行。
这篇教程就是给真正零基础的人准备的。我带你从“什么是 Shell 脚本”开始,一路写到能创建、赋权、运行、调试自己的 Bash 脚本,中间会穿插大量我实际踩过的坑和一些藏在文档之外的小技巧。适合刚装好 Linux 虚拟机、想提升工作效率的开发者,也适合正在准备面试、需要系统补一遍脚本基础的朋友。
1. 动手之前,先搞清楚 Shell 脚本到底是什么
很多教程上来就让你vim test.sh,然后写个echo就跑,结果读者一头雾水。咱不那样,先花几分钟把基础概念捋顺,后面的路会好走很多。
1.1 内核、Shell 和终端的关系
你打开终端窗口,看到的那个黑色界面叫终端模拟器(比如 GNOME Terminal、Konsole),它只是个门面。真正的“壳”是 Shell,它是用户和 Linux 内核之间的翻译官。你敲的每一条命令,都要经过 Shell 解释,翻译成内核能理解的系统调用,内核才去操作硬件来完成工作。
Bash(Bourne Again Shell)是目前几乎所有 Linux 发行版默认的 Shell,也是我们这篇教程的主角。你可以敲echo $SHELL看看当前用的什么,绝大多数人输出的都是/bin/bash。除了 Bash,还有 Zsh、Fish、Sh 这些,但万变不离其宗,Bash 学会了你就是走到哪儿都能上手干活。
1.2 什么是 Shell 脚本,它解决了什么问题?
Shell 脚本就是把多条要在终端执行的命令,按逻辑顺序写进一个纯文本文件里。它可以包含普通命令,也能包含变量、条件判断、循环、函数、算术运算,可以说是“揉进了编程语法的命令清单”。
为什么要用脚本?我打个比方:你每天上班都要干的固定流程——打卡、开电脑、打开浏览器、打开开发工具、拉取最新代码。如果你把这一串动作录成一个脚本,一条命令全搞定,每天能省下几分钟。放到服务器运维里,部署应用可能要敲几百条命令,一条条手工敲又慢又容易出错(敲错一个字符可能就崩了),写成脚本,一次执行、随时复用,效率和准确性直接天翻地覆。
1.3 Bash 脚本与直接敲命令的区别
直接敲命令就像打电话时一字一句地说话,脚本则像提前写好一封信。区别在于:
- 可复用性:脚本是文件,存着不会丢,下次直接跑;
- 可维护性:改了文件再跑一遍,不用重敲所有命令;
- 逻辑能力:终端里敲命令只能一条条来,脚本里可以用
if、for、while控制执行流程,有判断、有循环; - 自动化能力:脚本可以被定时任务(cron)调用,到点自动执行,不用人守着。
2. 创建你的第一个 Bash 脚本:全流程实操
现在开始动真格的。我以一个“查看系统状态”的脚本为例子,顺带把目录切换、文件编辑这些基本功也过一遍。
2.1 准备工作:打开终端,确认环境
先打开终端,执行bash --version确认 Bash 可用。如果提示找不到命令,那你的系统可能比较特殊,用包管理器装一个就行(Debian/Ubuntu 用sudo apt install bash,RHEL/CentOS 用sudo yum install bash)。不过实话说,Linux 上没装 Bash 的概率极低。
建议先建一个专门放练习脚本的目录:
mkdir -p ~/shell_practice cd ~/shell_practicemkdir -p的意思是“创建目录,如果父目录不存在就一并创建”,cd是切换目录。这俩命令以后你会天天用。
2.2 用 vim 创建脚本文件:遇到 vim 别慌
创建文件的方式很多,touch能建空文件,cat也能重定向写入,我推荐直接用 vim/nano 一步到位。Vim 初次上手容易懵,记住核心三步就行:
vim mysystem.sh按i进入插入模式(左下角会出现 INSERT 字样),然后输入以下内容:
#!/bin/bash # 第一个脚本:查看系统基本信息 echo "当前用户: $(whoami)" echo "当前目录: $(pwd)" echo "系统运行时间: $(uptime -p)" echo "内存使用情况:" free -h echo "磁盘使用情况:" df -h | grep '^/dev/'输完后按Esc退出插入模式,输入:wq回车保存退出。:w是写入,:q是退出,连起来就是“保存并退出”。如果你中途改主意不想存了,:q!是强制不保存退出。
2.3 脚本第一行#!的讲究
每个 Bash 脚本第一行基本都是#!/bin/bash,这一行叫shebang(也叫释伴行)。它告诉系统:“这个文件要用哪个解释器来执行。”你写#!/bin/bash,系统就调用 Bash 执行这个文件;写#!/usr/bin/env python3,就调用 Python 3。
有人会问,写作#!/usr/bin/env bash和#!/bin/bash哪个好?env的好处是它会去PATH(环境变量)里找 bash 的位置,兼容性更好;直接写路径则少一层查找,速度略快。在绝大多数 Linux 发行版上,两者效果一样,新手用#!/bin/bash就够了。
2.4 即时执行 vs 给文件加执行权限
写完脚本后,最直接的方式是交给 Bash 解释器执行:
bash mysystem.sh这样不用管文件有没有执行权限,因为你是显式调用 Bash 来读取并执行这个文件。对应地还有另一种方式:
chmod +x mysystem.sh ./mysystem.shchmod +x给文件加上执行权限,然后通过./前缀执行当前目录下的脚本。刚才我一直强调要加./,这是个新手特别容易踩的坑——如果你直接敲mysystem.sh,系统会报command not found,因为它只会在PATH环境变量指定的目录里找命令,不会搜索当前目录(这是安全设计,不给当前目录特殊待遇,防止恶意脚本被执行)。./就是明确告诉 Shell:“在当前目录下找这个文件”。
两种执行方式的选择,我的建议是:调试期用bash方式,方便且不用管权限;正式使用时给它加执行权限并用./执行,这样脚本更像一个“真正的程序”,也能让 shebang 行起到作用。
2.5 三种执行方式对比:bash、source、执行权限
这里再展开讲一个细节。执行脚本常见的有三种方式,很多人混淆:
| 方式 | 命令示例 | 是否开启新子Shell | 能否修改当前Shell的变量 |
|---|---|---|---|
| 直接执行 | ./script.sh | 是 | 否 |
| Bash执行 | bash script.sh | 是 | 否 |
| Source执行 | source script.sh或. script.sh | 否 | 是 |
前两种都会启动一个新的子 Shell 去跑脚本,脚本里设置的环境变量、切换的目录只对子 Shell 生效,脚本跑完就没了。source则是在当前 Shell 进程内直接执行,脚本里的cd真的会改变你当前的目录,定义的变量也会留在当前 Shell 里。
这个特性特别有用。比如你改了.bashrc(Shell 启动时加载的配置文件),直接在当前会话里让它生效的经典操作是:
source ~/.bashrc如果这时候笨笨地执行~/.bashrc(并且它有执行权限),大概率不会生效,因为它在子 Shell 里跑了,改不了当前会话。
3. 把脚本写“活”:变量、输入与命令行参数的实战用法
只学echo那不叫编程。一个能解决问题的脚本,必然能接收输入、保存中间结果、根据情况执行不同分支。这一节是 Bash 脚本的核心语法地带,我挑最常用的讲。
3.1 变量:定义、引用和常见坑
Bash 里定义变量非常随意:
name="张三" echo "你好, $name"注意两点。第一,等号两边不能有空格,name = "张三"是错的,Shell 会把name当成一个命令去执行。第二,引用变量加$,最好用花括号包住边界,比如:
fruit="apple" echo "我最喜欢 ${fruit}s"如果你写成$fruits,Shell 会去找名叫fruits的变量,结果当然是空的。加花括号是最安全的习惯,尤其是变量后要紧跟着其他字符时。
3.2 位置参数:像命令一样接收输入
脚本可以像命令一样接收参数。$1是第一个参数,$2是第二个,$0是脚本本身的名字,$#是参数个数,$@是所有参数列表。写个示例:
#!/bin/bash echo "脚本名: $0" echo "第一个参数: $1" echo "第二个参数: $2" echo "参数总数: $#"保存运行:
bash args_demo.sh hello world输出:
脚本名: args_demo.sh 第一个参数: hello 第二个参数: world 参数总数: 2有个正经的场景:你想写个备份脚本,备份路径和备份文件名每次不一样,那就让它接收参数:
#!/bin/bash # usage: ./backup.sh /home/user/data /backup src_dir="$1" target_path="$2" tar -czf "$target_path.tar.gz" "$src_dir" echo "备份完成: $target_path.tar.gz"这样你就不需要为每个备份任务改代码,传参就行。
3.3 与用户交互:read 命令
有时候脚本需要现场问用户问题,用read:
#!/bin/bash read -p "请输入你的名字: " username echo "欢迎你, $username"-p是 prompt 的缩写,后面跟提示文本,读到的值存进username变量。这在写交互式安装脚本时非常常用,比如确认“是否继续?[y/N]”。
3.4 条件判断 if:让脚本有“脑子”
Bash 里的条件判断写法略微古怪,容易让新手吓一跳。基础结构是:
if [ 条件 ]; then 分支1 else 分支2 fi注意:[和]两边必须有空格,then前要有分号或换行,最后必须用fi结尾(倒着写的 if)。完整的例子:
#!/bin/bash if [ -f "/etc/os-release" ]; then echo "这个系统有 os-release 文件" else echo "没找到 os-release 文件" fi-f是“是不是普通文件”的测试条件,还有一堆常用的测试操作符,我这儿列几个现在就能记住的:
-f 文件:是否是普通文件-d 路径:是否是目录-e 路径:路径是否存在-z 字符串:字符串为空-n 字符串:字符串非空字符串1 = 字符串2:判断相等数字1 -gt 数字2:数字大于(-lt 小于,-eq 等于)
条件还是多条件组合的话,用&&(与)和||(或),比如:
if [ -d "$dir" ] || [ -w "$dir" ]; then echo "目录存在或可写" fi3.5 for 循环:处理列表数据的利器
这是被问得最多、也是自动化里最核心的内容之一。比如要批量给一组服务器目录建日志文件:
#!/bin/bash for server in web01 db01 cache01; do echo "正在处理: $server" mkdir -p "/var/log/${server}" done循环遍历当前目录下的所有.txt文件:
for file in *.txt; do echo "找到文件: $file" done经典的“1 到 10”循环(用到大括号展开):
for i in {1..10}; do echo "第 $i 次" done还有 C 语言风格的 for 写法:
for ((i=1; i<=10; i++)); do echo "count: $i" done我在实际中用的最多的场景是“批量重命名文件”“批量为多个 IP 发 ping 探测”“批量检查远程端口”。
3.6 while 循环和函数:进一步抽象逻辑
while适合你不知道具体循环次数、靠条件来控制的场景:
counter=1 while [ $counter -le 5 ]; do echo "计数器: $counter" ((counter++)) done((counter++))是算术语法,让变量自增 1。新手经常忘记 Bash 的算术运算要用$(( ))或(( ))包裹,直接写counter=$counter+1会得到字符串拼接而不是相加。
函数则能把常用逻辑包起来,让脚本更清晰:
#!/bin/bash function log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" } log_info "开始执行" log_info "完成执行"这里$1是函数参数(不是脚本参数),date命令配合+%Y-%m-%d %H:%M:%S格式化当前时间,输出的日志自带时间戳,排查问题会很舒服。
4. 脚本跑不起来的那些糟心事:常见 Bug 和排查心得
这一节是大家最喜欢的“排雷”环节。说真的,我见过太多人脚本逻辑写对了,却被一些奇奇怪怪的小问题卡一下午,最后发现是个不可见字符的锅。
4.1 经典错误:/bin/bash^M: bad interpreter: No such file or directory
这个错误极其经典。你在 Windows 上用记事本编辑了脚本,然后传到 Linux 上执行,大概率就报这么一句。原因是 Windows 的换行符是\r\n,Linux 的是\n,脚本第一行变成了#!/bin/bash\r,系统找不到叫bash\r的解释器。
解决方案:
sed -i 's/\r$//' yourscript.sh这行命令会把每行末尾的\r清理掉。-i是原地修改文件,s/\r$//是替换掉行尾的\r。要是你经常要处理 Windows 和 Linux 之间的文本转换,装个dos2unix工具一键转换也行:
sudo apt install dos2unix dos2unix yourscript.sh4.2 报错command not found,但你明明装了
这个分两种情况。第一种,脚本里调用的命令确实不在PATH环境变量里。比如你刚刚自己编译安装了一个软件,它的可执行文件放在/usr/local/bin,而PATH没包含这个目录,自然找不到。查看 PATH:
echo $PATH临时添加:
export PATH=$PATH:/usr/local/bin永久生效就把这行写进~/.bashrc再source ~/.bashrc。
第二种情况是执行crontab -e却报-bash: crontab: command not found。这有两个可能:一是你装的系统是精简版,连 cron 都没装(Debian/Ubuntu 用sudo apt install cron,CentOS/RHEL 用sudo yum install cronie);二是你登录的用户 PATH 设置不完整。如果安装完还是找不到,可以排查一下是不是 PATH 的问题,用绝对路径/usr/bin/crontab试试。类似的还有lsusb: command not found——最小化安装的系统,连usbutils都没装,sudo apt install usbutils就能解决。回头想想“真的没装”还是“PATH 里没有”,这个排查思路能救你好多次。
4.3 权限被拒:Permission denied
这个报错很好懂,就是文件没有执行权限。解决:
chmod +x yourscript.sh如果你非要用bash yourscript.sh来跑,那不需要执行权限也可以,因为只是把脚本文件当作 Bash 的输入数据来读取。
4.4 脚本“不生效”:source 和执行的坑
前面讲过的老问题,很多人执行了修改 PATH 的脚本,发现当前 Shell 里 PATH 没变,以为是脚本写错了。其实只是因为执行方式不对——记得用source或.来执行修改环境变量的脚本。这是 Bash 初学者第一个“灵异事件”,我当年也卡过一晚。
4.5 中文乱码:为什么 grep 出来是花的?
解压一个 Windows 那边打包的 zip,里面的中文文件名和文件内容全是乱码。原因同样是编码差异:Windows 常用 GBK/GB18030,Linux 默认 UTF-8。处理办法:
# 乱码文件名 ls -b # 解压时指定编码 unzip -O GBK files.zip内容乱码就需要注意文件编码,用iconv -f GBK -t UTF-8 input.txt > output.txt转换。这个坑在我处理从客户那儿传来的“古董压缩包”时几乎每次都会踩。
4.6 玄学“不报错也不干活”:检查路径和引号
有时候脚本没任何报错,但结果就是不对。我踩得最多的是引号问题。路径里有空格的话,不加引号就会被拆成多个参数。比如:
file_path="/home/user/My Documents/data.txt" cat $file_path # 错误!被拆成3个参数 cat "$file_path" # 正确!整体作为一个参数所以,涉及路径和参数传递时,我习惯性地给变量加上双引号。这个习惯能消灭掉九成以上的“幽灵 bug”。
5. 进阶实用技巧:让脚本更专业、更健壮
如果你已经能把前面那些语法组合起来写小工具了,这节的内容能让你的脚本从“能跑”升级到“能拿得出手”。
5.1 调试利器:bash -x
脚本跑不出来、不知道卡在哪一步?用调试模式跑一下:
bash -x mysystem.sh它会逐行显示每一条实际执行的命令和展开后的变量值,前面带个+号,相当于代码里自动给你打印日志。比如变量传着传着变空了,一眼就能看出来。还有个偷懒技巧,在脚本第一行 shebang 后面加个-x:#!/bin/bash -x,以后直接执行也自动开调试。调试完毕记得改回来。
5.2 用 set 命令提前挡坑
脚本默认是很“宽容”的:某条命令执行失败,它不会中断,还会继续往下跑。这在很多场景下会引发连锁问题——上一步失败了,下一步却基于失败的结果继续。推荐在脚本开头加这两行:
set -e # 只要任何命令返回非0退出码,脚本立即终止 set -u # 使用未定义的变量直接报错,而不是当空值处理set -e相当于“出问题就停下来别硬跑”,set -u能帮你抓到拼错变量名的低级 bug。再加一个set -o pipefail,能让管道命令中任何一环失败时整体返回失败。我的所有正式脚本开头都是这三行。
5.3 写日志、做备份:构建你的工具库
随着你写的脚本越来越多,你会发现有些代码段反复出现。我建议建一个自己的脚本工具库,把“打印带时间戳的日志”“优雅退出函数”“检查依赖命令是否存在”这些通用函数放进去,需要用的时候source进来就行:
#!/bin/bash source ~/shell_lib/common.sh log "脚本开始执行" confirm "确定要覆盖原文件吗?" || exit 1这样日积月累,你就像攒乐高积木一样,写新脚本会越来越快。
5.4 别忽视:脚本和系统的第一道护栏
如果你写的脚本要处理关键数据,记得两个铁律:第一,改动重要文件前先备份或加“干跑模式”(dry-run),脚本里用--dry-run参数只打印将要执行的动作而不真正执行;第二,危险操作前让用户输入yes确认,而不是按个回车就默认通过。这些习惯能让那些“一把梭”的脚本不至于哪天把服务器搞崩。
6. 写在最后的几个小建议
从纯粹在终端里敲命令,到开始写第一个脚本,再到能熟练使用if、for、函数,这个过程的提升是跨越式的。我现在看到重复工作,下意识就会想“能不能写个脚本自动完成”。
根据我个人经验,还有几个建议值得分享。
首先,刻意练习“拆解重复操作”。你不需要一上来就写什么百行大脚本,今天觉得“切换目录、激活环境、运行测试”这套动作重复了三遍,那就可以写成一个小脚本。每天积累一点,技能和脚本库一起成长。
其次,习惯用man命令查手册。比如man bash、man grep。刚开始看不懂很正常,但只要学会在手册里搜关键词,你解决问题的能力会自动提升一大截。搜索引擎当然有用,但系统自带的文档永远是更可靠的。
还有个小技巧是给你的脚本写注释。这词听起来像废话,但你一个月后回来看脚本,绝对想不起来当时那个诡异的参数为什么要这么传。注释有两大核心:解释“这一步在干什么”,更要解释“为什么这样干”,特别是那些绕过某个坑的处理,不写上注释,过阵子自己都会看不懂。
最后提醒一下:Shell 脚本不是万能的。如果发现逻辑越来越复杂、处理的数据结构越来越庞大,别硬撑着,及时换 Python 或者 Go 来写,效率和可维护性都会更好。判断标准很简单:当你的 Bash 脚本里出现“在 for 循环里再套好几个 if 再处理多维数组”这种苗头,就该考虑换语言了。但日常工作里,Bash 依然是连接所有 Linux 工具链最轻巧高效的胶水,这个基本功学好了,从此终身受益。