Shell 脚本是把一堆 Linux 命令按顺序写进一个文件里,让计算机替你重复执行。从定时备份日志、批量改文件名,到部署项目时一键起服务,绝大多数运维自动化的起点都是一个 .sh 文件。这一篇从 Shebang 讲起,把变量、注释和三种执行方式一次说清楚,写完就能跑。
一、第一个 Shell 脚本
打开任意编辑器,新建一个文件 hello.sh:
bash
#!/bin/bash
# 这是我的第一个 Shell 脚本
echo "Hello, Shell!"
name="Benothing"
echo "你好,$name"
保存后先看一下它的权限:
bash
ls -l hello.sh
预期输出(新建的文件默认没有可执行权限):
text
-rw-r--r-- 1 user user 68 Sep 16 10:00 hello.sh
带 x 的那一位现在是 -,意思是不能直接执行。加上可执行权限再跑:
bash
chmod +x hello.sh
./hello.sh
预期输出:
text
Hello, Shell!
你好,Benothing
到这里你已经写完并运行了第一个脚本。接下来逐个拆解每一行在干什么。
二、Shebang:第一行 #! 的作用
脚本第一行固定长这样:
bash
#!/bin/bash
#! 两个字符合起来叫 Shebang(也叫 hashbang)。它的作用是告诉系统:这个脚本该用哪个解释器来跑。
你写的 .sh 文件本质是纯文本,内核自己看不懂。当你执行 ./hello.sh 时,内核读到 Shebang,就会把文件剩下的内容交给 /bin/bash 这个程序去逐行解释。常见写法还有:
| 写法 | 含义 |
|---|---|
#!/bin/bash |
用 bash 解释器(最常用) |
#!/bin/sh |
用系统默认的 sh,通常指向 dash 或 bash |
#!/usr/bin/env bash |
从 PATH 里找 bash,移植性最好 |
#!/usr/bin/python3 |
这个文件其实是 Python 脚本 |
第三种 #!/usr/bin/env bash 在团队协作和多台服务器之间最稳,因为不同发行版 bash 的安装位置可能不一样,写死 /bin/bash 在某些精简系统上会报 bad interpreter。
⚠️ 常见错误
- Shebang 前面有空行或 BOM :第一行必须真的是第一行。Windows 编辑器保存成 UTF-8 with BOM 时,文件开头会多三个不可见字节,内核会把 Shebang 识别成乱码,报
/usr/bin/env: 'bash\r': No such file or directory。用cat -A hello.sh能看到^M或M-oM-;M-?这类隐藏符号。 - 写成
#! /bin/bash:#!和路径之间不要有空格,虽然有些系统容忍,但不保证。 - 忘了 Shebang 也能跑 :用
bash hello.sh这种方式启动时,Shebang 被当普通注释跳过了,所以你删掉第一行也能执行。但一旦改成./hello.sh直接执行就会失败,Shebang 不能省。
三、变量:定义、引用与赋值规则
Shell 里定义变量非常直接:变量名=值,等号两边不能有空格。
bash
greeting="早上好"
count=3
引用变量时在名字前面加 $:
bash
echo $greeting
echo "今天是第 $count 天"
预期输出:
text
早上好
今天是第 3 天
赋值的几条铁律
| 写法 | 是否合法 | 说明 |
|---|---|---|
name="Tom" |
合法 | 等号两边无空格 |
name = "Tom" |
报错 | 中间有空格,shell 会把 name 当命令 |
name=Tom |
合法 | 无空格的值可以不加引号 |
name="Tom Lee" |
合法 | 带空格的值必须加引号 |
name =Tom |
报错 | 等号左边不能有空格 |
name="value" |
合法 | 行尾空格无所谓 |
为什么等号两边不能有空格?因为 shell 把空格当命令的分隔符。name = "Tom" 会被拆成三个 token:命令 name、参数 =、参数 Tom,shell 去找一个叫 name 的命令,自然找不到。
${name} 和 $name 的区别
变量名后面紧跟普通字符时,花括号能帮 shell 界定边界:
bash
fruit="apple"
echo "I like ${fruit}s"
预期输出:
text
I like apples
如果写成 echo "I like $fruits",shell 会去找一个叫 fruits 的变量,结果是空的。花括号在拼接字符串、数组下标、路径拼接时几乎是必选项。
命令替换:把命令结果存进变量
反引号和 $(...) 都能把一条命令的输出抓回来:
bash
now=$(date +%Y-%m-%d)
echo "今天日期是 $now"
预期输出:
text
今天日期是 2026-09-16
推荐用 $(...),它可以嵌套,反引号嵌套时要写一堆反斜杠,可读性很差。
四、注释怎么写
单行注释用 #,从 # 到行尾全是注释。Shebang 那一行虽然也是 # 开头,但它是特例,内核会特殊识别。
bash
#!/bin/bash
# 整行都是注释
echo "这行会执行" # 这行后面也是注释
Shell 没有原生的多行注释。常见做法是用一个"什么都不做"的函数或 here document 来折叠一段,但最实用的方式就是每行前面都加 #,IDE 里按 Ctrl+/ 一键注释一片。
五、执行脚本的三种方式
同一个脚本,三种跑法,行为差别不小。
| 方式 | 命令 | 是否需要可执行权限 | 是否在当前 shell 生效 |
|---|---|---|---|
| 直接执行 | ./hello.sh |
需要(chmod +x) |
子进程 |
| 解释器调用 | bash hello.sh |
不需要 | 子进程 |
| source 调用 | source hello.sh 或 . hello.sh |
不需要 | 当前进程 |
前两种方式都会fork 一个子 shell 来跑脚本,脚本里定义的变量、cd 切换目录,跑完就消失,不会影响你当前终端。第三种 source 是在当前 shell 里逐行执行,脚本里 export 的变量、cd 进的目录会一直留着。
这个区别在写环境变量配置脚本时是关键:安装软件后改 PATH,你必须 source 那个配置文件,改动才对当前终端生效;直接执行一个 .sh 去改 PATH,跑完什么都没发生。
完整示例:对比三种执行方式
写一个 test_env.sh:
bash
#!/bin/bash
my_var="from script"
cd /tmp
echo "脚本内 my_var=$my_var"
echo "脚本内 pwd=$(pwd)"
分别用三种方式跑:
bash
bash test_env.sh
echo "跑完后 my_var=$my_var, pwd=$(pwd)"
预期输出:
text
脚本内 my_var=from script
脚本内 pwd=/tmp
跑完后 my_var=, pwd=/home/user
换成 source:
bash
source test_env.sh
echo "跑完后 my_var=$my_var, pwd=$(pwd)"
预期输出:
text
脚本内 my_var=from script
脚本内 pwd=/tmp
跑完后 my_var=from script, pwd=/tmp
注意 source 把当前目录都改到 /tmp 了,用完记得 cd 回去。
六、echo 输出与引号的初步认知
echo 后面跟单引号和双引号,行为完全不同:
bash
who="Benothing"
echo "双引号: $who"
echo '单引号: $who'
预期输出:
text
双引号: Benothing
单引号: $who
双引号会先把里面的变量展开,单引号原样输出,连 $ 都不解释。写脚本时这个区别每天都在踩,后面讲变量那一篇会展开。
七、知识扩展:Shebang 背后,内核到底干了什么
你在 shell 里敲 ./hello.sh 并回车,整条链路是这样的:
- 当前 shell(比如你登录后的 bash)通过
fork()复制出一个子进程。 - 子进程调用
execve("./hello.sh", ...),把自己替换成新程序。 - 内核打开
hello.sh,看文件头两个字节是不是#!。 - 是,内核就把这一行剩下的字符串当作解释器路径,再
execve("/bin/bash", "/bin/bash", "./hello.sh")。 - bash 启动后逐行读取脚本,把每一行拆成命令、参数、变量,再去执行。
所以 Shebang 不是 shell 的功能,是操作系统内核 的约定。这也解释了为什么 Windows 上没有 Shebang------Windows 的 CreateProcess 根本不认这两个字符。
理解这条链路还能解释一个常见困惑:为什么 ./hello.sh 和 bash hello.sh 结果有时候不一样?因为前者是内核按 Shebang 找解释器,后者是你手动指定 bash,Shebang 那行被当注释。如果脚本第一行写的是 #!/bin/zsh 而系统没装 zsh,./hello.sh 直接报 No such file or directory,但 bash hello.sh 能跑------它绕开了 Shebang。
动手把这一篇里的 hello.sh、test_env.sh 都敲一遍,三种执行方式的差异是后面所有脚本知识的地基,值得花十分钟亲手验证。