首页 / 资讯中心 / 文章详情

Linux Shell 实战:find、read、数组与 seq 的高效组合用法

Linux Shell 实战:find、read、数组与 seq 的高效组合用法 ★ FEATURED ARTICLE
如果你经常在 Linux 上写脚本或者管理过一段时间的服务器那对 find、read、数组、seq 这几样应该都不陌生。它们不是多冷门的命令但真的到了要用的时候很多人会卡在“find 的 -exec 怎么写才对”“read 读文件时为什么变量老是只有第一行”“数组切片到底怎么切”“seq 除了生成 1 到 100 还能干嘛”这些细节上。我自己也经历过这个阶段命令背了不少组合起来却各种报错。后来把这几样东西串在一起反复练了几轮才算真正“顺手”。这篇博文就是把我实际操作中的用法、踩过的坑和优化思路整理出来给你一个能直接参考的版本。无论是刚接触 Shell 脚本的新手还是想夯实基础的老手都可以照着试一遍。1. find五星级搜索命令从路径匹配到批量处理先把 find 的骨架刻进脑子里find [路径] [条件] [动作]。路径决定去哪找条件决定找什么动作决定找到之后干什么。很多人的误区是只把 find 当成“搜索文件名”的工具其实它的核心是后面那个“动作”加上动作之后它就把搜索和批处理连成了一条流水线。1.1 先理解 find 的基本骨架路径、条件、动作最基本的用法是拿文件名匹配比如find /data/logs -name *.log意思是在/data/logs下找所有以.log结尾的文件。路径可以写多个比如一次查两个目录find /data/logs /backup/oldlogs -name *.log注意路径要写在最前面如果写成find -name *.log /data/logs大概率会报paths must precede expression。这个错误我一开始也常碰到原因就是 find 解析参数时要求路径在前、表达式在后顺序不能乱。-name 区分大小写。如果不想区分用-iname。比如要找 .LOG、.log、.Log 都能命中find . -iname *.log除了 -name我平时用得多的条件还有几个-type f只查普通文件-type d只查目录-size 10M大于 10M 的文件-mtime -77 天内修改过的文件-mtime 3030 天前修改过的文件-perm 644权限正好是 644 的文件条件之间可以组合。-a表示并且-o表示或者默认是并且。比如找/data/logs下大于 100M 的 .log 文件find /data/logs -name *.log -size 100M再比如找 7 天内修改过的并且是目录类型的find /data/project -type d -mtime -7这里有个小细节-mtime -7单位是“天”但 find 实际是按 24 小时来算的如果你需要更精确到分钟可以用-mmin -30表示 30 分钟内改过的。日志排查场景里我经常会用find /var/log -name *.log -mmin -30这样可以快速定位刚写入过的日志文件不用一个个打开看。1.2 -exec 与 xargs批量操作要这么配才稳find 最常见的“动作”就是-exec它能把搜到的文件一条条作为参数传给后面的命令。典型写法find /data/logs -name *.log -mtime 7 -exec rm {} \;这个命令的意思是找 7 天前的 .log 文件并删除。重点理解三个符号{}是 find 查到的结果占位符\;是命令结束符注意分号前要有反斜杠否则 shell 会把它当普通分号处理。-exec后面跟的是一整条命令每条结果执行一次 rm。这里有一个性能问题如果搜出来的文件非常多-exec会每处理一个文件就启动一次外部命令开销很大。更高效的做法是配合xargsfind /data/logs -name *.log -mtime 7 | xargs rm -fxargs会尽量往命令行里塞更多参数减少命令启动次数批量操作时明显更快。但这种方式有隐患如果文件名里有空格或特殊字符管道传参可能会出错。我现在的习惯是find /data/logs -name *.log -mtime 7 -print0 | xargs -0 rm -f-print0用空字符作为文件名分隔符xargs -0也按空字符来解析这样空格、换行、引号都安全了。你这个习惯一旦养成就不会再想回去用普通管道了。如果你用find ... -exec组合也有一个相对稳妥的写法-exec rm -f {} 。把结尾分号换成加号意思是让 find 一次性把多个结果拼到一条命令后面执行效果接近 xargs而且不用处理管道切割问题。我经常在两个方案之间切换结果数量不大直接-exec rm {} \;图简单结果量大优先-exec rm {} 或配合-print0 | xargs -0另外一个实用的小技能是用find加条件批量改权限。比如把某个目录下所有 shell 脚本加上执行权限find /opt/scripts -name *.sh -exec chmod x {} \;注意这个命令不会递归到子目录里的软链接find 默认会一定程度上跟随路径但你想彻底排查软链接指向的真实文件时可以加-L这个我一般按需使用不默认加。1.3 按时间、权限、精确定位文件时的常见组合拳实际运维里组合条件是最常见的。比如我要清理某个备份目录只保留最近 3 天的备份其他都删掉find /backup -name *.tar.gz -mtime 2 -exec rm -f {} \;这里是“文件修改时间早于 2 天前就删”。搞懂-mtime N和-mtime -N的语义很重要-mtime -2表示 2 天以内改过的-mtime 2表示 2 天以前改过的-mtime 2表示正好在 2 天前改过的别看只差一个正负号含义完全相反。我在脚本里凡是涉及时间条件都会额外加一条注释防止隔几天自己看的时候想不起来。按权限定位也能组合得很巧妙。比如找出当前用户可写的但是属于其他人的文件find /data/shared -type f -perm /ugow-perm /ugow中/表示“任一权限位匹配即可”这在权限排查时非常好用。找权限过宽的文件可以这样find /etc -type f -perm -002意思是找出其他用户可写的文件通常这类文件会有安全隐患值得重点关注。我自己常用的一个组合是找空文件 批量删除find /tmp -type f -size 0 -exec rm -f {} \;这样可以把空文件一下子清掉。-size 0匹配大小为 0 的文件也可以写成-empty效果一样。还有一个容易忽略的参数是-maxdepth它控制搜索深度。默认的 find 是无限递归的如果只想看一层务必加上find /data -maxdepth 1 -name *.json不然一不小心就会扫出几百层子目录的文件。在大目录下少了-maxdepthfind 的性能会下降得非常明显。2. read不只是读输入更是数据解析器read 的基本功能是从标准输入读取一行并赋值给变量。很多人只会用它做交互提示比如read -p 请输入用户名: username echo 你输入的是 $username这当然没问题但它在脚本里更重要的角色是和循环一起处理文本文件、解析输出结果。用好 read很多数据处理问题都能在 Shell 里直接解决不用写 Python。2.1 从文件逐行读取的正确姿势一个经典场景有一个 user_list.txt每行一个用户名我要循环读取并对每个用户执行一些操作。新手最容易写错的方式是for u in $(cat user_list.txt); do echo $u done如果文件里的用户名单词没有任何特殊字符这样确实能跑。但一旦有空格、通配符结果就不对了。更稳妥的是用 while readwhile read u; do echo $u done user_list.txt这里重点理解重定向它把文件内容作为 while 循环的标准输入read 每次读取一行。这个写法天然按行处理不会因为空格而分割。读取之后如果想同时处理行号和内容read 支持一次赋多个变量i1 while read u; do echo $i. $u i$((i 1)) done user_list.txt在循环里修改变量时要注意 Shell 的变量默认都是字符串数值计算要用$(( ))包起来直接i是行不通的。如果文件有可能最后一行没有换行符那 while read 也能正常处理最后一行但 for 循环配合管道的方式就会漏掉最后一行这是我没少踩过的坑。2.2 多变量拆分与自定义分隔符read 强大的一点是能把一行内容按“空白”拆开赋给多个变量。比如有一行张三 28 engineerread name age job 张三 28 engineer echo $name $age $job是 here-string 语法可以把字符串当成标准输入喂给 read非常方便。这里建议给这里加个说明等价于echo xxx | read但用起来更轻量。默认 read 按空格、Tab、换行来作分隔符。如果你想按冒号或逗号拆就需要修改内部字段分隔符 IFSdataapple,banana,orange IFS, read -r a b c $data echo $a $b $c导入 CSV 文件时这个技巧特别有用。比如有个简单的 CSVwhile IFS, read -r name city phone; do echo - $name lives in $city, phone $phone done users.csv注意两点。第一IFS, read ...这种写法只在 read 这一次命令执行时改变 IFS不会影响后面命令第二read 后面加了-r参数它会禁止反斜杠转义处理含反斜杠的内容时更安全。我几乎每次都带-r除非我明确想处理转义逻辑否则省这个参数只会给自己挖坑。另外read 还能配合指定分隔符解析/etc/passwd这种文件。把 IFS 设为:然后按字段读while IFS: read -r user pass uid gid comment home shell; do echo $user - $home done /etc/passwd这只是一种解析方式实际工作中使用时要留意文件格式变化但思路确实值得掌握。2.3 控制交互输入超时、回显与默认值写交互脚本时无脑等待用户输入是很危险的在无人值守脚本里尤其危险。read 提供了-t参数来设置超时秒数超时后返回非零状态码if read -t 5 -p 5秒内输入确认 (y/n): answer; then echo 你输入了 $answer else echo 超时未输入按默认 n 处理 answern fi这个模式非常实用因为默认设置的逻辑可以写在 else 分支里不会让脚本卡死。隐藏输入的参数是-s。需要输入密码时不希望内容显示在屏幕上read -s -p 请输入密码: password echo echo 密码长度: ${#password}这里${#password}是取变量长度的写法也是一个常用小语法。-s只控制不回显不改变输入内容本身密码还是明文存在 shell 变量里所以尽量不要在敏感脚本里长期保留密码变量。还有-p提示语和变量默认值配合的写法。比如read -p 请输入目录名 [默认: logs]: dir dir${dir:-logs} echo 目标目录: $dir${dir:-logs}这类展开的意思是如果 dir 没有值或为空就用logs。这样用户直接回车也能走一个合理默认值。如果你不想敲回车就读取单个按键read 还支持-n 1read -n 1 -p 按任意键继续... key echo 按键: $key这个在交互菜单里很常用。我自己在写提示脚本时会用read -n1 -r避免用户在按回车时输入多余的转义字符。3. 数组定义与引用方法从基础语法到实战很多刚接触 Shell 的人会以为数组是高级特性实际上它在 Bash 里非常基础只是入门教材总把它排到后面。数组搞明白之后很多循环逻辑会清爽很多。Bash 的数组分为“下标数组”和“关联数组”平时说的“数组”默认是下标数组。3.1 下标数组声明与元素引用最简单的定义方式是一对括号元素之间用空格分隔fruits(apple banana orange)引用第一个元素的写法是${fruits[0]}注意花括号不能省。如果写成$fruits[0]Shell 只会把$fruits当作变量名然后拼接后面的[0]结果完全不对。这种细节最容易出 bug。把数组所有元素列出来用[]或[*]echo ${fruits[]}两者的区别主要体现在加双引号时${fruits[]}会保留每个元素作为独立单元${fruits[*]}会把所有元素用一个空格拼成一个字符串。处理带空格的文件名时${fruits[]}是正确选择。这一点我在写循环时经常用到for f in ${fruits[]}; do echo 元素: $f done数组长度用#echo 数组长度: ${#fruits[]}注意是${#fruits[]}不是${#fruits}。如果写成后者得到的是第一个元素的字符长度。这两个概念很容易混淆。还可以通过下标单独赋值或追加fruits[3]grape fruits(watermelon)第二种写法是 Bash 3.1 之后支持的追加语法用着很方便。读取某个下标范围可以用切片语法这个后面单独说。3.2 数组遍历、切片与统计长度遍历数组除了 for 循环还有一种常见场景是从命令输出构造数组。因为数组定义是靠空格分词的所以可以直接把 find 或 ls 的结果放进去files(*.txt)注意这里靠的是路径通配符展开不是字符串切割。另一个常用法log_list($(find /var/log -name *.log -mtime -1))这种方式有个隐患如果文件名里有空格或换行构造出来的数组元素会被拆散。所以要处理复杂文件名时我一般避免直接在$( )里接数组而是用 while read 手动构建log_list() while IFS read -r line; do log_list($line) done (find /var/log -name *.log -mtime -1)这里 (...)是进程替换语法它能把命令输出当作文件传给 while好处是 while 循环在子 shell 里执行时数组变量仍然能在外部生效。这一点很关键如果你用管道| while read循环里对数组的修改在循环结束后是保留不下来的因为管道会启动子 shell。我早期在这里栽过不少跟头。数组切片也是一个高频需求。比如取第 1 到第 3 个元素fruits(apple banana orange grape) echo ${fruits[]:1:3}这个表示从下标 1 开始取 3 个元素结果就是 banana、orange、grape。切片也可以应用于单个元素echo ${fruits[0]:0:3}这是从第一个元素里取前 3 个字符得到 app。在解析带前缀的字符串时非常有用。统计长度可以用来判断数组是否为空if [ ${#my_arr[]} -eq 0 ]; then echo 数组为空 fi注意这里写的是-eq 0语法上是整数比较。如果你在脚本里要频繁检查数组长度这个写法比[[ -z ]]要直观一些而且不依赖数组第一个元素是否存在。数组元素重新排序或去重Shell 原生的能力比较弱。我之前处理很多文件名时是配合 sort 和 uniq 来做的sorted($(printf %s\n ${fruits[]} | sort))这种把数组内容一行一个交给外部命令然后重新组装的思路在 Shell 里非常常见。输出是字符串流读回来就是数组。3.3 关联数组与二维数组模拟Bash 4 以上才支持关联数组也是用括号声明但要加-A参数declare -A user_map user_map[zhangsan]1001 user_map[lisi]1002关联数组的键是字符串不是数字。引用方式和普通数组一样echo ${user_map[zhangsan]}遍历方式也是用[]加!符号来取所有键for key in ${!user_map[]}; do echo $key - ${user_map[$key]} done!在${!arr[]}里的意思是“返回所有下标或键名”。这个语法新手很容易忘但在关联数组里几乎是必备。如果你需要二维数组可以用“拼接键”的方式模拟。比如存一个 3 行 4 列的表可以用row_col作为键名declare -A matrix matrix[0_0]A matrix[0_1]B matrix[1_0]C matrix[1_1]D使用时循环遍历行列for r in 0 1; do for c in 0 1; do echo matrix[$r][$c] ${matrix[$r_$c]} done done这种模拟方式在处理简单的表格数据时非常管用比纯文本解析要直观得多。但要注意关联数组在旧版本 Bash 里不支持写脚本前要确认系统 Bash 版本用bash --version查一下。4. seq序列生成命令写出更优雅的循环seq 这个命令看起来简单就是生成一串数字但它的特点和边界很多新手并不清楚。最基础的用法seq 1 5输出 1 到 5。事实上 seq 至少有三种形式seq LAST输出 1 到 LASTseq FIRST LAST输出 FIRST 到 LASTseq FIRST INCREMENT LAST输出 FIRST 到 LAST步长是 INCREMENT第三种才是真正的核心。比如生成 1 到 10 之间的奇数seq 1 2 10注意步长写的是 2。用这个语法很多循环都可以省掉复杂的变量加减。4.1 seq 三参数的排列与默认值理解这三个参数的默认值很重要如果只写一个参数比如seq 5FIRST 默认是 1INCREMENT 默认是 1。如果写两个参数比如seq 3 6FIRST 是 3LAST 是 6INCREMENT 默认是 1。如果写三个参数那第一个就是起始数第二个是步长第三个是结尾数。我见过不少人以为seq 1 10 2是从 1 到 10 然后步长是 2结果执行发现输出是 1、3、5、7、9正好是他想要的。但如果你要的是从 1 到 10 且步长为 3应该写seq 1 3 10它会输出 1、4、7、10。参数顺序不看仔细反而容易绕晕。递增可以递减也可以seq 10 -2 1输出 10、8、6、4、2。注意步长是负数时LAST 小于 FIRST 是正确的顺序别写反。seq 还可以和 for 循环无缝配合for i in $(seq 1 10); do echo $i done不过这里有个性能问题如果循环次数很大$(seq ...)会先把所有数字都装进内存然后再遍历。如果只是生成 1 到 100 这种小范围没问题如果是 1 到 100 万建议直接写 C 风格 for 循环for ((i 1; i 1000000; i)); do ... done这块要留意避免脚本里出现不必要的内存或时间开销。Bash 本身的{1..10}花括号展开也能造数字序列但它只支持整数而且无法用变量作为边界不带 eval 的话不行。seq 的优势在于支持变量、支持小数步长、支持格式化输出这是花括号展开做不到的。4.2 格式化输出补零和固定宽度seq 有一个很少被提及但特别实用的功能格式化输出参数是-w和-f。-w表示等宽输出自动在短数字前面补零对齐。比如seq -w 1 10输出结果是 01 到 10。这个在生成有固定位数要求的序号时非常有用。更灵活的是-f可以指定输出格式比如seq -f file_%02g.log 1 5输出file_01.log file_02.log file_03.log file_04.log file_05.log这里的%g是数字占位符%02g表示最少两位不够补零。如果你要生成 0001 到 9999 的编号直接seq -f %04g 1 20这个对我建测试文件、生成批量目录名非常有用。有时候你需要按编号生成几百个目录一条 seq 就能把名字都准备好。配合 xargs还能直接批量创建文件seq -f data_%02g.txt 1 10 | xargs -I{} touch {}-I{}是 xargs 的参数表示把输入的每一行整体替换成{}然后在touch后面执行。这个组合在准备测试数据时效率极高。如果你想输出带前缀的区间还可以这样seq -f node-%g 5 10不过要注意-f会覆盖-w两者同时使用时以-f为准。-f里只写一个%g时效果和不带格式差不多但如果你还加了-w两者会有冲突干脆只用-f对齐。4.3 seq 在循环、批量建目录与测试中的实战用法除了简单的数字输出seq 更强大的地方在于和其他命令配合。批量建目录for i in $(seq 1 12); do mkdir -p month_$i done又或者按月份生成日志文件夹配合格式化for i in $(seq -w 1 12); do mkdir -p 2025_$i done这样会生成2025_01到2025_12的目录。如果目录名要求没有前导零也可以用普通 seq 配合字符串处理。seq 也可以用于生成文件切片。我经常用它配合 sed 来提取大文件的某几行seq 100 200 | xargs -I{} sed -n {}p big_file.txt这个组合不是最高效的但适合一次性查看连续区间。指定行内容时xargs 会每次执行一个 sed行数多了会偏慢。如果区间特别大还是建议直接用 sed 的范围表达式。生成随机测试样本这种场景seq 也有用。虽然 seq 本身只生成顺序数字但可以用-f造出一批中间有很多编号的文件名再配合head或抽样工具做随机选取。比如从 1 到 1000 中随机取 20 个数seq 1 1000 | shuf -n 20shuf 是随机打乱工具这更适合作为辅助方案。真正核心的“造数据”还是 seq 在管。seq 还能生成时间点序列用于批量轮询任务for t in $(seq 0 5 60); do echo 第 ${t} 秒... done这在模拟定时任务或生成配置文件时经常用到。另外还有一个冷门但会碰到的点seq 支持小数。比如生成 0 到 1 之间的 5 等分seq 0 0.2 1输出是 0、0.2、0.4、0.6、0.8、1。但如果写成seq -w 0 0.2 1你可能会得到浮点数补零的怪结果具体要看系统版本。所以涉及小数时我一般不建议加-w直接用-f %.1f控制格式更好。5. 组合实战用这四个命令写一个日志清理脚本前几部分都是单独拆开讲现在把它们串起来做一个真实的场景清理指定目录下的旧日志同时压缩保留 7 天内的日志并生成一份清理报告。这个脚本覆盖了 find、数组、read、seq 的配合使用。5.1 脚本需求与设计思路需求背景是某应用每天产生大量日志磁盘空间容易告警。我们需要找出/data/app/logs目录下所有.log文件根据最后修改时间判断处理方式3 天以内保留3 到 7 天打包成 tar.gz 压缩归档7 天以上删除输出一份统计报告说明处理了多少文件、释放了多少空间设计思路是先用 find 把文件按 mtime 分成两类一类是待压缩列表一类是待删除列表。把结果存到数组里再利用数组长度做统计。最后用 seq 加上日期拼接生成归档序号。5.2 代码实现与注释脚本的核心代码如下#!/bin/bash log_dir/data/app/logs archive_dir/data/app/logs/archive tmp_date$(date %Y%m%d) compressed_list($(find $log_dir -maxdepth 1 -type f -name *.log -mtime 2 -mtime -7)) deleted_list($(find $log_dir -maxdepth 1 -type f -name *.log -mtime 7)) report_file/tmp/cleanup_report_${tmp_date}.txt [ ! -d $archive_dir ] mkdir -p $archive_dir echo 日志清理报告 $(date) $report_file # 压缩 3 到 7 天的日志 for f in ${compressed_list[]}; do base$(basename $f) tar -czf $archive_dir/${base}.tar.gz -C $log_dir $base rm -f $f echo 压缩归档: $base $report_file done # 删除 7 天前的日志 for f in ${deleted_list[]}; do echo 删除旧日志: $f $report_file rm -f $f done echo 压缩文件数: ${#compressed_list[]} $report_file echo 删除文件数: ${#deleted_list[]} $report_file du -sh $archive_dir $report_file echo 清理完成。报告: $report_file这段代码里有几个细节值得多说-maxdepth 1是故意加的避免递归到 app 的子目录里翻出其他日志文件。-mtime 2 -mtime -7表示修改时间在 2 天前和 7 天前之间也就是“3 到 7 天”。压缩时用-C $log_dir $base这个参数组合的意思是先切换到 log_dir 目录再对 base 文件名打包这样 tar 包内部不会带路径层级解包时更干净。数组的长度用#取值直接写进报告。5.3 运行效果与改进空间把脚本放到 crontab 里每天晚上执行一次白天看到的报告内容大致如下 日志清理报告 2025-04-16 压缩归档: app_20250413.log 压缩归档: app_20250414.log 删除旧日志: app_20250408.log 删除旧日志: app_20250409.log 压缩文件数: 2 删除文件数: 2 4.0K /data/app/logs/archive如果日志特别多字符串拼接数组的方式会占用不少内存更好的方案是改用 while read 逐行处理。示例compressed_list() while IFS read -r f; do compressed_list($f) done (find $log_dir -maxdepth 1 -type f -name *.log -mtime 2 -mtime -7)这里的进程替换( )是重点它让 find 的输出继续作为文件供给 while但 while 所在的环境不是子 shell数组在循环结束后依然可用。如果你写成管道| while read循环结束后数组内容就丢了这是新手最常见的坑。改进空间也很明确可以加一个磁盘使用率判断当使用率超过阈值才触发清理也可以把报告按日期保留 N 份避免报告目录越来越大还可以用 seq 生成日期序列循环处理多天前的日志。思路打开之后这个脚本可以逐渐演变成一个小型运维工具。6. 常见问题、避坑要点与排查思路把所有命令串起来跑的时候总会遇到一些固定套路之外的报错。我把实际踩过的一些坑和一些排查思路整理成了速查表必要时可以直接过来对照。6.1 find: ‘paths must precede expression’ 报错这个报错几乎每个人都会碰到原因就是路径写在了表达式后面。比如find -name *.log /var/log正确的是find /var/log -name *.log还有一种情况是表达式里用了!或括号这些符号会被 shell 解释导致 find 收到意外参数。比如find . -type f ! -name *.log这个写法是可以的但如果你不加引号直接写! -name *.log某些 shell 版本会把!解释为历史扩展建议给特殊字符加反斜杠或引号find . -type f ! -name *.log实际上大部分发行版默认 bash 不会开历史扩展但在交互式终端里仍建议用引号包一下find . -type f \! -name *.log少写一个转义就可能看到奇怪的历史展开报错这一点我印象挺深。6.2 文件名带空格或特殊字符这是 find 和 xargs 组合时最典型的坑。假设有一个文件叫my note.log如果你用find /data -name *.log | xargs rmrm 会收到两个参数一个是my一个是note.log结果就是误删。解决办法有两个方向。第一个是用-print0和xargs -0find /data -name *.log -print0 | xargs -0 rm -f第二个方法是放弃管道完全用 find 的-exec或-exec ... find /data -name *.log -exec rm -f {} 这两种方式都能正确处理带空格的路径。我的习惯是如果只有单条命令要执行直接用-exec ... 如果有复杂逻辑就用-print0 | xargs -0把结果通过管道喂给后续处理。6.3 read 读文件时遇到换行符失效如果你在 Windows 和 Linux 之间来回传文件文件里的换行可能是\r\n这时 read 会把\r也读进变量里导致输出和判断都不对。具体表现为变量末尾多了一个回车符echo 看起来没问题用 if 比较就怎么都不相等。处理办法在 read 之后把行尾的\r去掉用参数扩展while IFS read -r line; do line${line%$\r} echo 处理: [$line] done dos_file.txt这里${line%$\r}是 Bash 的字符串截断语法会删除结尾的\r。如果文件是三列结构也可以直接设置 IFS 并配合tr先转换换行符tr -d \r dos_file.txt | while IFS, read -r a b c; do echo $a $b $c done这个思路在处理 Windows 来源的 CSV 时非常实用建议直接固化下来。6.4 数组下标越界和未初始化问题Bash 数组不要求声明直接用即可。但如果你访问一个不存在的下标不会报错只会得到空值。比如arr(a b c) echo ${arr[10]}结果是空行。这在脚本里容易掩盖 bug因为看起来命令执行成功了实际上逻辑可能根本没走到。解决办法是访问时先判断长度if [ -z ${arr[10]:-} ]; then echo 下标 10 不存在 fi${arr[10]:-}的意思是“如果 arr[10] 未定义则取空字符串”这样-z判断就不会受set -u影响。如果你在脚本开头开了set -u直接访问未定义下标会直接报错并退出虽然安全但有时显得过于严格。我的建议是重要脚本里直接set -u并在数组访问处多写默认值。未初始化数组参与拼接时也会产生问题。比如声明一个空数组arr() arr(a) echo ${#arr[]}这个没问题。但如果你写成arr($new_item)不小心覆盖了原有内容。所以往数组里追加时推荐统一用语法。6.5 seq 小数步长产生浮点误差seq 在生成小数序列时底层是浮点数运算输出可能会遇到 0.30000000000000004 这类长小数。比如seq 0 0.1 1在某些系统上可能出现精度尾巴。解决办法是用-f格式化seq -f %.2f 0 0.1 1强制保留两位小数显示上就干净很多。不过要注意格式化之后数字变成字符串不能再直接参与算术运算。如果需要高精度建议切换到 Python 或 awk。awk 的循环生成数字反而更稳awk BEGIN { for (i0; i1; i0.1) print i }awk 对小数的处理同样是浮点但输出可控。seq 还有一个容易被忽略的点它最后输出的数不一定等于 LAST。比如seq 1 2 10输出是 1、3、5、7、9不会输出 10因为 10 不在 1 2n 这个数列里。很多人在写循环时误以为一定会包含 LAST结果少算了一轮。设计循环时可以先心算一下序列或者直接多给一个略大的结尾数比如把 LAST 写成 11。最后分享一个我个人很受用的小技巧也是把本文几个命令真正融会贯通的关键一步。调试这类脚本时永远记得在文件里加bash -x script.sh跟踪执行或者在脚本开头临时加一行set -x能帮你快速看清变量到底被拆成了什么样子。很多时候报错的原因不在于语法而在于数据本身比如文件名里的空格、行尾的\r、数组里混进的空字符串。把数据先洗一遍再套命令成功率会高很多。
阅读完成 · 觉得有帮助?
咨询建站