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%dor it won't work ifPOSIXLY_CORRECTis 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:57renameversion. I appreciate your excellence in keeping your answer up to date! – Thunder Rabbit Aug 14 '21 at 07:59