news 2026/8/1 9:31:50

Shell脚本自动化合并文件:从cat命令到健壮脚本的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Shell脚本自动化合并文件:从cat命令到健壮脚本的完整实现

1. 项目缘起:一个被重复操作折磨出来的脚本

你有没有经历过这样的场景?手头有一个文件夹,里面散落着几十甚至上百个文本文件,可能是日志、代码片段、配置文件,或者是从某个系统导出的零散数据。老板或者同事突然跟你说:“把这些文件里的内容,按顺序合并成一个,发给我。” 你打开文件夹,看着密密麻麻的文件列表,深吸一口气,然后开始机械地操作:打开第一个文件,全选、复制,新建一个文档,粘贴;再打开第二个,复制,切回文档,粘贴…… 重复几十次后,不仅手腕发酸,还生怕漏掉或者重复了某个文件。

几年前,我就被这样一个看似简单却极其繁琐的任务折磨过。当时需要合并一批服务器日志进行分析,文件夹里有超过200个按日期命名的.log文件。手动操作了十几个之后,我就意识到这绝不是一个可持续的方法,不仅效率低下,而且极易出错。作为一个经常和命令行打交道的开发者,我的第一反应就是:“这事儿应该让 Shell 脚本来干。”

于是,一个自用的、用于合并文件夹内所有文件内容的 Shell 脚本就诞生了。它没有花哨的功能,核心目标就一个:给定一个文件夹路径,自动将其下的所有文件内容,按文件名顺序(或自定义顺序)拼接成一个大的输出文件。这个脚本后来成了我的“瑞士军刀”之一,无论是整理代码库、合并调研资料,还是预处理数据,都能派上用场。今天,我就把这个自用脚本的完整实现思路、代码细节以及踩过的坑,毫无保留地分享出来。你会发现,用 Shell 脚本自动化这类重复性文件操作,不仅省时省力,更是将你的工作流程推向专业化的关键一步。

2. 脚本核心设计:不只是cat命令的简单封装

很多人听到“合并文件”,第一反应就是用cat命令。没错,cat file1 file2 > output确实能合并文件。但当我们面对一个充满未知文件的文件夹时,问题就变得复杂了。一个健壮的合并脚本,需要考虑的远不止一个命令。我们需要设计一个清晰的流程来处理各种边界情况和用户需求。

2.1 需求拆解与功能规划

首先,我们明确这个自用脚本需要满足哪些核心和进阶需求:

  1. 基本功能:遍历指定目录,读取所有普通文件的内容,并按顺序写入一个新文件。
  2. 顺序控制:默认按文件名的字母顺序(ls默认排序)合并是否合理?是否需要支持按文件修改时间、创建时间或者自定义列表排序?
  3. 文件过滤:是否所有文件都需要合并?通常我们只需要合并文本文件(如.txt,.log,.csv,.json,.py等),而应该忽略二进制文件(如图片.jpg、可执行文件)或隐藏文件(以.开头的文件)。
  4. 内容格式化:直接拼接可能会导致文件内容“粘”在一起,缺乏分隔。是否需要在每个文件内容之间插入分隔符(如换行符、一行注释、文件名标记)?
  5. 递归处理:如果文件夹下还有子文件夹,是否需要递归地合并所有子文件夹中的文件?
  6. 输出控制:输出文件名如何定义?是否允许用户指定?如果输出文件已存在,是覆盖、跳过还是备份?
  7. 错误处理:处理过程中如果某个文件无法读取(权限不足、已被删除),脚本应该报错退出,还是跳过该文件继续执行?
  8. 交互与日志:脚本运行时是否需要显示进度信息?是否记录合并了哪些文件,便于事后核对?

对于自用脚本,我的原则是“够用就好,但基础要牢固”。因此,我决定实现一个具备良好默认行为,同时通过命令行参数提供关键定制能力的脚本。核心功能聚焦于:递归合并、文本文件过滤、添加分隔符、完整的错误处理与日志输出。

2.2 技术选型:为什么是 Bash Shell?

在 Windows 上有 PowerShell,Python 也能轻松处理文件,为什么选择 Bash Shell 脚本?

  • 无处不在:在 Linux、macOS 以及现代的 Windows(通过 WSL、Git Bash、Cygwin)上,Bash 或兼容的 Shell 是标配。脚本的移植性非常好。
  • 原生文件操作优势:Shell 本就是为操作文件和进程而生的。像遍历目录 (find,for)、读取文件 (cat,while read)、路径处理 (dirname,basename) 等操作,在 Shell 中写起来非常简洁和高效,通常是调用系统原生命令,速度极快。
  • 管道与重定向:Shell 的管道 (|) 和重定向 (>,>>) 机制,让数据流的处理变得直观且强大,非常适合这类“读取-处理-输出”的线性任务。
  • 启动成本低:无需安装额外的解释器或依赖库(Python 虽常见,但并非所有环境都预装了所需模块)。一个脚本文件,加上执行权限就能跑。

当然,Shell 脚本不适合处理极其复杂的逻辑或数据结构。但对于我们这个文件合并任务,它的简洁和高效是无可替代的。我选择在脚本开头使用#!/bin/bash,明确指定 Bash,以利用其比标准sh更丰富的功能,如数组和更强大的条件判断。

3. 手把手实现:从零构建合并脚本merge_files.sh

下面,我将分步拆解脚本的每一部分,并解释其背后的考量和原理。你可以跟着一步步操作,最终得到一个可直接使用的脚本。

3.1 脚本骨架与参数解析

首先,创建脚本文件,比如叫merge_files.sh,并赋予执行权限:

touch merge_files.sh chmod +x merge_files.sh

脚本的第一行是 Shebang,告诉系统用哪个解释器来执行:

#!/bin/bash

接下来,我们需要处理用户可能输入的参数。一个友好的脚本应该支持-h显示帮助,并允许用户指定输入目录和输出文件。这里使用 Bash 内置的getopts来解析命令行参数,它比手动解析$1,$2更健壮。

# 默认值设置 INPUT_DIR="." # 默认当前目录 OUTPUT_FILE="merged_output.txt" # 默认输出文件名 RECURSIVE=false # 默认不递归 SEPARATOR="\n---\n" # 默认分隔符,两个换行加横线加换行 SHOW_HELP=false # 使用 getopts 解析命令行参数 while getopts ":d:o:rs:h" opt; do case ${opt} in d ) INPUT_DIR="$OPTARG" ;; o ) OUTPUT_FILE="$OPTARG" ;; r ) RECURSIVE=true ;; s ) SEPARATOR="$OPTARG" ;; h ) SHOW_HELP=true ;; \? ) echo "无效选项: -$OPTARG" 1>&2 exit 1 ;; : ) echo "选项 -$OPTARG 需要一个参数." 1>&2 exit 1 ;; esac done shift $((OPTIND -1)) # 显示帮助信息 if [ "$SHOW_HELP" = true ]; then cat << EOF 用法: $(basename "$0") [选项] 选项: -d <目录> 指定要合并的源文件目录 (默认为当前目录) -o <文件> 指定合并后的输出文件名 (默认为 merged_output.txt) -r 递归处理子目录中的文件 -s <分隔符> 指定文件内容之间的分隔符 (默认为 '\\n---\\n') -h 显示此帮助信息 示例: $0 -d ./logs -o all_logs.txt 合并 ./logs 目录下所有文件 $0 -r -d . -o total.txt 递归合并当前目录及子目录下所有文件 $0 -s \"\\n==========\\n\" -d src 使用自定义分隔符合并 EOF exit 0 fi

关键点解析:

  • getopts ":d:o:rs:h":冒号:表示该选项需要一个参数。所以-d-o后面必须跟参数,-r,-h则不需要。
  • shift $((OPTIND -1))OPTINDgetopts内部索引,处理完所有选项后,这条命令会移除以处理过的参数,这样$@里剩下的就是非选项参数(本例中未使用)。
  • $(basename "$0")$0是脚本名,basename命令去掉路径,只保留文件名,使帮助信息更清晰。
  • 分隔符的注意点:默认分隔符\n---\n在赋值给变量时,反斜杠会被转义。在后续使用echo -e时,-e选项会解释这些转义字符,从而输出真正的换行。用户通过-s传入的分隔符字符串也会被同样处理。

3.2 输入验证与准备工作

参数解析完后,不能直接开始合并,必须先检查输入的合法性,避免脚本在错误的状态下运行。

# 1. 检查输入目录是否存在且可读 if [ ! -d "$INPUT_DIR" ]; then echo "错误:目录 '$INPUT_DIR' 不存在。" 1>&2 exit 1 fi if [ ! -r "$INPUT_DIR" ]; then echo "错误:目录 '$INPUT_DIR' 不可读。" 1>&2 exit 1 fi # 2. 检查输出文件路径是否可写(尝试创建或追加,如果文件不存在则创建) OUTPUT_DIR=$(dirname "$OUTPUT_FILE") if [ "$OUTPUT_DIR" != "." ] && [ ! -d "$OUTPUT_DIR" ]; then echo "错误:输出文件所在目录 '$OUTPUT_DIR' 不存在。" 1>&2 exit 1 fi # 尝试触摸一下输出文件,测试是否可写。如果文件不存在则创建空文件。 if ! touch "$OUTPUT_FILE" 2>/dev/null; then echo "错误:无法创建或写入输出文件 '$OUTPUT_FILE'。请检查权限。" 1>&2 exit 1 fi # 3. 清空或初始化输出文件 > "$OUTPUT_FILE" # 使用重定向清空文件,比 `echo -n "" > file` 更高效。 echo "开始合并文件..." echo "输入目录: $(cd "$INPUT_DIR" && pwd)" echo "输出文件: $(cd "$(dirname "$OUTPUT_FILE")" && pwd)/$(basename "$OUTPUT_FILE")" echo "递归模式: $RECURSIVE" echo "分隔符: $(echo -e "$SEPARATOR" | sed 's/$/\\n/g' | tr -d '\n' | head -c 50)..." echo "----------------------------------------"

关键点解析与踩坑记录:

  • [ ! -r "$INPUT_DIR" ]:检查目录可读性非常重要。你可能对目录有执行 (x) 权限可以进入,但没有读 (r) 权限来列出文件,这会导致findls命令失败。
  • $(dirname "$OUTPUT_FILE"):处理输出文件路径时,必须考虑用户可能输入了包含子目录的路径(如./results/merged.txt)。dirname提取目录部分,basename提取文件名部分。先检查目录是否存在,比直接写文件时再报错更友好。
  • touch "$OUTPUT_FILE" 2>/dev/null:这是一个巧妙的可写性测试。touch命令如果文件不存在则创建,存在则更新其时间戳。如果这个操作失败(权限不足、路径只读等),2>/dev/null将错误信息丢弃,然后通过if ! ...判断为失败,脚本退出。这比先判断文件是否存在再判断是否可写更简洁。
  • > "$OUTPUT_FILE":这是清空文件的标准且最高效的 Shell 方法。一个简单的重定向操作即可。
  • 打印分隔符预览echo -e解释转义字符,sed将换行符替换为\n字符串以便显示,tr -d '\n'去掉真实换行防止打印多行,head -c 50只取前50个字符防止过长。这个小技巧让用户能直观看到分隔符的样子。

3.3 核心文件查找与遍历逻辑

这是脚本的核心。我们需要根据是否递归,找到所有需要合并的文件。find命令是完成这个任务的不二之选。

# 初始化文件列表和计数器 file_list=() processed_count=0 skipped_count=0 # 构建 find 命令的基础部分 if [ "$RECURSIVE" = true ]; then find_cmd="find \"$INPUT_DIR\" -type f" else find_cmd="find \"$INPUT_DIR\" -maxdepth 1 -type f" fi # 使用 while read 循环安全地处理 find 的输出,避免文件名中的空格或换行符导致问题。 # IFS= 和 -r 参数是关键。 while IFS= read -r -d $'\0' file; do # 排除输出文件自身,防止无限循环或内容污染 if [[ "$(realpath "$file")" == "$(realpath "$OUTPUT_FILE")" ]]; then echo "跳过输出文件自身: $file" ((skipped_count++)) continue fi # 简单的文件类型检查:尝试读取文件前几个字节,判断是否为文本文件。 # 使用 `file -b --mime-type` 更准确,但依赖 `file` 命令。这里提供一个不依赖 `file` 的简单方法。 # 更健壮的做法是使用 `file -b --mime-type "$file" | grep -q '^text/'`,但考虑到兼容性,我们先采用简单方法。 # 高级检查:如果系统有 `file` 命令,则使用它。 if command -v file &> /dev/null; then if file -b --mime-type "$file" | grep -q '^text/'; then is_text=true else is_text=false fi else # 备用方案:检查文件是否包含空字符(NULL),二进制文件通常包含。 if grep -qI . "$file" 2>/dev/null; then # grep -I 跳过二进制文件,-q 安静模式,. 匹配任何非空行。如果命令成功,说明可能是文本。 is_text=true else is_text=false fi fi if [ "$is_text" = false ]; then echo "跳过非文本文件(可能是二进制): $file" ((skipped_count++)) continue fi # 将文件添加到列表 file_list+=("$file") done < <(eval "$find_cmd" -print0) # 检查是否找到了文件 if [ ${#file_list[@]} -eq 0 ]; then echo "在目录 '$INPUT_DIR' 中未找到可合并的文本文件。" exit 0 fi echo "找到 ${#file_list[@]} 个待合并的文本文件。"

关键点解析与深度避坑:

  • find -print0while IFS= read -r -d $'\0':这是处理任意文件名(包含空格、换行符等特殊字符)的黄金标准-print0使用空字符(NULL)作为输出分隔符,因为文件名中不可能包含空字符。read -d $'\0'同样使用空字符作为读取分隔符。IFS=防止read对行进行单词分割,-r防止反斜杠转义。不使用for file in $(find ...)是因为这种写法会对文件名进行分词和路径名扩展,遇到空格或通配符就会出错。
  • 排除输出文件自身:使用realpath获取文件的绝对路径并进行比较,这比比较字符串更可靠,能处理./output/full/path/output是同一个文件的情况。这是一个非常关键的检查,否则脚本可能会读取自己正在写入的文件,导致内容混乱或无限增长。
  • 文本文件检测:这是脚本的难点和易错点。严格来说,Shell 无法 100% 准确判断一个文件是否为文本文件。我们采用两种策略:
    1. 首选file命令file -b --mime-type会输出文件的 MIME 类型,如text/plain,application/pdfgrep -q '^text/'检查是否以text/开头。这是相对最可靠的方法。
    2. 备用grep方案:如果系统没有file命令(极少数精简环境),则使用grep -I .-I选项告诉grep直接跳过二进制文件(它自己有一套简单的启发式判断)。.匹配任何单个字符,-q安静模式。这个检查并不完美,但能过滤掉大多数典型的二进制文件。2>/dev/null是为了忽略grep对某些“二进制文件匹配”的警告信息。
  • 使用数组存储文件列表:将找到的合格文件存入 Bash 数组file_list,而不是立即处理。这样做的好处是,我们可以在合并前知道文件总数,便于显示进度,也方便后续实现更复杂的排序逻辑(虽然当前是按find默认顺序,通常是按文件系统条目顺序,并非严格字母顺序)。

3.4 文件合并与进度展示

现在,我们有了一个干净的文件列表,可以开始合并了。我们需要按顺序读取每个文件的内容,追加到输出文件中,并在每个文件之间插入分隔符。

total_files=${#file_list[@]} current_index=0 for file in "${file_list[@]}"; do ((current_index++)) # 显示进度信息 echo -n "[$current_index/$total_files] 正在处理: $(basename "$file") ... " # 尝试读取文件内容并追加到输出 if cat "$file" >> "$OUTPUT_FILE" 2>/dev/null; then # 成功读取后,如果不是最后一个文件,则添加分隔符 if [ $current_index -lt $total_files ]; then echo -e "$SEPARATOR" >> "$OUTPUT_FILE" fi echo "完成。" ((processed_count++)) else echo "失败!文件可能无法读取或已被删除。已跳过。" ((skipped_count++)) # 注意:这里我们选择跳过失败的文件,继续处理下一个。 # 如果希望严格一点,可以在这里选择退出 `exit 1`。 fi done

关键点解析与性能考量:

  • 进度显示echo -n不换行输出,在同一行更新进度,体验更好。[3/50]这样的格式让用户清楚知道进度。
  • cat命令的重定向cat "$file" >> "$OUTPUT_FILE"是核心操作。>>表示追加。这里没有使用cat file1 file2 > output的语法,是因为我们需要在文件间插入分隔符,并且要处理可能失败的单个文件。
  • 错误处理cat命令可能因为文件权限不足、文件在遍历后被删除等原因失败。2>/dev/null将错误信息(如“权限被拒绝”)丢弃,然后通过if判断命令的退出状态码($?)。如果失败,我们记录并跳过,而不是让整个脚本崩溃。这对于处理大量文件时偶尔出现的异常情况非常有用。
  • 分隔符的添加时机:只在非最后一个文件后添加分隔符,避免了输出文件末尾多出一个不必要的分隔符。逻辑清晰。
  • 性能思考:对于超大文件(几百MB或GB),使用cat是高效的,因为它是系统调用。如果需要在合并过程中进行复杂的行级处理(如过滤、修改),则可能需要改用while IFS= read -r line循环,但那样会慢很多。本脚本定位是“合并”,所以cat是最佳选择。

3.5 收尾工作与最终脚本

合并完成后,我们需要给出一个清晰的总结,告诉用户发生了什么。

echo "----------------------------------------" echo "合并完成!" echo "成功处理文件数: $processed_count" if [ $skipped_count -gt 0 ]; then echo "跳过文件数: $skipped_count (包含非文本文件、输出文件自身或读取失败的文件)" fi echo "输出文件大小: $(du -h "$OUTPUT_FILE" | cut -f1)" echo "输出文件行数: $(wc -l < "$OUTPUT_FILE")"

将以上所有代码块按顺序组合起来,就是一个功能完整、健壮的自用文件合并脚本merge_files.sh

4. 进阶技巧与场景化改造

基础的脚本已经很好用了,但在实际使用中,你可能会遇到更多定制化需求。下面分享几个我根据不同场景改造脚本的进阶技巧。

4.1 实现按修改时间排序合并

默认的find顺序并不总是符合预期。有时我们需要按文件修改时间从旧到新(或从新到旧)合并,比如合并日志文件时。我们可以利用find-printf功能和sort命令来实现。

修改文件查找和排序部分:

# ... 参数解析和验证之后 ... file_list=() # 使用 find 打印出文件的修改时间(秒时间戳)和路径,用特殊字符分隔 while IFS= read -r -d $'\n' line; do # 假设我们使用 @ 作为分隔符,find -printf 的输出格式为“时间戳@路径” # 但更安全的方法是使用两个 find 调用,或者使用 stat 命令。 # 这里展示一个使用 stat 命令(GNU coreutils)的版本,它更通用。 : done < <(find "$INPUT_DIR" -maxdepth 1 -type f -exec stat -c '%Y %n' {} \; | sort -n | cut -d' ' -f2-) # 上面的管道解释: # 1. `find ... -exec stat -c '%Y %n' {} \;` : 对每个文件执行 `stat`,输出修改时间戳(自Epoch的秒数)和文件名,空格分隔。 # 2. `sort -n` : 按数字(时间戳)升序排序(最旧的文件在前)。 # 3. `cut -d' ' -f2-` : 以空格为分隔符,取出第二列及之后的部分(即文件名)。注意,如果文件名包含空格,这种方法会出错! # 警告:此方法对含空格的文件名不友好!更健壮但复杂的方法是使用 \0 分隔符和 awk。 # 因此,对于生产环境,推荐以下更安全但稍复杂的方法(需要 GNU find 和 sort): # 将 find 结果存入临时数组,然后用循环配合 stat 获取时间戳,再排序。 temp_array=() while IFS= read -r -d $'\0' file; do # 同样的排除和文本检查逻辑... if [[ "$(realpath "$file")" == "$(realpath "$OUTPUT_FILE")" ]]; then continue fi # ... 文本检查 ... timestamp=$(stat -c '%Y' "$file" 2>/dev/null) # 获取时间戳 # 使用 printf 格式化,确保时间戳和文件名能正确解析 temp_array+=("$(printf "%d\t%s" "${timestamp:-0}" "$file")") done < <(find "$INPUT_DIR" -maxdepth 1 -type f -print0) # 按时间戳排序(第一列) IFS=$'\n' sorted_array=($(sort -n <<<"${temp_array[*]}")) unset IFS # 提取排序后的文件名(第二列,制表符分隔) for item in "${sorted_array[@]}"; do file_list+=("$(cut -f2- <<<"$item")") done

注意:按时间排序会显著增加脚本复杂度,并且stat命令的选项在不同系统(如 macOS 的 BSDstat和 GNUstat)上可能不同。除非必要,否则默认的文件系统顺序通常是可以接受的。如果需要此功能,最好将其作为一个可选的命令行参数(如-t)来实现。

4.2 添加文件名作为内容标题

有时,合并后的文件很难区分某段内容来自哪个源文件。一个实用的功能是在每个文件内容前,插入其文件名作为标题。

在合并循环中修改:

for file in "${file_list[@]}"; do ((current_index++)) echo -n "[$current_index/$total_files] 正在处理: $(basename "$file") ... " # 写入文件名标题 echo -e "\n\n【文件: $(basename "$file") - 路径: $file】\n" >> "$OUTPUT_FILE" if cat "$file" >> "$OUTPUT_FILE" 2>/dev/null; then if [ $current_index -lt $total_files ]; then echo -e "$SEPARATOR" >> "$OUTPUT_FILE" fi echo "完成。" ((processed_count++)) else echo "失败!已跳过。" ((skipped_count++)) # 如果写入标题后读取失败,最好也删除刚写入的标题。这里简化处理,只是跳过。 fi done

4.3 处理特定文件扩展名或排除文件

你可能只想合并.log.txt文件,或者排除所有.tmp临时文件。这可以通过增强find命令的-name-not选项来实现。

在构建find_cmd时添加过滤:

# 假设我们只想合并 .log, .txt, .md 文件 if [ "$RECURSIVE" = true ]; then find_cmd="find \"$INPUT_DIR\" -type f \( -name \"*.log\" -o -name \"*.txt\" -o -name \"*.md\" \)" else find_cmd="find \"$INPUT_DIR\" -maxdepth 1 -type f \( -name \"*.log\" -o -name \"*.txt\" -o -name \"*.md\" \)" fi # 或者,排除 .tmp 和 .bak 文件 if [ "$RECURSIVE" = true ]; then find_cmd="find \"$INPUT_DIR\" -type f -not \( -name \"*.tmp\" -o -name \"*.bak\" \)" else find_cmd="find \"$INPUT_DIR\" -maxdepth 1 -type f -not \( -name \"*.tmp\" -o -name \"*.bak\" \)" fi

你可以将这些过滤模式设计成通过命令行参数传入,使脚本更加灵活。

5. 实战演练与排错指南

让我们用一个具体的例子来跑通整个流程,并看看可能会遇到哪些问题。

场景:合并~/project/logs目录下所有的.log文件到一个名为all_logs_$(date +%Y%m%d).txt的文件中,并希望在每个日志文件间插入清晰的分隔线。

操作步骤:

  1. 保存脚本:将完整的merge_files.sh脚本保存到你的个人工具目录,例如~/bin/,并确保该目录在PATH环境变量中。
  2. 准备测试目录
    mkdir -p ~/test_merge cd ~/test_merge echo "Log entry 1 from app A" > app_a_20231001.log echo "Log entry 2 from app A" > app_a_20231002.log echo "Error: something happened in app B" > app_b_error.log echo "This is a binary file?" > dummy.bin
  3. 执行脚本
    # 假设脚本已在 PATH 中 merge_files.sh -d ~/test_merge -o ./merged_$(date +%Y%m%d).txt -s "\n========== 下一个文件 ==========\n"
    • -d指定目录。
    • -o使用命令替换动态生成带日期的输出文件名。
    • -s指定了更醒目的分隔符。

常见问题与排查:

  1. 错误:/bin/bash^M: 坏的解释器:没有那个文件或目录

    • 原因:脚本是在 Windows 系统编辑后传到 Linux/macOS 的,行尾是CRLF(\r\n),而 Unix 系统期望LF(\n)。
    • 解决:使用dos2unix merge_files.sh转换,或用sed -i 's/\r$//' merge_files.sh删除\r
  2. 错误:find: 路径必须在表达式之前

    • 原因:目录路径包含特殊字符(如空格、括号),在find命令中未正确引用。
    • 解决:脚本中已经用双引号包裹了"$INPUT_DIR",确保变量扩展后被引用。如果你在命令行手动测试,也要注意引用:find “/path/with spaces” ...
  3. 合并后的文件内容乱码或包含奇怪字符

    • 原因:源文件编码不一致(如 UTF-8, GBK, UTF-16)或者包含了非文本的二进制数据。
    • 解决
      • 确保脚本的文本文件检测生效,跳过了二进制文件。
      • 对于不同编码的文本文件,合并到一个文件后,编辑器可能无法正确识别。可以在合并前用iconv命令统一转码为 UTF-8,但这会大大增加脚本复杂度。一个折中方案是,在脚本中检测到非 UTF-8 文件时给出警告。
      • 可以在合并循环中加入编码检查:if ! iconv -f utf-8 -t utf-8 "$file" >/dev/null 2>&1; then echo “警告: $file 可能不是 UTF-8 编码”; fi
  4. 脚本运行速度慢,处理大量小文件时

    • 原因:对每个文件都调用filegrep -I命令进行类型检查,产生了大量进程开销。
    • 优化:如果确信目录下全是文本文件,或者愿意承担合并少量二进制文件的风险,可以添加一个-f(force)选项来跳过文件类型检查。在参数解析部分增加-f选项,然后在查找文件循环中,根据这个标志位决定是否执行检查。
  5. find: missing argument to '-exec'

    • 原因:在-exec中使用的{}\;格式不正确,或者find命令的构建字符串在变量中时,引用和扩展出现问题。
    • 解决:这是 Shell 脚本中引用和变量扩展的经典难题。如果find_cmd字符串非常复杂(包含-exec),使用eval或数组来构建命令更安全。我们之前使用的< <(eval "$find_cmd" -print0)中的eval需要谨慎处理。一个更安全的方法是避免在变量中存储复杂命令,而是直接使用find命令。

通过这个自用的 Shell 脚本项目,我们不仅解决了一个具体的文件合并需求,更深入实践了 Shell 脚本编程中的多个关键概念:健壮的命令行参数解析、安全的文件名处理、错误处理、文件类型判断以及通过管道和命令组合构建复杂功能。将这个脚本收入你的工具箱,下次再遇到合并文件的任务,你只需要一行命令就能轻松搞定,把时间和精力留给更有价值的工作。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/1 9:30:26

STM32CubeMX+VSCode+GCC开发环境搭建与工程实践指南

1. 项目概述&#xff1a;为什么选择 STM32CubeMX VSCode&#xff1f;如果你已经用了一段时间 Keil 或者 IAR 来开发 STM32&#xff0c;可能会对那个略显陈旧的界面、繁琐的工程配置&#xff0c;以及&#xff08;对于某些版本来说&#xff09;不太友好的代码编辑体验感到一丝疲…

作者头像 李华
网站建设 2026/8/1 9:29:56

水星电化学×煤化工/乙二醇:专业的事,交给专业的人

水星电化学是谁如果你在煤化工/乙二醇行业从事质检工作&#xff0c;水星电化学这个名字值得你记住。作为KEM&#xff08;京都电子工业株式会社&#xff09;合作伙伴&#xff0c;水星电化学是国内少数能够提供从高端进口仪器到高性价比国产替代的完整电化学分析解决方案的企业。…

作者头像 李华
网站建设 2026/8/1 9:26:05

Linux高级安装指南:逻辑卷管理器LVM

介绍LVM 之前这篇文章是基础安装指南&#xff0c;讲解了如何从安装镜像安装Arch Linux&#xff0c;请务必熟悉。 对于很多Linux发行版来说&#xff0c;都有自带的图形化界面与自动安装&#xff0c;所以本篇主要针对Arch Linux的手动安装。 逻辑卷管理器Logical Volume Manage…

作者头像 李华
网站建设 2026/8/1 9:24:56

DLP4500 EVM开发实战:从硬件连接到同步调试的避坑指南

1. 项目概述&#xff1a;为什么我们需要关注DLP Lightcrafter™ 4500 EVM&#xff1f;如果你正在从事3D扫描、机器视觉、光谱分析或者任何需要高精度、高速结构光投影的项目&#xff0c;那么德州仪器&#xff08;TI&#xff09;的DLP Lightcrafter™ 4500评估模块&#xff08;E…

作者头像 李华