4

How do I change the timestamp of a directory and all the sub-folders within that directory to reflect the modification times of the contained files?

For example with this directory structure:

[Jan 9]  root
├── [Jan 3]  file1
├── [Jan 7]  file2
├── [Jan 6]  sub1
│   ├── [Jan 2]  file3
│   └── [Jan 1]  file4
└── [Jan  4] sub2
    └── [Jan 8]  file5

Here is a one liner to generate that:

mkdir -p root/sub1 root/sub2 && touch -d '2018-01-08' root/sub2/file5 && touch -d '2018-01-04' root/sub2/ && touch -d '2018-01-01' root/sub1/file4 && touch -d '2018-01-02' root/sub1/file3 && touch -d '2018-01-06' root/sub1/ && touch -d '2018-01-07' root/file2 && touch -d '2018-01-03' root/file1 && touch -d '2018-01-09' root/

It can be listed with tree -D

I'd like to change the timestamps on the three directories to be:

[Jan 8]  root
├── [Jan 3]  file1
├── [Jan 7]  file2
├── [Jan 2]  sub1
│   ├── [Jan 2]  file3
│   └── [Jan 1]  file4
└── [Jan 8]  sub2
    └── [Jan 8]  file5

Note:

  • The current timestamps on the directories are completely ignored and the new time stamps are set only based on the contents.
  • Time stamps bubble up to multiple levels of parent directories.

The reason that I'm doing this is for a directory that gets copied with rsync. The directory is checked into git and could get rsynced from any place that has the repository checked out. To ensure that rsync is consistent and idempotent from the various places, I need to ensure that the time stamps and permissions of everything are in a known state. I already have a script that sets the timestamps of files based on when they were committed to git. I also have a script that sets the permissions on all files and directories to a known state. The only portion that I'm struggling with is bubbling time stamps from the files up to parent directories.

I would like one line or short script that I can run from the command line to set directory timestamps based on the timestamps of their contents.

2

If you can use Zsh, this seems to do what you wish:

$ mkdir -p x/y; touch x/y/a x/b; sleep .1; touch x/y/a; sleep .1; touch x/b
$ find -depth -type d -execdir zsh -c 'touch "$1" -r "$1"/*(om[1])' zsh {} \;

Test output with GNU find, ./x/b was the newest, its timestamp is copied to ./x and ..

$ find -printf "%TT %p\n"  | sort -n
15:16:24.0182222830 ./x/y
15:16:24.0182222830 ./x/y/a
15:16:24.1222150510 .
15:16:24.1222150510 ./x
15:16:24.1222150510 ./x/b

Finding the newest file by timestamp is somewhat tricky in general, if there are filenames with whitespace/newlines or such. (See BashFAQ 099.) Zsh does make it rather easy, though, the trick above is from Is it possible to reference the most recently modified file in a command line argument?

-depth tells find to handle subdirectories before their parents, so that the bubbling up actually works.

This doesn't actually fully ignore timestamps of directories since they must be counted when the timestamps bubble up. A directory containing files will get its timestamp from the files, but an empty directory will keep its original timestamp, and that timestamp will bubble up. I'm not sure if it's worth working around that.

  • Or for d (**/*(NDF/od)) touch -r $d/*(Dom[1]) -- $d – Stéphane Chazelas Jan 10 '18 at 13:44
  • Thanks for bringing up the wrinkles around empty directories. In my case I shouldn't have any because git removes empty directories during checkout. – Stephen Ostermiller Jan 10 '18 at 14:31
1

Unless I'm overlooking something (haven't tested this extensively):

find . -depth -type d -execdir \
    sh -c 'touch "$PWD/$0" -r "$PWD/$0/$( ls -t "$PWD/$0" | head -n 1 )"' \
    {} \;

What it does:

  • finds all subdirectories of .
  • fetches their full names
  • with ls -t, lists their contents in reverse datetime order and gets the first
  • uses touch to touch the directory using its newest object as timestamp

For correct datetime "bubbling", the -depth option ensures that subdirectories are processed before the current subdirectory ("depth first"). Once the subdirectories have been processed and their datetimes updated, the current directory is processed (and possibly inherits one of the daughter subdirectories' timestamp).

  • I put a command into the question to create the testing directory. When I run your answer against it the root directory gets a timestamp of Jan 7 rather than Jan 8 indicating that the bubbling up isn't working. – Stephen Ostermiller Jan 10 '18 at 12:45
  • Adding -depth to find like ilkkachu's anser makes this answer work as well. – Stephen Ostermiller Jan 10 '18 at 14:30
  • With that patch, this seems to work well. It has the same empty directory caveat brought up by ilkkachu and handles file names with spaces. – Stephen Ostermiller Jan 10 '18 at 14:41
  • That's good. As I wrote, I did not remember whether find went depth-first and had an option to go breadth-first, or the other way around. Apparently it was the other way around. Sorry for the inconvenience, I hadn't a Linux box available for testing. – LSerni Jan 10 '18 at 16:46
  • 1
    Can you edit your answer to include the -depth flag? If so, I can accept it. – Stephen Ostermiller Jan 11 '18 at 23:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.