拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux终端特殊符号详解:从通配符到重定向与管道

Linux终端特殊符号详解:从通配符到重定向与管道

大概每个刚开始碰 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 ssh

grep '^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 $status

4.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 1

grep -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 "没有错误" fi

if ! 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}、!!这类写法,至少不会一头雾水。

返回列表