17

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?

5 Answers 5

25

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


Update 2021-03-20

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.

5
  • 1
    You should make it 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
  • this should be the "correct answer" Commented Mar 2, 2016 at 9:10
  • Of course you don't need the braces in "${f}". Commented Mar 24, 2017 at 2:57
  • but how would you make it such that the dates we prepended e.g "Ymd-filename" ?
    – timebandit
    Commented Feb 10, 2018 at 9:30
  • Thank you for the new rename version. I appreciate your excellence in keeping your answer up to date! Commented Aug 14, 2021 at 7:59
13

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.

10
  • 2
    +1, looks good. you can make it a little more robust by using '--' as the first arg to mv. guards against filenaes begining with '-'
    – cas
    Commented Jul 18, 2012 at 3:28
  • 6
    What kind of date implementation is that? My GNU date doesn't seem to handle input.
    – manatwork
    Commented Jul 19, 2012 at 7:33
  • 2
    @jgoldschrafe, are you sure that code properly works for you? pastebin.com/nNYubf3A
    – manatwork
    Commented Jul 19, 2012 at 15:00
  • 5
    good catch manatwork. my GNU date 8.13 doesn't support reading from stdin either. It does support using the mod time of a file with "-r" - e.g. "date +%Y%m%d -r $f", so stat isn't even needed in this particular case.
    – cas
    Commented Jul 20, 2012 at 0:45
  • 1
    @cas, if 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
4

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}'
1
  • Or zmv -n '*' '$f:r-$(stat -F %Y%m%d +mtime -- $f)${(M)f%.*}' Commented Mar 20, 2021 at 7:22
1

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
9
  • 1
    read the man page for stat(1) to find out how you can shrink your script by about 3/4...most of the above is re-inventing the wheel. in particular, 'stat -c %Y' will give you the modification time of a file in seconds since the epoch (1970-01-01 00:00:00). This can then be used as input to date(1) to format the timestamp as required. see jgoldschrafe's answer above for an example.
    – cas
    Commented Jul 18, 2012 at 5:56
  • @CraigSanders The upside is that this script works under operating systems other than Linux, which have a different stat utility or none at all. Commented Jul 19, 2012 at 0:42
  • Note that parsing the output of 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
  • @giles: true, but anyone sensible :-) would install GNU coreutils (and all of the other GNU tools) on any non-linux system. It's the easiest way to get not only a good set of userland tools, but also a consistent set regardless of the underlying kernel.
    – cas
    Commented Jul 19, 2012 at 4:54
  • 1
    (Cont’d) …  (5) Another bug: it assumes that any date/time displayed by 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
0

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
1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .