Take the 2-minute tour ×
Super User is a question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

Previously I have ran dos2unix on every *.php file on my server recursively with this command:

find . -name '*.php' -type f -exec dos2unix --keepdate {} +

All files processed by dos2unix had its last modified datetime intact, but the parent directory containing such files still have their last modified datetime updated to the time I ran the command.

How do I, with an one liner, touch the directories recursively, setting the last modified datetime of the directories as the same datetime as the last modified file inside the directory?

From this:

dir_a/               2013-05-13 14:05
   abc.php           2012-09-01 12:34
   def.php           2012-09-15 23:45
   dir_b/            2013-05-13 14:05
       uvw.php       2012-10-01 01:23
       xyz.php       2012-10-08 09:10

To this:

dir_a/               2012-09-15 23:45
   abc.php           2012-09-01 12:34
   def.php           2012-09-15 23:45
   dir_b/            2012-10-08 09:10
       uvw.php       2012-10-01 01:23
       xyz.php       2012-10-08 09:10

The server is running on CentOS 5.6.

share|improve this question

2 Answers 2

up vote 2 down vote accepted

Maybe something like this:

find . -type d -print0 | while read -r -d '' dir; do file="$(find "$dir" -maxdepth 1 -type f  -printf '%T+ %p\n' | sort -r | head -1 | cut -d' ' -f2-)";if [ -n "$file" ]; then touch "$dir" -mr "$file"; fi; done

Explanation:

To pass modification date from one file to another we have to run:

touch file1 -mr file2

First we have to find subdirectories:

find . -type d -print0 | while read -r -d '' dir; do echo "$dir"; done

In this case -exec option would get too complex so I use while-read approach. You need to make sure to pipe the output from find with NUL as record separator, and tell read to split on that (-d '').

To find files with last modification date I can use:

find ./  -maxdepth 1 -type f  -printf '%T+ %p\n' | sort -r | head -1 | cut -d' ' -f2-

Combining it:

find . -mindepth 1  -type d -print0 | while read -r -d '' dir; do file="$(find "$dir" -maxdepth 1 -type f  -printf '%T+ %p\n' | sort -r | head -1 | cut -d' ' -f2-)";echo "$dir <- $file"; done

Finally, adding touch:

find . -mindepth 1  -type d -print0 | while read -r -d '' dir; do file="$(find "$dir" -maxdepth 1 -type f  -printf '%T+ %p\n' | sort -r | head -1 | cut -d' ' -f2-)";touch "$dir" -mr "$file"; done

I use -mindepth option here because I launched this command from directory containing dir_a which does not have any file to read date from. To eliminate this problem I can use if-else to ensure i do not try to read time from non existing file:

find . -type d -print0 | while read -r -d '' dir; do file="$(find "$dir" -maxdepth 1 -type f  -printf '%T+ %p\n' | sort -r | head -1 | cut -d' ' -f2-)";if [ -n "$file" ]; then touch "$dir" -mr "$file"; fi; done
share|improve this answer
    
Thanks for the answer, this worked almost perfectly, only failing when touch tries to get the last modified datetime from a file with spaces in the filename. –  Phil May 14 '13 at 7:43
2  
@Phil Yeah, that's probably because of the (broken) $(find …) construct in the middle. Try exchanging the -f2 to -f2- and surrounding the $() with double quotes. –  slhck May 14 '13 at 7:48
    
@slhck That fixed the problem with touch, but now I've noticed find is failing when a directory name contains spaces, can you point out how to fix this as well? Thanks. –  Phil May 14 '13 at 9:02
1  
@Phil True, that find … | while was broken as well. You need to call it with -print0 | while read … -d '' and quote the "$dir" in the inner loop. –  slhck May 14 '13 at 9:15

This should do it:

find -mindepth 1 -type d -print0 | while IFS= read -r -d '' dir; do 
 time=$(
  find "$dir" -mindepth 1 -maxdepth 1 -type f -exec stat -c %Y {} \; | 
  sort -nk2 | tail -n 1
 ); touch -d @$time "$dir"; 
 done

EXPLANATION:

  • find -mindepth 1 -type d | while IFS= read -r -d '' dir; do : iterate through all directories except the current one (-mindepth 1). Using a while loop with IFS= and the -r flag is to make sure the command works with directory names that contain spaces or new lines (\n) or other strange characters. The -d '' sets the delimiter to the null character so it can work with find's -print0 option.

  • find $dir -mindepth 1 -maxdepth 1 -type f -exec stat --printf="%Y\n" {} \; : Find all files of each directory $dir and run stat which prints their modification date in seconds since epoch.

  • sort -nk2 | tail -n 1 : this just sorts the dates and keeps the newest. The last two parts combined give the modification time of the newest file which is saved in the variable $time.

  • touch -d @$time $dir : here we use the touch command to set the modification time of each directory. The @ is there to tell touch that we a re giving time in seconds since epoch, this is a standard GNU coreutils feature.

share|improve this answer
    
Thanks for the answer, but I get a lot of touch: invalid date format `@' errors when I run this. –  Phil May 14 '13 at 7:41
    
@Phil Check if $time contains anything after the $(find …) is run. You should only get that error if $time is empty. –  slhck May 14 '13 at 8:35
    
@Phil that will happen if you have directories that don't contain any files. Since the value of $time depends on the newest file in each directory, it will be empty when the directories contain no files. This shouldn't be a problem though, just ignore those messages, you don't care about directories with no files. –  terdon May 14 '13 at 9:55

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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