shell 入门指南
本目录代码默认采用 Bash 3.2+;Bash 4+ 特性另行标明。macOS 交互终端通常为 Zsh,脚本解释器以实际调用方式为准;外部命令还需区分 GNU/BSD 实现。
Shell 基础速查:Shell 种类与区别 → 变量与参数 → 执行模型 → 函数与流程 → 错误处理 → 实用片段 → 性能与最佳实践。
# Shell 简介
# 什么是 Shell
Shell 是命令行解释器,负责接收用户输入的命令并调用操作系统执行。它既是交互式的命令行环境,也是脚本语言的解释器。
# 常见 Shell 种类
| Shell | 全称 | 特点 | 适用场景 |
|---|---|---|---|
| sh | Bourne Shell | 历史上的 Bourne Shell;现代 /bin/sh 通常由兼容 POSIX 的实现提供 | 跨平台脚本,最小兼容 |
| bash | Bourne Again Shell | GNU 项目的 shell,sh 的增强版 | 许多 Linux 发行版采用,功能丰富 |
| zsh | Z Shell | 独立发展的 Shell,支持强补全与交互定制 | macOS 默认,交互式体验好 |
| fish | Friendly Interactive Shell | 开箱即用,语法不同 | 新手友好,交互式 |
| dash | Debian Almquist Shell | 轻量 POSIX shell | Ubuntu 的 /bin/sh,启动快 |
# POSIX 是什么
POSIX(Portable Operating System Interface)是一组操作系统接口标准。
- POSIX shell 是 shell 语言的标准子集
- 符合 POSIX 的脚本可以在任何 POSIX 系统上运行
- 常见的 POSIX shell:
sh、dash、bash --posix、zsh --emulate sh
# GNU 是什么
GNU 是一个自由软件项目,目标是创建一套完全自由的类 Unix 操作系统。
bash是 GNU 项目的产物coreutils(ls、cp、mv等)也是 GNU 项目的- GNU 工具通常比 BSD 版本功能更多,但兼容性稍差
GNU 与 BSD 工具差异的典型实战见 sed 用法与 BSD/GNU 兼容性踩坑:同一脚本本地能跑、Xcode 里报错的全过程。
# Shebang(#!/)
脚本第一行的特殊注释,告诉系统用什么解释器来跑这个脚本。
| Shebang | 说明 | 兼容性 |
|---|---|---|
#!/bin/sh | 用 POSIX shell 跑 | 最好,跨平台 |
#!/bin/bash | 用 bash 跑 | Linux 默认,macOS 也有 |
#!/usr/bin/env bash | 自动找 bash 路径 | 更灵活,推荐 |
#!/usr/bin/env python3 | 用 python3 跑 | Python 脚本 |
选择建议:
- 写跨平台脚本 →
#!/bin/sh - 用 bash 特有功能 →
#!/usr/bin/env bash - 用
env更灵活,不用硬编码路径
# 脚本头部模板
#!/usr/bin/env bash
set -euo pipefail # 启用可选检查;-e 有上下文例外,见错误处理指南
# 按语义局部设置 IFS;正确引用变量,不能靠全局 IFS 替代
2
3
# 变量与参数
# 特殊变量
| 变量 | 含义 |
|---|---|
$0 | 脚本名(按调用方式可能是相对路径、绝对路径或文件名) |
$1 $2 … | 位置参数 |
$# | 参数个数 |
$@ | 全部参数;加引号 "$@" 时每个参数独立成词 |
$* | 全部参数;加引号 "$*" 时合并为一个字符串(以 IFS 首字符连接) |
$? | 上一命令退出码(0 成功,非 0 失败) |
$$ | 当前 shell 的 PID |
$! | 最近一个后台进程的 PID |
$_ | 上一命令的最后一个参数 |
$PWD / $OLDPWD | 当前 / 上一个工作目录 |
$HOME $USER $SHELL | 用户环境 |
$HOSTNAME | 主机名(bash 自设但不保证导出,脚本中用 hostname 命令更稳) |
$RANDOM | 0~32767 随机数(bash/zsh) |
$SECONDS | shell 已运行秒数(bash/zsh) |
$LINENO | 当前行号 |
$FUNCNAME | 函数名数组([0] 为当前函数,构成调用栈)(bash) |
$BASH_SOURCE | 源文件路径数组(被 source 时与 $0 不同)(bash) |
标注(bash)的变量是 bash 特有的,POSIX sh 里没有。
$* 与 $@ 的区别只在加双引号时体现:
set -- "a b" c
printf '[%s]' "$*" # [a b c] —— 一个词
printf '[%s]' "$@" # [a b][c] —— 两个词
2
3
遍历参数永远用 "$@"。
$0 的常见取值:
./script.sh→$0 = ./script.sh;/path/to/script.sh→$0 = /path/to/script.sh- 纯文件名:
basename "$0"或${0##*/} - 完整路径:
realpath "$0"或readlink -f "$0"(Linux)
$_ 实用技巧:
mkdir -p /path/to/dir && cd "$_" # 建目录后直接切进去
# 变量默认值
| 语法 | 含义 |
|---|---|
${var:-default} | var 未定义或为空时用 default(不赋值) |
${var-default} | 仅 var 未定义时用 default(为空不用) |
${var:=default} | 未定义或为空时赋值为 default |
${var:?message} | 未定义或为空时报错退出 |
${var:+alternative} | 已定义且非空时用 alternative(否则为空) |
export ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" # 未定义则用默认值
remote_name="${1:-origin}" # 无第一个参数则用 origin
2
# 字符串操作
| 语法 | 含义 |
|---|---|
${#string} | 长度 |
${string:position} ${string:position:length} | 从 position 起取子串 |
${string#pattern} / ${string##pattern} | 从头删除最短 / 最长匹配前缀 |
${string%pattern} / ${string%%pattern} | 从尾删除最短 / 最长匹配后缀 |
${string/pat/rep} / ${string//pat/rep} | 替换第一个 / 全部匹配 |
${string/#pat/rep} / ${string/%pat/rep} | 仅在前缀处 / 后缀处替换 |
script="${0##*/}" # 纯文件名:从左删最长 "*/" 前缀
name="${file%.*}" # 去扩展名:从右删最短 ".*" 后缀
ext="${file##*.}" # 只取扩展名
out="${file%.*}_compressed.jpg"
lower="${var,,}" upper="${var^^}" # 转小写 / 大写(bash 4+)
2
3
4
5
# 数组
arr=("apple" "banana" "cherry")
echo "${arr[0]}" # 第一个元素
echo "${arr[@]}" # 所有元素
echo "${#arr[@]}" # 数组长度
arr+=("date") # 追加元素
for item in "${arr[@]}"; do
echo "$item"
done
2
3
4
5
6
7
8
9
10
这里是 Bash 索引数组语法,下标从 0 开始。Zsh 也支持数组,但默认从 1 开始;POSIX sh 不提供这种数组。
# 关联数组(bash 4+)
declare -A colors
colors["red"]="#FF0000"
colors["green"]="#00FF00"
echo "${colors["red"]}"
for key in "${!colors[@]}"; do # ${!arr[@]} 取全部键
echo "$key: ${colors[$key]}"
done
2
3
4
5
6
7
8
9
# 执行模型
父子 Shell、进程替换、Here Document、重定向和逐行读取见 Shell 执行模型与环境加载。
# 函数与流程
# 函数
function_name() {
local var1="$1" var2="$2" # local 限制作用域;参数用位置变量
echo "参数1: $var1, 参数2: $var2"
return 0 # 退出码 0~255,不是返回值
}
function_name "hello" "world"
echo "退出码: $?"
2
3
4
5
6
7
8
函数"返回值"有两种,不要混淆:
return N/$?:退出码(0~255),表示成功失败- 返回数据:
echo输出 + 命令替换捕获
get_dir() {
echo "/data/local/tmp" # 数据用 echo
}
dir=$(get_dir) # 捕获
2
3
4
process_files() {
local count=0
for file in "$@"; do
if [ -f "$file" ]; then count=$((count + 1)); fi
done
echo "共处理 $count 个文件"
}
2
3
4
5
6
7
# test 命令
| 类别 | 操作符 |
|---|---|
| 数值 | -eq -ne -gt -ge -lt -le |
| 字符串 | -z(空) -n(非空) = != |
| 文件 | -e(存在) -f(普通文件) -d(目录) -r -w -x -s(非空) -c -b |
[ -f "$file" ] # 文件存在
[ -d "$dir" ] # 目录存在
[ -n "$var" ] # 字符串非空
[ "$num1" -gt "$num2" ]
2
3
4
bash 中推荐内建 [[ ]]:不做词分割、变量可不加引号、支持 &&/||/=~ 正则(bash/zsh):
if [[ -f "$file" && "$name" =~ ^v[0-9]+$ ]]; then ...; fi
# 参数验证
# 参数数量
[ $# -lt 2 ] && { echo "用法: $0 <参数1> <参数2>" >&2; exit 1; }
# 文件存在
[ ! -f "$1" ] && { echo "错误: 文件 '$1' 不存在" >&2; exit 1; }
# 目录可写
[ ! -w "$(dirname "$output")" ] && { echo "错误: 无法写入目录" >&2; exit 1; }
2
3
4
5
6
7
8
# 错误处理与健壮性
严格模式的适用边界、错误降级、信号清理和作业控制见 Shell 错误处理指南。
# 实用片段
# PATH 修改
# 追加到末尾(低优先级);变量未定义时会拼出 "/bin",注意先确认变量存在
export PATH="$PATH:$ANDROID_BUILD_TOOL/bin"
# 前插到开头(高优先级,优先命中)
export PATH="/usr/local/opt/postgresql@16/bin:$PATH"
2
3
4
5
# 常用小命令
printf "%x\n" 102 # 十进制转 16 进制 → 66
printf "%d\n" 0x66 # 16 进制转十进制 → 102
mktemp # 创建临时文件
basename /a/b/c.txt # → c.txt(取文件名)
dirname /a/b/c.txt # → /a/b(取目录名)
readlink file # 读取链接目标文本;递归规范化用 realpath(先确认系统支持)
type cmd # 显示命令类型(别名/函数/内建/文件)
command -v cmd # 查找命令路径(POSIX 标准,脚本判断命令存在的首选)
2
3
4
5
6
7
8
# 交互配置
set -o vi:bash 切到 vi 编辑模式,光标移动同 vi
# 输出着色
color_echo() {
printf '\033[%sm%s\033[0m\n' "$1" "$2"
}
color_echo "0;31" "红色文本" # 0;31 红色,0;32 绿色,0;33 黄色
2
3
4
# 调试
set -x # 打印执行的每条命令
set +x # 关闭(脚本内局部调试)
bash -x script.sh # 从外部启动调试
trap 'echo "错误发生在第 $LINENO 行" >&2' ERR
2
3
4
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
2
3
# 性能与最佳实践
# 性能要点
# 1. 拒绝无用 cat
grep "pattern" file.txt # ✓ 直接读文件
cat file.txt | grep "pattern" # ✗ 多余一次 fork
# 2. 循环内避免调用外部命令——用参数扩展替代
for f in *.txt; do
base="${f%.*}" # ✓ 内置,零开销
base=$(basename "$f" .txt) # ✗ 每轮 fork 一个进程
done
# 3. 统计类操作交给专用命令,一次调用远快于 shell 循环
count=$(wc -l < file.txt) # ✓ 一次调用
while IFS= read -r line; do count=$((count + 1)); done < file.txt # ✗ 逐行解释执行,大文件慢几个数量级
2
3
4
5
6
7
8
9
10
11
12
13
# 变量引用
echo "$variable" # 总是加引号:防词分割与通配展开
echo "${array[@]}" # 数组同样加引号
2
# 临时文件
temp_file=$(mktemp) || exit 1
trap 'rm -f "$temp_file"' EXIT # 注意单引号,退出时才展开
echo "data" > "$temp_file"
2
3
# 常用工具函数
# 安全备份
backup_file() {
local file="$1"
local target
[ -f "$file" ] || return 1
target=$(mktemp "${file}.bak.XXXXXX") || return 1
if cp "$file" "$target"; then printf '%s\n' "$target"; else rm -f "$target"; return 1; fi
}
# 目录可写且(文件不存在或文件可写)
is_writable() {
local file="$1"
[ -w "$(dirname "$file")" ] && { [ ! -f "$file" ] || [ -w "$file" ]; }
}
# 去除前后空格(纯内置实现)
trim() {
local var="$1"
var="${var#"${var%%[![:space:]]*}"}" # 去前导
var="${var%"${var##*[![:space:]]}"}" # 去尾随
echo "$var"
}
to_lower() { echo "${1,,}"; } # bash 4+
to_upper() { echo "${1^^}"; }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# link
- runoob.com (opens new window) linux shell 学习
- Bash Guide (opens new window) - 全面的Bash指南
- ShellCheck (opens new window) - Shell脚本静态分析工具
- Google Shell Style Guide (opens new window) - Shell编程规范
# 引用与展开
| 写法 | 展开与边界 | 示例 |
|---|---|---|
'…' | 全部原样保留 | printf '%s\n' '$HOME' 输出 $HOME |
"…" | 参数/命令/算术展开;保留展开结果为一个参数 | printf '%s\n' "$HOME" |
$'…' | Bash/Zsh 的转义字符串 | $'a\nb' 含换行;非 POSIX 基础语法 |
$(cmd) | 捕获 stdout,删除末尾换行 | name=$(basename "$path");另查退出码 |
$((expr)) | 算术展开 | count=$((count + 1)) |
*.txt | 未引用的通配符匹配路径 | 不匹配时 Bash 默认保留字面量,Zsh 默认报错 |
Bash 中未引用的参数/命令展开通常还会经过 IFS 分词、路径名展开。x='a b'; printf '<%s>\n' "$x" 得到一个参数,省略引号会拆成两个。Zsh 默认普通标量不做同样的分词,不能据此推断 Bash。"${ZSH_CUSTOM:-~/.oh-my-zsh/custom}" 中的 ~ 不会展开,应使用 $HOME。
glob(如 *.txt)、正则(如 ^v[0-9]+$)、字面量是三种接口;${var//pattern/replacement} 中 pattern 也是 Shell 模式,不能把用户传入的任意文本直接当模式。
# 流程控制
| 结构 | 用途与最小示例 |
|---|---|
| if / elif / else | if [ -f "$file" ]; then printf '%s\n' found; else printf '%s\n' missing; fi |
| case | case "$mode" in start) start_service ;; stop) stop_service ;; *) exit 2 ;; esac |
| for | for file in "$@"; do printf '%s\n' "$file"; done;逐参数遍历 |
| while | while IFS= read -r line || [ -n "$line" ]; do printf '%s\n' "$line"; done < "$file";保留末行无换行的内容 |
| until | until check_ready; do sleep 1; done;正式脚本增加重试次数与超时 |
| break / continue | 退出循环/跳过本轮;可带循环层数 |
| && / || | 按退出状态执行后续命令;a && b \|\| c 中 b 失败也会执行 c,并非完整 if/else |
函数默认返回最后执行命令的状态。日志不要掩盖关键操作失败;local result=$(cmd) 会让 local 的状态遮蔽 cmd,应先 local result 再赋值并检查。
# 权限与文件语义
| 命令/概念 | 用途与边界 |
|---|---|
chmod u+x script.sh | 允许直接执行;bash script.sh 只要求能读脚本,解释器由命令指定 |
chmod 600 config / chmod 700 directory | 分别限制文件和目录仅所有者访问 |
umask 077 | 当前进程及后代创建对象时屏蔽组/其他人权限;不改变已有权限 |
| 目录 r / w / x | 列出名称/增删目录项/穿越访问;删除主要取决于父目录权限及 sticky bit 等约束 |
ln / ln -s / readlink | 硬链接、符号链接及查看目标文本;见 ln 与 readlink |
备份函数的完整契约与重复备份处理见 代码片段:文件备份;日志、着色示例分别保留简版和按级别/颜色名的版本,按接口选择。
依据:Bash 引用规则 (opens new window)、展开顺序 (opens new window)、参数展开 (opens new window)。
迁移前的章节入口
保留旧文章的章节锚点,旧书签可从这里继续前往专题正文。