哈希多个文件
问题说明:
给定一个文件夹,我想要遍历这个文件夹以及它的非隐藏子文件夹,
并在非隐藏文件的名字中添加一个漩涡哈希值。
如果这个脚本再次运行,它会把旧的哈希值替换成新的。
<文件名>.<扩展名>
==><文件名>.<漩涡哈希值>.<扩展名>
<文件名>.<旧哈希值>.<扩展名>
==><文件名>.<新哈希值>.<扩展名>
问题:
a) 你会怎么做呢?
b) 在所有可用的方法中,是什么让你选择的方法最合适?
结论:
谢谢大家,我选择了SeigeX的答案,因为它速度快且便于移植。
它的速度比其他bash变体快,
而且在我的Mac OS X机器上运行时没有任何修改。
13 个回答
我对我之前的回答不是很满意,因为正如我所说,这个问题用perl来解决效果最好。你在问题的一个编辑中提到你在想要运行这个程序的OS X机器上已经安装了perl,所以我试了一下。
在bash中很难把所有事情都做好,比如避免奇怪文件名带来的引号问题,以及处理一些特殊文件名时的表现。
所以这里是用perl写的,给你的问题提供了一个完整的解决方案。它会处理命令行中列出的所有文件和目录。
#!/usr/bin/perl -w
# whirlpool-rename.pl
# 2009 Peter Cordes <peter@cordes.ca>. Share and Enjoy!
use Fcntl; # for O_BINARY
use File::Find;
use Digest::Whirlpool;
# find callback, called once per directory entry
# $_ is the base name of the file, and we are chdired to that directory.
sub whirlpool_rename {
print "find: $_\n";
# my @components = split /\.(?:[[:xdigit:]]{128})?/; # remove .hash while we're at it
my @components = split /\.(?!\.|$)/, $_, -1; # -1 to not leave out trailing dots
if (!$components[0] && $_ ne ".") { # hidden file/directory
$File::Find::prune = 1;
return;
}
# don't follow symlinks or process non-regular-files
return if (-l $_ || ! -f _);
my $digest;
eval {
sysopen(my $fh, $_, O_RDONLY | O_BINARY) or die "$!";
$digest = Digest->new( 'Whirlpool' )->addfile($fh);
};
if ($@) { # exception-catching structure from whirlpoolsum, distributed with Digest::Whirlpool.
warn "whirlpool: couldn't hash $_: $!\n";
return;
}
# strip old hashes from the name. not done during split only in the interests of readability
@components = grep { !/^[[:xdigit:]]{128}$/ } @components;
if ($#components == 0) {
push @components, $digest->hexdigest;
} else {
my $ext = pop @components;
push @components, $digest->hexdigest, $ext;
}
my $newname = join('.', @components);
return if $_ eq $newname;
print "rename $_ -> $newname\n";
if (-e $newname) {
warn "whirlpool: clobbering $newname\n";
# maybe unlink $_ and return if $_ is older than $newname?
# But you'd better check that $newname has the right contents then...
}
# This could be link instead of rename, but then you'd have to handle directories, and you can't make hardlinks across filesystems
rename $_, $newname or warn "whirlpool: couldn't rename $_ -> $newname: $!\n";
}
#main
$ARGV[0] = "." if !@ARGV; # default to current directory
find({wanted => \&whirlpool_rename, no_chdir => 0}, @ARGV );
优点:
实际上使用了whirlpool算法,所以你可以直接使用这个程序。(在安装libperl-digest-whirlpool之后)。想要更换成其他的摘要函数也很简单,因为你只需要使用perl的Digest通用接口,而不是不同程序和不同输出格式。
实现了所有其他要求:忽略隐藏文件(以及隐藏目录下的文件)。
能够处理任何可能的文件名而不会出错或产生安全问题。(有几个人在他们的shell脚本中做得很好)。
遵循了遍历目录树的最佳实践,通过进入每个目录来处理(就像我之前的回答中提到的,使用find -execdir)。这样可以避免PATH_MAX的问题,以及在运行时目录被重命名的问题。
巧妙地处理以点结尾的文件名,比如foo..txt...会变成foo..hash.txt...
能够处理已经包含哈希的旧文件名,而无需重命名再重命名回去。(它会去掉被“.”字符包围的128个十六进制数字序列。)在一切都正确的情况下,不会有磁盘写入活动,只是读取每个文件。你当前的解决方案在文件名已经正确的情况下会执行mv两次,导致目录元数据写入。而且速度更慢,因为需要执行两个进程。
效率高。没有程序被fork/execed,而大多数实际可行的解决方案最终都需要对每个文件进行sed处理。Digest::Whirlpool是用本地编译的共享库实现的,所以它并不是慢的纯perl。这应该比对每个文件运行一个程序要快,尤其是对于小文件。
Perl支持UTF-8字符串,所以包含非ASCII字符的文件名应该不会有问题。(不确定UTF-8中的多字节序列是否可能包含单独表示ASCII '.'的字节。如果可能的话,你需要支持UTF-8的字符串处理。sed不支持UTF-8,bash的通配符表达式可能支持。)
易于扩展。当你想把这个放入一个真正的程序中,并且想处理更多特殊情况时,可以很容易做到。例如,决定在你想重命名一个文件但哈希命名的文件名已经存在时该怎么办。
错误报告良好。大多数shell脚本都有这个功能,通过传递它们运行的程序的错误来实现。
- 测试了包含空格的文件,比如 'a b'
- 测试了包含多个扩展名的文件,比如 'a.b.c'
- 测试了包含空格和/或点的目录。
- 测试了在包含点的目录中没有扩展名的文件,比如 'a.b/c'
- 更新: 现在如果文件内容改变,会更新哈希值。
#!/bin/bash
find -type f -print0 | while read -d $'\0' file
do
md5sum=`md5sum "${file}" | sed -r 's/ .*//'`
filename=`echo "${file}" | sed -r 's/\.[^./]*$//'`
extension="${file:${#filename}}"
filename=`echo "${filename}" | sed -r 's/\.md5sum-[^.]+//'`
if [[ "${file}" != "${filename}.md5sum-${md5sum}${extension}" ]]; then
echo "Handling file: ${file}"
mv "${file}" "${filename}.md5sum-${md5sum}${extension}"
fi
done
关键点:
- 使用
print0
结合while read -d $'\0'
,这样可以正确处理文件名中的空格。 - md5sum 可以换成你喜欢的其他哈希函数。sed 用来去掉 md5sum 输出中的第一个空格及其后面的内容。
- 通过正则表达式提取基本文件名,找到最后一个点(.),并确保后面没有斜杠(/),这样目录名中的点就不会被算作扩展名的一部分。
- 扩展名是通过从基本文件名的长度开始的子字符串来找到的。
更新内容:
1. 文件名中可以包含'['或']'(实际上,现在可以包含任何字符。请查看评论)
2. 处理文件名中包含反斜杠或换行符时的md5sum计算
3. 将哈希检查算法封装成函数,以便于模块化
4. 重构哈希检查逻辑,去掉了双重否定
#!/bin/bash
if (($# != 1)) || ! [[ -d "$1" ]]; then
echo "Usage: $0 /path/to/directory"
exit 1
fi
is_hash() {
md5=${1##*.} # strip prefix
[[ "$md5" == *[^[:xdigit:]]* || ${#md5} -lt 32 ]] && echo "$1" || echo "${1%.*}"
}
while IFS= read -r -d $'\0' file; do
read hash junk < <(md5sum "$file")
basename="${file##*/}"
dirname="${file%/*}"
pre_ext="${basename%.*}"
ext="${basename:${#pre_ext}}"
# File already hashed?
pre_ext=$(is_hash "$pre_ext")
ext=$(is_hash "$ext")
mv "$file" "${dirname}/${pre_ext}.${hash}${ext}" 2> /dev/null
done < <(find "$1" -path "*/.*" -prune -o \( -type f -print0 \))
这段代码相比之前的其他版本有以下优点:
- 完全兼容Bash 2.0.2及更高版本
- 没有多余的调用其他程序,比如sed或grep,而是使用内置的参数扩展
- 使用进程替换来处理'find',而不是使用管道,这样不会创建子进程
- 可以将要处理的目录作为参数传入,并对其进行合理性检查
- 使用$()而不是``来进行命令替换,后者已经不推荐使用了
- 可以处理带有空格的文件
- 可以处理带有换行符的文件
- 可以处理有多个扩展名的文件
- 可以处理没有扩展名的文件
- 不会遍历隐藏目录
- 不会跳过已经计算过哈希的文件,会根据规范重新计算哈希
测试树
$ tree -a a a |-- .hidden_dir | `-- foo |-- b | `-- c.d | |-- f | |-- g.5236b1ab46088005ed3554940390c8a7.ext | |-- h.d41d8cd98f00b204e9800998ecf8427e | |-- i.ext1.5236b1ab46088005ed3554940390c8a7.ext2 | `-- j.ext1.ext2 |-- c.ext^Mnewline | |-- f | `-- g.with[or].ext `-- f^Jnewline.ext 4 directories, 9 files
结果
$ tree -a a a |-- .hidden_dir | `-- foo |-- b | `-- c.d | |-- f.d41d8cd98f00b204e9800998ecf8427e | |-- g.d41d8cd98f00b204e9800998ecf8427e.ext | |-- h.d41d8cd98f00b204e9800998ecf8427e | |-- i.ext1.d41d8cd98f00b204e9800998ecf8427e.ext2 | `-- j.ext1.d41d8cd98f00b204e9800998ecf8427e.ext2 |-- c.ext^Mnewline | |-- f.d41d8cd98f00b204e9800998ecf8427e | `-- g.with[or].d41d8cd98f00b204e9800998ecf8427e.ext `-- f^Jnewline.d3b07384d113edec49eaa6238ad5ff00.ext 4 directories, 9 files