大概每个刚开始碰 Linux 终端的人都会经历这样的阶段:一条命令看下来,单词全认识,但夹在里面的$、>、&、*、?这些符号让人一头雾水。我第一次执行find . -name "*.log" -mtime +7 -exec rm {} \;的时候,根本不理解{}和\;是干什么的,只能把整条命令当咒语背下来。后来专门把 shell 的符号体系过了一遍,才真正从“抄命令”变成“写命令”。
这篇东西不是命令大全,而是特殊符号大全。我按使用场景把 Linux 终端里最常见的几类符号拆开讲:通配符与路径展开、引号与转义、重定向与管道、变量与命令替换、历史命令符号、脚本逻辑符号,以及终端里的控制字符。适合刚开始学 Linux 的新手,也适合已经用了一段时间、遇到$(...)和${...}还要现查的老弟老妹。内容以 Bash 为准,大部分结论在 Zsh 下也成立。
1. 通配符与路径展开:*、?、[]、{}、~、- 这些符号在动手前会先被展开
1.1 路径展开:~、-、.、.. 到底指哪里
先记住一个底层概念:shell 在执行命令之前,会先做一堆“展开”动作。把~换成本用户的家目录,把*换成匹配到的文件名,把$(...)换成命令的输出结果。很多符号的坑都出在这个“先展开、后执行”的顺序上。
~是最常见的路径符号。echo ~会直接输出当前用户的家目录路径,比如/home/yourname。所以cd ~/Desktop等价于cd /home/yourname/Desktop。刚打开终端时pwd显示的就是家目录,这也是提示符里出现~的原因。
-代表“上一次所在目录”。cd -不是去上一级,而是回到上一次 cd 之前的位置。假设你从/var/log切到/etc,执行cd -就会跳回/var/log。连续执行两次cd -,就会在两个目录之间反复横跳。改完配置想回工作目录时,这个符号特别省事。
.和..分别代表当前目录和上级目录。ls .查看当前内容,cd ..回到上级目录,cd ../..连续上跳两级。在find .这条命令里,点号的意思就是“从当前目录开始递归查找”,后面会经常见到。
顺带把提示符也解释了。user@host:~$里的~表示当前目录在家里,$表示当前是普通用户。如果提示符最后是#,比如root@host:/etc#,说明你正以 root 身份操作。看到$和#就要立刻反应过来权限上下文,避免用错权限执行危险命令。
1.2 通配符匹配:*、?、[] 文件名的“模糊查询”
shell 通配符用来匹配磁盘上已经存在的文件名,不是正则表达式。很多新手把这两者混在一起,后面会吃大亏。
*匹配任意多个字符,包括零个字符。ls *.txt列出所有以.txt结尾的文件,rm -rf /tmp/backup-*删除/tmp下所有 backup- 开头的文件。注意它不会匹配以.开头的隐藏文件,ls *永远看不到.bashrc,这是刻意设计的安全机制,免得rm *把配置文件一波带走。想匹配隐藏文件得显式写.bashrc或用ls -a。
?匹配恰好一个字符。file?.log能匹配file1.log、fileA.log,但匹配不了file10.log,因为?只占一位。[ ]表示字符集合,file[0-9].txt匹配file0.txt到file9.txt,[abc]匹配 a、b、c 中任意一个,[!0-9]表示“非数字”。写备份清理脚本时,[0-9]{8}配合日期格式来筛文件是常规操作。
千万别把通配符和正则混为一谈。正则里*表示“前一个字符重复任意次”,^和$表示行首行尾,这些概念和 shell 通配符完全是两码事。后面聊grep '^d'时你会再看到^,那是在正则语境里,不是通配符。
1.3 大括号展开:{} 帮你生成批量清单
大括号展开和通配符有本质区别:通配符匹配已存在的文件,大括号展开则单纯生成字符串列表,才不管文件存不存在。
echo {a,b,c}输出a b c,echo {1..10}输出 1 到 10。更实用的用法是配合其他命令批量操作:
mkdir -p project/{src,bin,docs} cp app.log{,.bak}第二行等价于cp app.log app.log.bak,这是复制日志文件做备份的经典简写。{a,b}{1,2}能组合出a1 a2 b1 b2,嵌套、范围都可以自由组合。想要批量创建a.txt、b.txt、c.txt时,touch {a..c}.txt一句顶三句。
1.4 实战安全习惯:先 echo 再执行 rm
通配符展开发生在命令执行前,一旦展开出意外路径列表,删除操作不可逆。我自己的铁律是:凡是带*的删除命令,先跑一遍echo看展开结果。
echo rm -rf *.log看到输出的文件列表符合预期,再摘掉echo真正执行。也可以先用ls -l预览匹配结果。另外,当目录里文件数量极大时,通配符展开可能触发Argument list too long报错,因为展开后的参数列表超过了系统 ARG_MAX 限制。这时不要硬刚通配符,改用find /tmp -name "*.log" -delete或find ... | xargs rm -f更合适。
2. 引号与转义:''、""、\ 决定了变量和符号会不会被解析
2.1 单引号 vs 双引号 vs 反斜杠:三种“屏蔽”方式
引号和转义的作用是让 shell 暂时停止解释特殊符号。但三者的屏蔽力度完全不同,直接看对比表:
| 写法 | 屏蔽级别 | $HOME会展开吗 | 通配符会展开吗 |
|---|---|---|---|
'...' | 全部屏蔽 | 不会 | 不会 |
"..." | 部分屏蔽 | 会 | 不会 |
\ | 只屏蔽紧挨着的下一个字符 | 看情况 | 看情况 |
单引号是“所见即所得”,里面写什么就是什么。双引号则允许$、反引号和部分\继续工作,但*、?这些通配符不会展开。最直观的例子:
echo "$HOME" echo '$HOME'第一行输出/home/user,第二行输出$HOME。这就是区别。
2.2 常见场景:带空格文件名、变量拼接、通配符加引号
处理带空格的文件名时,引号和反斜杠都能解决问题:
cat "my report.txt" cat my\ report.txt两者等价,都是让 shell 把my report.txt当成一个参数,而不是拆成两个。我习惯用双引号,视觉上更清晰。
变量拼接是${}的主场。假设name=Linux,想输出Linux的笔记.txt:
echo "${name}的笔记.txt"如果不加大括号,shell 会认为变量名叫name的笔记.txt,结果啥也输出不了。
双引号里的通配符不会展开,echo "*.txt"输出字面的*.txt。特别容易踩坑的是ls "*.txt":这不是列出所有 txt 文件,而是找文件名就叫*.txt的文件。在脚本里拼接路径时务必想清楚这个区别。
2.3 反斜杠续行与 find -exec 的转义坑
命令太长时,行尾的\表示“命令还没完,下一行继续”。注意\和换行之间不能有空格,否则就不是续行,而是转义空格。排版上的常规做法:
find /data \ -name "*.log" \ -mtime +7 \ -exec rm {} \;这个例子同时解释了find -exec里的转义问题。末尾的\;必须转义,因为;如果裸露在外面,会被 shell 当成命令分隔符,find 直接语法报错。我第一次写脚本时把\;写成;,报错排查了半天才意识到是转义问题。
3. 重定向与管道:>、>>、<、2>&1、| 让命令之间互相传递数据
3.1 先理解文件描述符:0、1、2 分别代表什么
Unix 哲学里“一切皆文件”,每个进程默认有三个文件描述符:0 是标准输入 stdin,1 是标准输出 stdout,2 是标准错误 stderr。重定向干的事情,就是把这几个通道从默认的终端改接到文件或其他进程。
很多教程直接抛>和2>的用法,但如果不理解 0/1/2 的底层逻辑,碰到2>&1还是会懵。记住这三个编号,后面全是推导式理解。
3.2 输出重定向 >、>> 与输入重定向 <、heredoc
>是覆盖写。ls -l > files.txt把输出写进文件,文件若已存在会先被截断再写入,这是不可逆操作。>>是追加写,日志场景用得最多:echo "$(date)" >> app.log。
危险案例必须单独拎出来:sort file.txt > file.txt会得到一个空文件。原因是 shell 先执行重定向,打开file.txt并清空,然后 sort 读到的已经是空文件了。重定向的目标文件和输入文件绝不能是同一个。
输入重定向的<很简单,wc -l < /etc/passwd直接把文件内容喂给命令。更常用的是 heredoc:
cat <<EOF > config.txt key=value EOF<<后面跟定界符,内容写到再次出现定界符为止。如果把EOF写成'EOF',内部的$HOME这类变量不会展开,保持字面量,这在生成配置模板时很实用。Bash 还支持<<<here string,比如grep "foo" <<< "$var",直接把变量内容作为标准输入。
3.3 错误重定向 2> 与 2>&1:顺序写反了结果完全不同
默认>只接管标准输出,错误信息还是会打到终端。看一个常见的清理场景:
find / -name "*.conf" 2>/dev/null这里2>/dev/null的作用是把错误信息丢进黑洞,只保留正常结果。想要把正常输出和错误信息写进同一个文件,写法有讲究:
command > all.log 2>&1顺序是:先把标准输出指向 all.log,再把标准错误复制到“标准输出当前指向的位置”,也就是 all.log。如果反着写:
command 2>&1 > all.log先把标准错误复制到“标准输出当前指向的位置”,此时还是终端;再把标准输出指向 all.log。最后错误依然显示在终端。记忆口诀:谁先指向终点,谁就在终点。这个执行顺序问题面试经常被问,实际排错时也容易踩。
3.4 管道 | 与 tee:从“写文件”到“接程序”
|管道把左边命令的 stdout 直接接到右边命令的 stdin,不是中间落盘再读,而是实时流转:
ls -l | grep '^d' ps aux | grep sshgrep '^d'里的^是正则的行首锚点,作用是只保留以d开头的行,也就是目录条目。ps aux | grep ssh则是查进程的经典组合,注意 grep 进程本身也可能出现在结果里,嫌烦可以加| grep -v grep。
>和|的分工一个接文件、一个接命令。而tee能同时做到两者:
echo "hello" | tee file.txt输出显示在终端,同时写入 file.txt。tee -a是追加模式。调试脚本时想一边看日志一边存档,这个命令就是为此生的。
4. 变量、命令替换与算术扩展:$、${}、$()、$(( )) 是最容易搞混的一组
4.1 $var 与 ${var}:边界问题
变量赋值语法是name=Linux,注意等号两边不能有空格。读取变量时$name和${name}多数情况下等价,但拼接场景立刻见分晓:
name=Linux echo "${name}的笔记.txt"$name的笔记.txt会被解析成变量名name的笔记.txt,结果为空。加上大括号明确边界,输出才是Linux的笔记.txt。我的习惯是脚本里一律写${var},可读性更好,也彻底避开这种边界陷阱。
4.2 ${} 还能做默认值、字符串截断
这组是写健壮脚本的利器。服务端口没设置时给个默认值:
PORT="${PORT:=8080}" echo "${HOST:-localhost}" echo "${DATABASE:?数据库地址未设置}":=在变量为空时写入默认值,:-只在本次表达式中用默认值但不用改变量本身,:?在变量为空时直接报错退出,非常适合脚本参数检查。以上第一行别漏了开头那个空命令:,不然参数展开的结果会被当成命令执行而出错。
字符串截断是处理文件名的基本功:
file="archive.tar.gz" echo "${file%.*}" # 输出去掉最短后缀的结果:archive.tar echo "${file%%.*}" # 去掉最长后缀:archive echo "${file#*.}" # 去掉最短前缀:tar.gz echo "${file##*.}" # 去掉最长前缀:gz%从尾巴削,#从开头削,双写取最长匹配。注意这些 pattern 是通配符风格,不是正则。
4.3 $() 命令替换与反引号:为什么我推荐用 $()
命令替换的作用是把另一个命令的输出嵌入当前命令:
now=$(date +%F) echo "今天是 $now" echo "当前目录有 $(ls -1 | wc -l) 个文件"老式写法是反引号:
now=`date +%F`反引号没有消失,但在需要嵌套时非常痛苦:
result=`echo \`date\``这段代码的转义可读性极差。$()支持直接嵌套,逻辑更清晰。我统一建议用$(),新写的脚本里没必要再碰反引号。
命令替换也有坑:输出结尾的换行符会被吃掉,输出内容过大时可能超出参数长度限制。更隐蔽的是退出码问题,if value=$(maybe_fail); then判断的是这条赋值语句整体的退出码,如果想拿到里面命令的真实退出码,应拆开写:
value=$(maybe_fail) status=$? echo $status4.4 $(( )) 算术扩展与 (( )) 的流程控制
整数运算用$(( )):
echo $((10 + 2 * 3)) i=1 echo $((i + 1))算术扩展里变量可以省略$,$((i + 1))没问题。还有一个不带美元号的(( )),主要用在流程控制。if (( i >= 10 ))的退出码取决于括号内表达式结果是否为非零,(( 1 + 1 ))返回 0 表示真,(( 0 ))返回 1 表示假。是不是有点绕?多写几个条件脚本就习惯了。
5. 历史命令里的符号捷径:!!、!$、!^ 省去重复敲长命令
5.1 基础:history 与几个以 ! 开头的快捷引用
终端会记录你敲过的命令,history可以查看。围绕历史记录有一组以!开头的快捷引用,纯键盘操作,手都不用离开主键区:
| 符号 | 含义 |
|---|---|
!! | 上一条完整命令 |
!$ | 上一条命令的最后一个参数 |
!^ | 上一条命令的第一个参数 |
!* | 上一条命令除命令名外的所有参数 |
!n | 执行历史里的第 n 条命令 |
!string | 执行最近一条以 string 开头的命令 |
!?string | 执行最近一条包含 string 的命令 |
history | grep之后用!数字直接执行特定命令,也是常见组合。
5.2 实战场景:sudo !!、cd !$
最常用的场景是忘写 sudo:
apt update # 提示权限不足 sudo !!!!展开成上一条命令,sudo !!就是sudo apt update,省得重敲一遍。
!$在长路径场景里特别好用:
mkdir /data/tmp/backups/weekly cd !$第二行自动展开成cd /data/tmp/backups/weekly。先创建目录,再进入目录,一条!$搞定。!^则用于取上一条命令的第一个参数,比如mv file1 file2之后想查看 file1,用cat !^。
5.3 注意:历史扩展的坑与关闭方法
历史扩展在非交互式 shell 里默认关闭,脚本里写!!不会生效。交互终端里也有隐患:如果你粘贴一段带!的文本,比如一个含感叹号的密码,!后面的内容可能被当成历史展开请求,轻则展开成意外命令,重则泄漏信息。遇到这种情况可以临时关闭历史扩展:
set +H需要时再set -H恢复。不过日常写命令我还是更常按Ctrl+R做反向搜索,对记忆模糊的长命令更友好。
6. 脚本逻辑与位置参数:#、;、&&、||、&、!、$?、$@ 让命令学会“判断”
6.1 # 注释与提示符里的 root 标识
脚本里的#是注释符号,#后面的内容不执行。还有个特殊变体是脚本第一行的#!/bin/bash,也就是 shebang,告诉内核该用什么解释器执行这个脚本:
#!/bin/bash # 这是注释 echo "hi"#在脚本里是注释,但在上一篇讲过的终端提示符里表示 root 身份。同样一个字符,含义完全由上下文决定,这也是理解 shell 的关键思维:符号本身没有固定意义,要看它出现在什么语境里。
6.2 顺序执行 ; 与短路逻辑 &&、||
三个符号的语义区别很清晰:
;:按顺序执行,前一条成败都拦不住后一条&&:前一条成功才执行后一条||:前一条失败才执行后一条
实际用法:
make && make install grep -q "error" app.log || echo "没有错误" cd /tmp || exit 1grep -q成功返回 0,失败返回非 0,所以|| echo表示“没匹配到就提示”。这是把退出码和逻辑判断结合起来的典型写法。
圆括号( ... )用来分组并开启子 shell。(cd /tmp && ls)在子 shell 里切换目录,不会改变当前 shell 的工作目录。如果你不想让 cd 影响当前脚本上下文,用圆括号包起来是安全做法。
6.3 后台任务 &、退出码 $?、取反 !
命令结尾加&让任务后台运行:
sleep 100 &终端立刻返回提示符。用jobs查看后台任务,kill %1杀掉第一个后台任务。%1是作业号引用,%在作业控制里专门用来引用作业编号。
$?保存上一条命令的退出码,0 表示成功,非 0 表示失败:
ls /no/such/path echo $?条件里的!是逻辑取反:
if ! grep -q "error" app.log; then echo "没有错误" fiif ! command就是“命令失败时才执行”。这个!和上一章历史扩展的!!完全是两码事,别搞混。
$$是当前 shell 的 PID,$!是最近一个后台进程的 PID。写临时文件时经常用$$做后缀避免重名冲突。
6.4 位置参数 $0、$1、$#、$@:脚本入口的“数据接口”
写脚本必然要处理参数。位置参数的含义:
$0:脚本名$1、$2:第一个、第二个参数$#:参数个数$@:所有参数,每个作为独立词$*:所有参数,作为一个整体词
写个小脚本验证:
#!/bin/bash echo "脚本名: $0" echo "参数个数: $#" for arg in "$@"; do echo "参数: $arg" done保存为args.sh执行bash args.sh a b c,输出一目了然。"$@"和"$*"在参数包含空格时差异明显,遍历参数用"$@"更安全。
6.5 组合示例:一个带参数检查的备份脚本
把这一章的符号组合起来,写一个实用小脚本:
#!/bin/bash set -e BACKUP_DIR="${1:?请指定备份目录}" DEST="backup_$(date +%F).tar.gz" tar -czf "$DEST" "$BACKUP_DIR" && echo "备份完成: $DEST" || echo "备份失败"第一行set -e让脚本在出错时立即退出。${1:?请指定备份目录}在没有传参时直接报错并退出,这就是参数检查的简洁写法。$(date +%F)生成日期字符串拼进备份文件名,&&和||处理成功和失败两个分支。一个十几行的脚本里,符号占了半壁江山,但它们各司其职,并不神秘。
7. 终端显示里的控制字符:^C、^D、^Z、^L 这些“脱字符组合”代表什么
7.1 按下去的 Ctrl 键为什么显示成了 ^
在终端里按Ctrl+C,屏幕上会回显^C。这个^是 control 的缩写记号,意思是“这个字符由 Ctrl 组合键产生”,跟 shell 和正则里的^没有关系。很多新手看到^C以为是自己输错了字符,其实这是中断信号,用来终止当前正在运行的进程。
7.2 常用控制字符逐个拆解
| 按键 | 终端显示 | 作用 |
|---|---|---|
| Ctrl+C | ^C | 发送 SIGINT,中断当前进程 |
| Ctrl+Z | ^Z | 发送 SIGTSTP,挂起当前进程 |
| Ctrl+D | ^D | 发送 EOF,退出当前输入,连续按可退出 shell |
| Ctrl+L | 无(清屏) | 清屏,等效clear |
| Ctrl+A | 无 | 光标移到行首 |
| Ctrl+E | 无 | 光标移到行尾 |
| Ctrl+W | 无 | 删除光标前一个单词 |
| Ctrl+U | 无 | 删除光标到行首的全部内容 |
| Ctrl+K | 无 | 删除光标到行尾的全部内容 |
| Ctrl+R | 无 | 反向搜索历史命令 |
Ctrl+Z和Ctrl+C经常被新手混用。Ctrl+C是终止进程,Ctrl+Z只是把进程挂起,进程还在内存里,可以用fg调回来继续跑,jobs能查看挂起任务列表。
7.3 stty -a 看终端键位映射
stty -a输出当前终端驱动设置,里面能看到intr = ^C; eof = ^D; susp = ^Z; erase = ^?这样的条目。erase = ^?表示退格键产生的控制字符。如果某个快捷键突然失灵,先看看stty -a的映射是否被改过。stty修改的是当前终端的驱动设置,操作有风险,不建议乱试。
最后说句题外话。很多人学终端的时候习惯背命令,我倒觉得先把符号体系过一遍,后面遇到什么命令都能猜个七八分。命令会变,但 shell 这套符号语法几十年没有大变,花一个下午弄懂它们,比背一百条命令更值。以后在别人的博客里看到2>&1、${var:-def}、!!这类写法,至少不会一头雾水。