基础语法
Shell脚本第一行是什么?为什么重要?
#!/bin/bash(shebang),指定脚本解释器。
变量赋值为什么报错command not found?
等号两边不能有空格——name = "Alice"会被解析为命令,正确写法name="Alice"。
如何定义只读变量?
readonly var 或 declare -r var,后续无法修改。
如何撤销变量?
unset var(不能撤销只读变量)。
[ ]vs[[ ]]vs(())
| 语法 | 类型 | 支持逻辑运算符 | 支持正则/通配符 | 空变量安全性 |
|---|---|---|---|---|
[ ] | POSIX 通用 | -a(与)、-o(或) | 不支持 | 变量必须加双引号,否则报错 |
[[ ]] | Bash 特有 | &&、||、! | 支持 =~(正则)和 ==(通配符) | 变量无需加引号,自动处理空值 |
(( )) | Bash 特有 | 数学比较(>、<、>=) | 专用于数值计算 | 专用于 C 风格循环和数值判断 |
判断文件是否存在且为普通文件
[ -f "$file" ] 或 [[ -f $file ]]。
数值计算
$((...))(最推荐,纯 Bash 内建,速度最快):sum=$((a + b))let命令:let sum=a+b(无需$前缀)expr外部命令(慢,过时):sum=$(expr $a + $b)(注意加号两侧必须有空格)- 浮点数运算:必须借助
bc或awk,如echo "scale=2; 5/3" | bc
exit和return的区别?
exit N终止整个脚本;return N从函数返回。0表示成功,非0表示失败。
位置参数$0、$1、$#、$?、$$、$@的含义?
$0:脚本名,$1:第一个参数,$#:传递给脚本的参数个数,$?:上条命令退出码。$$:当前Shell进程的 PID(常用于生成临时唯一文件名,如/tmp/a_$$)。$!:后台最后一个进程的 PID(配合wait $!使用)。$@:所有参数(带引号保留空格),
什么是IFS?"$*" 和 "$@" 区别?
Internal Field Separator(内部字段分隔符),是 Shell 中的一个特殊环境变量,用来决定 Shell 在哪些字符处把一个字符串拆分成多个单词(字段)。默认值是:空格 + 制表符 + 换行符
假设这样调用:./test.sh "a b" c d
| 写法 | 是否保留每个参数的独立性 | 是否受 IFS 影响 | 结果 |
|---|---|---|---|
$@ / $* | ❌ 会重新分词 | ✅ 受影响 | “a” “b” “c” “d” |
"$@" | ✅ 每个参数独立 | ❌ 不受影响 | “a b” “c” “d” |
"$*" | ❌ 合并成一个字符串 | ✅ 受影响(用 IFS 连接) | “a b c d” |
重定向
用于改变命令的输入来源或输出目的地,替代默认的 键盘输入(stdin) 和 屏幕输出(stdout/stderr)
常用语法
- 输出覆盖:
command > file(覆盖写入) - 输出追加:
command >> file(追加写入) - 输入重定向:
command < file(从文件读取输入) - 错误输出:
command 2> file(仅错误信息) - 合并输出:
command > file 2>&1(stdout 和 stderr 合并) - Here Document:
command << EOF ... EOF(内联多行输入),常用生成配置文件。<<-可以忽略前导 Tab(缩进友好)
- Here String:
command <<< "string"(单行字符串作为标准输入)
2>&1 必须放在哪?
- 功能:将标准错误(2)重定向到标准输出(1)所在的地方。
- 正确写法:
command > file 2>&1(先让标准输出去file,再把错误也指向file)。 - 错误写法:
command 2>&1 > file(错误先指向了当前终端的标准输出,随后标准输出才去file,导致错误依然打印在屏幕上) - 简写:
command &> file等效于上面正确的写法。
作用域
子 Shell 与大括号 {} 的区别?
- 小括号
( ... ):在子 Shell 中执行,内部变量修改不影响父 Shell。 - 大括号
{ ... }:在当前 Shell 中执行,内部变量修改会影响父 Shell。注意大括号内部左侧必须有空格,且命令结尾必须加分号;。
进程替换(<())与管道(|)的本质区别
- 管道
|:会创建子 Shell,导致父 Shell 变量无法被子进程修改。a=1; echo "2" | read a; echo $a依然输出 1。 - 进程替换
<():不会引入子 Shell 的副作用,相当于把命令输出伪装成文件。while read line; do ...; done < <(cat file)可以在当前 Shell 中执行循环(规避管道变零)。
export 变量和无export区别?
- 有
export:变量会传递给所有子进程(子Shell、外部命令、脚本等),称为环境变量。 - 无
export:变量仅在当前Shell进程中有效,子进程不可见,称为局部变量(或Shell变量)。
source/bash script.sh 和 ./script.sh 和 exec ./mod.sh的区别?
source在当前Shell执行,变量和环境变量保留;./script.sh在子Shell执行,变量不保留。
| 执行方式 | 执行环境 | 变量继承 | 是否影响当前环境 | 权限 |
|---|---|---|---|---|
| source 或 . script.sh | 当前Shell | ✅ | ✅ | r |
| bash script.sh | 子Shell | ❌ | ❌ | r |
| ./script.sh | 子Shell | ❌ | ❌ | rx |
| exec script.sh | 脚本覆盖当前Shell | ❌ | ❌ | rx |
综合上面两个:
#!/bin/bashexport PATH="aaa"cd bbb记上述脚本为mod.sh,问:source/bash mod.sh 和 exec ./mod.sh 父 Shell 的 PATH 和当前目录怎么变化?为什么?
| 执行方式 | 父Shell的 PATH | 父Shell的PWD | 脚本运行结束后 |
|---|---|---|---|
source mod.sh | aaa | bbb | 变量和目录变更永久保留在父Shell中 |
bash mod.sh | 不变 | 不变 | 脚本在子Shell中运行,父Shell毫无影响 |
exec ./mod.sh | 消失 | 消失 | 父Shell被脚本替换,进程随脚本结束而退出 |
进阶与陷阱
如何安全地逐行读取文件?
while IFS= read -r line; do echo "$line"done < file使用IFS=将IFS设置为一个空字符串,以确保读取的行不会因为空格或制表符被截断,-r防止反斜杠转义。
set -e的作用?
set命令用于显示或设置 Shell 的特性和变量,-e 遇到非零退出码立即退出脚本。
注意:管道中命令失败不一定触发(需配合set -o pipefail)。
set -euxo pipefail 各参数含义?
-e:脚本中任何命令失败(非0退出码)立即终止退出(防止错误蔓延)。-u:引用未定义变量时报错退出(防止空变量酿成事故)。-x:调试模式,执行前把每条命令打印到终端(用于排错)。-o pipefail:管道中任何一个子命令失败,整个管道就算失败(弥补-e只管管道最后一条命令的缺陷)。-n:仅语法检查,不执行(可用于排错)。-v:打印读取到的原始输入行(与-x不同,-x打印展开后的命令)。
如何避免rm -rf误删?
先ls确认再执行;用rm -i交互确认;关键操作前备份;脚本中用set -u防止空变量。
Shell脚本中如何捕获信号并做清理?
trap 'cleanup; exit' INT TERM 捕获中断和终止信号。
信号捕获与后台控制
trap:捕获信号并执行自定义操作。常用于脚本退出时清理临时文件。- 语法:
trap "rm -f /tmp/temp_$$" EXIT(脚本无论正常/异常退出都会触发)。 - 常见信号:
INT(Ctrl+C)、TERM、EXIT。
- 语法:
wait:阻塞等待后台进程结束。wait $pid等待指定进程,无参则等待所有后台子进程。- 面试题:
sleep 3 & wait会等待sleep结束再继续。
- 面试题:
如何用Shell实现并发控制?
使用&后台执行 + wait等待,或用xargs -P N、GNU parallel控制并发数。
后台运行 & vs nohup 的区别
command &:放入后台运行,但进程依然属于当前终端(Shell)的子进程。终端关闭(收到 SIGHUP 信号),进程会随之消亡。nohup command &:让进程忽略 SIGHUP 信号(1信号),同时默认将输出重定向到nohup.out。终端关闭后,进程依然存活。- 最佳实践:想退出终端后服务不挂,必须用
nohup+&(或用screen/tmux)。
如何定义函数并获取返回值?
- 定义:
function func_name() { ... }或func_name() { ... } - 局部变量:必须用
local声明,否则默认全局污染。 - 返回值(数字):
return 0(成功)或return 1(失败),通过$?接收。但return只能返回 0-255 的状态码。 - 返回字符串:不能用
return,必须用echo输出,调用方用$(func)接收。
单引号 '、双引号 "、反引号 “和 $() 的区别
- 单引号
'':所见即所得,完全保留字面意思,无视所有变量和转义符。 - 双引号
"":部分保留,允许变量展开$var和命令替换$(),但会阻止通配符*展开。 - 反引号 “与
$():均为命令替换(先执行内部命令)。强烈推荐$(),\支持嵌套(如$(grep $(whoami) file))且引号处理更清晰。
登录Shell vs 非登录Shell 与 .bashrc/.bash_profile 加载逻辑
- 登录Shell(如
su -、SSH登录):先读/etc/profile,再找~/.bash_profile(或~/.bash_login、~/.profile),通常不读~/.bashrc。 - 非登录Shell(如手动敲
bash):只读~/.bashrc。 - 解决痛点:为了登录Shell也能加载别名,通常在
~/.bash_profile里强制写入source ~/.bashrc。
其他
命令连接符/逻辑判断:; 、&& 与 ||
;:无视成败,前面命令执行完(无论对错),无条件执行后面的命令。&&:短路与,前面命令执行成功($?=0)才执行后面的。||:短路或,前面命令执行失败($?≠0)才执行后面的。- 经典组合:
make && make install || echo "Failed"(编译成功才安装,任一失败则报错)。
分支与循环结构速记
| 结构 | 语法要点 |
|---|---|
if-then-else | if [ condition ]; then ...; elif [ cond2 ]; then ...; else ...; fi(分号和 then 同行) |
case | 适合字符串匹配,支持 * 通配符。末尾 ;; 跳出,*) 为默认 |
for 循环 | for i in {1..10}; do ...; done(序列) for ((i=0; i<10; i++)); do ...; done(C风格) |
while 循环 | while read line; do ...; done < file(逐行读文件标准做法) |
until 循环 | 条件为假时执行,与 while 相反 |
数组操作(Bash 特有)
| 操作 | 语法 |
|---|---|
| 定义索引数组 | arr=(a b c) 或 arr[0]="a" |
| 定义关联数组(类似Map) | declare -A map; map["key"]="value" |
| 获取所有元素 | ${arr[@]}(独立参数) vs ${arr[*]}(单字符串,类似于 "$@" vs "$*") |
| 获取数组长度 | ${#arr[@]} 或 ${#arr[*]} |
| 获取索引列表 | ${!arr[@]}(关联数组常用) |
| 切片 | ${arr[@]:1:2}(从下标1取2个) |
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 37 天
部分内容可能已过时