23

I was wondering if anyone knows how to change the timestamps of folders recursively based on the latest timestamp found of the files in that folder.

So for example:

jon@UbuntuPanther:/media/media/MP3s/Foo Fighters/(1997-05-20) The Colour and The Shape$ ls -alF
total 55220
drwxr-xr-x  2 jon jon    4096 2010-08-30 12:34 ./
drwxr-xr-x 11 jon jon    4096 2010-08-30 12:34 ../
-rw-r--r--  1 jon jon 1694044 2010-04-18 00:51 Foo Fighters - Doll.mp3
-rw-r--r--  1 jon jon 3151170 2010-04-18 00:51 Foo Fighters - Enough Space.mp3
-rw-r--r--  1 jon jon 5004289 2010-04-18 00:52 Foo Fighters - Everlong.mp3
-rw-r--r--  1 jon jon 5803125 2010-04-18 00:51 Foo Fighters - February Stars.mp3
-rw-r--r--  1 jon jon 4994903 2010-04-18 00:51 Foo Fighters - Hey, Johnny Park!.mp3
-rw-r--r--  1 jon jon 4649556 2010-04-18 00:52 Foo Fighters - Monkey Wrench.mp3
-rw-r--r--  1 jon jon 5216923 2010-04-18 00:51 Foo Fighters - My Hero.mp3
-rw-r--r--  1 jon jon 4294291 2010-04-18 00:52 Foo Fighters - My Poor Brain.mp3
-rw-r--r--  1 jon jon 6778011 2010-04-18 00:52 Foo Fighters - New Way Home.mp3
-rw-r--r--  1 jon jon 2956287 2010-04-18 00:51 Foo Fighters - See You.mp3
-rw-r--r--  1 jon jon 2730072 2010-04-18 00:51 Foo Fighters - Up in Arms.mp3
-rw-r--r--  1 jon jon 6086821 2010-04-18 00:51 Foo Fighters - Walking After You.mp3
-rw-r--r--  1 jon jon 3033660 2010-04-18 00:52 Foo Fighters - Wind Up.mp3

The folder "(1997-05-20) The Colour and The Shape" would have its timestamp set to 2010-04-18 00:52.

10 Answers 10

24

You can use touch -r to use another file's timestamp instead of the current time (or touch --reference=FILE)

Here are two solutions. In each solution, the first command changes the modification time of the directory to that of the newest file immediately under it, and the second command looks at the whole directory tree recursively. Change to the directory (cd '.../(1997-05-20) The Colour and The Shape') before running any of the commands.

In zsh (remove the D to ignore dot files):

touch -r *(Dom[1]) .
touch -r **/*(Dom[1]) .

On Linux (or more generally with GNU find):

touch -r "$(find -mindepth 1 -maxdepth 1 -printf '%T+=%p\n' |
            sort |tail -n 1 | cut -d= -f2-)" .
touch -r "$(find -mindepth 1 -printf '%T+=%p\n' |
            sort |tail -n 1 | cut -d= -f2-)" .

However note that those ones assume no newline characters in file names.

8
  • The commands work (I used the Linux ones) but it's not working recursively. I ran it in my root folder (/media/media/MP3s) and no such luck with the rest of the artist directories in there. Thank you for your help so far. Sep 3, 2010 at 1:01
  • @MonkeyWrench32: I answered your question as I understood it. Do you want to apply the command to every directory underneath /media/media/MP3s? Then in zsh: for d in /media/media/MP3s/**/*(/); do touch -r $d/*(Dom[1]) $d; done. Without zsh (but really, use zsh, it's just simpler): put the command in a script and run find /media/media/MP3s -type d -execdir /path/to/the/script \;. Sep 3, 2010 at 7:10
  • Perfection! You are a zsh master! :D Sep 3, 2010 at 15:30
  • If you use for d in ... how can you adapt the following, so it will also work with folders, that contain spaces? (that is still missing in my solution)
    – rubo77
    Aug 16, 2013 at 11:36
  • @rubo77 I don't understand what you're talking about. All the solutions I posted work on file names containing spaces (some of them fail on file names containing newlines). Note that some of my solutions require zsh, which doesn't require double quotes around variable substitutions. Aug 16, 2013 at 12:16
9

If you want to update all the timestamps in the current directory and children to the current time:

find . -exec touch {} \;
4

That's not "recursively", it's just changing all the timestamps in a folder. If that's what you mean, there's two steps.

stat -c '%Y' filename will output the timestamp of filename, and stat -c '%Y %n' * will output the timestamp and filename of every file in the folder, so this will find the filename of the most recently modified file in the current folder:

mostrecent="`stat -c '%Y %n' * | sort -n | tail -n1 | cut -d ' ' -f '2-'`"

On second thought, there's a way easier way to get the highest timestamp in the folder:

mostrecent="`ls -t | head -n1`"

Then you want to change all the files in the folder to have the same timestamp as that file. touch -r foo bar will change bar to have the same modified timestamp as foo, so this will change all the files in the folder to have the same modified timestamp as your most recently modified file:

touch -r "$mostrecent" *

So, the one-liner is:

touch -r "`ls -t | head -n1`" *
2
  • Parsing the output of ls is a bad idea, though I admit it's a tempting case. It's still risky if ls mangles file names containing non-ASCII characters. If you want one-liners, use zsh:-) Sep 2, 2010 at 21:42
  • @Gilles It is, but I've yet to run into a problem doing it; I suppose because my filenames don't contain non-ASCII characters. Your answer is better though Sep 2, 2010 at 21:45
1

I put the work together and now:

This would be a script that changes all directories inside /tmp/test/ to the timestamp of the newest file inside each directory:

#!/bin/bash
if [ -z "$1" ] ; then
  echo 'ERROR: Parameter missing. specify the folder!'
  exit
fi
#MODE=tail # change to newest file
MODE=head # change to oldest file
for d in "$1"/*/; do
  echo "running on $d"
  find "$d" -type d -execdir \
    echo touch --reference="$(find "$d" -mindepth 1 -maxdepth 1 -printf '%T+=%p\n' \
                              | sort | "$MODE" -n 1 | cut -d= -f2-)" "$d" \;
    # remove echo to really run it
done

you can add some testing files in /tmp like this:

mkdir /tmp/test
cd /tmp/test
mkdir d1
mkdir d2
touch d1/text1.txt
sleep 1
touch d1/movie1.mov
touch d2/movie2.mov
sleep 1
touch d2/text2.txt
touch notthis.file
2
1

I've taken @Gilles zsh command and enhanced it to work in subfolders, but it seems that zsh is terribly inefficient for the **/*(FDod) part.

# Don't do this
for d in **/*(FDod); do touch -r "$d"/*(Dom[1]) -- "$d"; done

The quotes allow directory entries containing spaces and tabs to work correctly. The FD makes it find non-empty directories, including those starting with ., the od makes it search in the depth-first manner, so that the parent folder timestamps get updated properly.

When testing, I noticed that the performance and memory footprint for **/*(FDod) was insane (over 1.4GB for just 650k folders), and it read the whole lot before starting to touch folders. After replacing it with find/read it became much faster, didn't burn memory, and started almost straight away.

#! /usr/bin/env zsh
# Do this instead
find "$@" -depth -type d ! -empty -print0 |while IFS= read -r -d ''; do
    touch -r "$REPLY"/*(Dom[1]) "$REPLY"
done

If you aren't running it in a script, replace "$@" with the folders you want to run it from.

1
  • zsh doesn't do split+glob upon unquoted parameter expansions, so the quotes are not necessary there (though they won't harm). Feb 25, 2022 at 6:38
1

I wanted to update the mtime on all directories and files in a particular directory to the current timestamp.

Here's what worked for me -

# recursively update mtime on all directories
find ./somedir -type d -exec touch -t $(date "+%C%y%m%d%H%M%S") {} + 

# recursively update mtimes on all files
find ./somedir -type f -exec touch {} +
2
  • touch -t expects a date in [[CC]YY]MMDDhhmm[.ss] format while date +%s gives the number of Unix seconds between 1970-01-01Z00:00:00 and now. Feb 25, 2022 at 6:31
  • Thanks for catching that. I updated the answer.
    – Thank you
    Feb 25, 2022 at 20:18
1

I use this script to recursively set folder timestamps to latest content. (It's very similar to Gilles' answer):

#! /bin/bash

# Change mtime of directories to that of latest file (or dir) under it, recursively
# Empty leaf directories, or dirs with only symlinks get the $default_timestamp

default_timestamp='1980-01-01 00:00:00'

dir="$1"

[ -d "$dir" ] || { echo "Usage: $0 directory" >&2; exit 1; }

find "$dir" -depth -type d | while read d; do
    latest=$(find "$d" -mindepth 1 -maxdepth 1 \( -type f -o -type d \) -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2-)
    if [ -n "$latest" ]; then
        touch -c -m -r "$latest" "$d" \
            || echo "ERROR setting mtime of '$d' using ref='$latest'" >&2
    else
        touch -c -m -d "$default_timestamp" "$d" \
            || echo "ERROR setting mtime of '$d' to default '$default_timestamp'" >&2
    fi
done
0

Very short, portable way:

find . -depth -type d -ls -exec sh -c 'cd "{}" && touch -r "$(ls -1td | head -n1)" .' \;

(Ok, maybe not very.)

2 tricks:

  1. Must use find's -depth so you start with the lowest folders first
  2. Use ls -1tdb | head to find the file (or directory) with the most recent time stamp in that folder. -1td == one per line, ordered by time, include directories.

This is not robust production code as it is vulnerable to injection attacks by creating very odd directory or file names. This is intended as a helper for a command line user, not as a robust piece of code suitable for a production script.

If your version of find include -execdir, use that instead of -exec and drop the cd "{}" for a safer version.

1
  • 1
    Never embed {} in the code argument of some language interpreter (here sh)! That makes it a command injection vulnerability. Passing * to ls is redundant, ls is the tool to list directories. You're not checking for success of cd before running the next command. That approach (or any that parses the output of ls) won't work if there are filenames with newline characters. Note that there's nothing Linux nor GNU specific to that code, though it's not portable as that {} won't be expanded by all find implementations. Feb 25, 2022 at 6:36
0

Some of these solutions are overly complex. Here is a simple bash for loop based solution.

# feel free to rename the function (lol)
function set_dir_timestamp_based_on_newest_file() {
  for d in $1/*; do touch -r "$1/$d/$(ls -t $1/$d|head -n1)" $1/$d; done
}

To use just:

set_dir_timestamp_based_on_newest_file /home/$USER/Downloads/

If you have no care about locking up your system, feel free to change the second ; to an & do run all of the touch -r commands in parallel.

function parallel_set_dir_timestamp_based_on_newest_file() {
 for d in $1/*; do 
    touch -r "$1/$d/$(ls -t $1/$d|head -n1)" $1/$d & 
 done
}
-1

I didn't really like the obscurity of the shell commands and quickly did it in python.

It recursively sets all directory mtimes to the newest non-excluded file/dir mtime inside.

import os
import sys

EXCLUDE_FILES = ['.DS_Store']
EXCLUDE_DIRS = ['.git', '.hg']

def inheritMTime(path):
    for root, dirs, files in os.walk(path, topdown=False, followlinks=False):
        currentMaxMtime = None
        for subpath in files:
            if subpath in EXCLUDE_FILES:
                print "ignore file", os.path.join(root, subpath)
                continue
            currentMaxMtime = max(currentMaxMtime, os.lstat(os.path.join(root, subpath)).st_mtime)
        for subpath in dirs:
            if subpath in EXCLUDE_DIRS:
                print "ignore dir", os.path.join(root, subpath)
                continue
            currentMaxMtime = max(currentMaxMtime, os.lstat(os.path.join(root, subpath)).st_mtime)
        if currentMaxMtime is not None:
            os.utime(root, (currentMaxMtime, currentMaxMtime))

if __name__ == "__main__":
    for dir in os.listdir(sys.argv[1]):
        if os.path.isdir(dir):
            current = os.path.join(sys.argv[1], dir)
            inheritMTime(current)

You must log in to answer this question.

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