大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Shell 与 Bash
这一课你将能做到
- 拆解「命令 = 程序 + 选项 + 参数」的结构,并用 Tab 补全、Ctrl+R 历史搜索把常用命令敲得飞快
- 熟练使用管道与重定向:把 Spring Boot 日志中的 ERROR 行过滤出来、统计条数、另存为文件
- 理解 PATH 与环境变量:会用
export、which、alias,知道为什么关掉窗口后变量会丢、如何让它永久生效
为什么要学
上一课你说清了「Linux 是底座」,但底座只能通过命令行操作——而命令行的「翻译官」就是 Shell。你在终端里敲的每一个字符,都是 Shell 先解析、再交给系统执行。
部署 Java 服务时你会反复遇到这类「天书」:
nohup java -jar backend.jar > backend.log 2>&1 &
看不懂 2>&1、&、nohup 的人只能复制粘贴祈祷;看懂的人知道它把日志去了哪、错误去了哪、进程放到了后台。本课之后,这条命令的每个字符你都能说清。后面所有课(vim 改配置、systemd 看日志、grep 排错)全部以本课的语法为基础。
先建立图像
Shell 是包裹在内核外面的「壳」:你说话(敲命令),它翻译成系统调用,内核去驱动硬件,再把结果翻译回来。
┌─────────────┐ 你敲: ls -l /var/log
│ 终端窗口 │ (只是个显示/输入的窗口,如 Windows Terminal、iTerm)
└──────┬──────┘
▼
┌─────────────┐ ① 解析:程序=ls,选项=-l,参数=/var/log
│ bash │ ② 找程序:按 PATH 里的目录逐个找 ls
│ (Shell) │ ③ 执行:fork 出子进程运行它
└──────┬──────┘
▼
┌─────────────┐ 系统调用(open/read/write…)
│ 内核 │
└──────┬──────┘
▼
硬件(磁盘上真的有 /var/log)
命令的解剖结构:
$ grep -c --color "ERROR" backend.log
│ │ │ │ │ └─ 参数:操作对象
│ │ │ └─ 长选项(--开头,完整单词)
│ │ └─ 短选项(-开头,单字母;-c 表示 count)
│ └─ 程序名(PATH 中查找)
└─ 提示符(不是命令的一部分!root 用户是 #)
跟着做
以下命令在 Ubuntu/WSL2 直接可跑。跟着敲一遍,比看十遍有效。
步骤 1:认识提示符与三个基本信息命令
whoami # 我是谁(root 或 ubuntu)
hostname # 机器名
echo $HOME # 我的家目录(/root 或 /home/ubuntu)
pwd # 当前在哪个目录(print working directory)
提示符读法:root@iZbp1xxx:~# = 用户@主机名:当前目录(~ 代表家目录),# 表示 root,$ 表示普通用户。看到 # 要格外小心:root 干坏事的成本为零。
步骤 2:命令结构的动手验证
ls # 无选项无参数:列出当前目录
ls -l # 一个短选项:长格式
ls -l -h # 两个短选项:-h 人类可读大小
ls -lh /var/log # 合并短选项 + 参数
ls --all # 长选项:等价于 ls -a
规律:选项改变「怎么做」,参数决定「对谁做」。绝大多数命令支持 --help 查看用法:
ls --help | head -20 # help 输出太长?接个 head 只看前 20 行——这就是管道
步骤 3:效率三件套(Tab、历史、中断)
# ① Tab 补全:敲 ca 然后按 Tab → 补成 cat
cat /etc/os- # 敲到这里按两次 Tab → 列出所有候选(os-release)
# ② ↑↓ 翻历史;Ctrl+R 搜索历史:按 Ctrl+R 后输 "apt" 反查用过的 apt 命令
history # 看最近 500 条历史,带编号
!1002 # 重新执行第 1002 条(编号以你的 history 为准)
# ③ Ctrl+C:终止当前命令。试跑一个耗时的:
sleep 100 # 然后立刻按 Ctrl+C 中断
肌肉记忆清单:Tab 补全、↑ 上条命令、Ctrl+C 中断、Ctrl+L 清屏、Ctrl+R 搜历史。
步骤 4:求助的正确姿势
which java # java 命令在哪?(找不到说明没装或不在 PATH)
which ls # → /usr/bin/ls
man ls # 完整手册:空格翻页,/关键词 搜索(如 /-h),q 退出
ls --help # 快速用法(比 man 短)
面试和实战共识:先 which(命令存在吗)→ 再 --help(怎么用)→ 不懂再 man(细节)。
步骤 5:变量与环境变量
name=coderoadmap # 定义变量:等号两边【不能有空格】
echo $name # 使用变量:$变量名 → coderoadmap
echo "app is $name" # 双引号里变量会被解析
echo 'app is $name' # 单引号里原样输出 $name —— 经典区别
# 环境变量:能给「子进程」用的变量
export APP_ENV=prod # export 之后才对子进程可见
env | grep APP_ENV # 在环境列表里确认
# PATH:shell 找命令的搜索路径清单,冒号分隔
echo $PATH
# /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:...
which java || echo "PATH 里找不到 java → 要么没装,要么装在 PATH 之外"
变量只在当前会话有效,关窗即失。永久生效要写进配置文件(下一步)。
步骤 6:重定向与管道(本课核心)
mkdir -p ~/shell-lab && cd ~/shell-lab
# 造一份模拟的 Spring Boot 日志
cat > app.log <<'EOF'
2026-10-06 10:00:01 INFO Tomcat started on port(s): 8080
2026-10-06 10:00:02 INFO Started BackendApplication in 4.2s
2026-10-06 10:05:11 ERROR java.sql.SQLException: Connection refused
2026-10-06 10:05:12 ERROR Request failed: /api/map/list
2026-10-06 10:06:00 INFO health check ok
EOF
# 标准输出重定向:> 覆盖写,>> 追加写
grep ERROR app.log > errors.txt # 把 ERROR 行写进文件(覆盖)
grep ERROR app.log >> errors.txt # 再追加一遍(现在文件里有两份)
wc -l errors.txt # 6 行,验证了 > 和 >> 的行为
# 标准错误 stderr:2>
ls /not-exist > out.txt # 报错没进文件!报错走的是 stderr
ls /not-exist > out.txt 2> err.txt # 正常输出和错误分开收
ls /not-exist > all.txt 2>&1 # 2>&1:把 stderr 并入 stdout 一起收
# 验证经典天书:java -version 恰好把版本信息打到 stderr
java -version 2> jv.txt; cat jv.txt # 没装 java 则此步改为:man --version 2> jv.txt
# 管道:前一个命令的 stdout 接到后一个命令的 stdin
cat app.log | grep ERROR # 等价于 grep ERROR app.log(但演示了流)
grep ERROR app.log | wc -l # 统计 ERROR 有几行 → 2
grep ERROR app.log | tail -1 # 只看最后一条 ERROR
history | grep apt | tail -5 # 查你最近用过的 apt 命令
步骤 7:alias——把长命令变成自己的快捷键
alias # 看当前已有别名(Ubuntu 默认 ll 就是 ls -alF)
alias logs='tail -f /www/wwwroot/coderoadmap/logs/backend.log' # 示例路径
logs # 实时滚动日志,Ctrl+C 退出(文件不存在会报错,正常)
# 让别名永久生效:写进 ~/.bashrc
echo "alias logs='tail -f /www/wwwroot/coderoadmap/logs/backend.log'" >> ~/.bashrc
source ~/.bashrc # 让刚写的配置立即生效(新开窗口会自动加载)
讲清楚要点
- bash 是 Shell 的一种:sh(最早的 Bourne Shell)、dash(Ubuntu 里
/bin/sh实际指向 dash,更精简)、zsh(macOS 默认)。日常说「写个 shell 脚本」通常指 bash 脚本。 - 空格是分隔符:
name=x合法,name = x会被当成三条命令;文件名带空格必须加引号cat "my file.txt"。 - 严格区分大小写:
LS不是命令;变量Path与PATH是两个东西。 - 重定向改变数据的「去向」,管道连接命令之间的「流水线」:
> file是把流改道去文件;cmd1 | cmd2是把 cmd1 的输出当场喂给 cmd2。 2>&1的顺序不能反:> log 2>&1正确(先定 stdout 去向,再让 stderr 跟随它);2>&1 > log错误(stderr 先指向屏幕,之后 stdout 才改道,stderr 仍在屏幕上)。- PATH 决定「命令去哪找」:
command not found的两大原因——没安装、安装了但不在 PATH。which 命令名一步定位。 ~/.bashrc是个人终端配置:别名、变量、提示符都写这里;改完source ~/.bashrc生效。/etc/profile、/etc/bash.bashrc是全局配置(对所有用户生效)。
你可能会懵的地方
-
>和>>的风险差别?>会先清空文件再写入——cat app.log > app.log会把文件瞬间变空(shell 先执行重定向)。追加一律用>>;不确定时先备份或用tee -a file(既上屏又落盘)。 -
为什么 export 的变量换个 SSH 窗口就没了? 每开一个终端都是独立进程,变量随进程生灭。要跨会话持久,就写进
~/.bashrc(交互式 shell)或 systemd 的Environment=(服务场景,见进程课)。 -
单引号和双引号到底差在哪? 双引号解析变量(
"$name"→ coderoadmap),单引号纯字面量('$name'→ $name)。写脚本的经验法则:要变量用双引号,要原样(含$、*)用单引号。 -
sh a.sh、./a.sh、source a.sh三种执行有什么不同? 前两者都开子 shell 执行(./a.sh还要求文件有可执行位chmod +x),脚本里的cd、变量改变不影响当前终端;source(或.)在当前 shell 里执行,所以source ~/.bashrc能让配置立即生效。 -
命令输出太长刷屏看不过来怎么办? 接管道:
| less(翻页查看,q 退出)、| head -20/| tail -20(掐头去尾)、| grep 关键词(只看相关行)、| wc -l(只看行数)。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
name = "prod"(等号两边加空格) | shell 把 name 当命令执行:not found | name="prod" 等号紧贴 |
cat app.log > app.log | 重定向先清空文件,内容全丢 | 用 tee app.log 或先写临时文件 |
nohup java -jar app.jar > log(忘 2>&1) | 报错信息仍打印到屏幕,日志不完整 | > log 2>&1 |
| 命令里出现全角引号「"」或中文空格 | 从聊天窗口/网页复制代码常见,报诡异语法错 | 手敲或用支持替换的编辑器粘贴后检查 |
rm -rf $DIR/* 而 DIR 未定义 | 展开成 rm -rf /*,毁掉系统 | 变量用前判断:: "${DIR:?未设置}" |
在提示符后连 # 一起复制 | # 是提示符不是命令,会报错 | 只复制命令本体 |
卡点急救
- command not found:先
which 命令名。找不到 → 包没装(装:sudo apt install 包名);装了还找不到 → 不在 PATH(echo $PATH检查,把 bin 目录export进去)。 - 终端卡死不动:先按
q(less/top 等翻页器),无效再Ctrl+C,再无效开新窗口重连——跑着的命令仍在服务器上执行,重连不会杀掉它。 - 粘贴的命令报 syntax error:多半混入了全角字符或换行断裂。清空重敲;长命令拆成多步执行。
~/.bashrc改坏了每次开终端都报错:cp /etc/skel/.bashrc ~/.bashrc用模板覆盖回来(改坏前最好先cp ~/.bashrc ~/.bashrc.bak)。
面试常问
-
Shell 和 Bash 是什么关系? Shell 是命令解释器的统称(负责接收命令、解析、调用系统执行);Bash 是最流行的一种 Shell 实现(Bourne Again Shell),Linux 各发行版默认交互 Shell 都是 Bash。
echo $SHELL可看当前默认 Shell。 -
管道和重定向的区别? 重定向(
>、>>、2>)改变数据流的终点——文件或设备;管道(|)把前一个进程的标准输出直接接到后一个进程的标准输入,形成进程间流水线。典型组合:grep ERROR app.log | wc -l > err.count。 -
2>&1是什么意思?为什么必须写在重定向后面? 把文件描述符 2(stderr)指向文件描述符 1(stdout)当前指向的位置。必须先让 stdout 重定向到目标文件,再2>&1,stderr 才会跟着去文件;顺序反了 stderr 仍指向屏幕。所以标准写法是> log 2>&1。 -
nohup java -jar app.jar > log 2>&1 &这条命令每个部分什么含义?nohup:挂断终端(SSH 断开)也不终止进程;java -jar app.jar:业务命令;> log:stdout 写入 log;2>&1:stderr 一并写入 log;结尾&:放到后台运行。这是无 systemd 环境下临时启动 Java 服务的经典写法(进程课会讲更规范的 systemd 方式)。 -
source script.sh和./script.sh的区别?source在当前 shell 进程内执行,脚本中的变量修改、cd对当前会话生效,适合加载配置;./script.sh(需可执行权限)会 fork 子 shell 执行,改动不影响当前会话,环境更干净,适合运行业务逻辑。 -
如何查看并快速复用历史命令?
history列出(带编号),!n重执行第 n 条,Ctrl+R交互式反查,history | grep 关键词 | tail过滤检索。生产上历史记录也是审计线索(谁在什么时候敲了什么)。
自测三问
ls -lh /var/log里哪部分是程序、哪部分是选项、哪部分是参数?-lh为什么能合写?- 把
app.log里的 ERROR 行数统计出来并存入count.txt,命令怎么写? echo $APP_ENV在新终端里输出为空,如何让它对该用户所有新终端永久生效?
小结一句话
Shell 把你的命令翻译给内核:选项定怎么做、参数定对谁做;管道串流水线、重定向定去向、环境变量和 PATH 决定程序找得到找不到——吃透这一课,后面所有课的命令你都只欠「意思」不欠「语法」。
延伸
- Bash 参考手册(GNU 官方)
- Linux 命令行大全(The Linux Command Line 中译)
- 下一课预告:文件系统与权限——
-rwxr-xr-- 1 www www 4096这串字符每一段是什么意思?为什么 Permission denied 是后端部署第一大坑?