前阵子有个新同事问我,Linux 下怎么把平时敲的命令存下来、一次性跑完。他说自己每次都一个个敲,既慢又容易漏。这个问题我太熟了,当年我也是从这儿入的门。所谓 Shell 脚本,简单说就是把一串命令写进一个文件里,然后交给解释器去执行;而在绝大多数 Linux 发行版上,这个解释器就是 Bash。今天这篇就围绕 Linux 下 Bash 脚本的创建、运行和排错,把基础路径完整走一遍。文章会从环境准备、第一个脚本、核心语法、常见报错几个方面展开,内容偏向动手实操,适合刚接触 Linux 或者想把日常操作自动化的朋友参考。
1. 先搞明白:Shell 脚本到底解决什么问题
很多人第一次接触 Shell 脚本时会有个误区,觉得它是一门“编程语言”,要去系统学一遍 syntax。其实不用这么想。Shell 脚本本质上就是你把命令行里能敲的指令,按顺序写进一个文件,再加点逻辑控制(判断、循环、变量),让系统自动去执行。
1.1 Bash 在 Linux 系统里的位置
Linux 系统启动后,你打开终端敲命令,那个“接收你的输入、解释并执行”的程序就是 Shell。Shell 有好几种,常见的有 Bash、Zsh、Fish 等,但 Bash(Bourne Again Shell)是绝大多数 Linux 发行版的默认 Shell,也是我们今天的主角。
你可以在终端里输入echo $SHELL查看当前默认 Shell,大概率会输出/bin/bash。这行路径就是 Bash 解释器所在的位置。当你写好一个脚本文件,想办法让系统知道“用哪个程序来解释这个文件”,这就引出了脚本第一行非常重要的内容——Shebang。
1.2 从“手动敲命令”到“自动化执行”
想象一下,你每天上班都要做这样一件事:进入某个目录、备份数据库、清理旧日志、重启服务。这些命令你每天都要敲一遍,不仅浪费时间,还容易敲错。如果用脚本,你只要把这些命令按顺序写进一个.sh文件,之后每天运行一次这个文件就行了。
再进一步,你还可以让脚本接收参数,比如“备份今天的日志”和“备份前天的日志”,用同一个脚本传入不同日期就能完成。甚至可以用cron定时任务让脚本每天凌晨自动跑,完全不用人管。这就是 Shell 脚本的核心价值:把重复劳动变成一次性投入。
1.3 什么场景适合用脚本
不是所有任务都适合写脚本,这点需要经验判断。我见过有人为了“显得专业”,把一条mv命令也包一层脚本,完全没必要。适合用脚本的场景通常具备这几个特征:
- 命令步骤多,且执行顺序固定。
- 需要反复执行,尤其是定期执行。
- 需要带参数执行,比如指定不同的目录或文件名。
- 需要做简单的判断,比如“文件存在才执行”“上一条命令成功了才继续”。
如果你只是偶尔手动敲一两条命令,直接敲就行,别给自己增加维护负担。但一旦你发现自己最近一周都在重复同一串命令,那就该停下来写个脚本了。
2. 动手前的准备工作:编辑器、终端与权限
写脚本不需要装什么复杂 IDE,Linux 下最简单也最通用的组合就是“一个文本编辑器 + 一个终端”。但正因为它看起来太简单,很多新手会在细节上栽跟头,比如文件没保存成 UTF-8、忘了给执行权限、换行符不对导致运行报错等。
2.1 选一个趁手的编辑器
图形界面下可以用 gedit、VS Code 之类的编辑器,但服务器上通常没有图形界面。所以我建议从一开始就适应终端里的编辑器,至少掌握两种里的一个:nano和vim。
nano上手极快,打开文件、编辑、按Ctrl+O保存、Ctrl+X退出,就这四个操作。适合刚开始写脚本、不想折腾的人。vim功能强大,但学习曲线比较陡,第一次打开可能连退出都不会。不过 vim 在远程服务器上几乎无处不在,后续想深入 Linux 操作时这关迟早要过。
我的建议是:第一天先用 nano 写脚本,把注意力放在脚本本身;等写到自己觉得有需要时,再花两周时间集中学 vim。
2.2 脚本文件的命名与存放位置
脚本文件名建议以.sh结尾,比如backup.sh。虽然不强制,但这样一眼就能看出是 Shell 脚本,方便自己和同事识别。
存放位置上,个人建议先放在自己的家目录下,建一个专门放脚本的文件夹,比如~/scripts,这样便于管理和备份。等你写了一些公共效率工具,想让系统任何位置都能直接调用时,再把脚本放(或软链接)到/usr/local/bin这种 PATH 目录下。
注意:不要在 Windows 下用记事本编辑脚本然后直接传到 Linux 上跑,十有八九会报
/bin/bash^M: bad interpreter错误,后面第 5 节我会细说这个问题。
2.3 关于“执行权限”这件事
Linux 下要把一个文件当作程序来运行,需要该文件有“可执行”权限。查看权限用ls -l 文件名,你会看到类似-rw-r--r--的输出,这表示文件主可读可写,其他人只能读,所有人都不可以执行。
给脚本加可执行权限的命令是:
chmod +x 你的脚本.sh这一步是 Linux 初学者的常见盲点。很多新手写完脚本,直接./脚本.sh运行,结果提示Permission denied,第一反应是“是不是系统坏了”。其实只是忘了赋权。
3. 创建并运行第一个脚本:从 Hello World 开始
前面做了那么多铺垫,现在正式动手。下面的步骤我会写得很具体,建议你照着操作一遍,先把“能跑起来”的感觉找到,后面再逐步叠加语法。
3.1 第一步:创建脚本文件
先进入你准备好的目录,然后新建一个文件。用 nano 的话命令是:
cd ~/scripts nano hello.shnano 会打开一个空文件。在文件里输入下面这几行:
#!/bin/bash # 这是我的第一个 Shell 脚本 echo "Hello, World!" echo "当前时间: $(date)"第一行#!/bin/bash就是前面说的 Shebang,它告诉系统用/bin/bash这个程序来解释执行本文件。第二行以#开头的是注释,只给人看,系统不会执行。第三、四行是实际要执行的命令,echo用来输出内容,$(date)表示先执行date命令再取它的输出结果。
按Ctrl+O保存,回车确认文件名,再按Ctrl+X退出 nano。
3.2 第二步:赋予可执行权限
回到终端,执行:
chmod +x hello.sh这时候你用ls -l hello.sh看文件权限,应该变成-rwxr-xr-x,多了几个x,说明可执行权限已经加上了。
3.3 第三步:运行脚本
运行脚本有几种方式,这里先介绍最最常用的两种。
方式一:相对路径运行
./hello.sh注意前面的./不能省略。这是很多新手会遇到问题的点:为什么不直接输hello.sh?因为当前目录默认不在 PATH 环境变量里,直接输文件名系统会去 PATH 定义的几个目录里找,找不到就报command not found。加了./就是明确告诉系统“在当前目录找”。
方式二:用 bash 解释器直接执行
bash hello.sh这种方式不要求文件有可执行权限,甚至不要求第一行有 Shebang,因为你就是明确指定了用 bash 来解释它。在脚本调不通的时候,暂时用bash跑一遍有助于缩小排查范围。
如果一切正常,你会看到三行输出。第一个小目标完成。
3.4 几种运行方式的区别
我之所以强调运行方式,是因为它们在细节上确实不同:
| 运行方式 | 是否需要执行权限 | 是否依赖 Shebang | 适用场景 |
|---|---|---|---|
./script.sh | 需要 | 需要 | 日常执行脚本 |
bash script.sh | 不需要 | 不需要 | 调试、无执行权限时 |
source script.sh | 不需要 | 不需要 | 在当前 Shell 中执行,用于加载配置 |
sh script.sh | 视情况 | 视情况 | 注意 sh 可能指向 dash,行为有差异 |
特别说一下source(可以缩写为.)。这种方式不是启动一个新的解释器进程来跑脚本,而是在当前终端进程里直接执行。这意味着脚本里设置的变量、切换的目录会保留在当前终端里。如果你写一个脚本想临时切个目录、设置一些环境变量,就用source跑;如果你希望脚本在一个隔离环境里跑完就结束,不污染当前终端,就用./。
这个区别,很多搞了两年 Linux 的人都未必说得清楚。我建议你亲自试一下:写个脚本cd /tmp然后分别用./和source运行,再pwd看看当前目录有没有变化,一下子就理解了。
4. 脚本语法核心:变量、条件、循环与函数
第一个脚本跑通后,你现在已经掌握了“把命令顺序放进去执行”的基本能力。但光会顺序执行,脚本的价值还很有限。真正让脚本变得“智能”的,是它能根据条件做决定、重复做某些事、接收外部输入、把重复块封装起来。这一节把这些核心语法一次讲透,全都配可运行的例子。
4.1 变量:让脚本“记住”一些东西
变量是脚本的基本存储单元,命名规则是“字母、数字、下划线,不能以数字开头”。定义和使用的写法如下:
#!/bin/bash # 变量定义,等号两边不能有空格 name="张三" today=$(date +%Y-%m-%d) echo "你好,$name" echo "今天是 $today"这里有两个新手极易踩的坑:
- 赋值时
name = "张三"这样加空格是错的。Shell 会把它解析成“执行 name 命令,参数是 = 和 "张三"”,然后报错。 - 使用变量时加不加
$区别很大。$name是取变量的值,name只是一个普通字符串。
变量还可以和其他字符串拼接,写法稍微讲究一点:
#!/bin/bash prefix="backup" suffix="20250101" filename="${prefix}_${suffix}.tar.gz" echo "$filename"输出结果是backup_20250101.tar.gz。花括号{}的作用是把变量名边界标清楚,避免解析歧义。比如echo "$prefix_suffix"会被当成一个名字为prefix_suffix的变量,这就错了。
4.2 位置参数与读取用户输入
脚本还能接收外部传入的参数,这在自动化任务里太常用了。比如你写一个备份脚本,想把备份的目录名作为参数传进来,而不是每次改脚本。位置参数用$1、$2表示第一个、第二个参数:
#!/bin/bash echo "第一个参数: $1" echo "第二个参数: $2" echo "参数个数: $#" echo "所有参数: $@"保存为args.sh后执行:
./args.sh hello world linux输出对应就是hello、world、3、hello world linux。如果参数个数不够,直接引用$2也不会报错,只是拿到一个空值。
需要交互输入时,用read命令:
#!/bin/bash read -p "请输入要备份的目录: " target_dir echo "你输入的目录是: $target_dir"-p参数表示先输出一段提示文字,然后等待用户输入,把结果存到变量target_dir里。
4.3 条件判断:让脚本“分情况处理”
条件判断让脚本不再是死板地顺序执行。最基本的写法是if ... then ... else ... fi,注意fi是结束标记,很多新手会漏掉。先看一个最简单的例子:
#!/bin/bash if [ $1 -gt 100 ]; then echo "参数大于 100" else echo "参数小于等于 100" fi保存后执行./check.sh 50会输出“参数小于等于 100”。这里的[ ... ]是test命令的简写,-gt是大于(greater than)的意思。除此之外还有:
-eq:等于(equal)-ne:不等于(not equal)-lt:小于(less than)-ge:大于等于(greater or equal)-le:小于等于(less or equal)
字符串和文件判断也很常用:
#!/bin/bash if [ -e /etc/passwd ]; then echo "文件存在" fi if [ -d /etc ]; then echo "是目录" fi str="abc" if [ "$str" = "abc" ]; then echo "字符串相等" fi-e判断文件是否存在,-d判断是否为目录。注意字符串比较用的是单个=,数字比较才用-eq。新手很容易把这两者搞混。
还有个细节:变量加引号。在if [ "$str" = "abc" ]里,如果变量没值或值里带空格,不加引号会出问题。建议所有变量引用都加双引号,这是经验之谈。
4.4 for 循环与 while 循环:让重复工作自动化
循环是脚本自动化中非常出彩的部分。比如你想批量把当前目录下所有.txt文件重命名为.bak,手动做很麻烦,用 for 循环几行搞定:
#!/bin/bash for file in *.txt; do mv "$file" "${file%.txt}.bak" echo "已处理 $file" done*.txt会被 Shell 扩展成当前目录下所有.txt文件列表,然后逐个赋值给file变量,循环体执行重命名操作。${file%.txt}是去掉结尾.txt的写法,是一个字符串截取技巧。
for 循环也可以直接遍历一段数字:
#!/bin/bash for i in {1..5}; do echo "第 $i 次循环" done还有个常见写法是 C 风格:
#!/bin/bash for ((i=1; i<=5; i++)); do echo "第 $i 次循环" donewhile 循环适合“当某个条件成立时一直跑”。最常见的用途是逐行读取文件:
#!/bin/bash while read line; do echo "读取到: $line" done < /etc/hostname<符号把文件内容重定向到循环中,read逐行读取并赋值给line变量。
4.5 函数:把重复逻辑抽出来
当一个操作在脚本里反复出现三遍以上,就应该考虑写成函数。函数定义的格式如下:
#!/bin/bash log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_info "脚本启动" log_info "开始备份" log_info "备份完成"这个例子里,log_info把记录日志的格式统一封装成了函数,每次调用只要传一段消息内容即可。函数体里的$1是函数自己的第一个参数,跟脚本的$1不是一回事,别混了。
函数定义之后,必须先定义后调用。脚本在跑的时候是一行一行解析的,如果调用写在函数定义之前,会报command not found。
4.6 综合小案例:自动备份脚本
把上面几个内容串起来,写一个稍完整的脚本。目标是:传入一个目录路径,检查目录是否存在,存在则打包备份到~/backups目录,备份文件名带当天日期,并打印日志信息。
#!/bin/bash # 自动备份脚本 v1.0 backup_dir="$1" backup_root="$HOME/backups" log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1" } if [ $# -lt 1 ]; then echo "用法: $0 <要备份的目录>" exit 1 fi if [ ! -d "$backup_dir" ]; then echo "[ERROR] 目录不存在: $backup_dir" exit 1 fi mkdir -p "$backup_root" filename="${backup_root}/$(basename "$backup_dir")_$(date +%Y%m%d_%H%M%S).tar.gz" tar -czf "$filename" "$backup_dir" log_info "备份完成: $filename"你可以直接复制保存为backup.sh,然后chmod +x backup.sh,再执行./backup.sh /etc试试效果。这个脚本里用到了参数判断、错误提示、变量拼接、函数定义、时间获取、打包命令,算是第一阶段语法的小综合。
5. 新手高频报错与排查技巧
脚本报错是必经之路,没有人能一次写对。我下面列的几个问题,十个新手有九个会遇到,提前把解决方法摆在这里,真碰上了直接按图索骥。
5.1/bin/bash^M: bad interpreter: No such file or directory
这个报错信息几乎可以排到“Shell 脚本报错榜”第一名。问题根源在于文件是在 Windows 下编辑的,Windows 每行结尾是\r\n(回车 + 换行),而 Linux 期望的是\n(仅换行)。在 Linux 里\r会被当成非法字符,导致解释器路径都读不对。
解决方法有两个:
方法一:安装 dos2unix 工具转换。
sudo apt install dos2unix # Debian/Ubuntu sudo yum install dos2unix # CentOS/RHEL dos2unix your_script.sh方法二:用 sed 直接删除换行符前面的\r:
sed -i 's/\r$//' your_script.sh我个人推荐方法二,因为sed在所有 Linux 系统上都有,不用额外安装。改完之后再运行一遍,问题就解决了。防范措施也很简单:脚本文件统一在 Linux 上编辑,或者编辑器里设置换行符为 LF。
5.2command not found
这个报错有两种完全不同的场景。
场景一:脚本文件本身没有执行权限,你直接输脚本名(不带./)运行,Shell 在 PATH 里找不到。解决方法是加./前缀,或者用bash直接跑。
场景二:脚本内部执行某条命令时提示command not found。比如lsusb: command not found,这是系统里压根没装这个命令对应的包,需要先安装。又比如你用了一些非标准路径下的工具,但脚本运行时 PATH 环境变量里没有包含它。排查思路很清晰:先看是哪一行报错,把那行命令单独拿到终端里执行,看是否成功,不成功就针对性解决。
5.3Permission denied
这个前面已经提到了,脚本没有可执行权限。解决方法是:
chmod +x script.sh如果你的脚本要给别人用,可以思考一下权限范围:chmod 755 script.sh表示文件主可读写执行,他人可读可执行;如果只是自己用,chmod 700 script.sh更安全。
5.4unexpected end of file或syntax error: unexpected end of file
这个报错通常是因为if没有配fi,或者for没有配done,再或者花括号、双引号没配对。Shell 脚本是按行执行的,一旦结构不完整,解析器读到文件结尾也没找到对应的结束标记,就会这样报。
排查方法:把一个脚本缩减到最小可复现粒度,逐段注释掉代码,找到出问题的区块,检查对应的闭合成对符号。平时写脚本注意缩进——虽然 Shell 不像 Python 那样强制缩进,但有缩进才能人眼发现结构不配对。
5.5 参数带空格导致脚本行为异常
如果调用脚本时传了个带空格的参数,比如./backup.sh /home/user/my documents,脚本里的$1只会拿到/home/user/my,空格后面的部分被拆成了另一个参数。解决方法是调用时加引号:
./backup.sh "/home/user/my documents"同时脚本内部所有引用$1、$2的地方也建议加双引号,养成习惯就不会被空格坑。
5.6 常见问题速查表
| 报错信息 | 常见原因 | 解决方法 |
|---|---|---|
/bin/bash^M: bad interpreter | Windows 换行符 | 执行sed -i 's/\r$//' 脚本 |
command not found | 权限/PATH/缺包 | 加./或用 bash;检查命令是否存在 |
Permission denied | 未加执行权限 | 执行chmod +x 脚本 |
unexpected end of file | 结构不配对 | 检查 fi、done、括号、引号 |
syntax error near unexpected token | 语法拼写错误 | 检查 if 是否拼成 i、do 是否漏掉 |
No such file or directory | 引用了不存在的路径 | 检查路径名,文件是否已创建 |
6. 让调试变得更轻松的实用小技巧
脚本写得多了,难免要调试。这里分享几个我平时一直在用、能显著提高效率的调试习惯。
6.1 用bash -x追踪执行过程
在运行脚本时加上-x参数,可以打印每一行实际执行的命令。这对排查“脚本跑完了但结果不对”的问题非常有效:
bash -x your_script.sh输出里每一条被执行的命令都会带上+前缀显示在屏幕上,你能清楚看到变量展开成了什么值、哪一行走到了哪一步。进阶一点,还可以在脚本内部通过set -x开启追踪、set +x关闭追踪,只用set -x包裹需要重点观测的区间。
6.2 善用echo打印关键状态
新手调试最容易犯的错是不打日志、闷头跑。在关键节点加一行echo,输出一下“现在执行到了哪里、某个变量的值是什么”,能节省大量排查时间。比如:
echo "DEBUG: backup_dir = $backup_dir"等确认逻辑没问题了,再把调试用的echo删掉,或者用#注释掉。这看起来笨,但它是最直接有效的调试方式,尤其适合初学者。
6.3 保持脚本语法检查的好习惯
正式运行之前,可以先做一次语法检查,不实际执行:
bash -n your_script.sh如果脚本有语法错误,-n会直接报出来;没有错误则什么都不输出。养成写完脚本先跑bash -n的习惯,能挡住很多低级错误。再把bash -x结合使用,一个查语法、一个查逻辑,基本可以覆盖初期的调试需求。
6.4 把“写脚本”当“搭积木”
一个常见问题是新手总想一次写一个几十行的大脚本,结果一运行全是错,不知道从哪改起。我的经验是拆分:先写一个最小功能的脚本,跑通后再往里加功能。比如先只做“打包某个目录”,通了再加“日志记录”,再加“参数校验”,再加“旧文件清理”。每加一个功能就运行一次,如此能保证你永远知道问题出在最新加的那部分。
6.5 脚本注释的重要性
注释不参与执行,但它是维护脚本生命力的关键。刚从教材里学语法的人,总觉得注释是多余的。但等你三个月后回来看自己写的脚本,面对一堆完全没有注释的命令,很可能要花大量时间回忆当时为什么要这样写。
建议每个脚本至少包含这几行注释:
#!/bin/bash # 脚本功能:备份指定目录到 ~/backups # 用法:./backup.sh <目录路径> # 最后修改:2025-01-01涉及特殊逻辑的地方,也值得补一句“为什么”,而不是“做了什么”——因为“做了什么”看代码本身就能懂,“为什么”往往是真正有价值的信息。
7. 写在最后的建议:从运行脚本到自己写脚本
到这里,你已经把 Bash 脚本的核心路径走了一遍:理解它是什么、会用 nano 创建文件、掌握变量、条件、循环、函数这些基础语法、知道怎么处理常见报错。接下来真正拉开差距的,是你有没有带着“解决实际问题”的目标去写。
我的建议很具体:把身边重复做的操作逐个列出来,从最简单的开始,每个都试着脚本化。比如:
- 一键清理某个目录下超过 7 天的临时文件。
- 批量重命名一批图片文件。
- 拉取最新代码并重新打包部署。
- 检查磁盘空间,低于阈值时发警告日志。
刚开始写不出来没关系,拆开去找每部分的解决方案,拼起来就是你的脚本。每完成一个,你对命令的熟悉度、对 Shell 逻辑的敏感度都会上一个大台阶。我现在回头看,很多更高级的 Linux 技巧,其实都是当初为了写好某个自动化脚本,一点点倒逼着自己去查、去试、去记下来的。这也算是最朴素也最有效的学习路径了。