我正在尝试使用如下命令将文件自动分组到子目录中:
$ rename 's/(.)(.)(.+)/$1\/$2\/$1$2$3/' *.*
使用参数进行的试运行-n
显示了我想要的内容:
test.jpg renamed as t/e/test.jpg
但实际重命名失败了
Can't rename test.jpg t/e/test.jpg: No such file or directory
因为子目录还不存在。
如何在不事先手动创建所有子目录的情况下实现此目的?
答案1
重命名和移动到某个地方是有区别的。在这种情况下,最简单的方法(在现代中bash
)是循环遍历所有文件:
for f in *.*
do
d=${f::1}/${f:1:1}
[ -d "$d" ] || mkdir -p "$d"
mv "$f" "$d"
done
解释
这利用了 bash 鲜为人知的“参数扩展/子字符串扩展”功能。
${var:offset:length}
扩展到最多
length
的值的字符var
从指定的字符开始offset
。
。 [稍稍转述自重击(1).]
我没有看到哪里记录了这一点,但是,offset
是从零开始的,0
第一个字符也是 从零开始,1
第二个字符也是从零开始。如果 offset
为 null(缺失),则视为零。
答案2
作为延伸科斯塔斯的回答,
for f in *.*
do
if [ "${f:0:1}" = . ] || [ "${f:1:1}" = . ]
then
printf 'Cannot handle "%s"\n' "$f"
else
d=${f:0:1}/${f:1:1}
if ! { [ -d "$d" ] || mkdir -p "$d" && mv "$f" "$d"; }
then
break
fi
fi
done
我添加了(第一个)
if
来处理像M.Butterfly.jpg
and 这样的文件名Q.jpg
,因为你不能(真的)创建像M/.
and 这样的目录Q/.
。同样,.Dragon.mp4
将尝试创建一个名为 的目录./D
。1我添加了
&&
(在mkdir
和 之间mv
),因此,如果mkdir
失败,我们不会尝试将文件移动到不存在的目录中。我添加了第二个,
if
因为如果出现问题,脚本终止并让用户找出(并修复)问题是有意义的。
(例如,如果您不小心在没有写入权限的目录中运行脚本,或者文件系统已满,因此无法创建新的子目录,该怎么办?)我可以把它写成
[ -d "$d" ] || mkdir -p "$d" && mv "$f" "$d || break
但这些
&&
/||
序列很快就会变得混乱。
另一种变化:
for f in *.*
do
if [ "${f:0:1}" = . ] || [ "${f:1:1}" = . ]
then
printf 'Cannot handle "%s"\n' "$f"
else
d=${f:0:1}/${f:1:1}
if ! { [ -d "$d" ] || mkdir -p "$d"; } # Note: no "mv" command in the loop.
then
break
fi
fi
done
rename 's/(.)(.)(.+)/$1\/$2\/$1$2$3/' *.*
这样做的优点是它只调用rename
一次而不是调用mv
一千次。它有几个缺点:
M.Butterfly.jpg
像和 这样的文件Q.jpg
被传递到rename
.rename
即使mkdir
失败也会运行。 (这可能很容易解决。一种方法是将 替换break
为exit
,但这看起来不优雅。)
我没有rename
和你相同的版本,但我尝试过mv M.Butterfly.jpg M/.
,它只是移到M.Butterfly.jpg
了目录中 M
。因此,如果您使用我的第二个解决方案,当您列出目录时 M
,您将看到子目录、、、、a
等(包含诸如、、、、等文件)和一些纯文件(如 )。然后你就可以随心所欲地处理它们。 ________________ 1 实际上,这些命令会成功,但它们会创建名为、 和 的 目录,这可能不是您想要的。e
i
o
Many.jpg
Memorable.jpg
Minnesota.jpg
Moments.jpg
M.Butterfly.jpg
M
Q
D
答案3
这可以完全在脚本中完成rename
,因为rename
允许使用任何perl 代码,它不仅仅限于简单的s///
运算符。
以下工作在简单的情况下,即所有要重命名的文件都在当前目录中,并rename
直接在命令行上传递而不带./
前缀(这通常不是一个好主意,因为可能-
会出现以下开头的文件名解释为rename
) 的命令行选项。
如果文件名包含目录前缀,则它将不起作用,例如它们被传递给rename
viafind
或 as ./*.*
。
$ touch test.jpg test.txt
$ rename -n 'my ($a, $b) = (/(.)(.)/);
mkdir $a unless -e $a;
mkdir "$a/$b" unless -e "$a/$b";
if (-e "$a/$b" && ! -d "$a/$b") {
print STDERR "$_: $a/$b exists but is not a directory!\n";
} else {;
s/(.)(.)(.+)/$1\/$2\/$1$2$3/;
}' *.*
rename(test.jpg, t/e/test.jpg)
rename(test.txt, t/e/test.txt)
更好的版本可以使用文件::基本名称和文件路径模块,这两个模块都包含在 perl 中。
$ rename -n 'use File::Basename;
use File::Path qw(make_path);
my ($bn, $dn) = fileparse($_);
my ($a, $b) = ($bn =~ /(.)(.)/);
make_path("$dn$a/$b", { verbose => 1 });
$_ = "$dn$a/$b/$bn"' ./*.*
mkdir ./t
mkdir ./t/e
rename(./test.jpg, ./t/e/test.jpg)
rename(./test.txt, ./t/e/test.txt)
这将在每个文件的原始目录下创建两个子目录。该make_path()
函数File::Path
已经检查目标目录是否存在(如果存在但不是目录,则将其视为致命错误,但如果您希望它只打印警告并继续执行,则可以实现自己的错误处理下一个文件名)。
如果您愿意,可以通过设置$dn
为目标顶级目录来对其进行硬编码,以将所有文件重命名为特定的目录树(您仍然需要使用该File::Basename
模块从完整路径名中提取基本名称)。