欢迎来到罐子的实验室
本次目标
把零散的命令组合起来,从「打一行跑一行」升级到「写一个脚本自动跑一堆命令」。这是自动化的起点。
学完你能做什么:
- 让脚本变可执行(shebang +
chmod +x)
1. 为什么要把命令连起来
你每天可能重复跑这样一组命令:
user@host:~$ cd ~/projectuser@host:~$ git pulluser@host:~$ makeuser@host:~$ ./run_tests
四条命令、四次回车。如果能写成「跑完 pull 再 make 再测试」一条命令,多省事。Shell 给了好几种「连接命令」的方式。
2. 分号 ;:依次执行
最简单:用 ; 把几条命令放一行,依次执行(不管前一条成不成功):
user@host:~$ echo first; echo second; echo thirdfirstsecondthird
等价于分三次敲回车。A ; B 表示「先跑 A,跑完(不管成败)跑 B」。
动手练习 1:串联
任务:用分号把 date、whoami、pwd 三条命令串成一行执行。
参考答案:
user@host:~$ date; whoami; pwdSun Jun 30 18:00:00 UTC 2024user/home/user
3. &&:成功才继续
A && B 表示「只有 A 成功了(退出码为 0)才跑 B」。这是「与」逻辑。
user@host:~$ mkdir newdir && cd newdir# 创建成功才进去user@host:~/newdir$user@host:~/newdir$ cd /tmpuser@host:/tmp$ cd /nope && echo OK# cd 失败,echo 不跑bash: cd: /nope: No such file or directoryuser@host:/tmp$
非常适合「前一步是后一步前提」的场景:编译成功才部署、下载成功才解压。
动手练习 2:条件串联
任务:写一条命令——「如果 /etc/passwd 存在,就打印 exists」。
思路引导:test -f 文件 在文件存在时返回 0;&& 接 echo。
参考答案:
user@host:~$ test -f /etc/passwd && echo existsexists
4. ||:失败才继续
A || B 表示「A 失败了(退出码非 0)才跑 B」。这是「或」逻辑——A 成功就用 A,A 不行就上 B。
经典用途:给命令配一个「备用方案」。
user@host:~$ cd /nope || echo ”目录不存在,留在原地”bash: cd: /nope: No such file or directory目录不存在,留在原地
user@host:~$ grep error logfile || echo ”没找到 error,一切正常”没找到 error,一切正常# grep 找不到时退出码非 0,于是 echo 跑
动手练习 3:容错
任务:写一条命令——「尝试 cat config.txt,如果文件不存在就打印 用默认配置」。
参考答案:
user@host:~$ cat config.txt || echo ”用默认配置”用默认配置
💡 && 和 || 可以组合:A && B || C = A 成功跑 B,B 又失败的话跑 C。但组合复杂时建议用后面的 if。
5. 你的第一个脚本
把多条命令写进一个文件,就成了 Shell 脚本。按惯例用 .sh 后缀。
user@host:~$ cd ~/linux-practiceuser@host:~/linux-practice$ cat > hello.sh <<EOFecho Helloecho WorlddateEOF
(<<EOF ... EOF 叫「here-document」,让你一次输入多行直到遇到 EOF。也可以用编辑器如 nano hello.sh 来写。)
然后用 bash 运行它:
user@host:~/linux-practice$ bash hello.shHelloWorldSun Jun 30 18:00:00 UTC 2024
bash hello.sh 的意思是:启动 bash,让它从 hello.sh 读命令执行,而不是从键盘读。
动手练习 4:写个问候脚本
任务:写一个 greet.sh,依次打印「欢迎」「当前用户」「当前时间」。
思路引导:文件里写三行 echo/whoami/date。
参考答案:
user@host:~/linux-practice$ cat > greet.sh <<'EOF'echo 欢迎whoamidateEOFuser@host:~/linux-practice$ bash greet.sh欢迎userSun Jun 30 18:00:00 UTC 2024
6. 让脚本「可执行」
每次都 bash greet.sh 太啰嗦。如果给脚本加执行权限,就能直接 ./greet.sh 跑(→ chmod 详见《模块 10》):
user@host:~/linux-practice$ ls -l greet.sh-rw-r--r-- ... greet.sh# 没有执行权限user@host:~/linux-practice$ chmod +x greet.shuser@host:~/linux-practice$ ls -l greet.sh-rwxr-xr-x ... greet.sh# 有了 xuser@host:~/linux-practice$ ./greet.sh欢迎...
注意必须写 ./greet.sh(带 ./),不能只写 greet.sh——出于安全考虑,Linux 不会在当前目录找命令,必须明确路径。这点《模块 13》会详细讲。
7. shebang:告诉系统用谁解释
./greet.sh 能跑,是因为 Linux 默认用当前 Shell解释它。但更可靠的做法是在脚本第一行写 shebang——告诉内核「请用这个解释器运行我」:
#!/bin/bashecho 欢迎whoamidate
#!/bin/bash 的意思是「用 /bin/bash 来执行本文件」。当 Linux 看到文件开头是 #!,就把后面那行当作解释器路径,相当于自动执行 /bin/bash 脚本路径。
💡 shebang 必须是第一行,前面不能有空行。常见值:
#!/bin/bash#!/usr/bin/env bash#!/usr/bin/python3#!/bin/sh
动手练习 5:加 shebang
任务:给 greet.sh 加上 shebang,确认 ./greet.sh 仍能正常跑。
参考答案:
user@host:~/linux-practice$ nano greet.sh# 或用别的编辑器,在第一行加 #!/bin/bash# 编辑后内容:# #!/bin/bash# echo 欢迎# whoami# dateuser@host:~/linux-practice$ ./greet.sh欢迎user...
8. 给脚本传参数
脚本可以接受命令行参数,用特殊变量读取(→ 变量详见《模块 08》):
例子:
#!/bin/bashecho ”脚本名: $0”echo ”第一个参数: $1”echo ”第二个参数: $2”echo ”共 $# 个参数”
运行:
user@host:~/linux-practice$ ./args.sh alice bob carol脚本名: ./args.sh第一个参数: alice第二个参数: bob共 3 个参数
💡 给 $1 等加引号:"$1",防止参数含空格时被拆词。
动手练习 6:参数脚本
任务:写个 reverse.sh,接受两个参数,逆序输出(先输第二个再输第一个)。
思路引导:echo "$2 $1"。
参考答案:
user@host:~/linux-practice$ ./reverse.sh hello worldworld hello
9. 条件判断:if
if 让脚本根据情况做不同事。基本语法:
if [ 条件 ]; then 命令1 命令2fi
注意几个坑:
[ 后面、] 前面必须有空格:[ "$1" = "x" ]。if- 结束符是
fi(if 倒过来写),不是 endif。
#!/bin/bashif [ ”$1” = ”ping” ]; then echo pongfi
user@host:~/linux-practice$ ./ping.sh pingponguser@host:~/linux-practice$ ./ping.sh hello# 没匹配,啥也不输出
💡 = 是字符串比较。数字比较用 -eq-ne-lt-gt(等于/不等于/小于/大于)。更多见 help test。
[ ] 里常用的判断:
| |
|---|
[ -f 文件 ] | |
[ -d 目录 ] | |
[ -e 路径 ] | |
[ -r 文件 ] | |
[ -z "$X" ] | |
[ -n "$X" ] | |
[ "$A" = "$B" ] | |
[ "$A" != "$B" ] | |
动手练习 7:简单的 if
任务:写个脚本,接受一个参数。如果参数是 pwn,输出 college;否则什么都不输出。
参考答案:
#!/bin/bashif [ ”$1” = ”ko” ]; then echo okfi
user@host:~/linux-practice$ ./chk.sh kookuser@host:~/linux-practice$ ./chk.sh foo# 无输出
常见错误:
- ❌
[$1 = "ko"](没空格)→ 语法错误。[ 和 ] 两边都要空格。 - ❌
if [...] then(没分号也没换行)→ 要 if ... ; then 或换行。
10. else 和 elif
else 处理「否则」:
if [ ”$1” = ”hello” ]; then echo Hielse echo 我不认识你fi
elif(else if)处理多个分支:
if [ ”$1” = ”hello” ]; then echo hielif [ ”$1” = ”hi” ]; then echo helloelif [ ”$1” = ”learn” ]; then echo linuxelse echo unknownfi
注意每个 elif 后面都要 then,else 后面不要 then,最后用 fi 收尾。
动手练习 8:多分支脚本
任务:写个脚本,参数是 start 时输出「启动中」,是 stop 时输出「停止中」,是 status 时输出「运行中」,其他情况输出「未知命令」。
参考答案:
#!/bin/bashif [ ”$1” = ”start” ]; then echo 启动中elif [ ”$1” = ”stop” ]; then echo 停止中elif [ ”$1” = ”status” ]; then echo 运行中else echo 未知命令fi
user@host:~/linux-practice$ ./svc.sh start启动中user@host:~/linux-practice$ ./svc.sh xxx未知命令
11. 实战:一个有用的脚本
把所学串起来,写个自动备份脚本backup.sh:
#!/bin/bash# 用法: ./backup.sh 要备份的目录 备份存放目录SRC=”$1”DST=”$2”# 检查参数个数if [ $# -ne 2 ]; then echo ”用法: $0 <源目录> <备份目录>” exit 1fi# 检查源目录是否存在if [ ! -d ”$SRC” ]; then echo ”错误: 源目录 $SRC 不存在” exit 1fi# 创建备份目录(如果不存在)mkdir -p ”$DST”# 生成带时间戳的备份文件名ARCHIVE=”$DST/backup-$(date +%Y%m%d-%H%M%S).tar.gz”# 打包压缩tar -czf ”$ARCHIVE” -C ”$SRC” . && echo ”备份完成: $ARCHIVE” || echo ”备份失败”
运行:
user@host:~/linux-practice$ chmod +x backup.shuser@host:~/linux-practice$ ./backup.sh ~/Documents ~/backups备份完成: /home/user/backups/backup-20240630-180000.tar.gz
这个脚本用到了:参数($1/$2/$#)、if 判断、&&/||、命令替换 $(date)、mkdir -p、tar——前 10 个模块的精华都在里面了。
动手练习 9:改造它
任务:把 backup.sh 改成——如果备份成功,再额外用 ls -lh 显示生成的备份文件大小。
思路引导:在 && 后面加 && ls -lh "$ARCHIVE"。
参考答案:
把最后那行改成:
tar -czf ”$ARCHIVE” -C ”$SRC” . && echo ”备份完成” && ls -lh ”$ARCHIVE” || echo ”备份失败”
本模块速查表
| |
|---|
A ; B | |
A && B | |
A || B | |
bash 脚本.sh | |
chmod +x 脚本 | |
./脚本.sh | |
#!/bin/bash | |
脚本里的特殊变量
if 语法
if [ 条件 ]; then ...elif [ 条件 ]; then ...else ...fi
铁律:[] 两边空格;then 前用 ; 或换行;fi 收尾。
下一步
你的脚本里如果有个耗时任务(比如长时间下载),万一 SSH 断了,脚本就死了——所有工作丢失。下一模块教你用「终端复用器」让任务脱离终端存活。