10个提升Bash脚本性能的必备技巧

通过减少进程分支、使用内建命令、批量文件操作和选择合适的文本工具来加速Bash脚本。

10个提升Bash脚本性能的必备技巧

Bash脚本的性能通常归结为一件事:你让shell一次处理一个进程的工作量。一个对十个文件感觉良好的脚本,当它循环处理五万个文件并每次调用sedgrepchmod时,可能会变得异常缓慢。

当你的脚本处理大量文件、解析大型文本输出或运行频繁以至于小延迟累积时,请使用这些技巧。


1. 最小化外部命令调用

每次Bash执行外部命令(例如grepawksed)时,它都会分支出一个新进程,这会产生大量开销。加速脚本最有效的方法是尽可能使用Bash内建命令。

优先使用内建命令而非外部工具

示例: 在条件检查中,使用内建的[[而非外部的test[

慢(外部) 快(内建)
if test -f "$FILE"; then if [[ -f "$FILE" ]]; then

提示: 对于算术运算,始终使用(( ... ))而不是exprlet,因为算术扩展由shell内部处理。

# 慢
COUNT=$(expr $COUNT + 1)

# 快(内建算术扩展)
(( COUNT++ ))

2. 使用高效的循环结构

传统的for循环遍历命令输出可能因进程生成或单词拆分问题而变慢。正确使用原生大括号扩展或while read循环。

避免使用for i in $(cat file)

使用$(cat file)会先将整个文件读入内存,然后进行单词拆分,这效率低下且如果文件名包含空格则容易出错。使用while read循环逐行处理:

# 逐行处理文件的首选方法
while IFS= read -r line;
do
    echo "处理: $line"
done < "data.txt"

关于IFS= read -r的说明: 设置IFS=可防止修剪前导/尾随空格,-r可防止反斜杠解释,确保数据完整性。

3. 使用参数扩展内部处理数据

Bash提供了强大的参数扩展功能(如子字符串删除、替换和大小写转换),这些功能在内部对字符串进行操作,避免了简单任务中使用sedawk等外部工具。

示例:删除前缀

如果需要从变量filename中删除前缀log_

filename="log_report_2023.txt"

# 慢(外部sed)
# new_name=$(echo "$filename" | sed 's/^log_//')

# 快(内建扩展)
new_name=${filename#log_}
echo "$new_name" # 输出: report_2023.txt

4. 缓存昂贵的命令输出

如果在脚本中多次执行相同的昂贵命令(例如调用API、复杂的文件查找),将结果缓存到变量或临时文件中,而不是重复运行。

# 只在开始时运行一次
GLOBAL_CONFIG=$(get_system_config_from_db)

# 后续使用直接读取变量
if [[ "$GLOBAL_CONFIG" == *"DEBUG_MODE"* ]]; then
    echo "调试模式已激活。"
fi

5. 使用数组变量处理列表

处理项目列表时,使用Bash数组而不是空格分隔的字符串。数组能正确处理包含空格的项目,并且在迭代和操作时通常更高效。

# 慢/易错的字符串列表
# FILES="file A fileB.txt"

# 快且健壮的数组
FILES_ARRAY=( "file A" "fileB.txt" "another file" )

# 高效迭代
for f in "${FILES_ARRAY[@]}"; do
    process_file "$f"
done

6. 避免过度引用和取消引用

虽然正确的引用对于正确性至关重要(尤其是在处理包含空格的文件名时),但过度引用和取消引用有时会增加少量开销。更重要的是,理解何时引用是强制性的,何时是可选的。

对于算术扩展((...)),表达式本身通常不需要引号,这与命令替换$()不同。

7. 尽可能使用进程替换进行管道操作

进程替换(<(cmd))有时可以创建比命名管道(mkfifo)更清晰、更快的管道,特别是当你需要将一个命令的输出同时输入到另一个命令的两个不同部分时。

# 高效比较两个排序文件的内容
if cmp <(sort file1.txt) <(sort file2.txt); then
    echo "排序后文件相同。"
fi

8. 使用printf代替echo

虽然通常可以忽略不计,但echo的行为在不同shell和系统之间可能有所不同,有时需要更复杂的处理来处理反斜杠解释。printf提供一致的格式化和卓越的控制,使其通常更可靠,并且在高容量输出操作中有时略快。

# 一致的输出
printf "用户 %s 在 %s 登录\n" "$USER" "$(date +%T)"

9. 优先使用find ... -exec ... {} +而不是-exec ... {} ;

当使用find命令对发现的文件执行另一个程序时,以分号(;)与加号(+)结束对性能的影响巨大。

  • {} ; 对每个文件执行一次命令。(高开销)
  • {} + 尽可能多地捆绑参数并执行一次命令(类似于xargs)。(低开销)
# 慢:执行'chmod 644'数千次
find . -name '*.txt' -exec chmod 644 {} \;

# 快:执行'chmod 644'一次或几次,带有多个参数
find . -name '*.txt' -exec chmod 644 {} +

10. 使用awkperl进行繁重文本处理

虽然目标是尽量减少外部调用,但当需要复杂、繁重的文本操作时,像awkperl这样的专业工具比链接多个grepsedcut命令要快得多。这些工具在单次遍历中处理数据。

如果你发现自己编写了cat file | grep X | sed Y | awk Z,请将其合并为一个优化的awk脚本。


实用规则

快速的Bash脚本在Bash循环内做更少的工作。使用shell内建命令进行简单测试和字符串编辑,使用find -exec ... {} +xargs批量文件操作,当文本处理成为主要任务时,切换到awkperl或其他真正的解析器。

在优化之前,使用time或shell跟踪测量代表性运行。然后修复生成最多命令的循环。