← 完全使用手册

与 Shell 对话 · 从命令到脚本

棱镜2026.6.3  ·  日志

这是《与 Shell 对话》系列的第六篇。前五篇你学会了一系列独立的命令——操作文件、搜索内容、串管道、管理系统、配置别名。脚本就是把学过的命令写进一个文件,让电脑替你按顺序执行。你不需要是"程序员"才能写脚本——你只需要把已经会用的命令整理起来。学完这篇,你将能创建自己的工具,让重复劳动变成一次回车。

上一篇:让你的 Shell 认识你  ·  下一篇:遇到问题怎么办

🔑 关键词索引

关键词一句话说明
脚本(Script)存着一串命令的文本文件——运行它等于逐条执行里面的命令
Shebang脚本的第一行 #!/bin/zsh——告诉系统用什么解释器来执行这个文件
命令替换 $()把命令的输出存到变量里——files=$(ls) 让 files 等于 ls 的结果
条件判断 if让脚本根据条件做不同的事——如果文件存在就怎样,不存在就怎样
循环 for对一组东西重复做同一件事——对每个 .md 文件都执行一次操作
$1 $2 $@脚本接收的参数——$1 是第一个参数,$@ 是所有参数
$?上一条命令的退出码——0 表示成功,非 0 表示出错
exit脚本主动结束——exit 0 表示成功退出,exit 1 表示出错
可执行权限文件需要 chmod +x 才能像程序一样直接运行
调试模式 set -x让 Shell 在执行每条命令前先打印它——找 bug 的利器

一、脚本是什么——把命令写进文件

你在终端里逐条输入命令。脚本就是把这些命令存到一个文件里,然后让 Shell 替你一条条执行。

1.1 你的第一个脚本

创建一个文件,写上你熟悉的命令:

# 用 echo 创建一个简单脚本(以后你会用编辑器写)
echo '#!/bin/zsh' > ~/Desktop/hello.sh
echo 'echo "你好,终端!"' >> ~/Desktop/hello.sh
echo 'echo "现在是 $(date)"' >> ~/Desktop/hello.sh
echo 'echo "你的家目录是 $HOME"' >> ~/Desktop/hello.sh

# 查看文件内容
cat ~/Desktop/hello.sh

现在你有两种方式运行它:

# 方式一:直接把文件交给 zsh 解释器(不需要执行权限)
zsh ~/Desktop/hello.sh

# 方式二:先加执行权限,然后直接运行(需要 chmod +x)
chmod +x ~/Desktop/hello.sh
~/Desktop/hello.sh            # 或者 ./hello.sh(如果当前在 Desktop 里)

输出:

你好,终端!
现在是 2026年 6月 3日 星期四 10:30:00 CST
你的家目录是 /Users/aodesai

1.2 shebang——第一行的秘密

脚本的第一行 #!/bin/zsh 叫 shebang(念作"希帮")。它告诉系统:用 /bin/zsh 这个程序来执行这个文件。

如果你写 #!/bin/bash,就会用 bash 执行。如果你写 #!/usr/bin/env python3,就会用 Python 执行。shebang 决定了脚本用什么语言写的。

这个系列的脚本都用 #!/bin/zsh,因为你的 Mac 默认 Shell 是 zsh。

1.3 命名与放置

规则说明
.sh 后缀约定俗成,不是必须——但加了便于识别
放到 ~/bin/ 或 ~/Desktop/随便放,但放 ~/bin/ 里并加入 PATH 后可以在任何位置直接调用
先加执行权限chmod +x 脚本.sh
用 ./ 运行当前目录下的脚本要加 ./——./脚本.sh

为什么当前目录的脚本要加 ./?因为 Shell 只在 PATH 里列出的目录中找命令。当前目录(.)一般不在 PATH 中——这是安全设计,防止你 cd 到一个陌生目录后不小心运行了里面的恶意脚本。用 ./ 明确地说"我就是想运行当前目录的这个文件"。

1.4 ⚠️ 一个重要区别:别名在脚本中无效

第五篇里你配置了一些别名(如 alias rm='rm -i')。这些别名在终端中交互式输入时有效,但在脚本文件中无效——因为脚本启动的是一个新的非交互式 Shell,不会读取 .zshrc。

#!/bin/zsh
# 即使你在 .zshrc 里设了 alias rm='rm -i'
# 下面的 rm 也不会询问确认——脚本中的 rm 是原始的 rm
rm 文件.txt

这意味着脚本中写命令时,必须写完整的、原始的版本。rm 就是 rm,不会自动变成 rm -ils 就是 ls,不会自动变成带颜色的版本。如果你需要脚本中的某个命令带特定选项,在脚本里显式写出来

#!/bin/zsh
# ✅ 正确:在脚本中显式写选项
rm -f 临时文件.tmp
ls -la ~/Desktop

这不是 bug——是设计。脚本需要在可预测的环境中运行,不依赖任何人的个性化配置。你在终端里用的别名是给你自己省事的,脚本的行为是写给所有人看的。

二、变量——让脚本记住东西

变量在第五篇(环境变量)已经接触过。在脚本里,变量的用法更灵活。

2.1 脚本内变量

#!/bin/zsh
# 设置变量(等号两边不能有空格!)
name="水母流星"
count=42

# 使用变量
echo "你好,$name"
echo "计数:$count"

# 字符串拼接
greeting="你好,${name}!今天第${count}次见面"
echo "$greeting"

${} 和 $ 的区别:花括号明确标出变量名的边界。${name}! 中 Shell 能正确识别变量名是 name 而不是 name!。不加花括号时,如果变量名后面紧跟字母或数字,Shell 可能认错。

2.2 命令替换——把命令的结果存进变量

#!/bin/zsh
# 把命令的输出存到变量中
today=$(date "+%Y-%m-%d")
files=$(ls ~/Desktop | wc -l)
current_dir=$(pwd)

echo "今天是 $today"
echo "桌面上有 $files 个项目"
echo "当前在 $current_dir"

$(命令) 语法会先执行括号内的命令,然后把它的输出字符串赋给变量。这是脚本中最常用的技巧之一——让脚本知道"当前状态是什么"。

也可以用反引号 `命令` 实现同样的效果,但 $() 更清晰且可以嵌套,建议始终用 $()

常见的反直觉点:count=$(ls | wc -l) 中,变量的值是字符串不是数字。虽然在 zsh 中它可以参与算术运算,但本质上它是字符 "42" 而不是数字 42。脚本中几乎所有变量都是字符串——这是 Shell 脚本和"真正的编程语言"的一个重要区别。

三、条件判断——让脚本做决定

没有条件判断的脚本只是"按顺序执行一遍"。有了 if,脚本可以根据不同情况走不同的路。

3.1 if 的基本结构

#!/bin/zsh

if [ 条件 ]; then
    # 条件为真时执行
    echo "条件成立"
fi

[ 条件 ] 中的方括号是 test 命令的简写形式。注意方括号前后必须有空格。 [ 条件 ] 是语法,[条件] 是错误。

3.2 常用的条件类型

条件为真的情况示例
[ -f 文件 ]文件存在且是普通文件[ -f ~/.zshrc ]
[ -d 目录 ]目录存在[ -d ~/Desktop ]
[ -e 路径 ]路径存在(不限类型)[ -e ~/Desktop ]
[ "$a" = "$b" ]两个字符串相等[ "$name" = "知白" ]
[ "$a" != "$b" ]两个字符串不等[ "$name" != "知白" ]
[ "$a" -eq "$b" ]两个数字相等[ "$count" -eq 42 ]
[ "$a" -lt "$b" ]数字 a 小于 b[ "$age" -lt 18 ]
[ -z "$a" ]变量 a 是空字符串[ -z "$name" ]

多个条件组合:[ 条件1 -a 条件2 ](并且)或 [ 条件1 -o 条件2 ](或者)。

#!/bin/zsh

file="$1"   # 脚本的第一个参数

if [ -f "$file" ]; then
    echo "✅ 文件存在:$file"
    echo "它的大小是:$(wc -c < "$file") 字节"
elif [ -d "$file" ]; then
    echo "📁 这是一个目录:$file"
    echo "里面的文件数量:$(ls "$file" | wc -l)"
else
    echo "❌ 文件不存在:$file"
fi

把这个脚本保存为 check.shchmod +x check.sh,然后运行 ./check.sh ~/.zshrc——它会告诉你文件是否存在、是文件还是目录。

关于 $file 两边的引号:在 [ -f "$file" ] 中加引号是必须的。如果 $file 包含空格(比如 "我的笔记.md"),不加引号的条件会变成 [ -f 我的 笔记.md ],Shell 会把它当成四个参数而不是一个文件名。引号让空格被正确识别为文件名的一部分。

四、循环——批量做同一件事

循环让你对一批文件或数据逐一执行相同的操作。

4.1 for 循环——遍历一组东西

#!/bin/zsh

# 最简单的 for 循环——遍历一个列表
for name in Alice Bob Charlie; do
    echo "你好,$name"
done

# 更实用的例子——遍历当前目录下的所有 .md 文件
for file in *.md; do
    echo "找到 Markdown 文件:$file"
    echo "行数:$(wc -l < "$file")"
    echo "---"
done

for 变量 in 列表; do ... done 是基本结构。列表 可以是直接写的(如 Alice Bob Charlie),也可以是通配符展开的结果(如 *.md),也可以是命令替换的输出(如 $(ls))。

#!/bin/zsh

# 备份指定目录下的所有 .jpg 文件
backup_dir="$HOME/Desktop/图片备份"
mkdir -p "$backup_dir"

for file in *.jpg; do
    echo "备份:$file"
    cp "$file" "$backup_dir/"
done

echo "✅ 备份完成,共备份了 $(ls "$backup_dir" | wc -l) 个文件"

4.2 while 循环——在条件满足期间持续执行

#!/bin/zsh

# while 循环:当条件为真时重复执行
count=1
while [ "$count" -le 5 ]; do
    echo "第 $count 次"
    count=$((count + 1))    # $(( )) 是算术运算
done

# 更实用的例子:逐行读取文件
while read line; do
    echo "读取到:$line"
done < ~/.zshrc

$(( )) 是 zsh 的算术运算语法。$((count + 1)) 会做数字加法,然后把结果赋给变量。

while 配合重定向 < 文件 可以逐行读取文件——这是脚本中处理配置文件、日志文件的常用模式。

for 和 while 的选择:如果你知道要对谁做循环(一组文件)→ for。如果你在等某个条件发生(比如计数器到某个值、读文件到末尾)→ while。 日常脚本中 for 的使用频率远高于 while。

五、脚本的专业化——参数、退出码和调试

前面的例子已经在用 $1 了。这一节系统梳理脚本的输入输出机制。

5.1 接收参数

#!/bin/zsh
echo "脚本名:$0"           # 脚本自己的名字
echo "第一个参数:$1"        # 第一个参数
echo "第二个参数:$2"        # 第二个参数
echo "参数总数:$#"          # 参数个数
echo "所有参数:$@"          # 所有参数组成的列表

实际脚本中,常先用 $# 检查参数数量是否足够,避免用户忘记传参:

#!/bin/zsh
if [ "$#" -lt 1 ]; then
    echo "❌ 用法:$0 文件名" >&2
    exit 1
fi

file="$1"
echo "要处理的文件:$file"

保存为 args.sh,运行 ./args.sh 苹果 香蕉 橘子,输出:

脚本名:./args.sh
第一个参数:苹果
第二个参数:香蕉
参数总数:3
所有参数:苹果 香蕉 橘子

5.2 退出码

每条命令执行后都会返回一个数字给系统——0 表示成功,非 0 表示出错。脚本也可以用 exit 主动设定退出码:

#!/bin/zsh

if [ -f "$1" ]; then
    echo "文件存在"
    exit 0          # 成功退出
else
    echo "文件不存在" >&2   # 错误消息输出到 stderr
    exit 1          # 失败退出
fi

退出码可以被脚本的调用者检查。&&|| 就是基于退出码工作的——只有左边退出码为 0 时 && 才执行右边。

5.3 调试模式——set -x

当脚本不按预期工作时,在脚本开头加一行 set -x

#!/bin/zsh
set -x                # 开启调试模式

file="$1"
if [ -f "$file" ]; then
    echo "存在"
fi

set +x                # 关闭调试模式

运行后,Shell 会在执行每条命令前把它打印到屏幕上(前面带 + 号),让你看到变量展开后的真实值、条件判断走了哪条路。

调试模式是排查脚本问题的第一工具。比瞪着眼看代码快得多。看到 + file='' 你就知道变量为什么是空的了——你忘了传参数。

六、第一个实用脚本——桌面备份工具

把以上所有概念串起来,写一个真正有用的脚本。这个脚本会把桌面上的重要文件备份到一个带日期的文件夹中。

#!/bin/zsh

# ═══════════════════════════════════════
# 桌面备份脚本
# 用法:./backup.sh [来源目录] [目标目录]
#   来源目录:要备份的文件夹(默认 ~/Desktop)
#   目标目录:备份存放位置(默认 ~/Desktop/备份)
# ═══════════════════════════════════════

# --- 参数处理 ---
source_dir="${1:-$HOME/Desktop}"     # 默认备份桌面
backup_dir="${2:-$HOME/Desktop/备份}" # 默认放在桌面的"备份"文件夹

# --- 检查来源目录是否存在 ---
if [ ! -d "$source_dir" ]; then
    echo "❌ 错误:来源目录不存在:$source_dir" >&2
    exit 1
fi

# --- 创建带日期的备份目录 ---
today=$(date "+%Y-%m-%d")
target="$backup_dir/备份_$today"
mkdir -p "$target"

echo "📦 开始备份:$source_dir"
echo "📂 目标位置:$target"
echo ""

# --- 备份所有 .md 和 .txt 文件 ---
count=0
for file in "$source_dir"/*.md "$source_dir"/*.txt; do
    if [ -f "$file" ]; then
        cp "$file" "$target/"
        echo "  ✅ 已备份:$(basename "$file")"
        count=$((count + 1))
    fi
done

# --- 统计结果 ---
echo ""
if [ "$count" -eq 0 ]; then
    echo "⚠️  没有找到 .md 或 .txt 文件"
    rmdir "$target" 2>/dev/null   # 空目录删掉
else
    echo "✅ 备份完成!共 $count 个文件"
    echo "📂 打开备份文件夹:$target"
    open "$target"
fi

exit 0

把这个脚本保存为 ~/Desktop/backup.sh,然后:

chmod +x ~/Desktop/backup.sh
~/Desktop/backup.sh                 # 备份桌面上的 .md 和 .txt 文件
~/Desktop/backup.sh ~/Downloads     # 备份下载文件夹里的文件

脚本中用到了 basename "$file"——它从完整路径中提取文件名部分。例如 basename "/Users/aodesai/Desktop/日记.md" 返回 日记.md。搭配它的还有 dirname(提取目录部分),它们是脚本中处理路径时的标配工具。

这个脚本包含了本篇学到的几乎所有概念:

这个脚本不是终点——它是你可以持续改进的起点。你可以给它添加更多文件类型、压缩备份包、只备份今天修改过的文件、删除超过 30 天的旧备份……每个改动的想法都是一个学习的机会。脚本是活的工具,不是一次性的作业。

七、综合练习——写你自己的脚本

任务一:写一个"文件查找器"脚本

创建一个脚本,接收一个文件名模式(如 *.md),在桌面范围内搜索匹配的文件,显示每个文件的大小和行数。

👆 查看参考命令
# 保存为 finder.sh
#!/bin/zsh
pattern="$1"
echo "搜索模式:$pattern"
echo "---"
for file in "$HOME/Desktop"/$pattern; do
    if [ -f "$file" ]; then
        size=$(du -sh "$file")
        lines=$(wc -l < "$file")
        echo "$(basename "$file")  |  大小:$size  |  行数:$lines"
    fi
done
# du -sh 的输出自带大小信息和文件名(如 "4.0K 文件.md"),
# 这里直接显示就行。如果你只想看大小不看名字,可以用 cut
# 处理,但 cut 我们还没学——先这样用着。

任务二:写一个"系统速查"脚本

创建一个脚本,运行后一次性显示:用户名、系统版本、运行时间、内存大小、桌面文件数。

👆 查看参考命令
# 保存为 sysinfo.sh
#!/bin/zsh
echo "═══════════════════════════"
echo "  系统速查"
echo "═══════════════════════════"
echo "用户:$(whoami)"
echo "系统:$(sw_vers -productVersion)"
echo "运行时长:$(uptime | grep -o 'up .*' | head -1)"
echo "CPU 核心:$(sysctl -n hw.ncpu)"
echo "桌面文件:$(ls ~/Desktop | wc -l) 个项目"
echo "═══════════════════════════"
# 提示:如果想看内存大小,可以用 sysctl -n hw.memsize,
# 但它的输出是以字节为单位的纯数字(如 17179869184),
# 需要换算成 GB(除以 1073741824),这超出了本篇范围。
# 以后学了 bc 或 python 可以完成换算。

任务三:让脚本可以随处运行

把你写的脚本放到 ~/bin/ 目录(需要自己创建),然后把该目录加入 PATH。

👆 查看参考命令
# 创建 ~/bin 目录
mkdir -p ~/bin

# 把脚本移进去
cp ~/Desktop/backup.sh ~/bin/

# 在 .zshrc 中添加一行
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# 现在可以在任何位置直接运行了
backup.sh

当你开始在 ~/bin 里存放自己写的脚本时,你就真正成了终端的主人。你不再只是使用别人写的命令——你在创造自己的命令。写脚本的能力会把计算机操作的自由度提升一个层级:任何重复劳动,都可以变成一个脚本。

📌 本篇核心语法速查

语法含义示例
#!/bin/zshShebang——用 zsh 解释器执行脚本第一行
变量="值"给变量赋值(等号前后无空格)name="知白"
$变量取变量的值echo "$name"
$(命令)命令替换——把命令输出赋给变量today=$(date)
$1 $2 ... $@脚本参数file="$1"
${1:-默认值}参数默认值dir="${1:-$HOME}"
[ 条件 ]条件判断(前后有空格!)[ -f "$file" ]
if ... then ... elif ... else ... fi分支结构if/then/elif/else/fi
for var in 列表; do ... done遍历循环for f in *.md; do
while 条件; do ... done条件循环while [ $n -le 5 ]
exit n以退出码 n 结束脚本exit 0(成功)exit 1(错误)
set -x / set +x开启/关闭调试模式脚本开头加 set -x
$(( ))算术运算n=$((n + 1))
$?上一条命令的退出码后面常用 if [ $? -eq 0 ]

待延伸线索


← 上一篇:让你的 Shell 认识你 下一篇:遇到问题怎么办 →