So, say, I have a directory with a bunch of files like g.txt, where g.txt was last modified on, say, June 20, 2012.
How would I batch-rename all of the files (like g.txt) with the last modified date of June 20, 2012 appended on the end?
So, say, I have a directory with a bunch of files like g.txt, where g.txt was last modified on, say, June 20, 2012.
How would I batch-rename all of the files (like g.txt) with the last modified date of June 20, 2012 appended on the end?
here's a version of goldschrafe's one-liner that:
doesn't use stat
works with earlier versions of GNU date
correctly copes with any spaces in the filenames
also copes with filenames beginning with a dash
for f in *; do mv -- "$f" "$f-$(date -r "$f" +%Y%m%d)"; done
My answer to this question has been bothering me for years (well, only when I remember it - on days like today when it gets another upvote) so I'm finally updating it. My original answer above still works, but the updated answer below is better.
Like any batch file renaming operation, this should be done with the perl rename
utility, not with some klunky shell for loop.
The perl rename utility is effectively a specialised scripting language that allows you to use ANY perl code to rename files, from simple s/search/replace/
regular expression operations (which suffices for most renaming tasks) to complex multi-line scripts.
e.g.
rename -n 'BEGIN {use Date::Format};
die $! unless -f $_;
next if (m/-\d{8}$/);
my $ts=(stat($_))[9];
my $dt=time2str("%Y%m%d",$ts);
s/$/-$dt/;' *.txt
This requires only perl and the Date::Format
module (a module that is so useful that it should be installed on any system with perl. IMO it, along with the author Graham Barr's Date::Parse
module, should be part of perl's core module library but it isn't so you'll have to install it with cpan
or a distro package like Debian's libtimedate-perl
package).
BTW, this script skips any file that looks like it already has a date (i.e. 8 digits) at the end of the filename.
or, for a fancier version that puts the date before the file's suffix (if any):
rename -n 'BEGIN {use Date::Format; use File::Basename};
die $! unless -f $_;
my ($filename,$dirs,$suffix) = fileparse($_,qr/\.[^.]*/);
next if (m/-\d{8}${suffix}$/);
my $ts=(stat($_))[9];
my $dt=time2str("%Y%m%d",$ts);
s/${suffix}$/-${dt}${suffix}/;' *.txt
This version has no extra requirements because the File::Basename
module has been included as a standard core module with perl for as long as I can remember (a decade at least, probably longer).
Note: both rename
scripts above use rename's -n
(aka --nono
) "dry-run" option so that the results can be tested/simulated before being applied. Remove the -n
(or replace it with -v
for verbose output) when you're sure that it does what you want.
Also Note: as with any other perl rename
script, this can rename filenames supplied on the command line and/or from standard input. e.g. to rename all .txt files in the current directory and all sub-directories:
find . -type f -iname '*.txt' -print0 |
rename -0 --nofullpath -n '..........'
BTW, I have used rename
's --nofullpath (aka -d
, --filenmame
, --nopath
) option here to ensure that it renames only the filename portion of any filepaths found. It's not needed in this particular case (because the example rename scripts only change the end of the filename) but is generally a good idea when you don't want to rename the path as well as the filename (e.g. a rename script like 's/ //g'
to strip spaces from filenames would try to remove any spaces in the path as well as the filename without --nofullpath
, probably causing it to fail with an error).
Finally: do not confuse the perl rename script (aka File::Rename
, or sometimes called prename
on Fedora & RedHat, or perl-rename
) with any other program called rename. Only this perl-based rename utility can rename files using arbitrary perl code as shown above, any other rename utility will have different capabilities and different & incompatible command-line options.
You can check if you have the right rename installed:
$ rename -V
/usr/bin/rename using File::Rename version 1.13, File::Rename::Options version 1.10
The executable might be called prename
or perl-rename
or file-rename
on your system, so try -V
with those, and adjust the examples above to use the correct executable name.
Quick-and-dirty Bash one-liner to rename all (globbed) files in the current directory from filename.txt
to filename.txt-20120620
:
for f in *; do mv -- "$f" "$f-$(stat -c %Y "$f" | date +%Y%m%d)"; done
An enterprising Bash nerd will find some edge case to break it, I'm sure. :)
Obviously, this doesn't do desirable things like checking whether a file already has something that looks like a date at the end.
date
implementation is that? My GNU date
doesn't seem to handle input.
– manatwork
Jul 19 '12 at 07:33
date -r
. If this is uncommon, it must be some weird Red Hat customization.
– jgoldschrafe
Jul 20 '12 at 03:59
for f in *; do echo "${f%.*}-$(stat -f %SB -t "%Y%m%d" $f).${f##*.}"; done
So this will go from filename.txt to filename-20120620.txt
– bergie3000
Dec 04 '14 at 06:54
date
that did that though.
– cas
Sep 01 '15 at 09:22
date
started to read its stdin as soon as it's not a tty, that would break all scripts using date that are not run in a terminal or with their stdin redirected to something. Programs that do read their stdin, like interactive ones (think shells or rm -i
) may change their behaviour when stdin is not a tty, but those that are not meant to read from stdin are not going to start doing so when they're not run in a terminal.
– Stéphane Chazelas
Sep 01 '15 at 09:39
Obligatory zsh one-liner (not counting the one-time loading of optional components):
zmodload zsh/stat
autoload -U zmv
zmv -n '(*)' '$1-$(stat -F %Y%m%d +mtime -- $1)'
We use the stat
builtin from the zsh/stat
module, and the zmv
function to rename files. And here's an extra which places the date before the extension, if any.
zmv -n '(*)' '$1:r-$(stat -F %Y%m%d +mtime -- $1)${${1:e}:+.$1:e}'
zmv -n '*' '$f:r-$(stat -F %Y%m%d +mtime -- $f)${(M)f%.*}'
– Stéphane Chazelas
Mar 20 '21 at 07:22
As I understood we don't know beforehand what is the modification date. So we need to get it from each file, format the output and rename each file in a way so that it includes the modification date in the filenames.
You can save this script as something like "modif_date.sh" and make it executable. We invoke it with the target directory as the argument:
modif_date.sh txt_collection
Where "txt_collection" is the name of the directory where we have all the files that we want to rename.
#!/bin/sh
# Override any locale setting to get known month names
export LC_ALL=c
# First we check for the argument
if [ -z "$1" ]; then
echo "Usage: $0 directory"
exit 1
fi
# Here we check if the argument is an absolute or relative path. It works both ways
case "${1}" in
/*) work_dir=${1};;
*) work_dir=${PWD}/${1};;
esac
# We need a for loop to treat file by file inside our target directory
for i in *; do
# If the modification date is in the same year, "ls -l" shows us the timestamp.
# So in this case we use our current year.
test_year=`ls -Ggl "${work_dir}/${i}" | awk '{ print $6 }'`
case ${test_year} in *:*)
modif_year=`date '+%Y'`
;;
*)
modif_year=${test_year}
;;
esac
# The month output from "ls -l" is in short names. We convert it to numbers.
name_month=`ls -Ggl "${work_dir}/${i}" | awk '{ print $4 }'`
case ${name_month} in
Jan) num_month=01 ;;
Feb) num_month=02 ;;
Mar) num_month=03 ;;
Apr) num_month=04 ;;
May) num_month=05 ;;
Jun) num_month=06 ;;
Jul) num_month=07 ;;
Aug) num_month=08 ;;
Sep) num_month=09 ;;
Oct) num_month=10 ;;
Nov) num_month=11 ;;
Dec) num_month=12 ;;
*) echo "ERROR!"; exit 1 ;;
esac
# Here is the date we will use for each file
modif_date=`ls -Ggl "${work_dir}/${i}" | awk '{ print $5 }'`${num_month}${modif_year}
# And finally, here we actually rename each file to include
# the last modification date as part of the filename.
mv "${work_dir}/${i}" "${work_dir}/${i}-${modif_date}"
done
stat
utility or none at all.
– Gilles 'SO- stop being evil'
Jul 19 '12 at 00:42
ls
is not reliable. On some unix variants, user and group names can contain spaces, which will throw off the alignment of the date columns.
– Gilles 'SO- stop being evil'
Jul 19 '12 at 00:48
LC_ALL=c
gives an error message. It would probably be better for this to be LC_ALL=C
(capitalized). (2) The script checks whether $1
is null (zero-length), but not whether it is a directory. (3) Big fail: for i in *
iterates over filenames in the current directory, but the rest of the script expects $i
to be the name of a file in $work_dir
. … (Cont’d)
– G-Man Says 'Reinstate Monica'
Mar 27 '17 at 19:39
num_day
, *and then,* on a separate line, set modif_date
from the concatenation of $num_day
, $num_month
, and $modif_year
. (And, IMHO, it might be slightly clearer if it didn’t use so many unnecessary braces, but, yeah, I know that’s pretty much a style issue.) … (Cont’d)
– G-Man Says 'Reinstate Monica'
Mar 27 '17 at 19:40
ls
as a date and a time (rather than a date and a year) must be in the current year. This is not true. It is in the past six months; that could be in the prior year (e.g., 27-Sept-2016 through 31-Dec-2016 are within the past six months). (6) At the risk of parroting Greg’s Wiki (which @Gilles already cited), filenames that contain newline(s) and spaces can cause this to fail. … (Cont’d)
– G-Man Says 'Reinstate Monica'
Mar 27 '17 at 19:41
z(newline)a b c d e f
could cause $name_month
to be set to something like Apr(newline)d
and $test_year
to 2011(newline)f
. (This could be fixed by the simple kludge of saying awk 'NR==1 { print $4 }'
.) (7) Similarly, this blows up if subdirectories are present, because the ls
commands don’t include the -d
option. (For want of a nail, …) (8) Opportunity for optimization: ls
each file only once. (9) Why do we need to convert relative path names to absolute?
– G-Man Says 'Reinstate Monica'
Mar 27 '17 at 19:41
Here is version of cas's one-liner, (based on goldschrafe's oneliner) extended with idempotence,
i.e. extended to prefix files with date time and to do this only for those not having date time prefix yet .
Use-case: helpful if you add new files into directory and want to add date time prefix to those not having one yet.
for f in * ; do
if [[ "$f" != ????-??-??' '??:??:??' - '* ]]; then
mv -v -- "$f" "$(date -r "$f" +%Y-%m-%d' '%H:%M:%S) - $f" ;
fi;
done
for f in *.jpg ; do jhead -ft "$f" ; done
source: https://unix.stackexchange.com/a/290755/9689
– Grzegorz Wierzowiecki
Jun 30 '18 at 14:37
date -r "$f" +%Y%m%d
or it won't work ifPOSIXLY_CORRECT
is in the environment. Generally, options should go before other arguments. – Stéphane Chazelas Sep 01 '15 at 09:44"${f}"
. – G-Man Says 'Reinstate Monica' Mar 24 '17 at 02:57rename
version. I appreciate your excellence in keeping your answer up to date! – Thunder Rabbit Aug 14 '21 at 07:59