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.
date -r "$f" +%Y%m%d
or it won't work if POSIXLY_CORRECT
is in the environment. Generally, options should go before other arguments.
Commented
Sep 1, 2015 at 9:44
"${f}"
.
Commented
Mar 24, 2017 at 2:57
rename
version. I appreciate your excellence in keeping your answer up to date!
Commented
Aug 14, 2021 at 7:59
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.
Commented
Jul 19, 2012 at 7:33
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.
Commented
Sep 1, 2015 at 9: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%.*}'
Commented
Mar 20, 2021 at 7: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.
Commented
Jul 19, 2012 at 0: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.
Commented
Jul 19, 2012 at 0:48
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)
Commented
Mar 27, 2017 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: unix.stackexchange.com/a/290755/9689
Commented
Jun 30, 2018 at 14:37