Ivan Porto Carrero is an independent Ruby and .NET developer. Adam Burmister is a polyglot front- and back-end developer, usability evangelist, and designer. Ivan and Adam are the authors of IronRuby in Action.
We routinely watch certain paths on the file system for changes and subsequently perform actions when something happens there. The .NET Framework provides us with a FileSystemWatcher class to help you with that task. This FileSystemWatcher can be defined on a certain path, which can be a folder, and optionally you can set the watcher instance to include the subdirectories of that path as well. But, for more advanced filtering, you're either stuck with defining different paths to watch or providing your own filter when an event is fired. Although there are some other issues involved, in the interest of keeping the code simple, this version of the DSL won't compensate for all of them because that isn't the topic of the discussion. Get ready to dive in and learn several metaprogramming tricks as we build a file-watching Domain-Specific Language (DSL).
Starting with the End Result
In the DSL, I'd like to use regular expressions for the filters as well as a limited form of glob patterns, limited in the sense that it only needs to understand the **, ?, and * wildcard operators. I would like to be able to specify multiple regular expressions for a given path. I would also like to be able to specify a blanket regular expression for a path that is used as the default filter and takes precedence over the more specific regular expressions. Listing 1 shows the end result for what I'd like to achieve.
filesystem do
watch("/path/to/watch", /_spec.rb$/ui) do #A
include_subdirs
on_change { |args| # do stuff here with args.path or args.name } #B
on_change /integration\/.*_spec.rb/ { |args| # do stuff here }
on_rename { |args| # do stuff here }
on_rename "*.rb" { |args| # do stuff here }
on_delete { |args| # do stuff here with args.path or args.name }
on_create { |args| # do stuff here with args.path or args.name }
on_error { |args| # do stuff here }
end
watch("/another/path/to/watch", /app\/.*\.rb$/ui) do
top_level_only
on_change { |args| # do stuff here with args.path or args.name }
on_rename { |args| # do stuff here }
on_delete { |args| # do stuff here with args.path or args.name }
on_create { |args| # do stuff here with args.path or args.name }
on_error { |args| # do stuff here }
end
end
#A Define a path to watch
#B Attach event handlers
As you can see, we have an entry point called filesystem, which takes a block and then proceeds with defining new watch paths. Each watch path in turn takes another block where it defines the event handlers for this watch. This actual setup doesn't really make sense in some cases but it does show the complete syntax we're going for. For this example, we won't need any external libraries and so we can start right away with collecting the paths and filters.



