Skip to content

Commit

Permalink
Merge pull request #835 from guard/named_matches
Browse files Browse the repository at this point in the history
rework watch matching + allow regexp named groups
  • Loading branch information
e2 committed May 19, 2016
2 parents 7a3e5f8 + a52fa1a commit c45ed59
Show file tree
Hide file tree
Showing 14 changed files with 471 additions and 119 deletions.
42 changes: 5 additions & 37 deletions lib/guard/watcher.rb
Expand Up @@ -2,6 +2,7 @@
require "guard/deprecated/watcher" unless Guard::Config.new.strict?

require "guard/ui"
require "guard/watcher/pattern"

module Guard
# The watcher defines a RegExp that will be matched against file system
Expand All @@ -21,30 +22,8 @@ class Watcher
# the Guard plugin
#
def initialize(pattern, action = nil)
@pattern, @action = pattern, action
@@warning_printed ||= false

# deprecation warning
regexp = /(^(\^))|(>?(\\\.)|(\.\*))|(\(.*\))|(\[.*\])|(\$$)/
return unless @pattern.is_a?(String) && @pattern =~ regexp

unless @@warning_printed
UI.info "*" * 20 + "\nDEPRECATION WARNING!\n" + "*" * 20
UI.info <<-MSG
You have a string in your Guardfile watch patterns that seem to
represent a Regexp.
Guard matches String with == and Regexp with Regexp#match.
You should either use plain String (without Regexp special
characters) or real Regexp.
MSG
@@warning_printed = true
end

new_regexp = Regexp.new(@pattern).inspect
UI.info "\"#{@pattern}\" has been converted to #{ new_regexp }\n"
@pattern = Regexp.new(@pattern)
@action = action
@pattern = Pattern.create(pattern)
end

# Finds the files that matches a Guard plugin.
Expand Down Expand Up @@ -81,20 +60,9 @@ def self.match_files(guard, files)
end
end

# Test the watchers pattern against a file.
#
# @param [String] file the file to test
# @return [Array<String>] an array of matches (or containing a single path
# if the pattern is a string)
#
def match(string_or_pathname)
# TODO: use only match() - and show fnmatch example
file = string_or_pathname.to_s
return (file == @pattern ? [file] : nil) unless @pattern.is_a?(Regexp)
return unless (m = @pattern.match(file))
m = m.to_a
m[0] = file
m
m = pattern.match(string_or_pathname)
m.nil? ? nil : Pattern::MatchResult.new(m, string_or_pathname)
end

# Executes a watcher action.
Expand Down
24 changes: 24 additions & 0 deletions lib/guard/watcher/pattern.rb
@@ -0,0 +1,24 @@
require "guard/ui"

require_relative "pattern/match_result"
require_relative "pattern/matcher"
require_relative "pattern/deprecated_regexp"
require_relative "pattern/simple_path"
require_relative "pattern/pathname_path"

module Guard
class Watcher
class Pattern
def self.create(pattern)
if DeprecatedRegexp.new(pattern).deprecated?
DeprecatedRegexp.show_deprecation(pattern)
return DeprecatedRegexp.convert(pattern)
end

return PathnamePath.new(pattern) if pattern.is_a?(Pathname)
return SimplePath.new(pattern) if pattern.is_a?(String)
Matcher.new(pattern)
end
end
end
end
45 changes: 45 additions & 0 deletions lib/guard/watcher/pattern/deprecated_regexp.rb
@@ -0,0 +1,45 @@
require_relative "matcher"

module Guard
class Watcher
class Pattern
# TODO: remove before Guard 3.x
class DeprecatedRegexp
def initialize(pattern)
@original_pattern = pattern
end

def self.convert(pattern)
Matcher.new(Regexp.new(pattern))
end

def deprecated?
regexp = /(^(\^))|(>?(\\\.)|(\.\*))|(\(.*\))|(\[.*\])|(\$$)/
@original_pattern.is_a?(String) && regexp.match(@original_pattern)
end

def self.show_deprecation(pattern)
@warning_printed ||= false

unless @warning_printed
msg = "*" * 20 + "\nDEPRECATION WARNING!\n" + "*" * 20
msg += <<-MSG
You have a string in your Guardfile watch patterns that seem to
represent a Regexp.
Guard matches String with == and Regexp with Regexp#match.
You should either use plain String (without Regexp special
characters) or real Regexp.
MSG
UI.deprecation(msg)
@warning_printed = true
end

new_regexp = Regexp.new(pattern).inspect
UI.info "\"#{pattern}\" will be converted to #{new_regexp}\n"
end
end
end
end
end
18 changes: 18 additions & 0 deletions lib/guard/watcher/pattern/match_result.rb
@@ -0,0 +1,18 @@
module Guard
class Watcher
class Pattern
class MatchResult
def initialize(match_result, original_value)
@match_result = match_result
@original_value = original_value
end

def [](index)
return @match_result[index] if index.is_a?(Symbol)
return @original_value if index.zero?
@match_result.to_a[index]
end
end
end
end
end
23 changes: 23 additions & 0 deletions lib/guard/watcher/pattern/matcher.rb
@@ -0,0 +1,23 @@
module Guard
class Watcher
class Pattern
class Matcher
def initialize(obj)
@matcher = obj
end

def match(string_or_pathname)
@matcher.match(normalized(string_or_pathname))
end

private

def normalized(string_or_pathname)
path = Pathname.new(string_or_pathname).cleanpath
return path.to_s if @matcher.is_a?(Regexp)
path
end
end
end
end
end
15 changes: 15 additions & 0 deletions lib/guard/watcher/pattern/pathname_path.rb
@@ -0,0 +1,15 @@
require_relative "simple_path"

module Guard
class Watcher
class Pattern
class PathnamePath < SimplePath
protected

def normalize(string_or_pathname)
Pathname.new(string_or_pathname).cleanpath
end
end
end
end
end
23 changes: 23 additions & 0 deletions lib/guard/watcher/pattern/simple_path.rb
@@ -0,0 +1,23 @@
module Guard
class Watcher
class Pattern
class SimplePath
def initialize(string_or_pathname)
@path = normalize(string_or_pathname)
end

def match(string_or_pathname)
cleaned = normalize(string_or_pathname)
return nil unless @path == cleaned
[cleaned]
end

protected

def normalize(string_or_pathname)
Pathname.new(string_or_pathname).cleanpath.to_s
end
end
end
end
end
28 changes: 28 additions & 0 deletions spec/lib/guard/watcher/pattern/deprecated_regexp_spec.rb
@@ -0,0 +1,28 @@
require "guard/watcher/pattern/deprecated_regexp"

RSpec.describe Guard::Watcher::Pattern::DeprecatedRegexp do
describe ".deprecated?" do
specify { expect(described_class.new("^spec_helper.rb")).to be_deprecated }
specify { expect(described_class.new("spec_helper.rb$")).to be_deprecated }
end

describe "Matcher returned by .convert" do
let(:matcher) { Guard::Watcher::Pattern::Matcher }

before { allow(matcher).to receive(:new) }

{
"^foo.rb" => /^foo.rb/,
"foo.rb$" => /foo.rb$/,
'foo\.rb' => /foo\.rb/,
".*rb" => /.*rb/,
}.each do |pattern, regexp|
context "with #{pattern}" do
it "creates a Matcher with #{regexp}" do
expect(matcher).to receive(:new).with(regexp)
described_class.convert(pattern)
end
end
end
end
end
46 changes: 46 additions & 0 deletions spec/lib/guard/watcher/pattern/match_result_spec.rb
@@ -0,0 +1,46 @@
require "guard/watcher/pattern/match_result"

RSpec.describe Guard::Watcher::Pattern::MatchResult do
let(:match_result) { double("match_data") }
let(:original_value) { "foo/bar.rb" }
subject { described_class.new(match_result, original_value) }

describe "#initialize" do
context "with valid arguments" do
it "does not fail" do
expect { subject }.to_not raise_error
end
end
end

describe "#[]" do
context "with a valid match" do
let(:match_result) { double("match_data", to_a: %w(foo bar baz)) }

context "when asked for the non-first item" do
let(:index) { 1 }
it "returns the value at given index" do
expect(subject[index]).to eq("bar")
end
end

context "when asked for the first item" do
let(:index) { 0 }
it "returns the full original value" do
expect(subject[index]).to eq("foo/bar.rb")
end
end

context "when asked for a name match via a symbol" do
let(:index) { :foo }
before do
allow(match_result).to receive(:[]).with(:foo).and_return("baz")
end

it "returns the value by name" do
expect(subject[index]).to eq("baz")
end
end
end
end
end
82 changes: 82 additions & 0 deletions spec/lib/guard/watcher/pattern/matcher_spec.rb
@@ -0,0 +1,82 @@
require "guard/watcher/pattern/matcher"

RSpec.describe Guard::Watcher::Pattern::Matcher do
subject { described_class.new(obj) }
describe "#match" do
let(:expected) { double("match_result") }

context "when constructed with valid matcher object" do
let(:obj) { double("matcher") }

context "when matched against a Pathname" do
before do
allow(obj).to receive(:match).and_return(expected)
end
let(:filename) { Pathname("foo.rb") }

it "returns the match result" do
expect(subject.match(filename)).to be(expected)
end

it "passes the Pathname to the matcher" do
allow(obj).to receive(:match).with(filename)
subject.match(filename)
end
end

context "when matched against a String" do
before do
allow(obj).to receive(:match).and_return(expected)
end
let(:filename) { "foo.rb" }

it "returns the match result" do
expect(subject.match(filename)).to be(expected)
end

it "passes a Pathname to the matcher" do
allow(obj).to receive(:match).with(Pathname(filename))
subject.match(filename)
end
end
end
end

describe "integration" do
describe "#match result" do
subject { described_class.new(obj).match(filename) }
context "when constructed with valid regexp" do
let(:obj) { /foo.rb$/ }

context "when matched file is a string" do
context "when filename matches" do
let(:filename) { "foo.rb" }
specify { expect(subject.to_a).to eq(["foo.rb"]) }
end

context "when filename does not match" do
let(:filename) { "bar.rb" }
specify { expect(subject).to be_nil }
end
end

context "when matched file is an unclean Pathname" do
context "when filename matches" do
let(:filename) { Pathname("./foo.rb") }
specify { expect(subject.to_a).to eq(["foo.rb"]) }
end

context "when filename does not match" do
let(:filename) { Pathname("./bar.rb") }
specify { expect(subject).to be_nil }
end
end

context "when matched file contains a $" do
let(:filename) { Pathname("lib$/foo.rb") }
specify { expect(subject.to_a).to eq(["foo.rb"]) }
end
end
end
end
end

0 comments on commit c45ed59

Please sign in to comment.