July 26, 2007

Posted by John

Older: Customizing Logger

Newer: I Can Has Command Line?

Last Modified File in Directory

The code below recursively loops through a directory and returns the file that was modified most recently. You can even pass in an extension to limit the possible returns to.

require 'find'

def last_modified_file(dir, o={})
  options = {:ext => nil}.merge(o)
  files = []
  Find.find('.') do |f|
    if options[:ext]
      files << f if File.extname(f) == options[:ext]
      files << f
  files.max { |a, b| File.mtime(a) <=> File.mtime(b) }

puts last_modified_file('.')                # last modified file
puts last_modified_file('.', :ext => '.as') # last modified file with .as extension
puts last_modified_file('.', :ext => '.rb') # last modified file with .rb extension
puts last_modified_file('.', :ext => '')    # last modified file with no extenstion

I’m using it on an ActionScript project. I got tired of rebuilding the class every minute to check for errors and see if the code was working properly. This method is part of a script that checks if any .as files have changed in the directory since the last build and if so rebuilds the main document class into a swf. I tried to slim it down more but couldn’t really think of anything. Anyone have any better ideas? If not, just take this post as an encouragement to check out the Find class.


  1. Something like this might work:

      def last_modified_file(dir, options = {})
        Dir["#{dir}**/*#{options[:ext]}"].map { |p| [ p, File.mtime(p) ] }.max { |a,b| a[1] &lt;=&gt; b[1] }[0] rescue nil

    This uses a Swartzian Transform so that mtime is only called once per file, rather than twice per comparison.

  2. Looks like I spoke too soon. No sooner did I submit this when I remembered sort_by, which does a “Swartzian Transform” under the hood:

    def last_modified_file(dir, options = {})
      Dir["#{dir}**/*#{options[:ext]}"].sort_by { |p| File.mtime(p) }.first
  3. Stephane Wirtel Stephane Wirtel

    Jul 27, 2007

    Did you test your code ? Because you don’t use the ‘dir’ argument in your function.

    But thanks for this tips.

  4. Yes he does, in:

    Dir["#{dir}**/*#{options[:ext]}"] ^ | here

    Too bad I don’t have irb next to me to check, but I would :
    - put a “.” between * and {options[:ext]}
    - not use a hash for only one option
    - check if by sort_by mtime we don’t get the last one first, so that we would have to ask last file from the list.

    def last_modified_file(dir, ext=nil) Dir["#{dir}**/*#{ext &amp;&amp; '.'&lt;&lt;ext}"].sort_by { |p| File.mtime(p)}.last end

    My 2 cents.

  5. Eric (again) Eric (again)

    Jul 27, 2007

    Sorry for the bad presentation! (a Preview button could be nice, though)

    def last_modified_file(dir, ext=nil)
      ext='.'&lt;&lt;ext unless ext.nil? or ext[0,1]=='.'
      Dir["#{dir}**/*#{ext}"].sort_by {|p|

    Still needs to be tested though!

  6. Building projects when files change is the kind of thing Rake and Make was built to do. But I guess you know that :-)

  7. Still, you need to put some code in your .task files.

    But I guess you know that :-)

  8. Yes, I know. I’m just wondering why you would choose to build your own instead of using Rake, when the same can be achieved with Rake by simply saying:

    file “project.swf” => Dir.glob(“*.as”) do |t| … end

  9. Alternatively

    def last_modified_file(dir, ext=nil) `cd "#{dir.to_s}"; ls -At "*#{ext.to_s}" | head -1`.chomp end

    Use the OS Luke.

  10. Alternatively

    def last_modified_file(dir, ext=nil)
      `cd "#{dir.to_s}"; ls -At "*#{ext.to_s}" | head -1`.chomp

    Use the OS Luke.

  11. It sounds like you’ve got the triggering of the build covered. But lest you havn’t, it’s worth knowing about stakeout, an OS X-only app that watches files for modifications.

    As topfunky suggests you can use it to trigger a rake task:

    $ stakeout "rake my:task" watch/this/file.rb

    You can hook it up to Growl to be notified of the result of the triggered action.

  12. @Andy – Holy sweetness. Thanks for that link. I never quite finished the script because I was having some problems daemonizing the script. I’ll look into stakeout.

Sorry, comments are closed for this article to ease the burden of pruning spam.


Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.


Release your software more often with fewer problems.
Flip your features.