#!/bin/bash、#!/bin/sh./test.sh,如果使用 /bin/bash test.sh运行,则不需要在脚本中指定解释器,写了也不起作用$'It\'s ok' 单引号前的 $ 表示其后的字符支持转义
# 或若需兼容 POSIX:
set -euo pipefail
set -e:任一命令失败(非零退出码)则立即退出。set -u:引用未定义变量时报错(防止 $undefined_var 变成空字符串)。set -o pipefail:管道中任一命令失败即视为整个管道失败(否则只看最后一个命令)。# 始终用双引号包裹变量,防止空格、通配符展开等问题:
echo "$my_var" # 安全
echo $my_var # 危险!若含空格或 * 会出错
# 提供默认值(避免依赖外部环境)
name="${USER:-unknown}"
# 不要假设命令一定成功
if ! mkdir -p "$dir"; then
echo "Error: failed to create directory $dir" >&2
exit 1
fi
# 或使用 || 简写(适用于简单场景):
cp file.txt dest/ || { echo "Copy failed"; exit 1; }
if [[ ! -f "$config_file" ]]; then
echo "Config file missing: $config_file" >&2
exit 1
fi
temp_file=$(mktemp) || { echo "Failed to create temp file"; exit 1; }
trap 'rm -f "$temp_file"' EXIT # 自动清理
trap 确保脚本退出时(包括异常)都会清理临时文件。cleanup() {
echo "Caught signal, cleaning up..."
rm -f "$temp_file"
exit 1
}
trap cleanup INT TERM
| 信号 | 编号 | 触发场景 |
|---|---|---|
| EXIT/0 | 0 | 脚本正常或异常退出时(最常用) |
| INT | 2 | 用户按Ctrl+C(中断) |
| TERM | 15 | 收到kill命令(默认信号) |
| ERR | - | 任一命令返回非零退出码(需配合set -e) |
echo "Error" >&2getopts(内置)或严谨的手动解析,验证输入合法性:while getopts "f:v" opt; do
case $opt in
f) file="$OPTARG" ;;
v) verbose=1 ;;
*) echo "Usage: $0 [-f file] [-v]" >&2; exit 1 ;;
esac
done
# 验证必需参数
if [[ -z "${file:-}" ]]; then
echo "Error: -f is required" >&2
exit 1
fi
getopts 用法while getopts "选项字符串" 变量名; do
case $变量名 in
选项1) ... ;;
选项2) ... ;;
\?) ... ;; # 处理无效选项
:) ... ;; # 处理缺少参数(如果选项需要参数)
esac
done
# 跳过已处理的选项(可选,用于处理非选项参数)
shift $((OPTIND - 1))
"f:v" 表示支持 -f 和 -v)。f: → -f file(file 是参数)v → -v(无参数,布尔开关)$OPTARG:当前选项的参数值(如 -f config.txt 中的 config.txt)。$OPTIND:下一个要处理的参数在 $@ 中的位置(通常用于跳过已处理的选项)。:,可关闭 getopts 的默认错误提示,由用户自定义处理:\? 分支:处理无效(未定义的)选项getopts 选项字符串中声明的选项。-f 和 -v,但用户输入了 -x。$OPTARG 的值:无效的选项字符(如 x)opt 的值为 ?。: 分支:处理“需要参数但未提供”的情况:),但没有跟参数。-f 需要参数(定义为 "f:"),但用户只写了 -f。$OPTARG 的值:缺失参数的那个选项字符(如 f)。| 行为 | 默认模式f:vh | 静默模式:f:vh |
|---|---|---|
遇到无效选项(如-x) | getopts自动输出错误到 stderr:script.sh: illegal option -- x 并将opt设为? | 不输出任何错误;将opt设为?,由脚本自行处理 |
遇到缺少参数(如-f后无值) | getopts自动输出错误:script.sh: option requires an argument -- f并将opt设为? | 不输出错误;将opt设为 : |
,由脚本自行处理
关键点:只有在静默模式下,才会触发 case 中的 : 分支。 在默认模式下,缺少参数也会变成 ?(和无效选项一样),无法区分。
cat -A a.txt 查看不可打印字符,如果输出中包含 M-BM- 等,使用命令替换为空格 sed 's/\xc2\xa0/ /g' a.txtM-BM- 字符是字节序列 0xc2 0xa0 的ASCII表示,它是Unicode字符 A0 的UTF8编码(一个不间断空格字符)。可以使用键盘序列 Ctrl+Shift+SPACE 在 LibreOffice 和 Microsoft Word 文档中插入此字符。\r\n,Linux是\n,mac是 \r,所有Windows复制过去的文本可能带有 ^M,解决方式::set ff=unix 设置文件为 unix,并 :wqsed -i "s/\r$//" filename 或者 sed -i "s/^M//" filename 直接替换结尾符为unix格式fileformats 选项,指定你希望Vim能自动识别的格式的集合。下面的这个命令就可以让Vim能自动识别UNIX格式和MS-DOS格式::set fileformats=unix,dosM-oM-;M-? 或十六进制 0xef 0xbb 0xbf 或 357 273 277,它是与 file 命令报告的文件类型一致的 UTF8 字节顺序标记(BOM)。hexdump -C 文件名 | head -n 1:00000000 23 21 2f 62 69 6e 2f 62 61 73 68 0a |#!/bin/bash.|(23 21 是 #! 的ASCII码)00000000 ef bb bf 23 21 2f 62 69 6e 2f 62 61 73 68 0a |...#!/bin/bash.|(开头的 ef bb bf 就是BOM)file 文件名ASCII textUTF-8 Unicode (with BOM) text:set bomb? 若显示 bomb 表示有BOM:set nobomb 直接移除BOM,并 :wqsed -i 's/^\xEF\xBB\xBF//' 文件名vim -c ":set nobomb" -c ":wq" bom_example.txt$ tail -c +4 bom_example.txt > without_bom.txt:使用 tail -c 选项跳过文件的开头,并从偏移字节开始打印剩余部分,由于 BOM 由 3 个字节组成,因此使用 -c +4 跳过它。将输出重定向到文件 without_bom.txt。dos2unix bom_example.txt 需要安装 dos2unixvar_name="runoob",变量名和等号之间不能有空格,因为空格通常用于分隔命令和参数$var_name、${var_name},加大括号是为了帮助解释器识别变量的边界。$ 相当于从变量中取值(解引用),不加 $ 则表示变量本身readonly var_name,变量值不可被改变unset var_name,删除后不能再次使用,不能删除只读变量'或双引号"定义,var_name='zmy' var_name="zmy"。注意:单引号中的任何字符都会原样输出,其中的变量是无效的,单引号字符串内不能出现一个单独的单引号(转义也不行)但可成对出现,作为字符串拼接使用:
var1='zmy_'$var_name'_flm' # 输出 zmy_runoob_flm
var2='zmy_$var_name_flm' # 输出 zmy_$var_name_flm
var1="Hi, "$var_name"! \n" # 输出 Hi, runoob
var2="Hi, ${var_name}! \n" # 输出 Hi, runoob
${#var_name},等价于${#var_name[0]} ${var_name:1:2}输出un string="runoob is a great site"
echo `expr index "$string" io` # 输出 4
declare -i my_int=42# 使用 (( )) 算术表达式(推荐)
a=2
((a++)) # a 自增 1 → a=3
((a--)) # 自减
((a += 1)) # 等价于 a = a + 1
((a = a + 1))
# 使用 $(( )) 算术扩展
a=2
a=$((a + 1)) # a=3
a=$((a * 2)) # a=6
# 使用 let 命令(较老,不推荐)
a=2
let a=a+1
let "a += 1" # 带空格时需加引号
my_array=(1 2 3 4 5 6) declare -A my_array, my_array['name']='zmy',my_array['age']=30# 获取数组的某个值
value=${my_array[n]}
# 使用 @ 或 * 获取数组中所有元素
${my_array[@]}
${my_array[*]}
# 使用 ! 获取数组中所有键
${!my_array[@]}
${!my_array[*]}
# 获取数组元素的个数
length=${#my_array[@]};
length=${#my_array[*]}
# 获取单个元素的长度
length=${#my_array[n]}
# 判断关联数组中是否存在键
if [[ -n "${my_array[$key]+exists}" ]]; then
echo "$key 存在"
fi
if [[ -v "my_array[$key]" ]]; then
echo "$key 存在"
fi
# 判断普通数组中是否存在值
[[ "${my_array[*]}" =~ "$value" ]] && echo "存在" || echo "不存在"
mapfile 是一个强大的内置命令,用于将文件或标准输入的内容按行读入数组。它等价于 readarray,语法和功能完全一致。# 如果不指定数组名,数据将默认存储到 MAPFILE 数组中。
mapfile [选项] 数组名 < 输入源
# 或
mapfile [选项] 数组名 文件名
-n count 最多读取 count 行-O origin 从数组索引 origin 开始赋值(默认 0)-s count 跳过前 count 行-t 删除每行末尾的换行符(默认行为)-d DELIM 指定行分隔符(默认是换行符)-u fd 从文件描述符 fd 读取(默认 0)-C callback 每读 callback 行调用一次命令-c quantum 与 -C 搭配,指定调用频率,每读取 quantum 行执行一次回调(包括当前行)$1: 当前正在读取的行号,从 0 开始,$2: 当前正在读取的行内容$1 参数)$0 # 脚本名称$1-$9 # 第1-9个位置参数${10} # 第10个参数(需要大括号)$# # 参数个数$@ # 所有参数,每个都是独立字符串,如果 "$@" 则以 "$1" "$2" ... "$n" 的形式输出所有参数$* # 所有参数,作为一个字符串,如果是 "$*" 则以 "$1 $2 ... $n" 的形式输出所有参数$$ # 当前进程PID$! # 最后一个后台进程的PID$? # 上一个命令的退出状态码$_ # 上一个命令的最后一个参数$- # 当前shell选项标记!! # 上一条命令$BASH # Bash的完整路径$BASH_VERSION # Bash版本$BASH_VERSINFO[0-6] # Bash版本信息数组$BASH_SOURCE # 当前执行的源文件(脚本)路径$BASH_SUBSHELL # 子shell层级$LINENO # 当前在脚本中的行号$FUNCNAME # 当前函数名(数组,[0] 是当前函数)$BASH_COMMAND# 当前正在执行的命令$PIPESTATUS # 管道中每个命令的退出状态数组$SECONDS # 脚本运行秒数$RANDOM # 随机数(0-32767)$REPLY # read命令默认读取的变量$OPTARG # getopts的选项参数$OPTIND # getopts的选项索引$COLUMNS # 终端列数$LINES # 终端行数$UID # 当前用户UID$EUID # 当前有效用户UID$GROUPS # 用户所属组数组$BASH_XTRACEFD # 用于set -x输出的文件描述符$BASH_LINENO # 函数调用栈的行号数组$BASH_ARGC # 函数参数个数数组$BASH_ARGV # 函数参数值数组$IFS # 内部字段分隔符(默认是空格、制表符、换行)$GLOBIGNORE # 通配符忽略模式$BASH_REMATCH # 正则表达式匹配结果数组$BASH_ALIASES # 别名数组$BASH_CMDS # 命令哈希表# 这是一个注释 :是空命令,内容不会执行:<<EOF(EOF也可以使用其他符号:COMMENT ' !)
注释内容
注释内容
注释内容
EOF
: '
这是注释的部分。
可以有多行内容。
'
parameter 是数字,则是参数扩展;如果 parameter 是字符串,则是变量扩展;如果 parameter 是数组,遵循数组的扩展规则;parameter 还可以是 @, *, #, ?, - 等,参考特殊参数的扩展。# 无引号:Shell 扩展
echo *.txt # Shell 扩展
# 单引号:完全字面
echo '*.txt' # 输出: *.txt
# 双引号:变量扩展,但通配符不扩展
echo "$HOME/*.txt" # 输出: /home/user/*.txt
除了 ${variable:=word} 和 ${variable::=word}(强制修改变量为 word 无论原变量是否为空,Bash 4.4+ 新增)之外,其他变量扩展不会修改变量的值,仅是返回扩展结果
直接扩展和间接扩展 a=b; b=123;
echo ${a} 基本扩展,输出结果为becho ${!a} 间接参数扩展,输出结果为b的值123,${!var}将参数扩展为变量的值,在处理变量名作为字符串时很有用变量引用
${variable} 或 $variable 直接引用变量的值设置默认值
${variable:-word} - 如果变量 variable 已被设置且非空,则展开为变量的值;否则,展开为 word。${variable:=word} - 类似于 ${variable:-word},但是还会将 word 的值赋给变量 variable。${variable:+word} - 如果变量 variable 已被设置且非空,则展开为 word;否则,什么也不做。${variable:?word} - 如果变量 variable 已被设置且非空,则展开为变量的值;否则,打印错误消息至标准错误并退出 shell,错误消息包含 word字符删除
${variable#pattern} 删除 variable 中 pattern 匹配的开头最短部分。${variable##pattern} 删除 variable 中 pattern 匹配的开头最长部分。${variable%pattern} 删除 variable 中 pattern 匹配的结尾最短部分。${variable%%pattern} 删除 variable 中 pattern 匹配的结尾最长部分。pattern 是 Glob 模式? 匹配任何一个字符。* 匹配任意数量的字符(包括零个字符)。[...] 匹配括号内的任何一个字符。例如,[abc] 匹配 'a'、'b' 或 'c'。{a,b,c} 匹配 a 或 b 或 c。pattern 必须能够匹配开头(对于 % )或者结尾(对于 # ),如果只匹配了中间部分则无法删除
${variable/pattern/string} 替换变量值 variable 的第一次出现的 pattern 为 string。${variable//pattern/string} 替换变量值 variable 中所有出现的 pattern 为 string。${variable/#pattern/string}:如果变量 variable 值以 pattern 开头,则将其替换为 string${variable/%pattern/string}:如果变量 variable 值以 pattern 结尾,则将其替换为 string其中 /string 部分可省略,表示替换为空,即删除,例如 ${url//:*} 删除 :之后的所有内容
子字符串截取
${variable:start:length} - 截取变量值 variable 的子字符串,从索引 start 开始(从0开始计数),长度为 length 字符。${variable:start} - 截取变量值 variable 的子字符串,从索引 start 开始(从0开始计数)直到末尾。大小写转换
${variable,,} - 将变量值 variable 转换为小写。${variable^^} - 将变量值 variable 转换为大写。${parameter^pattern} 和 ${parameter^^pattern} 用于将变量值中的匹配部分转换为大写,分别转换第一个匹配和所有匹配。${parameter,pattern} 和 ${parameter,,pattern} 用于将变量值中的匹配部分转换为小写,分别转换第一个匹配和所有匹配。特定操作( Bash 4.4 及以上版本)
${variable@operator} operator 取值:val=`expr 2 + 2` # 运算符要和表达式间要有空格,不能是 expr 2+2,写在`(())`中不受此限制
# 假设 a=10,b=20
`expr $a + $b` # 30
`expr $a - $b` # -10
`expr $a \* $b` # 200
`expr $a / $b` # 2
`expr $a % $b` # 取余 0
a=$b # 把b的值赋给a
[ $a == $b ] # 数值比较 false
[ $a != $b ] # true
条件表达式要放在方括号中,并且要有空格,[$a==$b]是错误的
[ $a -eq $b ] # a==b equal
[ $a -ne $b ] # a!=b not equal
[ $a -ge $b ] # a≥b great equal
[ $a -le $b ] # a≤b less equal
[ $a -gt $b ] # a>b great than
[ $a -lt $b ] # a<b less than
[ ! false ] # ! 非运算
[ $a -lt $b -a $b -gt $a ] # -a 与运算 and
[ $a -lt $b -o $b -gt $a ] # -o 或运算 or
# 优先级:! > -a > -o
[ $a -lt $b && $b -gt $a ] # && 逻辑的and
[ $a -lt $b || $b -gt $a ] # || 逻辑的or
# a="abc", b="efg"
[ $a = $b ] # a==b
[ $a != $b ] # a!=b
[ -z $a ] # 字符串长度为0返回true,a.size()==0
[ -n $a ] # 字符串长度不为0返回true,a.size()!=0
[ $a ] # 字符串不为空返回true a.isNotEmpty()
[ -b $file ] # file是块设备文件返回true
[ -c $file ] # file是字符设备文件返回true
[ -d $file ] # file是目录返回true
[ -f $file ] # file是普通文件返回true(既不是目录也不是设备文件)
[ -g $file ] # file设置了SGID位返回true
[ -k $file ] # file设置了粘着位(Sticky Bit)返回true
[ -p $file ] # file是有名管道返回true
[ -u $file ] # file设置了SUID位返回true
[ -r $file ] # file可读返回true
[ -w $file ] # file可写返回true
[ -x $file ] # file可执行返回true
[ -s $file ] # file为不为空返回true(文件大小大于0)
[ -e $file ] # file存在返回true(包括目录)
[ -S $file ] # file是socket文件返回true
[ -L $file ] # file存在并且是一个符号连接返回true
[ -t $fd ] # 检查文件描述符 fd 是否连接到一个终端
[ -v $var ] # 检查变量 var 是否设置(Bash 4.2+)
echo "It is a test",可省略双引号 echo It is a testecho "\"It is a test\""echo "$var_name It is a test"echo -e "OK! \n It is a test" # -e 开启转义echo -e "OK! \c It is a test" # -c 不换行echo "It is a test" > myfileecho `date` printf format-string [args...]printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876
%-10s:字符串,宽度为10个字符,-表示左对齐,没有则右对齐
%d:十进制整数
%-4.2f:浮点数,.2表示保留两位小数
%c:字符
%x:十六进制数
%o:八进制数
%b:二进制数
%e:科学计数法表示的浮点数
test命令:用于检查某个条件是否成立,对数值、字符和文件进行检测
a=100
b=200
if test $[a] -eq $[b]
then
echo '两数相等!'
else
echo '两数不相等!'
fi
# 输出:两数不相等! []执行基本的算数运算:res=$[a+b] res为300
[] 中用 -gt,-lt,-eq,[ $a -gt $b ](()) 中可直接用 >,<,=,(( $a > $b ))# 多行写法
if condition; then
command1
else
commandN
fi
# 一行写法
if [ $(ps -ef | grep -c "ssh") -gt 1 ]; then echo 'true'; fi
if [ $a -gt $b ]
then
echo 'a > b'
elif [ $a -eq $b ]
then
echo 'a = b'
else
echo 'a < b'
fi
if [ ... ]:POSIX 兼容的测试命令(等价于 test)[ 实际上是 test 命令(/usr/bin/[ 或内置命令),前后必须有空格。] 不是独立命令,它是 [ 命令的语法要求:最后一个参数必须是 ]&&、|| 在 [ ] 内部直接使用(需用 -a、-o,但已过时)=,!=,没有 <, >, =~ 等运算符,数值使用 -eq, -ne 等,逻辑运算:-a, -o, !if [[ ... ]]:Bash/Zsh 的增强测试(推荐在 Bash 中使用),是 Bash 内置语法,不是命令,更安全、功能更强。支持:== 可用于模式匹配,如 if [[ $var == file*.txt ]]; if [[ $var == $pattern ]],file*.txt $pattern 不能加双引号,会阻止通配符展开[[ $str =~ ^[0-9]+$ ]]&&、||、!/bin/sh 在 Debian/Ubuntu 下是 dash,不支持)==/=, !=, >, <, =~,数值比较使用 -ne, -eq, -ge 等,逻辑运算:!, &&, ||if (( ... )):整数算术条件判断$(当然加了也可以)==、!=、<、<=、>、>=、&&、||、!、位运算等if command; then:以命令的退出状态作为条件if command:命令成功(exit 0)if ! command:命令失败(exit ≠ 0)if command1 && command2:两个都成功if command1 || command2:至少一个成功if 最本质的用法,其他 [ ]、[[ ]]、(( )) 都是特例。# 多行写法
for var in item1 item2 ... itemN
do
command1
command2
...
done
# 一行写法
for var in item1 item2 ... itemN; do command1; command2... done;
while [until] [ $a -gt $b ] # : 冒号用于无限循环,也可使用 true,for (( ; ; ))
do
command
done
while read FILM # read 读取键盘信息,按<Ctrl-D>结束
do
echo "是的!$FILM 是一个好网站"
done
case 分支以 ) 开始,用 ;; 表示 break,执行结束,esac 作为结束标记case $var in
var1|var2|var3|...)
command1
...
;;
varN)
command2
...
;;
*) # 以上未匹配时捕获
command3
...
;;
esac
break:跳出所有循环(终止执行后面的所有循环)continue:跳出当前循环# 函数定义
# 通过 $1 $2 ... $n 获取参数,当n>=10时,需使用${n}获取参数
[ function ] fun (){
action;
[return int;]
}
# 函数调用 只需用函数名 fun
fun arg1 agr2 ... agrN
function关键字是可选的return则以最后一条命令的运行结果作为返回值return返回值只能是数值,介于(0-255)return值超出255,直接使用echo输出而不是用return$?来获得大多数Unix系统从终端接收输入并将产生的输出发送至终端。命令通常从标准输入读取数据,默认指向终端。通常将输出写入到标准输出或标准错误,默认也是指向终端
终端 /dev/pts/0 是一种特殊的文件;标准输入是文件描述符 0 的别名;文件描述符是进程访问文件(包括终端、管道、普通文件)的索引。(stdin 可理解为就是 FD 0)
/dev/stdin 通常是一个符号链接,指向 /proc/self/fd/0,而在不进行输入重定向时 /proc/self/fd/0 通常指向 /dev/pts/0,所以说标准输入通常来自终端,因为命令大多从标准输入读取数据,而标准输入通常指向终端
改变 FD 0 指向通常是由 shell 来完成的,在命令实际执行前,shell 会解析输入的命令行,不论是管道、HereDocument还是HereString,shell 会提前把 FD 0 的指向设置好,实际执行的命令只需要从标准输入读取数据,而不关心数据的来源
命令接收的数据包括两部分:
grep "error" log.txt"error" 是匹配规则(配置)。log.txt 是数据源的位置(配置)。cat file.txt | grep "error"grep 不需要知道文件名,它只关心“给我数据流,我过滤给你”。| 命令 | 说明 |
|---|---|
| command > file | 将 stdout 重定向到 file |
| command >> file | 将 stdout 追加到 file |
| command < file | 将 stdin 重定向到 file |
| n > file | 将文件描述符为n的文件重定向到file |
| n >> file | 将文件描述符为n的文件追加到file |
| n>&m | 将 FD n 输出重定向 FD m 指向的文件,让 n 变成 m 的只写副本(如果 m 是只写) |
| n<&m | 将 FD n 输入重定向 FD m 指向的文件,让 n 变成 m 的只读副本(如果 m 是只读) |
| exec n>&m | 打开文件描述符 n(复制 m) |
| exec n>&- | 关闭文件描述符 n |
| <<tag abc tag | 将开始标记 tag 和结束标记 tag 之间的内容 abc 作为输入 |
fd = open("file", O_WRONLY|O_CREAT|O_APPEND) dup2(fd, x)> 和 >> 默认操作 FD 1,< 默认操作 FD 0。&m 的理解:& 的作用是 “类型标记”,告诉 Shell 解析器:“后面的数字是文件描述符编号,而不是文件名”。(类似于指针解引用) < 写在命令开头:<file grep "pattern",重定向操作符(<, >, >>, 2> 等)不属于命令参数,它们是由Shell解释的特殊标记# 同时有输入和输出重定向
< input.txt grep "error" > errors.txt 2> /dev/null
# 混合位置
> output.txt < input.txt command args
2> errors.log command < input.txt > output.txt
/dev/null 文件:它是特殊文件,写入到它的内容都被丢弃,起到禁止输出的效果. filename 或 source filename,类似于import,可以直接使用导入的脚本中的变量
fork() 出一个子进程,同时复制父进程的 FD 表给子进程(两个独立的副本),重定向操作(open, dup2)只在子进程中进行,父 Shell 的 FD 表从未被修改,子进程结束后,其 FD 表随进程销毁exec 不带命令名(修改当前 Shell 的 FD),exec 不带命令名时,重定向在当前 Shell 进程中执行,而不是子进程。# 这会修改当前 Shell 进程的 FD 表!
exec 3> /tmp/log.txt
# 后续所有使用 FD 3 的命令都会写入 /tmp/log.txt
echo "line1" >&3
echo "line2" >&3
# 甚至新的命令也受影响(如果它们继承 FD 3)
bash -c 'echo "line3" >&3' # 也会写入 /tmp/log.txt
# 恢复:关闭 FD 3
exec 3>&-
# 子 Shell 中的重定向
(
exec 3> /tmp/sub.txt
echo "in sub" >&3
)
# 父 Shell 的 FD 3 不存在(子 Shell 的 FD 表已销毁)
echo "test" >&3
# 报错:Bad file descriptor
# 管道中的重定向只影响对应命令
cat file.txt | grep "pattern" > out.txt
# cat 的 stdout → 管道
# grep 的 stdin ← 管道,stdout → out.txt
# 两个命令的 FD 表独立,互不影响
source 或 . 执行的脚本# source 在当前 Shell 中执行,不 fork
source script.sh
# 如果 script.sh 中有 exec 重定向,会影响当前 Shell
# FD 3 也指向 FD 1 指向的文件,相当于记录原 FD 1 的指向(复制 FD 1)
exec 3>&1
# 现在可以安全地重定向 FD 1
echo "to file" > /tmp/output.txt
# 让 FD 1 再指向 FD 3 所指向的文件,相当于恢复 FD 1 原来的指向
exec 1>&3
# 关闭 FD 3
exec 3>&-
在脚本开头写入 exec >> "$log_file" 2>&1 将所有输出(包括错误)自动记录到日志文件,无需每条命令单独重定向
exec >> "$log_file" 2>&1 的机制:将脚本中所有命令的标准输出(stdout)和标准错误(stderr)追加(>>)到日志文件 $log_file 中,不显示在终端。
exec > >(tee -a "$log_file") 2>&1 的机制:通过 tee 命令将标准输出和错误同时显示在终端并追加到日志文件。>(tee -a "$log_file"):使用进程替换(Process Substitution),将 tee 命令作为临时文件描述符接收输入。tee -a:追加模式,保留原有日志。
> >(...) 的语法:第一个 > 表示重定向标准输出(exec >)。>(...) 是进程替换语法(Bash 特性,需确保脚本以 #!/bin/bash 开头而非 #!/bin/sh),将 tee 命令的输出管道作为文件描述符传递给 exec。这种写法是 Bash 中实现同时输出到终端和文件的标准方式。
>(...) 的基本语法与功能
>(command_list) 其中 command_list 是一组命令(用分号 ; 分隔),> 和 ( 之间不能有空格。/dev/fd/63),将左侧命令的输出重定向到该文件,并作为右侧命令的输入。>(...) 的工作原理:
/dev/fd/63 ),用于中转数据。echo "hello" > >(cat) # 实际步骤:
# 1. 创建 /dev/fd/63
# 2. echo "hello" → /dev/fd/63
# 3. cat 从 /dev/fd/63 读取并输出
>(...) 和 <(...)

进程替换(<()或 >())和管道(|)都是 Shell 中用于命令间数据传递的机制
数据流方向
command1 | command2 command1 的输出传给 command2 作为输入/dev/fd/63)传递,可以模拟文件输入或输出。cmd1 < <(cmd2):cmd1读取cmd2的输出作为输入(类似输入重定向)。cmd1 > >(cmd2):cmd2读取 cmd1 的输出作为输入(类似输出重定向)。diff <(ls dir1) <(ls dir2) # 比较两个目录的文件列表子进程与变量作用域
echo "hello" | read var; echo $var 输出为空(var 在子 Shell 中失效)read var < <(echo "hello"); echo $var 输出 hello适用场景
cat log.txt | grep "error" | sort | uniq -c进程替换:适合需要多输入/输出源或命令参数化的场景。示例:
# 比较两个命令的输出
diff <(command1) <(command2)
# 将输出同时传递给多个命令
# tee从 log.txt读取数据,并同时输出到所有进程替换的临时文件描述符
# 同时输出到终端
tee >(grep "error" > errors.txt) >(grep "warning" > warnings.txt) < log.txt
# 相当于
cat log.txt|tee >(grep "error" > errors.txt) >(grep "warning" > warnings.txt)
ls > >(grep "txt") 中,>(grep "txt") 会生成 /dev/fd/63,ls 的结果输出通过此文件,grep 从此文件中查找 txt。cat <(echo "hello") 中,<(echo "hello") 会生成 /dev/fd/45,echo "hello" 的结果写入此文件,cat 读取此文件-)的核心都是围绕标准输入(stdin) 的数据来源和处理方式。pipe() 系统调用,内核创建一个管道对象(内存缓冲区),返回两个 FD:fd[0] (读端) 和 fd[1] (写端)。fork 出两个子进程(命令 A 和 命令 B)。dup2(fd[1], 1)。将 A 的 stdout (FD 1) 重定向到管道的写端 fd[1]。命令 A 的输出将写入该管道对象dup2(fd[0], 0)。将 B 的 stdin (FD 0) 重定向到管道的读端。命令 B 将从该管道读取数据exec。exec 之前。<< 'EOF' 变量不展开,所有内容视为字面量<< "EOF" 展开变量,但抑制通配符展开<<< "$var" 展开变量,自动加换行<<-EOF Shell 只删除制表符 (Tab),不删除空格 (Space)。<< 'EOF' 防止展开<<:它必须严格顶格(第 1 列),前面不能有任何空格或 Tab。内容行中的Tab和空格会保留<<-:它前面可以有 Tab(会被忽略),但不能有空格。内容行中的Tab也会被删除,但空格会保留<<- 是为了让 Shell 脚本本身更美观(允许脚本缩进),而不是为了让传递的内容缩进。| 写法 | 行为 | 场景 |
|---|---|---|
<<EOF | 保留所有前导空白(Tab 和空格)。内容中的缩进会原样保留。 | 需要严格保持格式(如 Python 代码、Makefile、YAML)。 |
<<-EOF | 仅去除前导 Tab 字符,保留空格。允许你在脚本中对 Here Doc 内容进行缩进以提高可读性。 | 大多数 Shell 脚本场景,希望代码整洁且内容不受缩进影响。 |
<< 'EOF' | 禁止变量展开和命令替换,但保留所有前导空白。 | 内容包含 $、` 或 \ 且不希望被 Shell 解析时。 |
<<-'EOF' | 禁止变量展开,且仅去除前导 Tab 字符。 | 最推荐的通用写法:既整洁又安全。 |
- 决定是否删除前导tab' 单引号决定内容是否展开- 不是 Shell 的特性,也不是内核的特性,它是应用程序(Command)的代码逻辑,是约定而非机制- 只是一个普通的字符串参数 argv[1] = "-"。Shell 不做任何特殊处理。argv,发现 "-",于是主动去读取 FD 0。cat file | tar -cvf out.tar -tar 的 argv 包含 "-cvf", "out.tar", "-"tar 解析到最后一个 "-",知道这里需要填入一个文件流。tar 读取 FD 0(此时 FD 0 已被 Shell 通过管道连接到了 cat 的 FD 1)。echo -e "\033[31m \033[42m 这是红色字体绿色背景 \033[0m" 后跟 \033[0m 恢复原色\033[30m 或 \033[0;31m,加 0; 会重置所有属性(清除加粗、下划线等),推荐使用\033[31m\033[32m\033[33m\033[34m\033[35m\033[36m\033[37m\033[40m\033[41m\033[42m\033[43m\033[44m\033[45m\033[46m\033[47m