1. 引言:为什么需要“内置”?
在 Linux 系统中,当我们输入一条命令时,Shell 需要找到对应的可执行文件并执行它。这个过程涉及在 $PATH 环境变量指定的目录中搜索、加载、执行等步骤,存在一定的性能开销。
然而,有一类特殊的命令完全绕过了这个过程——Shell 内置命令(Builtin Commands)。它们直接内嵌在 Shell 程序中,由 Shell 自身解释执行,无需创建额外进程。
核心价值:
- 性能极致:避免进程创建开销,执行速度极快
- 环境感知:可以直接修改 Shell 自身的状态(如当前目录、环境变量)
- 特殊能力:可以执行外部命令无法实现的操作(如
exit终止 Shell、jobs查看后台任务)
graph LR
subgraph "外部命令执行流程"
A1[输入 ls] --> B1[PATH 搜索]
B1 --> C1[fork 子进程]
C1 --> D1[exec 加载]
D1 --> E1[执行]
E1 --> F1[进程终止]
end
subgraph "内置命令执行流程"
A2[输入 cd] --> B2[Shell 内部查找]
B2 --> C2[Shell 函数直接调用]
C2 --> D2[执行]
end
2. 内置命令的分类体系
2.1 按功能分类
| 类别 | 命令 | 功能描述 |
|---|---|---|
| 作业控制 | bg, fg, jobs, disown, wait |
管理后台进程和作业 |
| 进程/环境 | cd, pwd, pushd, popd, dirs |
目录栈操作 |
export, unset, set, env |
环境变量管理 | |
alias, unalias, type, which |
别名与命令查找 | |
| 控制结构 | if, then, else, elif, fi |
条件分支 |
for, while, until, do, done |
循环控制 | |
case, esac, select |
模式匹配与菜单 | |
break, continue |
循环中断 | |
return, exit |
函数/脚本退出 | |
| 信息查询 | echo, printf |
输出 |
read, readonly |
输入与只读变量 | |
declare, typeset, local |
变量属性 | |
shift, getopts |
参数处理 | |
test, [ ], [[ ]] |
条件测试 | |
| 杂项 | source / . |
脚本执行 |
exec |
替换当前进程 | |
eval |
动态执行字符串 | |
trap |
信号捕获 | |
ulimit, umask |
资源限制与权限掩码 |
2.2 Bash 特有内置命令
| 命令 | 功能 |
|---|---|
mapfile / readarray |
将文件行读入数组 |
compgen |
生成补全列表 |
complete |
自定义补全行为 |
enable |
启用/禁用内置命令 |
shopt |
设置 Shell 选项 |
fc |
历史命令编辑 |
history |
历史记录管理 |
logout |
退出登录 Shell |
coproc |
协同进程 |
3. 核心内置命令详解
3.1 cd:改变目录
cd 是最具代表性的内置命令——外部命令不可能改变父 Shell 的当前工作目录。
工作原理:每个进程都有独立的当前工作目录(存储在 PWD 环境变量和内核 fs->pwd 结构中)。cd 通过 chdir() 系统调用修改 Shell 自身的当前目录,而不是创建子进程。
常用用法:
cd /path/to/dir # 切换到指定目录
cd .. # 上级目录
cd ~ # 家目录
cd - # 上一次的目录(存储在 OLDPWD 中)
cd # 无参数回到 HOME
相关变量:
PWD:当前工作目录OLDPWD:上一个工作目录CDPATH:cd的搜索路径(类似PATH)
3.2 export:环境变量传递
export 将 Shell 变量标记为需要传递给子进程。
核心概念:
- Shell 变量:仅在当前 Shell 中有效
- 环境变量:通过
environ数组传递给子进程
# 创建 Shell 变量
MYVAR="hello"
# 创建环境变量
export MYVAR
# 一步完成
export MYVAR="hello"
# 查看环境变量
env | grep MYVAR
3.3 alias 与 unalias:命令别名
工作原理:alias 命令将字符串替换存储在 Shell 内部哈希表中。当输入命令时,Shell 在查找内置命令和外部命令之前,先进行别名展开。
# 创建别名
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
# 查看所有别名
alias
# 删除别名
unalias ll
# 临时跳过别名(使用 \ 或引号)
\ls
"ls"
'ls'
3.4 type、command、which:命令查找
这些命令用于确定命令的类型和位置。
| 命令 | 功能 | 示例 |
|---|---|---|
type |
显示命令类型 | type ls → ls is aliased to 'ls --color=auto' |
command -v |
显示命令路径(POSIX 标准) | command -v python |
which |
外部命令(只查 PATH,不查内置/别名) | which ls |
command 的实用技巧:
# 绕过别名/函数,直接执行外部命令
command ls
# 检查命令是否存在
if command -v nvim >/dev/null 2>&1; then
export EDITOR=nvim
fi
3.5 read:读取用户输入
read 从标准输入读取一行,按 IFS(Internal Field Separator)分割后赋给变量。
# 基础用法
read name
echo "Hello, $name"
# 带提示(使用 -p)
read -p "Enter password: " -s password
# 超时(-t 秒)
read -t 5 -p "You have 5 seconds: " input
# 限制字符数(-n)
read -n 1 -p "Press any key to continue"
# 读取到数组(-a)
read -a colors <<< "red green blue"
IFS 的影响:
# 默认 IFS=空格制表符换行
echo "a:b:c" | IFS=: read x y z # x=a, y=b, z=c
# 保留整行(IFS=空字符串)
IFS= read -r line
3.6 eval:命令的二次解析
eval 将其参数拼接成字符串,然后再次交给 Shell 解析执行。这是元编程能力的体现,但也伴随着极大的安全风险。
# 动态生成变量名
for i in 1 2 3; do
eval "var_$i=$i"
done
echo $var_1 $var_2 $var_3 # 输出: 1 2 3
# 执行变量中的命令
cmd="ls -la"
eval $cmd
# ⚠️ 危险:用户输入经过 eval 会导致命令注入
read user_input
eval $user_input # 输入 'rm -rf /' 会执行!
3.7 exec:进程替换
exec 用新程序替换当前 Shell 进程,不创建子进程。替换后原 Shell 不再存在。
# 直接替换 Shell 为其他程序
exec bash --norc # 启动新 Shell,原 Shell 消失
# 重定向当前 Shell 的文件描述符
exec 3> output.txt
echo "Hello" >&3
exec 3>&- # 关闭文件描述符
# 修改标准输出(常用于脚本)
exec > log.txt 2>&1
echo "This goes to log.txt"
典型应用:容器入口脚本中 exec 替换 Shell 进程,使应用程序成为 PID 1。
3.8 source / .:在当前 Shell 中执行脚本
与直接执行 ./script.sh(创建子进程)不同,source 在当前 Shell 进程中执行脚本。脚本中的变量定义、目录切换等会保留。
# 两种等价写法
source ~/.bashrc
. ~/.bashrc
# 加载环境变量文件
source .env
3.9 trap:信号捕获
trap 设置 Shell 在收到特定信号时执行的命令。
常用信号:
| 信号名 | 数字 | 触发场景 |
|---|---|---|
INT |
2 | Ctrl+C |
TERM |
15 | 进程被终止 |
EXIT |
0 | Shell 退出(伪信号) |
ERR |
- | 命令返回非零(Bash 特有) |
DEBUG |
- | 每执行一条命令前(调试用) |
使用示例:
# 清理临时文件
temp_file=$(mktemp)
trap "rm -f $temp_file" EXIT
# 忽略 Ctrl+C
trap "" INT
# 恢复默认处理
trap - INT
# 调试模式:打印每条命令
trap 'echo "Running: $BASH_COMMAND"' DEBUG
3.10 set 与 shopt:Shell 选项
set 和 shopt 控制 Shell 的行为模式。
常用的 set 选项:
| 选项 | 功能 |
|---|---|
set -e |
遇到错误立即退出 |
set -u |
使用未定义变量时报错 |
set -x |
打印每条命令(调试) |
set -o pipefail |
管道中任一命令失败则失败 |
set -C |
禁止覆盖已存在文件 |
常用的 shopt 选项:
# 启用/禁用 glob 特性
shopt -s dotglob # 通配符匹配以 . 开头的文件
shopt -s nullglob # 无匹配时返回空字符串
shopt -s globstar # 支持 ** 递归匹配
# 历史相关
shopt -s histappend # 多个 Shell 共享历史
shopt -s cmdhist # 多行命令存为一行
# 其他
shopt -s nocaseglob # 不区分大小写匹配
shopt -s extglob # 启用扩展通配符
3.11 作业控制:jobs、fg、bg
这些命令管理 Shell 的后台作业。
# 启动后台作业
long_running_task &
# 查看作业
jobs -l # 显示 PID
jobs -r # 只显示运行中
jobs -s # 只显示停止的
# 前台/后台切换
fg %1 # 将作业1调到前台
bg %2 # 将作业2在后台恢复运行
# 脱离 Shell(避免 SIGHUP)
disown %1
disown -h %2 # 标记为不受 HUP 影响
# 等待作业完成
wait %1
作业状态:
Running:正在执行Stopped:被 Ctrl+Z 挂起Terminated:已终止Done:正常退出
3.12 printf:格式化输出
printf 是 echo 的增强版,行为更可预测,支持格式化控制。
# 基础格式
printf "Hello %s\n" "World"
# 数字格式化
printf "%d %04d\n" 42 42 # 输出: 42 0042
printf "%.2f\n" 3.14159 # 输出: 3.14
# 宽字符串对齐
printf "%-10s %5s\n" Name Age
printf "%-10s %5d\n" Alice 30
# 输出到变量(-v)
printf -v myvar "Hello %s" "World"
echo vs printf:
| 特性 | echo |
printf |
|---|---|---|
| 可移植性 | 不同 Shell 行为差异大 | 行为统一 |
| 转义字符 | 需 -e 显式启用 |
默认支持 |
| 格式化 | 不支持 | 完全支持 |
| 换行 | 自动添加 | 需显式 \n |
4. 内置命令的识别与管理
4.1 判断命令是否为内置
# 使用 type
type cd # cd is a shell builtin
type ls # ls is aliased to `ls --color=auto'
type python # python is /usr/bin/python
# 使用 command -V(更详细)
command -V cd
# 使用 builtin(强制解释为内置)
builtin cd /tmp
4.2 内置命令列表
# 查看所有内置命令
compgen -b
# 查看所有 Shell 关键字(if, for, while 等)
compgen -k
# 查看所有别名
compgen -a
# 查看所有函数
compgen -A function
4.3 启用/禁用内置命令
# 禁用内置命令
enable -n kill
# 重新启用
enable kill
# 查看禁用状态
enable -a | grep kill
# 禁用所有内置命令(危险!)
enable -n $(enable -a | cut -d' ' -f2)
5. 实战示例
5.1 使用 read 解析 CSV
#!/bin/bash
while IFS=',' read -r name age city; do
echo "Name: $name, Age: $age, City: $city"
done < data.csv
5.2 使用 exec 实现日志重定向
#!/bin/bash
LOG_FILE="/var/log/myapp.log"
# 将脚本所有输出重定向到日志
exec > >(tee -a "$LOG_FILE") 2>&1
echo "Starting application..."
./myapp
5.3 使用 trap 实现安全脚本
#!/bin/bash
set -euo pipefail
cleanup() {
echo "Cleaning up..."
rm -f "$temp_file"
exit 0
}
temp_file=$(mktemp)
trap cleanup EXIT INT TERM
# 主逻辑
echo "Working..." > "$temp_file"
5.4 使用 eval 动态执行(慎用)
# 安全使用 eval 的模式:先验证输入
sanitize() {
[[ "$1" =~ ^[a-zA-Z0-9_]+$ ]] || return 1
}
var_name="user_input"
if sanitize "$var_name"; then
eval "value=\$$var_name"
fi
5.5 使用 set -x 调试复杂脚本
#!/bin/bash
set -x # 开启调试
# 后续每条命令都会打印执行过程
for i in {1..3}; do
echo "Iteration $i"
done
set +x # 关闭调试
5.6 使用 shopt 增强文件匹配
#!/bin/bash
shopt -s dotglob nullglob
# 匹配所有文件包括隐藏文件
for file in *; do
echo "Found: $file"
done
6. 执行优先级
Shell 解析命令时的查找顺序:
graph TD
A[输入命令] --> B{别名?}
B -->|是| C[展开别名]
C --> B
B -->|否| D{关键字?<br>if, for, while}
D -->|是| E[执行关键字逻辑]
D -->|否| F{函数?}
F -->|是| G[执行函数]
F -->|否| H{内置命令?}
H -->|是| I[执行内置]
H -->|否| J[$PATH 搜索]
J -->|找到| K[外部命令]
J -->|未找到| L[报错]
优先级总结:
- 别名(
alias) - Shell 关键字(
if、for、while、function) - 函数
- 内置命令
- 外部命令(
$PATH搜索)
7. 常见问题与注意事项
7.1 cd 失败时不退出脚本
cd /nonexistent && echo "Success" || echo "Failed"
# 或使用 ||
cd /nonexistent || { echo "Cannot cd"; exit 1; }
7.2 子 Shell 中的环境变量丢失
# 管道创建子 Shell,变量不会保留
echo "hello" | read var
echo $var # 空!
# 解决方法
var=$(echo "hello") # 命令替换
# 或使用 lastpipe(Bash 4.2+)
shopt -s lastpipe
echo "hello" | read var
7.3 read 与 IFS 的陷阱
# 默认 IFS 会去除首尾空白
echo " hello world " | read line # line="hello world"
# 保留空白
IFS= read -r line
7.4 set -e 的“陷阱”
某些命令失败不会触发 set -e:
if、while、until条件中的命令&&或||右侧的命令- 管道中除最后一个以外的命令(除非设置
pipefail)
8. 总结:内置命令是 Shell 的“特权指令”
Shell 内置命令的核心价值在于它们能够直接访问和修改 Shell 内部状态。这一能力是外部命令永远无法实现的:
| 能力 | 内置命令 | 外部命令 |
|---|---|---|
| 改变当前目录 | ✅ cd |
❌ |
| 修改环境变量 | ✅ export |
❌ |
| 切换作业状态 | ✅ fg/bg |
❌ |
| 捕获 Shell 信号 | ✅ trap |
❌ |
| 退出当前 Shell | ✅ exit |
❌ |
| 定义别名/函数 | ✅ alias/function |
❌ |
理解内置命令的本质——它们不是外部程序,而是 Shell 程序的内部函数——是掌握命令行高效使用的关键一步。无论是 cd 切换目录、export 传递变量,还是 trap 捕获信号,这些操作都绕过了“创建子进程”的开销,直接与 Shell 内核交互。
在日常使用中,善用 type、command 区分命令类型,用 help 查看内置命令的帮助(而非 man),在脚本中优先使用内置命令(如 printf 优于外部 echo),都是提升效率和代码健壮性的实践。
原文 https://blog.csdn.net/2301_79518550/article/details/148175830