由于从前两个答案和评论中学到了我第一次问时不知道的东西,这个问题被完全重写了。
拍完照片后,我带着看起来像 的文件回家_DSC1234.NEF
。NEF
是尼康的相机原始文件格式,因此EXIF
文件中存在数据。
我想用三个部分自动重命名它们:
- 格式中的创建日期
YYYYMMDD
- 拍摄名称
- 图片数量
所以最终的文件名应该是这样的
20140707_NameOfShoot_0001.NEF
有几个问题:
广告 1. 创建日期
有时我只能在拍摄后几天复制和重命名文件,因此日期应该反映照片的拍摄日期,而不是复制的日期。mtime
似乎是最好的选择,或者如果可能的话,创建日期为EXIF
。
广告2:拍摄名称 理想情况下,这是一个我可以在调用脚本时设置为参数的变量。
广告3.图片数量
这应该反映图像的年龄,最古老的数字最少。问题是摄像机通常会0000
在击中 后重新开始编号9999
。所以9995-9999
可能比 更老0000-0004
。我正在寻找一种反映文件年龄的解决方案,在这种特殊情况下将重命名
- _DSC0000.NEF -> 20140707_FOO_0004.NEF
- _DSC0001.NEF -> 20140707_FOO_0005.NEF
- _DSC0002.NEF -> 20140707_FOO_0006.NEF
- ...
- _DSC9997.NEF -> 20140707_FOO_0001.NEF
- _DSC9998.NEF -> 20140707_FOO_0002.NEF
- _DSC9999.NEF -> 20140707_FOO_0003.NEF
同样,mtime
或者如果可能的话,创建日期EXIF
似乎是正确的。
从这里我有一个工作解决方案,可以按日期重命名文件夹中的所有 .NEF 文件:
find -name '*.NEF' |
gawk 'BEGIN{ a=1 }{ printf "mv %s %04d.NEF\n", $0, a++ }' |
bash
硬编码文件修改日期和拍摄名称效果很好,但如果可以自动化那就太好了。
对于时间戳,我找到了 , 上的文章strftime()
,mktime()
但systime()
我不明白如何使用它们返回文件修改日期。我还尝试添加DATE=$(date +"%Y%m%d")
和添加$DATE
到 gawk-line,这会导致删除当前文件夹中的所有文件(并且可能是 systime 无论如何,而不是更改时间)。
对于变量,我尝试过
gawk 'BEGIN{ a=1 }{ printf "mv %s $1_%04d.NEF\n", $0, a++ }' |
并使用 调用脚本./rename FOO
,但重命名时会忽略 FOO。
答案1
大多数 unice 不跟踪文件的创建日期。无论如何,“创建日期”定义不明确(复制文件会创建新文件吗?)。您可以使用文件的修改时间,根据合理的解释,该时间是创建数据最新版本的日期。如果您复制该文件,请确保保留修改时间(例如,cp -p
如果cp -a
您使用该cp
命令,则不要使用 bare cp
)。
一些文件格式在文件内有一个字段,创建者应用程序在其中填写创建日期。照片通常就是这种情况,相机会填充一些内容埃克斯夫JPEG 或 TIFF 图像中的数据,包括创建时间。尼康的 NEF 图像格式环绕 TIFF 并支持 Exif。
有现成的工具可以重命名包含 Exif 数据的图像文件,以在文件名中包含创建日期。重命名图像以在名称中包含创建日期显示了两个解决方案,其中出口工具和exiv2。
我认为这两种工具都不允许您在文件名中包含计数器。您可以分两遍进行重命名:首先在文件名中包含日期(尽可能高分辨率以保留顺序),然后根据该日期部分对文件进行编号(并丢弃时间)。由于现代 DSLR 可以连续拍摄图像(尼康的 D4 以 11 fps 拍摄),因此建议在第一阶段保留原始文件名,否则可能会导致多个文件具有相同的文件名。
exiv2 mv -r %Y%m%d-%H%M%S:basename: *.NEF
# exiv2 uses `strftime(3)`, so `%Y%m%d-%H%M%S` returns YYYYMMDD-hhmmss
# :basename: is a naming variable exiv2's `-r`-handle provides. See `exiv2 -h` for more
# Now you have files with names like 20140630-235958_DSCC1234.NEF.
# Note that chronological order and lexicographic order agree with this naming format.
i=10000
for x in *.NEF; do
i=$((i+1))
mv "$x" "${x%-*}_FOO_${i#1}.NEF"
done
${x%-*}
删除字符后面的部分-
。计数器变量i
从 10000 开始计数,使用时1
去掉前导数字;这是获取前导零的技巧,以便所有计数器值具有相同的数字。
通过增加文件名中的数字来重命名文件还有其他解决方案可以重命名一堆文件以包含计数器。
如果您想使用文件的时间戳而不是 Exif 数据,请参阅在文件名末尾重命名一堆带有修改日期时间戳的文件?
一般而言,不要生成 shell 代码,然后将其通过管道传输到 shell 中。这是不必要的复杂。例如,代替
find -name '*.NEF' |
gawk 'BEGIN{ a=1 }{ printf "mv %s %04d.NEF\n", $0, a++ }' |
bash
你可以写
find -name '*.NEF' |
gawk 'BEGIN{ a=1 }{ system(sprintf("mv %s %04d.NEF\n", $0, a++)) }'
请注意,如果文件名包含 shell 特殊字符(例如空格、、、、等),这两个版本都可能导致灾难性结果,'
因为文件名被解释为 shell 代码。有多种方法可以将其转变为健壮的代码,但这不是最简单的方法,因此我不会采用这种方法。$
`
1请注意,有一个叫做“ctime”的东西,但它c
不适用于创建,这是为了改变。每次文件内容或元数据(名称、权限等)发生任何变化时,ctime 都会发生变化。 ctime 几乎是创建时间的对立面。
答案2
stat --printf='}" "%z_${SN}_${LINENO}"\n' -- * |
nl -nln -w1 -s '' |
sort -k2,3 |
sed 's| ..:[^_]*||;s|-||g;s|^|echo mv "${|' |
SN=SHOOTNAME sh -s -- *
上面的命令应该可以满足您的需要。它可能比您所要求的更通用 - 但请参阅此答案的底部以获取更具体的示例。
它的工作原理是*
遍历当前目录中的所有文件并打印出它们的更改时间。对于当前目录中包含后缀的文件,.NEF
您需要将*
第 1 行和第 5 行末尾的 globstar 更改为*.NEF
.它将一些 shell 变量和引号附加到末尾 - 这些名称仅存在于sh
子 shell 中管道的另一端。
另外,因为我们仅通过文件名的全局顺序或${1}
shell 类型参数来指定文件名,所以这对任何文件名都适用 - 无论它可能包含什么奇怪的字符。
目前该命令包含一个echo
- 它已被削弱。运行本质上是一个空操作——它只是向你展示它想要做什么。这是我的主目录的输出,然后将其提供给sh
:
echo mv "${2}" "20140611_${SN}_${LINENO}"
echo mv "${4}" "20140614_${SN}_${LINENO}"
echo mv "${11}" "20140617_${SN}_${LINENO}"
echo mv "${7}" "20140622_${SN}_${LINENO}"
echo mv "${8}" "20140622_${SN}_${LINENO}"
echo mv "${1}" "20140624_${SN}_${LINENO}"
echo mv "${10}" "20140704_${SN}_${LINENO}"
echo mv "${5}" "20140704_${SN}_${LINENO}"
echo mv "${9}" "20140704_${SN}_${LINENO}"
echo mv "${12}" "20140705_${SN}_${LINENO}"
echo mv "${3}" "20140705_${SN}_${LINENO}"
echo mv "${13}" "20140706_${SN}_${LINENO}"
echo mv "${6}" "20140706_${SN}_${LINENO}"
这是之后的sh
:
mv Desktop-1 20140611_SHOOTNAME_1
mv Library 20140614_SHOOTNAME_2
mv target.txt 20140617_SHOOTNAME_3
mv script.sh 20140622_SHOOTNAME_4
mv script.sh~ 20140622_SHOOTNAME_5
mv Desktop 20140624_SHOOTNAME_6
mv shot-2014-06-22_17-11-06.jpg 20140704_SHOOTNAME_7
mv Terminology.log 20140704_SHOOTNAME_8
mv shot-2014-06-22_17-10-16.jpg 20140704_SHOOTNAME_9
mv test 20140705_SHOOTNAME_10
mv Downloads 20140705_SHOOTNAME_11
mv test.tar 20140706_SHOOTNAME_12
mv new
file 20140706_SHOOTNAME_13
您可能会注意到,我的输出显示了一些已根据其创建时间命名的图像文件,但它们的新指定名称不匹配。这不是sort
按规定工作的效果,而是这些文件最后在该日期发生了状态更改。尽管如此,正如您在这个问题的评论中指定的那样,ctime
您正在寻找的属性是此处提供的排序和名称属性。尽管如此,这里还是stat
附加了文件名的输出:
stat -c '%z %n' -- *
2014-06-24 16:50:09.110283839 -0700 Desktop
2014-06-11 23:34:02.981981145 -0700 Desktop-1
2014-07-05 01:00:43.213344635 -0700 Downloads
2014-06-14 10:32:13.537014418 -0700 Library
2014-07-04 23:02:25.079690701 -0700 Terminology.log
2014-07-06 11:24:05.398936386 -0700 new
file
2014-06-22 11:26:53.658004123 -0700 script.sh
2014-06-22 11:26:53.658004123 -0700 script.sh~
2014-07-04 13:34:00.063296353 -0700 shot-2014-06-22_17-10-16.jpg
2014-07-04 13:34:00.066629687 -0700 shot-2014-06-22_17-11-06.jpg
2014-06-17 19:59:38.475358571 -0700 target.txt
2014-07-05 23:53:39.097065292 -0700 test
2014-07-06 00:38:57.060521397 -0700 test.tar
上面的输出还应该帮助我演示整个管道正在做什么。
因此
stat --printf='}" "%z_${SN}_${LINENO}"\n'
打印出如下所示的行:}" "YYYY-MM-DD HH:MM:SS.NS -TZ_${SN}_${LINENO}"
...它们代表的YMDHMS.NS -TZ
各个组件在哪里。其输出格式与- 文件生成时间 -上次访问时间 - 或- 上次修改时间相同,因此在上面的语句中替换其中任何一个都将扩展为它们的值。正如我们已经在评论中讨论的那样,文件生成时间并不是一个可靠的属性,并且在不支持它的情况下,它仅扩展为.strftime
ctime
%w
%x
%y
%z
%w
0
它对 shell glob 中的每个文件*
或您提供的任何 shell glob 执行此操作 - 例如*.NEF
仅针对当前目录中带有.NEF
后缀的文件。
该列表被传递给
nl
每行递增 1 的数字。它的行-n
编号是ln
左对齐的,并且不是用零填充的,最小-w
idth 为 1,并且只有一个''
空字符串将-s
它们与行的内容分开。它输出:I}" "YYYY-MM-DD HH:MM:SS.NS -TZ_${SN}_${LINENO}"
...I
其中是每行的编号。
sort
-k2,3
对从第二个字段到第三个 - 或 on 的输入进行排序YYYY-MM-DD HH:MM:SS.NS
。由于此时任何行的唯一独特质量是日期I
或日期并被I
跳过,因此无需比这更具体。这也解决了您对按编号而非日期命名的文件所做的评论。我应该首先已经完成了sort
之前的工作,但我没有想到要按分钟和秒等进行排序。sed
我的测试基础是这样生成的:
for s in 9 8 7 6 5 4 3 2 1; do touch $s && sleep 1; done
如果我sort
后 sed
- 正如我在这次编辑之前所做的那样 - 那么这将重命名文件,9
因为${DATE}.SHOOTNAME.9
每个文件的YMD
字段都是相同的,并且sort
不会影响它们的行顺序。但通过此调整,该命令sort
特定于纳秒,因此9
被重命名为${DATE}.SHOOTNAME.1
,反之亦然1
。谢谢@Seul,让我注意到这一点。
sed
然后删除第一个看起来像的字符串<space><any char><any char>:<colon>
以及按顺序跟随的所有不是^
下划线的字符_
。所以此时该行看起来像:I}" "YYYY-MM-DD_${SN}_${LINENO}"
...接下来它会删除所有-
破折号。最后它插入echo mv "${
到^
每行的头部,所以它看起来像这样:
echo mv "${I}" "YYYYMMDD_${SN}_${LINENO}"
- 最后一个
sh
ell 被调用并声明了环境变量$SN
- 这里它的值为SHOOTNAME
。 POSIX 指定 shell 为其读入的每一行增加 var$LINENO
,因此对于我们提供的每一行,文件名中的值应扩展为比最后一行多 1。如果 - 正如您的评论所示 - 由于某种原因这种情况不会发生,则完全有效的替代品$((i=i+1))
首先由stat
管道的第 1 行打印,正如我在下面的具体示例中提供的那样。
shell 也会被调用,其位置参数设置为我们的 glob - 这里是*
当前目录中所有文件的 globstar。正如已经提到的,*.NEF
在这一行和第一行中,仅对当前目录中文件名后缀为.NEF
.
只要它的 glob 与第一行中的相同,它就会按照nl
编号的顺序对它们进行 glob。因此,无论它出现在哪一行,"${1}"
都将扩展为我们根据nl
输出分配的相同文件名。这样您就可以按日期顺序快速、安全地以正确的顺序重命名文件。
- 正如已经提到的,我已经削弱了
echo
这里的命令。但是,如果您运行echo
并发现它适合您,那么您将需要删除echo
.
像这样:
stat --printf='}" "%z_${SN}_${LINENO}"\n' -- * |
nl -nln -w1 -s '' |
sort -k2,3 |
sed 's| ..:[^_]*||;s|-||g;s|^|mv "${|' |
SN=SHOOTNAME sh -s -- *
或者可能:
export SN=SHOOTNAME SUFX=.NEF
stat --printf='}" "%z_${SN}_$((i=i+1))${SUFX}"\n' -- *$SUFX |
nl -nln -w1 -s '' |
sort -k2,3 |
sed 's| ..:[^_]*||;s|-||g;s|^|mv "${|' |
sh -s -- *$SUFX
这里它被加工成一个 shell 函数:
_batch_date_rename () ( # a big one
ERR= # for error reporting
export "DIR=$1" "SUFX=$2" \ # args 1,2 must be dirname and file suffix
"NAME=${3-${ERR:?no rename string specified}}" \ # need name string
"TIME=${4-%y}" INT=$((${INT:-25}*3)) ${NOCONFIRM+NOCONFIRM=}
#all above vars are exported to all points below
_path_chk () { #run once at start - fn quits if any below test fails
[ -d "$1" ] && [ -w "$1" ] && set -- "$1"/*"$2" && [ -e "$1" ]
} # chks for user writable dirname and resolvable $1/*$2 glob
_print_fmt () { #shell printf now not stat - last field zero padded
printf 'mv "${%d}" "${DIR}/%d_${NAME}_%04d${SUFX}"\n' "$@"
}
_print_mv () { #prints copy of mv action before attempting
echo '(set -x' #uses shells debug printer to show expanded vals
printf ': ${0+%s}\n' "$@" \
${NOCONFIRM-'Key "ENTER" to accept or "CTRL+C" to quit'}
echo \) #above can be disabled by declaring NOCONFIRM at invocation
} #by default fn batches 25 mvs at a time, displays them, and confirms
_read_loop () { #parses piped in with IFS, batches in INTerval of 25
argc=${1-$argc} ; ${1+shift} #total globbed files - quit point
while IFS=' -' read nl y m d na ; do #split on -
set -- "$@" "$nl" "$y$m$d" "$((i=i+1))" #build array until
[ "$#" -ge "$INT" ] && break #hit interval
done ; IFS='
'; set -- $(_print_fmt "$@") && unset IFS #finalize array in _print_fmt
_print_mv "$@" #do the debug out
${NOCONFIRM+:} read < /dev/tty #if $NOCONFIRM not set confirm
printf '%s\n' "$@" #now print the actual command
[ $((argc>i)) -eq 1 ] || echo 'exit 0' #check if quit point
_read_loop #if not quit repeat
}
_pipeline () { #this is mostly same - no sed though
stat -c "$TIME" -- "$@" | nl -nln -w1 -s ' ' | sort -k2,3 | {
_read_loop $# || echo 'exit 1' #read loop stands in for sed
} | sh -s -- "$@" #sh still evaluates on args
} #only two calls from main function below
_path_chk "$1" "$2" || ${ERR:?Invalid pathname parameters specified}
_pipeline "$DIR"/*"$SUFX" #if _path_chk do _pipeline
) #that's all folks
它使用 shell 来完成我使用其他实用程序所做的一些事情。概念是相同的 - 全局文件列表,以不同的方式排序并存储排序顺序。什么是真的这种想法的不同之处在于,它按时间间隔批量执行移动操作,向用户显示它将要执行的操作,并在继续之前等待提示。我用它记录了自己的情况这里这样您就可以观看终端会话以了解其工作原理。