ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux Shell 括号用法:${}、$()、[]、[[]]、(()) 区别

Linux Shell 括号用法:${}、$()、[]、[[]]、(()) 区别 1. 括号家族的分工地图先把六个符号钉在各自岗位上刚接手别人脚本的时候最容易被一堆符号劝退。linux下${}、$()、$[]、$(())、[]、[[]]、(())这几组东西长得像亲兄弟实际干的活完全不挨着。有人把它们统称为Linux 括号用法但它们分属三个不同的层面变量处理、命令执行、条件判断还有一个横跨两界的(())。搞不清这一点写出来的linux脚本就会时好时坏报错信息还特别含糊bash: [: missing ]或者syntax error near unexpected token之类的看着就头疼。我自己的经验是先建立一张符号—归属—用途的对照表再去抠每个符号的细节效率比零散地背语法点高得多。这张表后面我会详细展开先给你一个心智模型${}属于参数扩展它只跟变量打交道$()和 属于命令替换把命令输出当字符串用$(())和$[]属于算术扩展只算整数[]是一个真实的命令test的别名[[]]是bash 的关键字两者都是做条件测试(())则是算术求值命令同时还能兼任for、while的循环条件。适合谁来读如果你正在看别人的运维脚本、写 Docker 的 entrypoint、调 Jenkins 的构建脚本或者单纯想把linux常用命令里的那些if [ ... ]看懂这篇内容就是给你准备的。全文的代码都在bash和dash下实测过差异点我会单独标出来。1.1 一张表看清谁是谁先把框架立起来细节后面慢慢补符号归属层面一句话作用典型场景${}参数扩展取变量值、切字符串、给默认值拼接字符串、取文件名后缀$()命令替换把命令的标准输出塞进变量记录时间戳、取 IP 地址$[]算术扩展已废弃整数四则运算等价$(())老旧脚本里见得到$(())算术扩展整数运算、三元、进制转换计数器、阈值判断[]外部命令test条件测试if [ -f $f ][[]]bash 关键字增强版条件测试正则匹配、逻辑组合(())算术命令求值并按结果返回退出码for ((i0;in;i))注意最后两行有个非常关键的差别[]是会真的去PATH里找test这个可执行文件的尽管 bash 也有同名内建而[[]]和(())是 bash 直接解析的语法结构根本不走命令查找那套流程。这个差别解释了很多为什么[[]]能写而[]不能写的问题。1.2 场景驱动的选择思路与其死记语法不如按需求反推该用哪个符号。我把日常最常见的选择逻辑整理成几条要把命令的输出存起来用$()。比如now$(date %F)。不要用反引号理由第 3 章会讲。要处理一个可能为空的变量用${var:-默认值}而不是写一长串if [ -z $var ]。要做数字比较优先(())比如if (( a b ))。它比[ $a -gt $b ]短而且不用加引号、不用记-gt这些助记符。要判断文件、字符串是否相等用[[]]bash 环境下。它容错高、支持、||、~。写#!/bin/sh的脚本那[[]]和(())就别用了dash不认。这是我在 Alpine 镜像里翻车最多的地方——本地用 bash 跑得好好的构建脚本一进容器就报[[: not found。镜像里为了体积默认是dash[[]]直接当命令去找自然找不到。1.3 三个高频误用现场先剧透三个坑后面章节会逐一展开原理。坑一[]里变量不加引号。if [ $name zhang ]当name为空时这一行会被展开成[ zhang ]test看到三个参数里第一个是直接报unary operator expected。写成[ $name zhang ]就没事。坑二把$[]当$(())用。功能上确实等价但$[]在 bash 5.x 里已标记废弃且不支持某些嵌套写法新脚本没有理由再写它。坑三在(())里给变量加$。(( $i ))在绝大多数情况下能跑但涉及嵌套或特殊表达式时会出意外而且属于风格污染。(())内部的变量名会自动当整数解析不需要$。2.${}变量替换远不止取个值那么简单${}是日常出现频率最高的一个。很多人对它的认知停留在$var的高级写法实际上它是一整套字符串处理工具能顶掉相当一部分sed、awk、cut的活。我见过不少脚本为了取个文件扩展名去调basenamecut其实一句${file##*.}就够了还省掉一次 fork。2.1 从界定变量边界说起$var的语法有个天然缺陷它不知道变量名到哪里结束。echo $varabc会被解析成取变量varabc的值。这时候{}的作用就是画一条边界线namekernel echo $name_debug # 取的是变量 name_debug通常为空 echo ${name}_debug # 输出 kernel_debug这个细节在拼路径、拼文件名时特别重要。我写日志轮转脚本时习惯统一用${}因为日志名经常是${date}_${service}.log这种形式用$date_就会直接踩坑。另外${#var}返回字符串长度注意是字符数不是字节数在 bash 4.4 的多字节 locale 下shello echo ${#s} # 52.2 子串截取与大小写转换${var:offset:length}是截取子串。offset 从 0 开始length 可省略。负数 offset 前面必须留一个空格这是语法要求因为${var:-3}会被理解成默认值 -3sabcdefghij echo ${s:3} # defghij echo ${s:3:4} # defg echo ${s: -3} # hij注意 -3 前面那个空格 echo ${s: -4:2} # gh大小写转换是 bash 4.0 引入的处理配置项时很顺手vLinux echo ${v^} # Linux - Linux首字母已大写 echo ${v,} # linux echo ${v^^} # LINUX echo ${v,,} # linux^^和,,是全部转换^和,是只转第一个字符。配合通配符还能做条件转换比如${v^^[aeiou]}只把元音变大写这个偏门用法知道就行别在团队脚本里用可读性差。2.3 默认值四兄弟:-、:、:?、:这四个是${}里最实用的部分也是面试题常客。核心区别在于冒号在不在带冒号时变量未设置和设置为空字符串都算触发条件不带冒号时只有未设置才触发。写法触发条件行为是否修改原变量${v:-default}未设置或为空返回 default否${v-default}仅未设置返回 default否${v:default}未设置或为空返回 default 并赋值给 v是${vdefault}仅未设置同上是${v:?msg}未设置或为空打印 msg 到 stderr 并以非零退出否${v:alt}已设置且非空返回 alt否${valt}已设置可空返回 alt否实操中${v:?}是我最常用的一个用来做参数校验。比如脚本要求必须传环境变量: ${API_HOST:?必须设置 API_HOST 环境变量}这一行等价于五六行的if判断而且报错信息干净。:?冒号后什么都不写bash 会给出默认提示parameter null or not set。${v:alt}的典型用法是判断变量非空才追加某段内容opts-O2 opts${opts}${DEBUG: -g} # DEBUG 为空则 opts 保持 -O2非空则变成 -O2 -g2.4 模式删除与替换的四把刀#、##、%、%%这四个符号负责从字符串两端删内容配合通配符使用。记忆口诀#在键盘左边管开头%在右边管结尾符号翻倍表示贪婪匹配删得最多。path/var/log/nginx/access.log echo ${path#*/} # var/log/nginx/access.log 删最短前缀 echo ${path##*/} # access.log 删最长前缀等价 basename echo ${path%/*} # /var/log/nginx 删最短后缀等价 dirname echo ${path%%/*} # 空字符串 删最长后缀取扩展名就用##*.这个组合我用得最多filearchive.tar.gz echo ${file##*.} # gz echo ${file%.*} # archive.tar替换用/写法含义${v/pat/repl}替换第一个匹配${v//pat/repl}替换全部${v/#pat/repl}只匹配开头${v/%pat/repl}只匹配结尾sa-b-c-d echo ${s/-/_} # a_b-c-d echo ${s//-/_} # a_b_c_d echo ${s/#a/X} # X-b-c-d echo ${s/%d/Y} # a-b-c-Y注意这里用的是通配符模式glob不是正则。*代表任意字符串?代表单个字符[abc]代表字符集。写正则那一套.*、\d在这里不生效。2.5 间接引用与变量名列举${!var}是间接引用先把var的值取出来再把这个值当变量名去取值。做配置映射时有用keyprod prod_host10.0.0.1 echo ${!key} # 10.0.0.1${!prefix*}和${!prefix}会列出所有以某前缀开头的变量名遍历配置项时很方便cfg_a1 cfg_b2 for k in ${!cfg_*}; do echo $k${!k} done这两个的区别是*展开后是一个整体字符串受IFS影响展开成独立的单词列表。做for循环时用更安全。3.$()命令替换以及反引号为什么该退休了$()的语义非常直白执行括号里的命令把它的标准输出注意不含 stderr原样替换到当前位置。尾部换行会被吃掉中间换行保留。3.1 基本用法与嵌套today$(date %F) count$(ls -1 | wc -l) ip$(ip -4 addr show eth0 | awk /inet /{print $2} | cut -d/ -f1)$()最舒服的一点是可以直接嵌套不用转义inner$(echo $(uname -r) | tr -d \n)如果换成反引号嵌套就得写成echo \uname -r\反斜杠一多眼睛就花了。这是我坚决抛弃反引号的首要理由。第二个理由是$() 内部的双引号可以正常使用而反引号内部的引号处理规则非常别扭echo a b 里的引号会被反引号解析器先吃掉一层。顺带说一个容易被忽略的点命令替换的退出状态。$()整体的退出码等于括号内最后一条命令的退出码所以可以这样写if ! out$(grep -q pattern file); then echo 没找到 fi3.2 分词与换行的坑命令替换的结果如果是多行不加引号时会被 shell 按单词拆分这在处理文件名列表时几乎是灾难files$(ls) for f in $files; do echo $f done如果文件名里有空格$files会被拆成多个单词for循环拿到的就不是完整文件名了。正确做法是加引号或者干脆用数组mapfile -t files (find . -maxdepth 1 -type f -name *.log) for f in ${files[]}; do echo $f donemapfile按行读进数组每个元素是一整行不管里面有没有空格。配合进程替换 (...)使用这是 bash 里处理命令输出列表的标准姿势。进程替换的(...)虽然在形式上也用了括号但它跟本文的主题是两套机制它把命令输出变成一个/dev/fd/N文件路径不属于$()的范畴。提示如果你写的是#!/bin/sh脚本mapfile是不可用的退回while IFS read -r line的写法。4.$(())与$[]算术运算的两种面孔linux常用命令里提到整数运算多半会看到expr、let、$[]、$(())四条路。expr是外部命令每次调用都要 fork性能最差let和$(())都是 shell 内建的算术求值$[]是最早的写法现在已废弃。4.1$(())能做什么它支持 C 语言风格的整数运算 - * / % ** | ^ ~ ! || ?:以及赋值运算符 - * / % | ^。a10; b3 echo $((a b)) # 13 echo $((a / b)) # 3整数除法直接截断 echo $((a % b)) # 1 echo $((2 ** 10)) # 1024 echo $((a b ? a : b)) # 10两个必须记住的点第一只支持整数没有浮点。$((1 / 3))结果是0不是0.333。要算小数得交给bc或awkecho scale2; 1/3 | bc # .33 awk BEGIN{printf %.2f\n, 1/3} # 0.33第二括号里的变量名不需要$写$((ab))和$(( $a $b ))效果一样但前者更清爽。更重要的是在(())里做自增$((i))这种写法$反而可能导致解析歧义统一不加最稳。另外$((...))里可以直接写变量赋值这在需要同时算多个值时有用echo $(( x 5, y x * 2, y 1 )) # 114.2 进制陷阱08与10#这是我认为最容易翻车的算术细节。$(())里以0开头的数字会被当成八进制echo $((08 1)) # bash: 08: value too great for base (error token is 08)八进制里没有 8直接报错。而date %H在上午 8 点返回的正是08所以下面这段代码在 8 点、9 点会突然崩掉h$(date %H) next$((h 1)) # 8 点、9 点直接报错解决办法是强制十进制用10#前缀h$(date %H) next$((10#$h 1))10#告诉 shell按十进制解析这个数。其他进制同理2#1010是 1016#ff是 2558#17是 15。这个写法在解析固定宽度的配置字段时特别好用。4.3$[]的来历与弃用理由$[]是 bash 2.x 时代引入的算术扩展写法语义和$(())完全一样a5 echo $[a 1] # 6它被弃用不是因为它算错了而是因为语法上有歧义$[和数组下标[...]长得太像解析器要额外回溯判断同时它无法嵌套$[$[11]1]这种写法会出问题。bash 5.x 会在某些情况下给出 deprecation 警告shellcheck也会直接标红。结论很简单看到别人的脚本里有$[]可以放心改成$(())自己新写的一律不用。linux面试题里偶尔会考这个点标准答案是功能等价$[]已废弃推荐$(())。5.[]与[[]]条件测试的正确打开方式这一组是出错重灾区。核心认知只有一条[]是一个命令[[]]是 bash 语法。5.1[]是命令不是语法[实际上是test命令的另一个名字。你可以在系统里找到它$ type -a [ [ is a shell builtin [ is /usr/bin/[既然是命令那么几条规则就顺理成章了必须有空格。[$x 1]会被当成一个叫[$x的命令报command not found。正确写法是[ $x 1 ]左括号后、右括号前都要有空格。变量必须加引号。不加引号的话空变量会直接消失导致参数个数错乱name if [ $name a ]; then echo yes; fi # bash: [: : unary operator expected if [ $name a ]; then echo yes; fi # 正确返回 false和要转义。因为在命令的位置上这两个符号会被 shell 当成重定向[ $a \ $b ] # 字符串比较需要转义 [ $a -gt $b ] # 数字比较用 -gt不需要转义逻辑用-a/-o不用/||。虽然[ a ] [ b ]是可以的这是两个命令用 shell 的连接符但[ a -a b ]才是test内部的与运算。-a和-o在 POSIX 里已被标记为过时参数一多容易有歧义我现在基本只用串联两个独立的[]。常用的测试运算符速查类别运算符说明数字-eq -ne -lt -le -gt -ge等于/不等/小于/小于等于/大于/大于等于字符串 ! -z -n相等/不等/空/非空文件-e -f -d -r -w -x -s -L存在/普通文件/目录/可读/可写/可执行/非空/符号链接文件比较-nt -ot -ef更新/更旧/同一 inode5.2[[]]的增强点[[]]是 bash、zsh、ksh 支持的关键字dash不支持。它解决了[]的几个痛点不需要给变量加引号。内部不做单词拆分和通配展开空变量就是空字符串不会消失。支持、||、!。写起来跟其他语言接近if [[ -f $f -r $f ]]; then echo 可读文件 fi支持、不用转义。而且这两个符号在[[]]里做的是字典序比较不是重定向。支持~正则匹配。这是[]完全做不到的if [[ $ip ~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then echo 看起来像 IPv4 fi匹配到的分组会存进BASH_REMATCH数组BASH_REMATCH[0]是整个匹配[1]起是各捕获组。这个特性用来从一行文本里抠字段非常方便比sed少一次 fork。5.3 差异对照表对比项[ ][[ ]]本质命令testshell 关键字变量加引号必须可以不加支持||用-a-o已过时直接写需转义直接写正则~不支持支持右侧通配不支持且需转义支持词分割/通配展开会不会可移植性POSIX 通用bash/zsh/ksh5.4~正则与通配匹配这两个放一起说因为行为差异很微妙我自己也踩过。右侧不加引号时按通配符模式匹配freport_2024.csv [[ $f *.csv ]] echo CSV 文件 [[ $f report_* ]] echo 报告文件但如果把右侧加引号通配符就失去特殊含义变成纯字符串比较[[ $f *.csv ]] # 永远为假除非文件名真的叫 *.csv这个规则跟直觉相反我第一次写的时候在这卡了半小时。记忆方法引号会保护模式去掉引号才让它活起来。~右侧绝对不要加引号加了就当字面量处理[[ abc123 ~ ^[a-z][0-9]$ ]] echo ok # 正确 [[ abc123 ~ ^[a-z][0-9]$ ]] echo ok # 错变成字面匹配如果正则本身要拼变量推荐把正则先存进变量再写[[ $s ~ $re ]]这样引用变量时不会触发引号问题re^[0-9]$ if [[ $input ~ $re ]]; then echo 纯数字 fi6.(())既是计算器又是循环控制器(())和$(())长得很像差一个$用途完全不同。$(())是表达式会被替换成计算结果(())是命令它本身有个退出码但没有输出。6.1 退出码的语义(())的退出码规则特别巧妙表达式结果非零退出码为 0成功结果为 0退出码为 1失败。(( 1 1 )) # 结果为 2非零$? 0 (( 0 )) # 结果为 0$? 1这正好跟真/假的语义对齐所以可以直接当条件用a10; b20 if (( a b )); then echo a 小于 b fi while (( retry-- )); do echo 还剩 $retry 次 donewhile (( retry-- ))这个写法很值得记当retry减到 0 时表达式值是 0退出码变成 1循环刚好结束。整个过程只用了--不用写比较。不过要注意retry是后减进循环体时的值已经是减过的了边界要自己算清楚。6.2 C 风格for循环这是(())最有价值的用法。处理数组、做固定次数循环比for i in $(seq 1 10)干净得多而且不产生子进程for ((i 0; i 10; i)); do echo 第 $i 次 done # 遍历数组 arr(a b c) for ((i 0; i ${#arr[]}; i)); do echo ${arr[i]} doneseq那种写法每次迭代都要等一个外部命令输出循环次数上万时差距会很明显。我做过一个简单的对比10 万次空循环((...))大概几十毫秒$(seq)要几秒属于量级差异。6.3 和$(())怎么分工一句话要值用$(())要判断用(())。# 要结果赋值给变量 total$((a * b c)) # 要拿来做条件 if (( total 1000 )); then echo 超标 fi另外两者在自增运算的副作用上也有区别。$((i))会在扩展过程中修改i但它同时也有输出((i))只修改不输出还能拿到退出码。混着写容易让代码意图模糊我的习惯是修改就用(())单独一行取值就用$(())放在赋值右侧不交叉。还有一点必须提醒$(())和(())内所有运算都在有符号 64 位整数范围内溢出会静默回绕不会报错。$((2**63))会得到负数。处理大数比如纳秒时间戳运算时要留意这个边界。7. 组合实战写一个健壮的日志清理脚本光看语法记不住还是得上个完整例子。需求是这样的扫描指定目录下的.log文件删除 N 天前的保留最新的 K 个同时支持--dry-run预览目录不存在要报错退出。7.1 设计思路参数解析部分用${}的默认值机制处理可选参数用[[]]和(())做校验文件筛选用find配合$()收集结果时间戳运算用$(())循环和计数用(())。这个脚本基本把本文所有符号都用上了一遍。7.2 完整代码#!/usr/bin/env bash set -euo pipefail DIR DAYS7 KEEP3 DRY_RUNfalse usage() { echo 用法: $0 -d 目录 [-n 天数] [-k 保留数] [--dry-run] } while [[ $# -gt 0 ]]; do case $1 in -d) DIR${2:?缺少目录参数}; shift 2 ;; -n) DAYS${2:?缺少天数}; shift 2 ;; -k) KEEP${2:?缺少保留数}; shift 2 ;; --dry-run) DRY_RUNtrue; shift ;; -h|--help) usage; exit 0 ;; *) echo 未知参数: $1 2; usage; exit 2 ;; esac done # 参数校验 [[ -n $DIR ]] || { echo 必须指定 -d 目录 2; exit 2; } [[ -d $DIR ]] || { echo 目录不存在: $DIR 2; exit 2; } if ! [[ $DAYS ~ ^[0-9]$ ]] || ! [[ $KEEP ~ ^[0-9]$ ]]; then echo 天数和保留数必须是非负整数 2 exit 2 fi (( DAYS 0 KEEP 0 )) || { echo 参数不能为负 2; exit 2; } # 收集候选文件按修改时间倒序跳过最新的 KEEP 个 mapfile -t all_logs ( find $DIR -maxdepth 1 -type f -name *.log -printf %T %p\n \ | sort -rn | awk { $1; sub(/^ /,); print } ) total${#all_logs[]} echo 共找到 $total 个日志文件保留最新 $KEEP 个清理超过 $DAYS 天的 now$(date %s) deleted0 for ((i 0; i total; i)); do f${all_logs[i]} # 前 KEEP 个直接跳过 (( i KEEP )) continue # 取修改时间并判断是否超期 mtime$(stat -c %Y -- $f 2/dev/null) || continue age_days$(( (now - mtime) / 86400 )) if (( age_days DAYS )); then if [[ $DRY_RUN true ]]; then echo [预览] 将删除 ($age_days 天): $f else rm -f -- $f { echo 已删除 ($age_days 天): $f; (( deleted )); } fi fi done # 汇总 if [[ $DRY_RUN true ]]; then echo 预览模式结束未做任何修改 else echo 清理完成共删除 $deleted 个文件 fi7.3 关键行拆解DIR${2:?缺少目录参数}用:?做即时校验。如果$2没传或者为空脚本会立刻以非零码退出并打印缺少目录参数。这一行顶得上一段if。用它替代[ -z $2 ]的好处是报错位置明确能直接定位到是哪个参数漏了。[[ -n $DIR ]] || { ... }||后面用{}包了一个命令块注意{后面要有空格}前面要有分号或换行。这是 bash 里写紧凑断言的惯用法。不过我更推荐写成if因为set -e下||的短路行为有时候不好预判尤其在函数里。[[ $DAYS ~ ^[0-9]$ ]]正则校验是不是纯数字。这里为什么不用(( DAYS 0 ))直接判因为如果DAYS传的是abc(())会把abc当变量名解析值为 0不报错但结果是错的。先做字符串层面的正则校验再进算术运算顺序不能反。mapfile -t all_logs (find ...)把 find 的输出按行读进数组。这里没有用files$(find ...)因为文件名带空格时会被拆散。-printf %T %p\n输出时间戳 路径再用sort -rn按时间倒序awk把第一列切掉只留路径。这个三步组合是我处理按时间排序的文件列表的标准套路。age_days$(( (now - mtime) / 86400 ))86400是一天的秒数整数除法直接取整。这里两个时间戳都是十进制、无前导零所以不涉及八进制问题。但如果是从date %d之类拿到的两位数字就一定得用10#前缀否则每月 8 号、9 号必挂。(( i KEEP )) continue用(())的退出码做判断。i KEEP为真时退出码是 0后面的continue执行。这是个很紧凑的写法但要注意在set -e下如果这行整体返回非零即条件为假脚本不会退出因为连接的命令列表的退出状态被set -e豁免。这个规则记不住的话就老老实实写if。rm -f -- $f { ...; (( deleted )); }--用来防止文件名以-开头被当成选项。(( deleted ))在这里既做了自增又让整个命令块的退出码变成 0因为自增前的deleted若是 0 会导致退出码 1所以用了{ ...; }包起来最后一条命令的退出码才生效。这里其实有个隐患第一次执行(( deleted ))时deleted是 0表达式的值是 0退出码为 1配合set -e会让脚本退出。上面代码块里用了{ ...; }把自增放在中间紧跟分号所以最终退出码取自后面的}才没出问题。更稳的写法是deleted$((deleted 1))纯赋值没有退出码陷阱。8. 常见问题与排查速查表这部分是我这些年攒下来的报错—原因—修法对照按出现频率排序。8.1 错误速查报错信息大概率原因修法[: : unary operator expected[]里变量没加引号空值导致参数错位加引号[ $x a ][: missing ]中括号内侧没留空格写成[ $x 1 ]command not found: [[用sh/dash跑了[[]]脚本改 shebang 为 bash或改用[]syntax error near unexpected token (在sh下用了(())同上08: value too great for base$(())把前导 0 当八进制加10#前缀integer expression expected[]用了-gt但变量是字符串先校验是数字或换(())bad substitution${}语法在sh下不被支持如${v^^}换 bash 或改写unbound variable开了set -u且变量未定义用${v:-}给默认值[(]相关奇怪提示[后紧跟了(被当成通配检查空格和转义8.2 几个只有踩过才知道的点set -u和${arr[]}的冲突。开启set -u后空数组的${arr[]}在 bash 4.3 及更早版本会报unbound variable。修法是写${arr[]:-}。升级到 bash 4.4 后这个行为被修正了但如果你的脚本要跑在 CentOS 7 那类老系统上就得防着。[]里-a的歧义。[ -f $f -a -r $f ]看着没问题但如果$f展开成!或者以-开头的字符串test的参数解析会乱。这是-a/-o被弃用的真实原因不是单纯的风格问题。$(())里的除零。$((a / 0))会让 bash 打印division by 0并中止当前命令在脚本里表现为一个非零退出码。开启set -e的话脚本直接结束。做除法前一定要判断分母if (( denom ! 0 )); then result$((num / denom)) else result0 fi[[]]里~配合变量的引号问题。前面提过但再强调一次正则是变量时不能加引号。很多人为了安全习惯性加引号结果正则变成了字面量条件永远为假还不报错排查起来很费劲。为了避开这个坑我通常把整个正则存成变量再[[ $s ~ $re ]]这样引用变量时不涉及引号展开的歧义。(())里比较浮点数。直接写会报syntax error: invalid arithmetic operator因为1.5里的.不是合法算术字符。要么乘 100 转整数要么用awk。我一般用awk BEGIN{exit !(1.5 1.2)}靠退出码判断。命令替换吃掉尾部换行。$(cat file)会把文件末尾的所有换行全部删掉如果你需要原样保留得用别的办法比如var$(cat file; echo x)再${var%x}或者干脆直接重定向。这个特性在做把文件内容当变量传时经常咬人。$(())里的!是逻辑非不是阶乘。从 C 过来的朋友容易想当然$((5!))的结果是0把 5 当真非之后是 0不是 120。最后分享一个我自己的习惯新写脚本一律用#!/usr/bin/env bash加set -euo pipefail然后用shellcheck过一遍。shellcheck对本文这些符号的误用覆盖得很全$[]会警告[]里变量不加引号会警告$(())的八进制问题也会提示。跑一遍比人肉 review 快得多尤其是在 CI 里挂一个检查步骤能挡掉相当一部分低级错误。至于[[和((的选择我的标准很简单只要 shebang 是 bash、脚本不会被人拿去用sh跑就统一用增强版如果脚本明确要兼容 POSIX那${}的字符串处理、$()、[]这三样足够撑起绝大部分场景。
返回列表