跳到主要内容

Bash shopt 命令详解

是什么 / 主要用途

shopt(shell options)是 bash 专有的选项管理命令,用于开启或关闭 bash 的扩展行为特性

set 的分工:

setshopt
管理范围POSIX 标准 shell 行为Bash 专有扩展特性
典型用途-e 错误退出、-x 调试、-u 未定义变量检测glob 模式、历史记录、补全、目录切换

shopt 覆盖的核心能力域:

  • 文件名展开:控制 * ** 等通配符的匹配规则
  • 模式匹配case / [[ 的大小写敏感性、扩展模式语法
  • 历史记录:追加模式、多行命令保存方式
  • 交互式增强autocdcdspell、自动补全行为
  • 脚本健壮性inherit_errexitlastpipenullglob
  • 调试支持extdebug 配合 declare -F 显示函数位置

语法

shopt [-pqsu] [-o] [optname ...]

shopt -s 开启,shopt -u 关闭,与 set 的区别在于 shopt 管理 bash 扩展行为,set 管理 POSIX 标准行为。


选项说明

选项说明
-s开启指定选项
-u关闭指定选项
-p显示选项列表(可复用格式)
-q静默模式,用返回值表示选项状态(0=开启,非零=关闭)
-o限定为 set -o 的选项集合
shopt # 显示所有选项及状态
shopt -s extglob # 开启 extglob
shopt -u extglob # 关闭 extglob
shopt -q extglob # 静默检测,$? 为 0 表示已开启
shopt extglob # 显示单个选项状态

返回值

情况返回值
列出选项时,所有指定选项均已开启0
列出选项时,有未开启的选项非零
设置/取消选项成功0
optname 无效非零

选项分类详解

文件名展开(Globbing)

选项默认说明
dotglob* 匹配以 . 开头的文件(隐藏文件)
globstar** 递归匹配所有文件和目录
nullglob无匹配时展开为空,而非保留原样
failglob无匹配时报错(与 nullglob 互斥)
extglob启用扩展模式匹配
nocaseglob文件名匹配忽略大小写
globskipdots展开时永远不匹配 ...
globasciiranges字符范围 [a-z] 使用 C locale 而非本地 locale
# dotglob:包含隐藏文件
shopt -s dotglob
ls * # 包含 .bashrc .gitignore 等

# globstar:递归查找
shopt -s globstar
ls **/*.sh # 找出所有子目录下的 .sh 文件

# nullglob:无匹配时不报错
shopt -s nullglob
files=(*.xyz)
echo ${#files[@]} # 0,而非展开为 *.xyz

# extglob:扩展模式
shopt -s extglob
ls !(*.log) # 匹配除 .log 外的所有文件
ls +(*.sh|*.py) # 匹配 .sh 或 .py 文件

扩展模式语法:

模式含义
?(pattern)匹配 0 或 1 次
*(pattern)匹配 0 或多次
+(pattern)匹配 1 或多次
@(pattern)恰好匹配 1 次
!(pattern)不匹配该模式

目录切换

选项默认说明
autocd输入目录名直接 cd(仅交互式 shell)
cdspellcd 时自动纠正轻微拼写错误
cdable_varscd 的参数可以是存储路径的变量名
# autocd:直接输入目录名进入
shopt -s autocd
/etc # 等同于 cd /etc

# cdspell:自动纠错
shopt -s cdspell
cd /ect # 自动纠正为 /etc

# cdable_vars:变量名作为路径
shopt -s cdable_vars
mydir=/var/log
cd mydir # 等同于 cd /var/log

历史记录

选项默认说明
histappendshell 退出时追加历史而非覆盖 HISTFILE
cmdhist多行命令保存为单条历史记录
lithist多行命令用换行而非 ; 保存到历史
histverify! 历史展开后先加载到编辑缓冲区,不立即执行
histreedit历史替换失败时允许重新编辑
# histappend:多个 shell 不互相覆盖历史
shopt -s histappend

# histverify:!! 展开后先预览,按 Enter 再执行
shopt -s histverify
!! # 展开上条命令到缓冲区,可修改后再执行

模式匹配

选项默认说明
nocasematchcase[[ 匹配时忽略大小写
extglob扩展模式匹配(见上文)
shopt -s nocasematch
read -p "继续?(y/n): " ans
case $ans in
y|yes) echo "继续" ;;
n|no) echo "取消" ;;
esac
# 输入 Y / YES / Yes 均能匹配

调试与开发

选项默认说明
extdebug启用调试模式(declare -F 显示行号,DEBUG trap 增强)
execfail非交互式 shell exec 失败时不退出
shift_verboseshift 超出位置参数数量时打印错误
# extdebug:declare -F 显示函数定义位置
shopt -s extdebug
declare -F my_func
# 输出:my_func 12 /path/to/script.sh

补全行为

选项默认说明
progcomp启用可编程补全
progcomp_alias对别名也尝试可编程补全
dirspell补全时自动纠正目录名拼写
direxpand补全时将目录名替换为展开结果
hostcomplete@ 后尝试主机名补全
no_empty_cmd_completion空行时不搜索 PATH 补全
complete_fullquote补全文件名时引用所有元字符

管道与作业

选项默认说明
lastpipe管道最后一条命令在当前 shell 而非子 shell 执行
checkjobs退出前列出运行中/停止的作业
huponexit登录 shell 退出时向所有作业发送 SIGHUP
checkwinsize命令执行后更新 LINES/COLUMNS
# lastpipe:管道最后一段的变量改动可传回当前 shell
shopt -s lastpipe
count=0
cat file.txt | while read line; do
((count++))
done
echo $count # 有 lastpipe:正确值;无 lastpipe:0

错误处理

选项默认说明
inherit_errexit命令替换继承 errexitset -e)设置
set -e
shopt -s inherit_errexit
result=$(false) # 没有 inherit_errexit:子 shell 不受 -e 影响
# 有 inherit_errexit:子 shell 也会因 false 退出

局部变量

选项默认说明
localvar_inheritlocal 变量继承外层同名变量的值和属性
localvar_unsetunset 局部变量后,上层作用域的同名变量也不可见

其他实用选项

选项默认说明
expand_aliases开(交互式)启用别名展开
interactive_comments交互式 shell 支持 # 注释
sourcepathsource 使用 PATH 查找文件
xpg_echoecho 默认展开转义序列(如 \n
bash_source_fullpathBASH_SOURCE 存储完整路径而非相对路径
varredir_close{var} 重定向的 fd 在命令完成后自动关闭
checkhash执行缓存命令前验证其是否仍存在
login_shell只读,标识当前是否为登录 shell
restricted_shell只读,标识当前是否为受限 shell

实际使用场景

1. 脚本推荐开头组合

#!/bin/bash
set -euo pipefail
shopt -s inherit_errexit # 命令替换也遵守 errexit
shopt -s nullglob # 无匹配的 glob 展开为空,避免意外

2. 递归处理文件

shopt -s globstar nullglob
for f in **/*.log; do
echo "处理: $f"
gzip "$f"
done

3. 大小写不敏感的用户输入

shopt -s nocasematch
read -p "环境 (dev/prod): " env
case $env in
dev) echo "开发环境" ;;
prod) echo "生产环境" ;;
*) echo "未知环境" ;;
esac

4. 多 shell 共享历史

# 写入 ~/.bashrc
shopt -s histappend
HISTSIZE=10000
HISTFILESIZE=20000
HISTCONTROL=ignoredups:erasedups

5. 管道变量传回

shopt -s lastpipe
declare -A word_count
while IFS= read -r line; do
for word in $line; do
((word_count[$word]++))
done
done < /etc/hosts
echo "单词数: ${#word_count[@]}"

6. 检测选项用于条件分支

# 保存并临时修改选项
if shopt -q extglob; then
extglob_was_set=1
else
extglob_was_set=0
shopt -s extglob
fi

# 使用 extglob 特性
ls !(*.bak)

# 恢复原状
[ $extglob_was_set -eq 0 ] && shopt -u extglob

set -oshopt 对比

维度set -oshopt
标准POSIX 标准选项Bash 专有扩展选项
语法set -e / set -o errexitshopt -s extglob
查看set -oshopt
使用 -oshopt -o 可访问 set 的选项

速查表

shopt -s dotglob # * 匹配隐藏文件
shopt -s globstar # ** 递归匹配
shopt -s nullglob # 无匹配展开为空
shopt -s extglob # 扩展模式 !(*.log) 等
shopt -s nocaseglob # 文件名匹配忽略大小写
shopt -s nocasematch # case/[[ 匹配忽略大小写
shopt -s autocd # 直接输目录名进入
shopt -s cdspell # cd 自动纠错
shopt -s histappend # 追加而非覆盖历史文件
shopt -s lastpipe # 管道末段在当前 shell 执行
shopt -s inherit_errexit # 命令替换继承 errexit
shopt -s checkjobs # 退出前列出作业
shopt -q extglob # 静默检测选项是否开启($? 判断)

详细使用示例与经典场景

场景一:批量处理文件(globstar + nullglob)

#!/bin/bash
shopt -s globstar nullglob

# 递归压缩所有 .log 文件,无匹配时安全跳过
for f in /var/log/**/*.log; do
echo "压缩: $f"
gzip "$f"
done

# 统计所有 .sh 脚本行数
total=0
for f in **/*.sh; do
lines=$(wc -l < "$f")
((total += lines))
done
echo "总行数: $total"

场景二:文件清理(extglob)

shopt -s extglob

# 删除除 .sh 和 .py 之外的所有文件
rm -f !(*.sh|*.py)

# 删除所有备份文件(.bak .bak1 .bak2 等)
rm -f *.bak*(*)

# 匹配 image001 ~ image999
ls image+([0-9])

场景三:交互式 shell 增强(~/.bashrc)

# 输入目录名直接进入
shopt -s autocd

# cd 时自动纠正拼写
shopt -s cdspell

# 变量名作路径
shopt -s cdable_vars
LOGS=/var/log
CONF=/etc/nginx
# 之后直接 cd LOGS 即可

# 多 shell 共享并保留完整历史
shopt -s histappend
HISTSIZE=50000
HISTFILESIZE=100000
HISTCONTROL=ignoredups:erasedups
HISTTIMEFORMAT="%F %T " # 历史记录带时间戳

# 多行命令保存为单条记录(便于重新编辑)
shopt -s cmdhist
shopt -s lithist

场景四:健壮的生产脚本

#!/bin/bash
set -euo pipefail
shopt -s inherit_errexit # $(cmd) 中的错误也会触发 -e 退出
shopt -s nullglob # glob 无匹配时展开为空,避免 "文件不存在" 错误

process_configs() {
local configs=(/etc/app/*.conf)
if [[ ${#configs[@]} -eq 0 ]]; then
echo "没有配置文件" >&2
return 1
fi
for conf in "${configs[@]}"; do
# inherit_errexit 保证此处命令替换的错误不被吞掉
version=$(grep '^version=' "$conf" | cut -d= -f2)
echo "处理 $conf (version=$version)"
done
}

场景五:管道结果传回主 shell(lastpipe)

shopt -s lastpipe

# 不用 lastpipe:count 始终为 0(while 在子 shell 运行)
count=0
cat /etc/passwd | while IFS=: read user _; do
((count++))
done
echo $count # 0

# 用 lastpipe:while 在当前 shell 运行,count 正确传回
count=0
cat /etc/passwd | while IFS=: read user _; do
((count++))
done
echo $count # 正确值

场景六:大小写不敏感匹配

shopt -s nocasematch

# case 语句不区分大小写
read -p "是否继续?(yes/no): " ans
case $ans in
yes|y) echo "继续执行" ;;
no|n) echo "已取消" ;;
*) echo "无效输入" ;;
esac
# 输入 YES / Yes / Y 均能匹配

# [[ ]] 条件也不区分大小写
os="Linux"
if [[ $os == linux ]]; then
echo "是 Linux" # 能匹配
fi

# 文件名补全忽略大小写(交互式)
shopt -s nocaseglob
ls *.PNG # 也能匹配 .png .Png

场景七:调试脚本(extdebug)

shopt -s extdebug

# declare -F 显示函数定义的文件和行号
declare -F my_func
# 输出:my_func 42 /home/user/scripts/utils.sh

# DEBUG trap 控制执行流(返回非零跳过下一条命令)
trap '[[ $BASH_COMMAND == "rm"* ]] && { echo "拦截 rm 命令"; return 1; }' DEBUG
rm -rf /tmp/test # 被拦截,不执行

场景八:临时修改选项(保存/恢复模式)

# 库函数中临时开启 extglob,执行后恢复原状
use_extglob_temporarily() {
local restore=0
shopt -q extglob || { shopt -s extglob; restore=1; }

# 使用 extglob 特性
local files=( !(*.bak) )
echo "有效文件: ${files[*]}"

[[ $restore -eq 1 ]] && shopt -u extglob
}

场景九:dotglob 处理隐藏文件

shopt -s dotglob

# 复制目录下所有文件(包括 .gitignore .env 等隐藏文件)
cp -r /source/dir/* /dest/dir/

# 检查目录是否为空(含隐藏文件)
is_empty() {
local files=("$1"/*)
[[ ${#files[@]} -eq 0 ]]
}

场景十:varredir_close 自动关闭文件描述符

shopt -s varredir_close

# 不用 varredir_close:需要手动关闭 fd
exec {fd}> /tmp/output.txt
echo "写入内容" >&$fd
exec {fd}>&- # 手动关闭

# 用 varredir_close:命令完成后自动关闭 fd
echo "写入内容" >{fd}/tmp/output.txt
# fd 自动关闭,无需 exec {fd}>&-