一句话总结
Shell 脚本就是把平时一条条敲的命令写进文件,让它们按顺序自动执行.
这一篇打好编程基础:变量怎么存,引号有什么坑,条件怎么判断,循环怎么写--掌握这四样,就能把重复操作变成一个可复用的脚本.
从手敲命令到写脚本
前面学的命令都是"敲一条执行一条".但当一组操作需要反复执行(每天清理日志),或步骤很多容易敲错(部署流程)时,就该把它们写进一个文件--这就是 Shell 脚本.
手敲命令像每次做菜都临时翻菜谱,逐步操作;写脚本则是把整套流程录成一份"自动食谱",以后喊一声就能从头跑到尾,不会漏步,不会敲错.
第一行:shebang
脚本文件的第一行通常是 #! 开头的一行,称为 shebang.它告诉系统"用哪个解释器来跑这个文件".
|
| 写法 | 说明 |
|---|---|
#!/bin/bash |
写死 bash 的绝对路径,简单直接 |
#!/usr/bin/env bash |
推荐--通过 env 在 PATH 里找 bash,跨系统更通用(macOS 的新版 bash 常不在 /bin) |
#!/bin/sh |
用系统默认 sh,语法更受限,只在需要极致可移植时用 |
|
两种运行方式的区别
./hello.sh 会读 shebang 启动一个新的子 Shell 执行;bash hello.sh 则显式指定解释器,此时 shebang 被忽略.
还有第三种 source hello.sh(或 . hello.sh),它在当前 Shell 里执行,脚本里设的变量会留在会话中--这正是修改 .zshrc 后用 source 生效的原理(第 5 篇).
变量:存取数据
变量是脚本里临时存数据的盒子.Shell 变量有几条和其他语言很不一样的规矩,踩坑全在这里:
|
等号两边不能有空格
name = "alice"(带空格)会被 Shell 理解成"运行名为 name 的命令,参数是 = 和 alice",直接报错.赋值时等号紧贴两边:name="alice".这是初学者第一个高频错误.
变量的几种来源
| 类型 | 例子 | 说明 |
|---|---|---|
| 普通变量 | x="hello" |
只在当前 Shell/脚本有效 |
| 环境变量 | export PATH=... |
export 后子进程也能继承(第 5 篇) |
| 位置参数 | $1 $2 ... |
运行脚本时传进来的参数 |
| 特殊变量 | $? $# $$ |
Shell 内置,记录退出码,参数个数等 |
命令替换:把命令输出存进变量
用 $(...) 把一条命令的输出抓出来赋给变量,这是脚本里极其常用的操作:
|
用 $() 不要用反引号
老教程里会看到 today=`date`(反引号).它和 $() 等价,但反引号不能嵌套,容易和单引号看混.现代写法一律用 $(),可读,可嵌套.
引号:脚本里最大的坑
Shell 的单引号,双引号,不加引号,行为完全不同.搞混引号是脚本 bug 的头号来源,务必理清:
| 形式 | 变量 $var 会展开吗? |
用途 |
|---|---|---|
双引号 "$var" |
会展开 | 默认就用它--既展开变量,又防止空格被拆分 |
单引号 '$var' |
不展开,原样输出 | 需要字面量,不想让 $ 起作用时 |
不加引号 $var |
会展开,但会按空格拆分 | 危险,变量含空格时出 bug |
|
黄金法则:变量引用永远加双引号
除非明确需要按空格拆分,否则所有 $var 都写成 "$var".这一条习惯能消灭脚本里大半的诡异 bug--尤其是文件名带空格,变量为空的情况.
条件测试:[[ ]] 与退出码
判断的基础是退出码(exit code):每条命令执行完都会返回一个数字,0 表示成功,非 0 表示失败.可以用特殊变量 $? 查看上一条命令的退出码.
|
条件判断用 [[ ... ]](bash 推荐)或 [ ... ](更老,更通用).判断成立时整体退出码为 0.方括号内侧必须有空格:
字符串与数值比较
| 比较 | 字符串 | 数值 |
|---|---|---|
| 相等 | = / == |
-eq |
| 不等 | != |
-ne |
| 大于 | > |
-gt |
| 小于 | < |
-lt |
| 大于等于 | - | -ge |
| 小于等于 | - | -le |
数值比较别用 > <
数值大小一定要用 -gt / -lt 这类字母运算符.在 [ ] 里写 > 会被当成重定向(把输出写到名为后面那个值的文件!),写 = 则是字符串比较--[ 10 = 10.0 ] 为假,但 [ 10 -eq 10 ] 为真.
记住:数字用字母,字符串用符号.
文件测试
脚本里经常要先判断"文件在不在,是不是目录"再操作:
| 测试 | 含义 |
|---|---|
-e 路径 |
存在(exists) |
-f 路径 |
是普通文件(file) |
-d 路径 |
是目录(directory) |
-r / -w / -x |
可读 / 可写 / 可执行 |
-z 字符串 |
字符串长度为零(空) |
-n 字符串 |
字符串非空 |
if 条件语句
把退出码和 [[ ]] 组合起来,就是分支:
|
多个条件可以用逻辑运算符组合:
|
if 后面可以直接跟任何命令,它判断的是该命令的退出码是否为 0.所以 if grep -q "error" log.txt; then ... 完全合法--grep 找到就退出 0,走 then 分支.[[ ]] 只是其中一种"返回退出码的命令"而已.
case 多分支
当要对同一个变量匹配多个值时,case 比一长串 elif 清爽得多,常用于解析菜单选项或命令参数:
|
case 的匹配模式支持通配符,比如 *.txt) 匹配所有 txt 文件,[0-9]*) 匹配数字开头--比逐个 if 灵活.
循环:for 与 while
for:遍历列表
Shell 的 for 本质是遍历一组词,这组词可以来自字面量,通配符展开,命令输出:
|
while:条件循环
while 在条件成立时反复执行,适合"不知道要循环几次,靠条件决定何时停"的场景:
|
逐行读文件要用 while read,不要用 for
用 for line in $(cat file) 逐行读是经典错误:它按空格和换行一起拆分,一行里有空格就会被拆成多个词.正确做法是 while IFS= read -r line--IFS= 保留行内空格,-r 防止反斜杠被转义.这是处理文本文件的标准模板,背下来.
break 与 continue
和其他语言一致:break 跳出整个循环,continue 跳过本次进入下一轮.常配合 if 在循环里做提前退出或跳过.
快速回顾
- shebang(
#!/usr/bin/env bash)决定用哪个解释器;chmod +x后用./脚本运行 - 变量赋值等号两边不能有空格;取值用
${var};命令替换用$(命令)把输出存进变量 - 引号黄金法则:变量引用永远加双引号
"$var"--单引号不展开,不加引号会按空格拆分酿成 bug - 退出码:0 为成功,非 0 为失败,
$?查看;if判断的就是退出码 - 条件测试:用
[[ ]],内侧要有空格;数值比较用-eq/-gt/-lt,字符串用=/!=,别用错 - 文件测试:
-e存在 ·-f文件 ·-d目录 ·-z空串 ·-n非空 - if/elif/else/fi 做分支,case/esac 做多值匹配(支持通配符)
- for 遍历列表/通配符,while 条件循环;逐行读文件用
while IFS= read -r line
动手练习
- shebang 与 source:写一个
hello.sh,加 shebang,chmod +x,分别用./hello.sh和source hello.sh运行,观察脚本里设的变量是否留在当前 Shell - 引号差异:定义
name="alice smith",分别用双引号,单引号,不加引号 echo 它,观察三者输出差异 - 文件判断:写一个脚本,用
$1接收一个文件名,用if [[ -f ]]判断它是否存在并打印不同提示;不传参数时报错退出 - case 分支:用
case写一个service.sh {start|stop|restart}的骨架,兜底分支打印用法 - for 遍历:用
for循环遍历当前目录所有.txt文件并打印文件名 - while read 对比:用
while IFS= read -r line逐行读取一个文本文件并加行号打印,对比用for line in $(cat file)在含空格行上的区别 - 数值判断:写一个
count=$(ls | wc -l),用if判断文件数是否大于 10(用-gt)并给出不同提示