Shell 是工程环境里最基础的交互层。它不只是”输命令的地方”——它是内核与用户之间的翻译官,是管道、重定向、变量、脚本和自动化的运行环境。真正理解 Shell 的操作模型,才能把临时操作变成可靠脚本,也才能让命令行成为 CI 和 Agent 都能信任的接口。
1. Shell 是什么
Shell(壳层)是为用户提供操作界面的程序——在操作系统内核和用户之间充当翻译官。内核不直接与用户交互,Shell 接收你的命令,调用内核提供的系统调用,再把结果返回给你。
Unix Shell 家族中,Bash 是 Linux 默认、最广泛兼容的选择;Zsh 是 macOS 默认、交互体验更丰富的选择;sh(Bourne Shell) 是最基础的公共语法子集,CI 脚本和 #!/bin/sh shebang 用它保证兼容性。
这里有一个重要区分:交互 Shell 和脚本 Shell 是两件事。.zshrc 的那些别名、插件、提示符是为个人交互服务的;真正要交给 CI、服务器、同事或 Agent 执行的脚本,应该有意识地用 #!/bin/bash 或 #!/bin/sh 开头,不依赖任何个人 Shell 配置。
2. 命令结构
命令 [命令参数] [命令对象]- 命令名称:语法中的动词,表达想要做的事情(
ls、cp、git) - 命令参数:调整命令行为(
-l、--verbose、-r) - 命令对象:命令作用的目标(文件名、目录、用户名)
| 格式 | 示例 |
|---|---|
| 长格式 | ls --all --human-readable |
| 短格式 | ls -ah |
Linux/macOS 中的命令、参数、对象都严格区分大小写。ls -R 和 ls -r 是不同的参数。
3. 目录与上下文
目录位置是 Shell 操作的上下文。任何会删除、移动、生成文件的命令,执行前都应该先确认 pwd。
| 命令 | 说明 |
|---|---|
pwd | 打印当前目录 |
cd / cd ~ | 回到当前用户的主目录 |
cd - | 回到上一次所在目录 |
pushd <path> | 进入目录并把当前目录压栈 |
popd | 从目录栈弹出,回到上一个目录 |
cd - 在两个目录之间来回切换时很实用。pushd/popd 适合需要临时跳到另一个目录再回来的场景。
4. 键盘快捷键
这些快捷键在 Bash 和 Zsh 中通用,是日常输入效率的基础:
| 快捷键 | 功能 |
|---|---|
Tab | 自动补全命令或文件名 |
Ctrl+R | 反向搜索命令历史 |
Ctrl+A / Ctrl+E | 光标移到行首 / 行尾 |
Ctrl+U / Ctrl+K | 删除光标到行首 / 行尾 |
Ctrl+W | 删除光标前一个单词 |
Ctrl+Y | 粘贴被 Ctrl+U/K/W 删除的内容 |
Alt+B / Alt+F | 向前 / 向后移动一个单词 |
Ctrl+L | 清屏(等价于 clear) |
Ctrl+C | 终止当前命令 |
Ctrl+Z | 把当前进程挂起放到后台 |
Ctrl+D | 退出当前终端(或发送 EOF) |
Ctrl+R 是最值得记住的——反向搜索历史,输入关键词实时过滤,比上下键翻历史高效得多。
5. 特殊符号
Shell 中有一批符号承担特殊含义,是组合命令和控制流程的基础:
| 符号 | 功能 | 示例 |
|---|---|---|
&& | 前一个命令成功才执行后一个 | make && make install |
|| | 前一个命令失败才执行后一个 | test -d dir || mkdir dir |
; | 顺序执行,不论成功与否 | cd /tmp; ls |
| | 管道,前一个命令的 stdout 作为后一个的 stdin | ps aux | grep nginx |
> | 输出重定向(覆盖) | ls > files.txt |
>> | 输出重定向(追加) | echo "done" >> log.txt |
< | 输入重定向 | sort < data.txt |
$() | 命令替换,将结果嵌入 | echo "today is $(date)" |
{} | 大括号扩展 | echo {1..5} 输出 1 2 3 4 5 |
!! | 引用上一条命令 | sudo !!(提权重跑) |
!$ | 上一条命令的最后一个参数 | mkdir /some/path && cd !$ |
# | 注释 | # 这行不会执行 |
&& 是脚本里最常用的控制符——只有前一步成功才继续,是最简单的错误防护。
6. 引号与转义
Shell 对引号的处理有四种不同的语义,混淆了是大多数 Shell 错误的来源之一:
# 反斜杠(\):转义紧跟的单个字符
echo \$HOME # 输出字面量 $HOME,不展开变量
# 单引号(''):完全转义,内部所有字符视为字面量
echo '$HOME' # 输出 $HOME
# 双引号(""):保留变量展开和命令替换,但阻止通配符
echo "$HOME" # 输出 /Users/username
# $():命令替换,执行内部命令并用其输出替换
echo "today: $(date +%Y-%m-%d)"推荐用 $() 替代反引号 ` `:两者功能等价,但 $() 可以嵌套,也更易阅读。
拿不准时先加引号——尤其是路径中可能包含空格的情况下,"$path" 比 $path 安全得多。
7. 环境变量
环境变量是 Shell 的全局配置层,影响命令的行为和搜索路径。
命名约定:变量名通常全大写(PATH、HOME),这是约定俗成的规范。
| 变量 | 作用 |
|---|---|
PATH | 可执行文件搜索路径,冒号分隔 |
HOME | 当前用户的主目录 |
SHELL | 当前使用的 Shell 路径 |
EDITOR | 默认文本编辑器(git commit 等会用到) |
LANG | 系统语言和字符集 |
PWD / OLDPWD | 当前目录 / 上一个目录 |
USER | 当前登录用户名 |
HISTSIZE / HISTFILESIZE | 历史记录条数 |
PS1 | Bash 提示符格式 |
常用操作:
# 查看某个变量
echo $PATH
# 设置变量(当前 Shell 会话有效)
export EDITOR="nvim"
# 临时覆盖(仅对该命令有效,不修改当前 Shell 环境)
EDITOR=vim git commit
# 追加到 PATH
export PATH="$HOME/.local/bin:$PATH"写进 .zshrc 或 .bash_profile 的 export 在每次启动 Shell 时生效。只在命令行临时 export 的变量,关掉终端就消失了。
8. Tab 补全与历史
Tab 补全是 Shell 的可发现性入口。它不仅减少输入,也能告诉你当前工具支持哪些子命令、参数和值。对陌生 CLI 来说,补全往往比翻文档更直接。
$ git ch<Tab>
checkout cherry-pick cherry历史记录:
history # 列出历史命令(带编号)
!42 # 执行历史中第 42 条命令
!git # 执行最近一条以 git 开头的命令
^old^new^ # 把上一条命令中的 old 替换为 new 并执行重要习惯:从历史里复制命令时,要重新看一遍上下文——路径、环境变量、目标主机和危险参数可能已经不同了。
9. 别名
别名把长命令缩短为简短入口:
# 定义(在 .zshrc 中永久生效)
alias ll="ls -la"
alias gs="git status"
alias cat="bat"
# 临时取消(当前 Shell 会话)
unalias ll
# 查看所有已定义的别名
alias边界:别名适合个人交互,不适合团队脚本。写给 CI、服务器或同事执行的脚本应该写完整命令,不依赖任何人的 Shell 配置。
10. 管道
管道把前一个命令的 stdout 直接连接到下一个命令的 stdin,是 Unix 文本流范式的核心:
# 统计 /etc/passwd 中禁止登录的用户数量
grep /sbin/nologin /etc/passwd | wc -l
# 查找进程
ps aux | grep nginx
# 提取日志中的 ERROR 行,去重计数
grep "ERROR" app.log | sort | uniq -c | sort -rn管道可以无限延伸,但组合越长,可读性越差,失败定位越难。三四段之后通常应该考虑拆成脚本或保存中间结果。
管道可靠的前提是:上游输出格式足够稳定,下游能正确理解输入。如果上游命令改变了输出格式,整条管道就会静默地产生错误结果。
11. 重定向
Shell 的输入输出有三条标准流,每条可以独立重定向:
| 流 | 文件描述符 | 说明 |
|---|---|---|
| stdin | 0 | 标准输入,默认来自键盘 |
| stdout | 1 | 标准输出,默认输出到屏幕 |
| stderr | 2 | 错误输出,默认输出到屏幕 |
输出重定向:
ls -l > files.txt # stdout 写入文件(覆盖)
ls -l >> files.txt # stdout 追加到文件
ls /noexist 2> err.txt # stderr 写入文件
ls /noexist 2>/dev/null # 丢弃 stderr
cmd >> out.txt 2>&1 # stdout 和 stderr 都追加到文件
cmd &>> out.txt # 等价简写输入重定向:
sort < data.txt # 用文件内容作为 stdin
sort < data.txt > sorted.txt # 同时重定向输入和输出
# Here 文档(heredoc):把多行文本作为 stdin
cat << EOF
line one
line two
EOF脚本中的关键原则:区分 stdout 和 stderr,是为了让结果、日志和错误能被不同系统接住。不要把进度日志混进要被下游解析的数据流。
12. 通配符
通配符用于模式匹配,方便批量操作文件。批量操作前先用 ls 预览匹配范围,再执行删除或移动。
| 通配符 | 含义 | 示例 |
|---|---|---|
* | 匹配任意字符(含空字符串) | *.log 匹配所有 .log 文件 |
? | 匹配单个任意字符 | ?.txt 匹配 a.txt,不匹配 ab.txt |
[a-z] | 匹配范围内的单个字符 | [a-z]*.md 匹配小写字母开头的 .md |
[0-9] | 匹配单个数字 | log[0-9].txt |
[[:alpha:]] | 匹配任意字母 | POSIX 字符类 |
[[:digit:]] | 匹配任意数字 | POSIX 字符类 |
# 安全做法:先预览,再执行
ls *.log
rm *.log # 确认无误后再删
# 批量操作
mv report_[0-9][0-9].txt archive/13. 命令替换
命令替换把一个命令的执行结果嵌入另一个命令中:
# 推荐写法(可嵌套)
echo "当前目录:$(pwd)"
echo "Git 分支:$(git branch --show-current)"
# 等价的传统写法(不推荐,不可嵌套)
echo "当前目录:`pwd`"
# 嵌套(只有 $() 支持)
echo "父目录:$(dirname $(pwd))"
# 赋值给变量
branch=$(git branch --show-current)
echo "当前在 $branch 分支"14. 任务管理
Shell 可以管理前台和后台进程。
后台运行:
# & 放到后台,但关闭终端会终止
sleep 300 &
# nohup:忽略挂断信号,关闭终端后仍继续运行
nohup sleep 300 &
# 重定向输出(nohup 默认输出到 nohup.out)
nohup ./server.sh > server.log 2>&1 &管理后台任务:
jobs # 列出当前会话的后台任务
fg %1 # 把任务 1 切换到前台
bg %1 # 把挂起的任务 1 放到后台继续运行
Ctrl+Z # 把前台任务挂起(发送 SIGTSTP)查看和终止进程:
ps aux # 查看所有进程
ps aux | grep nginx # 过滤特定进程
kill %1 # 终止后台任务(按 jobs 编号)
kill <PID> # 终止进程(按 PID)
kill -9 <PID> # 强制终止(SIGKILL,进程无法拦截)边界:& 和 nohup 适合临时任务;长期运行的服务应该交给 systemd、supervisor、tmux 或专门的任务系统,带日志、重启策略和停止入口。
15. 获取帮助
# man 手册(最权威)
man ls
man bash
# man 页面导航
# 空格:下翻页 /keyword:搜索 n/N:下/上一个匹配 q:退出
# 内置帮助(快速查参数)
ls --help
git commit --help
# 查命令类型(内置 / 外部 / 别名 / 函数)
type ls
type cd # 输出 "cd is a shell builtin"
# 查命令位置
which git
whereis pythonman 和 --help 的侧重不同:man 是完整参考文档,--help 是快速摘要。对陌生命令先看 --help,需要深入细节时看 man。
16. 使用原则
先读上下文:pwd、whoami、echo $SHELL、echo $PATH 经常比命令本身更重要。在生产服务器上操作之前,先确认你在哪台机器、哪个目录、哪个用户。
交互配置和自动化分开:别名、补全、历史增强适合提高个人效率;要交给 Agent、CI 或同事执行的动作,应该写成完整命令、脚本或任务入口,不依赖任何人的 Shell 配置。
变量和引号要保守:路径、空格、引号、通配符是 Shell 错误高发区。拿不准时先 echo、先加双引号、先 dry-run。
stdout 是数据,stderr 是诊断:脚本和 Agent 都依赖这个边界。不要把进度日志混进要被下游解析的数据流。
先预览,再写入:通配符、重定向、rm、mv、xargs 组合前,先确认匹配范围和输出目标。
命令短不等于可靠:一行管道能解决问题,但如果不可读、不可调试、不可复用,就应该拆成有名字的脚本或任务。