本篇继续来学习 shell脚本,对上一篇的文件检查与拷贝脚本进行简化修改。
1 功能说明
在Linux系统中,通过一个shell脚本,实现将一个目录中的所有文件(包括子目录中的),拷贝到指定的目标目录,要求:
- 在拷贝前,先检查两个目录中文件是否一样,不一样的才拷贝
- 执行拷贝时,打印详细的拷贝信息
2 脚本实现
下面分功能模块来讲解脚本。
2.1 源目录与目标目录
这部分和上次的脚本一样,不再详细介绍。
2.2 统计源目录的总文件数和总目录数
# 源目录的所有文件
mapfile -t FILES < <(find "$SRC_DIR" -type f)
解释:
find "$SRC_DIR" -type f:在 $SRC_DIR 目录及其子目录下查找所有普通文件,并输出它们的路径
<(find "$SRC_DIR" -type f):进程替换(process substitution),将 find 命令的输出作为一个临时文件(或管道)提供给其他命令使用
mapfile -t FILES < ...:读取文件
mapfile 是一个读取文件内容到数组的命令
-t 选项表示去除每行末尾的换行符(trim trailing newline)
FILES 是要创建的数组变量名
< 表示从前面的输入源读取数据
2.3 对比文件是否一样并拷贝
上篇文章是通过MD5来比较文件是否相同的,会比较耗时,这次改用 cmp 指令来比较。
copyed_count=0
processed_count=0
# 递归遍历源目录下所有文件
for src_file in "${FILES[@]}"; do
# 计算相对路径
rel_path="${src_file#$SRC_DIR/}"
dest_file="$DEST_DIR/$rel_path"
# 创建目标子目录
dest_dir=$(dirname "$dest_file")
mkdir -p "$dest_dir"
# 比较文件并执行拷贝
if [[ ! -f "$dest_file" ]] || ! cmp -s "$src_file" "$dest_file"; then
# 执行复制
cp -pv "$src_file" "$dest_file" || echo "警告:复制 $rel_path 失败!"
echo "copy: $rel_path"
(( copyed_count++ ))
fi
(( processed_count++ ))
process=$(( (processed_count * 100 + TOTAL_FILES - 1) / TOTAL_FILES ))
printf "\rprocess: [%d/%d] (%d%%)" "$processed_count" "$TOTAL_FILES" "$process"
done
echo -e "\n=== 操作完成,实际需拷贝 $copyed_count 个文件 ==="
2.3.1 遍历每个文件
前面通过 mapfile 将文件读到了 FILES 数组中,然后就可以通过 for 循环来依次处理文件了。
for src_file in "${FILES[@]}"; do
# ...
done
解释含义:
FILES:是数组的名称(在之前的 mapfile 命令中,这个数组被用来存储文件路径)
[@]:是数组的扩展标志,作用是展开数组中的所有元素,并且每个元素会被视为一个独立的参数
"${FILES[@]}"(带双引号)时,会保留每个元素的原始格式,包括元素中包含的空格
2.3.2 计算路径并创建子目录
从源目录文件的完整路径中提取出相对路径,然后根据目标位置,组成目标文件的路径。
# 计算相对路径
rel_path="${src_file#$SRC_DIR/}"
dest_file="$DEST_DIR/$rel_path"
# 创建目标子目录
dest_dir=$(dirname "$dest_file")
mkdir -p "$dest_dir"
解释一下:
${变量#模式},从变量值的开头开始匹配"模式",并删除最短的匹配部分
src_file 存储着文件的完整路径,例如 /home/user/src/docs/readme.txt
$SRC_DIR/ 作为匹配的前缀模式,例如 /home/user/src/
rel_path 最终得到 docs/readme.txt
$DEST_DIR/$rel_path,拼接成目标文件的路径,例如 /home/user/src2/docs/readme.txt
dirname,从文件路径中提取其所在的目录部分,例如 /home/user/src2/docs
mkdir -p "$dest_dir":用于创建目录
mkdir:是"make directory"的缩写,用于创建新目录的基础命令
-p:是一个常用选项,全称是"parent"(父目录),作用是递归创建目录。当需要创建的目录路径中包含不存在的父目录时,-p 会自动创建所有缺失的父目录,而不会报错
"$dest_dir":是要创建的目标目录路径(通常是一个变量),双引号用于处理路径中包含空格或特殊字符的情况
2.3.3 复制文件
# 比较文件并执行拷贝
if [[ ! -f "$dest_file" ]] || ! cmp -s "$src_file" "$dest_file"; then
# 执行复制
cp -pv "$src_file" "$dest_file" || echo "警告:复制 $rel_path 失败!"
echo "copy: $rel_path"
(( copyed_count++ ))
fi
(( processed_count++ ))
process=$(( (processed_count * 100 + TOTAL_FILES - 1) / TOTAL_FILES ))
printf "\rprocess: [%d/%d] (%d%%)" "$processed_count" "$TOTAL_FILES" "$process"
解释:
[[ ! -f "$dest_file" ]]:如果目标文件 $dest_file 不存在,则此部分条件为真
[[ ... ]] 是 Bash 的高级条件判断语法
! 表示否定
-f 是文件测试运算符,用于检查指定路径是否为普通文件
||:逻辑或运算符
! cmp -s "$src_file" "$dest_file":如果源文件 $src_file 与目标文件 $dest_file 内容不同,则此部分条件为真
- 若两文件内容完全相同,
cmp -s 退出状态码为 0(表示成功)
- 若内容不同,退出状态码为非 0(表示失败)
cmp 是用于比较两个文件内容的命令
-s:选项表示"silent"(静默模式),比较结果不同时不输出具体差异,只通过退出状态码表示
! 否定了 cmp -s 的结果
3 完整的脚本
#!/bin/bash
SRC_DIR="./curl-8.15.0" # 源目录路径
DEST_DIR="./curl-8.15.0-test2" # 目标目录路径
# 确保目录路径不以斜杠结尾,避免路径处理问题
SRC_DIR=${SRC_DIR%/}
DEST_DIR=${DEST_DIR%/}
# 检查源目录是否存在
if [ ! -d "$SRC_DIR" ]; then
echo "错误:源目录 $SRC_DIR 不存在!"
exit 1
fi
# 检查目标目录,不存在则创建
if [ ! -d "$DEST_DIR" ]; then
echo "目标目录 $DEST_DIR 不存在,正在创建..."
mkdir -p "$DEST_DIR" || { echo "创建目标目录失败!"; exit 1; }
fi
echo "源目录: $SRC_DIR"
echo "目标目录: $DEST_DIR"
# 统计源目录的总文件数和总目录数
TOTAL_FILES=$(find "$SRC_DIR" -type f | wc -l)
TOTAL_DIRS=$(find "$SRC_DIR" -type d | wc -l)
# 减去源目录本身
TOTAL_DIRS=$((TOTAL_DIRS - 1))
echo "源目录总文件数: $TOTAL_FILES"
echo "源目录总目录数: $TOTAL_DIRS"
# 源目录的所有文件
mapfile -t FILES < <(find "$SRC_DIR" -type f)
copyed_count=0
processed_count=0
# 递归遍历源目录下所有文件
for src_file in "${FILES[@]}"; do
# 计算相对路径
rel_path="${src_file#$SRC_DIR/}"
dest_file="$DEST_DIR/$rel_path"
# 创建目标子目录
dest_dir=$(dirname "$dest_file")
mkdir -p "$dest_dir"
# 比较文件并执行拷贝
if [[ ! -f "$dest_file" ]] || ! cmp -s "$src_file" "$dest_file"; then
# 执行复制
cp -pv "$src_file" "$dest_file" || echo "警告:复制 $rel_path 失败!"
echo "copy: $rel_path"
(( copyed_count++ ))
fi
(( processed_count++ ))
process=$(( (processed_count * 100 + TOTAL_FILES - 1) / TOTAL_FILES ))
printf "\rprocess: [%d/%d] (%d%%)" "$processed_count" "$TOTAL_FILES" "$process"
done
echo -e "\n=== 操作完成,实际需拷贝 $copyed_count 个文件 ==="
4 测试结果
这里用 curl 的源码目录进行测试,先拷贝一份到 curl-8.15.0-test2 目录,然后删除一些文件,再运行脚本。
可以看到,脚本检查到两个目录存在不一样的文件,确认拷贝后,执行了拷贝。
再次执行脚本,结果提示所有文件都已经一致,无需拷贝。
5 总结
本篇通过一个文件检查与拷贝的实例,介绍了shell脚本的一些实用语法,并通过实际测试验证了脚本的功能。使用 mapfile 读取文件列表、cmp 进行内容比对,既提升了效率,又保证了增量拷贝的准确性,非常适合日常运维中的目录同步需求。