find: Updating A Timestamp File

1 
1 10.3 Updating A Timestamp File
1 ==============================
1 
1 Suppose we have a directory full of files which is maintained with a set
1 of automated tools; perhaps one set of tools updates them and another
1 set of tools uses the result.  In this situation, it might be useful for
1 the second set of tools to know if the files have recently been changed.
1 It might be useful, for example, to have a 'timestamp' file which gives
1 the timestamp on the newest file in the collection.
1 
1    We can use 'find' to achieve this, but there are several different
1 ways to do it.
1 
1 10.3.1 Updating the Timestamp The Wrong Way
1 -------------------------------------------
1 
1 The obvious but wrong answer is just to use '-newer':
1 
1      find subdir -newer timestamp -exec touch -r {} timestamp \;
1 
1    This does the right sort of thing but has a bug.  Suppose that two
1 files in the subdirectory have been updated, and that these are called
1 'file1' and 'file2'.  The command above will update 'timestamp' with the
1 modification time of 'file1' or that of 'file2', but we don't know which
1 one.  Since the timestamps on 'file1' and 'file2' will in general be
1 different, this could well be the wrong value.
1 
1    One solution to this problem is to modify 'find' to recheck the
1 modification time of 'timestamp' every time a file is to be compared
1 against it, but that will reduce the performance of 'find'.
1 
1 10.3.2 Using the test utility to compare timestamps
1 ---------------------------------------------------
1 
1 The 'test' command can be used to compare timestamps:
1 
1      find subdir -exec test {} -nt timestamp \; -exec touch -r {} timestamp \;
1 
1    This will ensure that any changes made to the modification time of
1 'timestamp' that take place during the execution of 'find' are taken
1 into account.  This resolves our earlier problem, but unfortunately this
1 runs much more slowly.
1 
1 10.3.3 A combined approach
1 --------------------------
1 
1 We can of course still use '-newer' to cut down on the number of calls
1 to 'test':
1 
1      find subdir -newer timestamp -and \
1           -exec test {} -nt timestamp \; -and \
1           -exec touch -r {} timestamp \;
1 
1    Here, the '-newer' test excludes all the files which are definitely
1 older than the timestamp, but all the files which are newer than the old
1 value of the timestamp are compared against the current updated
1 timestamp.
1 
1    This is indeed faster in general, but the speed difference will
1 depend on how many updated files there are.
1 
1 10.3.4 Using '-printf' and 'sort' to compare timestamps
1 -------------------------------------------------------
1 
1 It is possible to use the '-printf' action to abandon the use of 'test'
1 entirely:
1 
1      newest=$(find subdir -newer timestamp -printf "%A%p\n" |
1                 sort -n |
1                 tail -1 |
1                 cut -d: -f2- )
1      touch -r "${newest:-timestamp}" timestamp
1 
1    The command above works by generating a list of the timestamps and
1 names of all the files which are newer than the timestamp.  The 'sort',
1 'tail' and 'cut' commands simply pull out the name of the file with the
1 largest timestamp value (that is, the latest file).  The 'touch' command
1 is then used to update the timestamp,
1 
1    The '"${newest:-timestamp}"' expression simply expands to the value
1 of '$newest' if that variable is set, but to 'timestamp' otherwise.
1 This ensures that an argument is always given to the '-r' option of the
1 'touch' command.
1 
1    This approach seems quite efficient, but unfortunately it has a
1 problem.  Many operating systems now keep file modification time
1 information at a granularity which is finer than one second.  Findutils
1 version 4.3.3 and later will print a fractional part with %A@, but older
1 versions will not.
1 
1 10.3.5 Solving the problem with 'make'
1 --------------------------------------
1 
1 Another tool which often works with timestamps is 'make'.  We can use
1 'find' to generate a 'Makefile' file on the fly and then use 'make' to
1 update the timestamps:
1 
1      makefile=$(mktemp)
1      find subdir \
1      	\( \! -xtype l \) \
1      	-newer timestamp \
1      	-printf "timestamp:: %p\n\ttouch -r %p timestamp\n\n" > "$makefile"
1      make -f "$makefile"
1      rm   -f "$makefile"
1 
1    Unfortunately although the solution above is quite elegant, it fails
1 to cope with white space within file names, and adjusting it to do so
1 would require a rather complex shell script.
1 
1 10.3.6 Coping with odd filenames too
1 ------------------------------------
1 
1 We can fix both of these problems (looping and problems with white
1 space), and do things more efficiently too.  The following command works
1 with newlines and doesn't need to sort the list of filenames.
1 
1      find subdir -newer timestamp -printf "%A@:%p\0" |
1         perl -0 newest.pl |
1         xargs --no-run-if-empty --null -i \
1            find {} -maxdepth 0 -newer timestamp -exec touch -r {} timestamp \;
1 
1    The first 'find' command generates a list of files which are newer
1 than the original timestamp file, and prints a list of them with their
1 timestamps.  The 'newest.pl' script simply filters out all the filenames
1 which have timestamps which are older than whatever the newest file is:
1 
1      #! /usr/bin/perl -0
1      my @newest = ();
1      my $latest_stamp = undef;
1      while (<>) {
1          my ($stamp, $name) = split(/:/);
1          if (!defined($latest_stamp) || ($tstamp > $latest_stamp)) {
1              $latest_stamp = $stamp;
1              @newest = ();
1          }
1          if ($tstamp >= $latest_stamp) {
1              push @newest, $name;
1          }
1      }
1      print join("\0", @newest);
1 
1    This prints a list of zero or more files, all of which are newer than
1 the original timestamp file, and which have the same timestamp as each
1 other, to the nearest second.  The second 'find' command takes each
1 resulting file one at a time, and if that is newer than the timestamp
1 file, the timestamp is updated.
1