单双引号凭什么输出不一样、2>&1 写反就失效、脚本里的 cd 为什么带不回当前目录?这十三个问题是 Shell 面试和写脚本的高频坑,每条结论都配可直接粘贴运行的例子(验证环境 GNU Bash 5.x)。
一、Shell 是什么:用户与内核之间的命令解释器
使用者没法直接操作 Kernel,Shell 就是包在内核外面的一层"壳":把人敲的命令翻译给内核处理,再把结果翻译回屏幕,本质就是命令解释器(Command Interpreter)。
| Shell | 定位 |
|---|---|
sh |
Bourne Shell,POSIX 语法基准 |
bash |
Bourne-Again Shell,多数 Linux 发行版默认 |
csh/tcsh |
C 语言风格语法 |
ksh |
Korn Shell,AIX 等商用 Unix 常见 |
bash
echo "$SHELL" # 当前登录 Shell 的路径,例如 /bin/bash
cat /etc/shells # 本机已安装的全部 Shell 清单
登录后光标左边那串字符叫提示符,由变量 PS1 控制:普通用户是 $,root 是 #。命令不是敲完就执行,要读到回车(CR)才提交。提交之后 Shell 先按 IFS(Internal Field Separator,默认是空格、Tab、换行)把整行拆成一个个字段,再处理 *、$ 这类特殊字符,然后才重组出完整命令行。
bash
printf '%s' "$IFS" | od -c # 预期输出能看到空格、\t、\n 三个默认分隔符
echo 就是把 argument 送到 stdout。进程默认开三条数据流:stdin(0,标准输入)、stdout(1,标准输出)、stderr(2,标准错误),第六节展开。

二、单双引号与 export:变量为什么"时灵时不灵"
单引号 '' 叫 hard quote,关掉一切替换,里面写什么就是什么;双引号 "" 叫 soft quote,保留 $ 变量替换和命令替换,但空格不再被拆词;不加引号则既拆词又展开通配符。
| 写法 | $ 变量替换 |
空格拆词 | 通配符 * |
|---|---|---|---|
'$x' |
关闭 | 不拆 | 不展开 |
"$x" |
保留 | 不拆 | 不展开 |
$x(裸写) |
保留 | 拆 | 展开 |
bash
name=world
echo '$name' # 原样输出 $name
echo "$name" # ✅ 预期输出: world
变量语法是 name=value,等号两边不能有空格,否则会被当成"命令+参数"。这样定义的只是当前 Shell 变量,子进程看不到;export 后才进入环境,能被子进程继承。
bash
name=tom
bash -c 'echo [$name]' # 输出 [],子进程拿不到自定义变量
export name
bash -c 'echo [$name]' # 输出 [tom],环境变量被继承
三、source、exec 与子 Shell:改了的变量带不回来
环境变量只能从父进程单向传给子进程,子进程再怎么改都不会回流。直接执行脚本会 fork 一个 sub-shell 子进程;source(等价 .)不建子进程,在当前 Shell 逐行执行;exec 用新程序替换当前 Shell 进程,不新增进程,其后的脚本行不再执行。
bash
# setvar.sh 里只有一行:myvar=hello
bash setvar.sh; echo "[$myvar]" # [],子进程的变量带不回父进程
source setvar.sh; echo "[$myvar]" # [hello],当前 Shell 直接被改写
( ) 把命令组丢进 sub-shell 执行,{ } 在当前 Shell 内执行。花括号有硬性语法要求:{ 后要有空格,} 前要有分号或换行,少一个都报错。
bash
cd /tmp
( cd /; pwd ); pwd # 括号内输出 /,出来后仍是 /tmp
{ cd /; pwd; }; pwd # 当前 Shell 被切到 /,结束后停在 /
💡 面试官可能追问 :为什么脚本里写了
cd /data,脚本跑完终端目录却没变?因为脚本默认在 sub-shell 中执行,子进程的工作目录变更回不到父 Shell;想让切换在当前终端生效,就用source运行。

四、 ( ) 、 ()、 ()、(())、${} 与位置参数
三种括号各管一件事,别混着记。
| 语法 | 作用 | 示例 | 结果 |
|---|---|---|---|
$( ) |
命令替换,取命令的输出 | $(echo hi) |
hi |
$(( )) |
整数算术运算 | $((3+4)) |
7 |
${ } |
界定变量名、参数展开 | ${f##*/} |
取文件名 |
${} 里的删改规则有个键盘记忆法:# 在 $ 左边,负责删前缀;% 在 $ 右边,负责删后缀;单符号是最短匹配,双符号是最长匹配。
bash
f=/home/tom/report.tar.gz
echo "${f##*/}" # report.tar.gz,删掉最长的 */ 前缀,等价 basename
echo "${f%.*}" # /home/tom/report.tar,删最短后缀,去掉末尾扩展名
echo "${f%%.*}" # /home/tom/report,删最长后缀,去掉全部扩展名
参数展开兜底要分清三种状态:unset(未定义)、null(空串)、non-null(有值)。带冒号 :- 时空串也算"没值",不带冒号 - 只在 unset 时兜底。
bash
unset x; echo "${x:-d}" # d,未定义,给默认值
x=; echo "${x:-d}" # d,带冒号,空串也兜底
x=; echo "${x-d}" # 输出空,不带冒号,空串不算 unset
位置参数里,$@ 和 $* 只有加上双引号才有区别。保存下面脚本为 args.sh,执行 bash args.sh p1 "p2 p3" p4:
| 写法 | 展开结果 | 词数 |
|---|---|---|
"$@" |
p1、p2 p3、p4 |
3 个词,带空格的参数原样保留 |
"$*" |
p1 p2 p3 p4 |
1 整串,用 IFS 首字符连接 |
bash
#!/usr/bin/env bash
# 调用方式:bash args.sh p1 "p2 p3" p4
for a in "$@"; do echo "[$a]"; done # 三行:[p1]、[p2 p3]、[p4]
echo '---'
for a in "$*"; do echo "[$a]"; done # 一行:[p1 p2 p3 p4]
五、状态码、&&/|| 与分支循环怎么选
test 有两种等价写法:test expression 与 [ expression ]。[ 本身是命令,它和表达式、结尾 ] 之间必须有空格。测试成立返回 0,否则返回非 0,和 C 语言真假值相反。
| 测试对象 | 常用操作符 |
|---|---|
| 字符串 | =、!=、-z(为空)、-n(非空) |
| 整数 | -eq、-ne、-lt、-le、-gt、-ge |
| 文件 | -e 存在、-f 普通文件、-d 目录、-r/-w/-x 权限 |
bash
true; echo $? # 0,命令成功
false; echo $? # 1,命令失败
[ -f /etc/passwd ] && echo 存在 # 文件存在才打印
cmd1 && cmd2:cmd1 返回 0 才执行 cmd2;cmd1 || cmd2:返回非 0 才执行 cmd2。注意 Shell 是先完成变量替换、再做比较。
⚠️ 踩坑 :
a && b || c并不等价于 if/else。如果 a 成功、但 b 自己执行失败,c 照样会被触发;b 有可能失败时,老老实实写 if。
离散多值匹配用 case 比一串 -o 的 if 清爽。原笔记的 if 示例有个真实错误:[ "$YN"=Y ] 等号两边没空格,会被当成一个非空参数,判断永远为真。
bash
read -p "Do you want to continue?(Yes/No): " YN
case "$YN" in
[Yy]|[Yy][Ee][Ss]) echo "continue" ;; # [Yy] 匹配单个字符,| 并列多个模式
*) exit 0 ;; # *) 是兜底分支,;; 结束该分支
esac
循环的选择看退出条件:已知次数或遍历列表用 for,条件为真时继续用 while,条件为真时停止(即"直到......才停")用 until。
bash
for ((i=1;i<=3;i++)); do echo "num is $i"; done # C 风格计数循环
num=1; while [ "$num" -le 3 ]; do echo "$num"; num=$((num+1)); done
num=1; until [ "$num" -gt 3 ]; do echo "$num"; num=$((num+1)); done
原笔记 until 示例里的 $nu 是笔误,应为 $num,否则变量永远是空值。三个跳出关键字也别混:
| 关键字 | 结束对象 |
|---|---|
break |
当前层循环(continue 只跳过本轮) |
return |
所在 function,并可带回返回值 |
exit |
整个脚本 / 当前 Shell |
六、> 与 <:文件描述符与重定向顺序
< 改变 stdin 的数据来源,让命令从指定文件读入;> 改变 stdout/stderr 的去向,把输出写进指定文件。
| 写法 | 含义 |
|---|---|
> f / >> f |
stdout 覆盖写入 / 追加到 f |
2> f |
stderr 单独写入 f |
1>o 2>e |
正常输出与报错分流到两个文件 |
>f 2>&1 |
stderr 并进 stdout,一起写入 f |
>/dev/null 2>&1 |
两类输出全部丢弃 |
bash
ls my.file no.such.file 1>file.out 2>file.err # 文件名进 out,报错进 err
ls my.file no.such.file >file.out 2>&1 # 正常输出和报错都进 file.out
ls my.file no.such.file >/dev/null 2>&1 # 静默执行,屏幕什么都不显示
顺序不能写反:2>&1 >file 先让 stderr 复制此刻的 stdout(仍指向屏幕),再把 stdout 指向文件,报错还是上屏。重定向从左往右读、实际从右往左准备。
⚠️ 踩坑 :永远不要写
cat < file > file。命令启动前 Shell 先备齐重定向,>file先清空文件,等<file读入时已经空了,内容归零。原地处理要先写临时文件再mv回来。
结尾速记(三句):
- 引号:单引号关掉全部替换,双引号只保留
$,含空格的变量永远套双引号。 - 进程:直接执行开子 Shell,
source留在当前 Shell,exec直接替换当前进程。 - 重定向:
2>&1写在>文件之后才生效,cat <f >f会清空文件。