Shell 脚本经典十三问:引号、变量、子 Shell 与重定向一次讲清问:引号、变量、子 Shell 与重定向一次讲清

单双引号凭什么输出不一样、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

写法 展开结果 词数
"$@" p1p2 p3p4 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 会清空文件。

参考:GNU Bash Reference Manualman 1 bashPOSIX Shell 命令语言

相关推荐
昌原的儿子LEO1 小时前
Linux IO多路复用与SQLite数据库核心知识点总结
linux·数据库·oracle
GeW1 小时前
RHCE通关秘籍:从零基础到高分拿证的5个加速器
linux
前端世界1 小时前
基于 Linux 的本地 AI 智能问答平台搭建实践 —— Ollama + FastAPI + Nginx 实现本地大模型 Web 服务
linux·运维·人工智能
倔强的石头1062 小时前
【Linux指南】动静态库系列(七):符号表与重定位:链接器如何把函数调用接起来
linux·前端·javascript
平原20183 小时前
GPT Image 2 / Nano Banana API成本拆解:$0.0081、84.7%与单张有效成本
linux·服务器·gpt
T1mzhou3 小时前
ARM64 Linux 6.10内核启动流程2-rootfs 怎么挂上
linux·服务器·前端
2301_815091013 小时前
TCP 并发服务器模型:4 种 IO 模型完整梳理
服务器·网络·tcp/ip
捷瑞电子工坊3 小时前
Ubuntu 终端指令速查手册(持续更新)
linux·ubuntu·教程·开发工具·命令行·速查手册·终端指令
雯宝3 小时前
6. USB 转串口驱动
linux