globby 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Jon Jensen
1
+ Copyright (c) 2013 Jon Jensen
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,15 +1,23 @@
1
1
  # globby
2
2
 
3
- globby is a [.gitignore](http://www.kernel.org/pub/software/scm/git/docs/gitignore.html)-compatible file globber for ruby
3
+ globby is a [`.gitignore`](http://www.kernel.org/pub/software/scm/git/docs/gitignore.html)-compatible
4
+ file globber for ruby.
5
+
6
+ ## Installation
7
+
8
+ Put `gem 'globby'` in your Gemfile.
4
9
 
5
10
  ## Usage
6
11
 
7
- Globby.new(rules).matches
12
+ Globby.select(rules) # all files matched by the rules
13
+ Globby.reject(rules) # all other files
8
14
 
9
15
  ### An example:
10
16
 
11
17
  > rules = File.read('.gitignore').split(/\n/)
12
- > pp Globby.new(rules).matches
18
+ -> ["Gemfile.lock", "doc", "*.gem"]
19
+
20
+ > pp Globby.select(rules)
13
21
  ["Gemfile.lock",
14
22
  "doc/Foreigner.html",
15
23
  "doc/Foreigner/Adapter.html",
@@ -26,9 +34,24 @@ globby is a [.gitignore](http://www.kernel.org/pub/software/scm/git/docs/gitigno
26
34
  with those files.
27
35
  * You're writing a library/tool that will have its own list of ignored/tracked
28
36
  files. My use case is for an I18n library that extracts strings from ruby
29
- files... I need to provide users a nice configurable way to whitelist given
37
+ files... I need to provide users a nice configurable way to blacklist given
30
38
  files/directories/patterns.
31
39
 
40
+ ## Compatibility Notes
41
+
42
+ globby is compatible with `.gitignore` rules; it respects negated patterns, and
43
+ ignores comments or empty patterns. That said, it supports some things that may
44
+ or may not work in your version of git. These platform-dependent `.gitignore`
45
+ behaviors are platform independent in globby and can always be used:
46
+
47
+ * Recursive wildcards à la ant/zsh/ruby. `**` matches directories recursively.
48
+ * [glob(7)](https://www.kernel.org/doc/man-pages/online/pages/man7/glob.7.html)-style
49
+ bracket expressions, i.e. character classes, ranges, complementation, named
50
+ character classes, collating symbols and equivalence class expressions. Note
51
+ that the syntax for some of these is slightly different than what you would
52
+ find in regular expressions. Refer to [the documentation](https://www.kernel.org/doc/man-pages/online/pages/man7/glob.7.html)
53
+ for more info.
54
+
32
55
  ## License
33
56
 
34
- Copyright (c) 2012 Jon Jensen, released under the MIT license
57
+ Copyright (c) 2013 Jon Jensen, released under the MIT license
@@ -1,54 +1,53 @@
1
1
  require 'set'
2
+ require '../globby/lib/globby/glob'
2
3
 
3
- class Globby
4
- def initialize(patterns = [])
5
- @patterns = patterns
6
- end
7
-
8
- def matches
9
- result = Set.new
10
- @patterns.each do |pattern|
11
- if pattern[0, 1] == '!'
12
- result.subtract matches_for(pattern[1..-1])
13
- else
14
- result.merge matches_for(pattern)
4
+ module Globby
5
+ class << self
6
+ def select(patterns, source = get_files_and_dirs, result = {:files => Set.new, :dirs => Set.new})
7
+ evaluate_patterns(patterns, source, result)
8
+
9
+ if result[:dirs] && result[:dirs].size > 0
10
+ # now go merge/subtract files under directories
11
+ dir_patterns = result[:dirs].map{ |dir| "/#{dir}**" }
12
+ evaluate_patterns(dir_patterns, {:files => source[:files]}, result)
15
13
  end
16
- end
17
- result.to_a.sort
18
- end
19
-
20
- def matches_for(pattern)
21
- return [] unless pattern = normalize(pattern)
22
- expects_dir = pattern.sub!(/\/\z/, '')
23
14
 
24
- files = Dir.glob(pattern, File::FNM_PATHNAME)
25
- result = []
26
- files.each do |file|
27
- next if ['.', '..'].include?(File.basename(file))
28
- if directory?(file)
29
- result.concat matches_for("/" + file + "/**/{*,.*}")
30
- elsif !expects_dir
31
- result << file
15
+ result[:files].to_a.sort
16
+ end
17
+
18
+ def reject(patterns = [])
19
+ source = get_files_and_dirs
20
+ (source[:files] - select(patterns, source)).sort
21
+ end
22
+
23
+ private
24
+
25
+ def evaluate_patterns(patterns, source, result)
26
+ patterns.each do |pattern|
27
+ next unless pattern =~ /\A[^#]/
28
+ evaluate_pattern pattern, source, result
32
29
  end
33
30
  end
34
- result
35
- end
36
31
 
37
- def normalize(pattern)
38
- pattern = pattern.strip
39
- first = pattern[0, 1]
40
- if pattern.empty? || first == '#'
41
- nil
42
- elsif first == '/'
43
- pattern[1..-1]
44
- else
45
- "**/" + pattern # could be anywhere
32
+ def evaluate_pattern(pattern, source, result)
33
+ glob = Globby::Glob.new(pattern)
34
+ method, candidates = glob.inverse? ?
35
+ [:subtract, result] :
36
+ [:merge, source]
37
+
38
+ dir_matches = glob.match(candidates[:dirs])
39
+ file_matches = []
40
+ file_matches = glob.match(candidates[:files]) unless glob.directory? || glob.exact_match? && !dir_matches.empty?
41
+ result[:dirs].send method, dir_matches unless dir_matches.empty?
42
+ result[:files].send method, file_matches unless file_matches.empty?
43
+ end
44
+
45
+ def get_files_and_dirs
46
+ files, dirs = Dir.glob('**/*', File::FNM_DOTMATCH).
47
+ reject { |f| f =~ /(\A|\/)\.\.?\z/ }.
48
+ partition { |f| File.file?(f) || File.symlink?(f) }
49
+ dirs.map!{ |d| d + "/" }
50
+ {:files => files, :dirs => dirs}
46
51
  end
47
- end
48
-
49
- protected
50
-
51
- def directory?(pattern)
52
- File.directory?(pattern) && !File.symlink?(pattern)
53
52
  end
54
53
  end
@@ -0,0 +1,89 @@
1
+ module Globby
2
+ class Glob
3
+ def initialize(pattern)
4
+ @inverse = pattern.sub!(/\A!/, '')
5
+ # remove meaningless wildcards
6
+ @pattern = pattern.
7
+ sub(/\A\/?(\*\*\/)+/, '').
8
+ sub(/(\/\*\*)+\/\*\z/, '/**')
9
+ end
10
+
11
+ def match(files)
12
+ return [] unless files
13
+ files.grep(to_regexp)
14
+ end
15
+
16
+ def inverse?
17
+ @inverse
18
+ end
19
+
20
+ def directory?
21
+ @pattern =~ /\/\z/
22
+ end
23
+
24
+ def exact_match?
25
+ @pattern =~ /\A\// && @pattern !~ /[\*\?]/
26
+ end
27
+
28
+ # see https://www.kernel.org/doc/man-pages/online/pages/man7/glob.7.html
29
+ GLOB_BRACKET_EXPR = /
30
+ \[ # brackets
31
+ !? # (maybe) negation
32
+ \]? # (maybe) right bracket
33
+ (?: # one or more:
34
+ \[[^\/\]]+\] # named character class, collating symbol or equivalence class
35
+ | [^\/\]] # non-right bracket character (could be part of a range)
36
+ )+
37
+ \]/x
38
+ GLOB_ESCAPED_CHAR = /\\./
39
+ GLOB_RECURSIVE_WILDCARD = /\/\*\*(?:\/|\z)/
40
+ GLOB_WILDCARD = /[\?\*]/
41
+
42
+ GLOB_TOKENIZER = /(
43
+ #{GLOB_BRACKET_EXPR} |
44
+ #{GLOB_ESCAPED_CHAR} |
45
+ #{GLOB_RECURSIVE_WILDCARD}
46
+ )/x
47
+
48
+ def to_regexp
49
+ parts = @pattern.split(GLOB_TOKENIZER) - [""]
50
+
51
+ result = parts.first.sub!(/\A\//, '') ? '\A' : '(\A|/)'
52
+ parts.each do |part|
53
+ result << part_to_regexp(part)
54
+ end
55
+ if result[-1, 1] == '/'
56
+ result << '\z'
57
+ elsif result[-2, 2] == '.*'
58
+ result.slice!(-2, 2)
59
+ else
60
+ result << '\/?\z'
61
+ end
62
+ Regexp.new result
63
+ end
64
+
65
+ private
66
+
67
+ def part_to_regexp(part)
68
+ case part
69
+ when GLOB_BRACKET_EXPR
70
+ # fix negation and escape right bracket
71
+ part.sub(/\A\[!/, '[^').sub(/\A(\[\^?)\]/, '\1\]')
72
+ when GLOB_ESCAPED_CHAR
73
+ part
74
+ when GLOB_RECURSIVE_WILDCARD
75
+ part[-1, 1] == '/' ? "/(.+/)?" : "/.*"
76
+ when GLOB_WILDCARD
77
+ (part.split(/(#{GLOB_WILDCARD})/) - [""]).inject("") do |result, p|
78
+ result << case p
79
+ when '?'; '[^/]'
80
+ when '*'; '[^/]*'
81
+ else Regexp.escape(p)
82
+ end
83
+ end
84
+ else # literal path component (maybe with slashes)
85
+ part
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,100 @@
1
+ require 'globby'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ RSpec.configure { |config| config.mock_framework = :mocha }
6
+
7
+ describe Globby do
8
+ around do |example|
9
+ gitignore_test { example.run }
10
+ end
11
+
12
+ describe ".select" do
13
+ it "should match .gitignore perfectly" do
14
+ rules = prepare_gitignore
15
+ Globby.select(rules.split(/\n/)).should == all_files - git_files - untracked
16
+ end
17
+ end
18
+
19
+ describe ".reject" do
20
+ it "should match the inverse of .gitignore, plus .git" do
21
+ rules = prepare_gitignore
22
+ Globby.reject(rules.split(/\n/)).should == git_files + untracked
23
+ end
24
+ end
25
+
26
+ def gitignore_test
27
+ Dir.mktmpdir do |dir|
28
+ Dir.chdir(dir) do
29
+ prepare_files
30
+ `git init .`
31
+ yield
32
+ end
33
+ end
34
+ end
35
+
36
+ def prepare_gitignore
37
+ ignore = <<-IGNORE.gsub(/^ +/, '')
38
+ # here we go...
39
+
40
+ # some dotfiles
41
+ .hidden
42
+
43
+ # html, but just in the root
44
+ /*.html
45
+
46
+ # all rb files anywhere
47
+ *.rb
48
+
49
+ # except rb files immediately under foobar
50
+ !foobar/*.rb
51
+
52
+ # this will match foo/bar but not bar
53
+ bar/
54
+
55
+ # this will match nothing
56
+ foo*bar/baz.pdf
57
+
58
+ # this will match baz/ and foobar/baz/
59
+ baz
60
+ IGNORE
61
+ File.open('.gitignore', 'w'){ |f| f.write ignore }
62
+ ignore
63
+ end
64
+
65
+ def prepare_files
66
+ files = <<-FILES.strip.split(/\s+/)
67
+ .gitignore
68
+ foo.rb
69
+ foo.html
70
+ bar
71
+ baz/lol.txt
72
+ foo/.hidden
73
+ foo/bar.rb
74
+ foo/bar.html
75
+ foo/bar/baz.pdf
76
+ foobar/.hidden
77
+ foobar/baz.txt
78
+ foobar/baz.rb
79
+ foobar/baz/lol.wut
80
+ FILES
81
+ files.each do |file|
82
+ FileUtils.mkdir_p File.dirname(file)
83
+ FileUtils.touch file
84
+ end
85
+ end
86
+
87
+ def untracked
88
+ `git status -uall`.gsub(/.*#\n|#\s+|^nothing.*/m, '').split(/\n/)
89
+ end
90
+
91
+ def git_files
92
+ Dir.glob('.git/**/*', File::FNM_DOTMATCH).
93
+ select{ |f| File.symlink?(f) || File.file?(f) }.sort
94
+ end
95
+
96
+ def all_files
97
+ Dir.glob('**/*', File::FNM_DOTMATCH).
98
+ select{ |f| File.symlink?(f) || File.file?(f) }.sort
99
+ end
100
+ end
@@ -1,157 +1,85 @@
1
1
  require 'globby'
2
- RSpec.configure { |config| config.mock_framework = :mocha }
3
2
 
4
- # A blank line matches no files, so it can serve as a separator for
5
- # readability.
6
- #
7
- # A line starting with # serves as a comment.
8
- #
9
- # An optional prefix ! which negates the pattern; any matching file excluded by
10
- # a previous pattern will become included again. If a negated pattern matches,
11
- # this will override lower precedence patterns sources.
12
- #
13
- # If the pattern ends with a slash, it is removed for the purpose of the
14
- # following description, but it would only find a match with a directory. In
15
- # other words, foo/ will match a directory foo and paths underneath it, but
16
- # will not match a regular file or a symbolic link foo (this is consistent with
17
- # the way how pathspec works in general in git).
18
- #
19
- # If the pattern does not contain a slash /, git treats it as a shell glob
20
- # pattern and checks for a match against the pathname relative to the location
21
- # of the .gitignore file (relative to the toplevel of the work tree if not from
22
- # a .gitignore file).
23
- #
24
- # Otherwise, git treats the pattern as a shell glob suitable for consumption by
25
- # fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will not
26
- # match a / in the pathname. For example, "Documentation/*.html" matches
27
- # "Documentation/git.html" but not "Documentation/ppc/ppc.html" or
28
- # "tools/perf/Documentation/perf.html".
29
- #
30
- # A leading slash matches the beginning of the pathname. For example, "/*.c"
31
- # matches "cat-file.c" but not "mozilla-sha1/sha1.c".
3
+ RSpec.configure { |config| config.mock_framework = :mocha }
32
4
 
33
5
  describe Globby do
34
- describe "#matches_for" do
35
-
36
- let(:globby) { Globby.new }
37
-
6
+ describe ".select" do
38
7
  context "a blank line" do
39
8
  it "should return nothing" do
40
- Dir.expects(:glob).never
41
- globby.matches_for("").should == []
9
+ files = files("foo")
10
+ Globby.select([""], files).should == []
42
11
  end
43
12
  end
44
13
 
45
14
  context "a comment" do
46
15
  it "should return nothing" do
47
- Dir.expects(:glob).never
48
- globby.matches_for("#comment").should == []
16
+ files = files("foo")
17
+ Globby.select(["#"], files).should == []
49
18
  end
50
19
  end
51
20
 
52
21
  context "a pattern ending in a slash" do
53
22
  it "should return a matching directory's contents" do
54
- globby.stubs(:directory?).returns true, false
55
- Dir.expects(:glob).twice.returns ["foo"], ['foo/bar']
56
- globby.matches_for("foo/").should == ['foo/bar']
23
+ files = files(%w{foo/bar/baz foo/bar/baz2})
24
+ Globby.select(%w{bar/}, files).should == %w{foo/bar/baz foo/bar/baz2}
57
25
  end
58
26
 
59
27
  it "should ignore symlinks and regular files" do
60
- globby.stubs(:directory?).returns false
61
- Dir.expects(:glob).once.returns ["foo"]
62
- globby.matches_for("foo/").should == []
28
+ files = files(%w{foo/bar bar/baz})
29
+ Globby.select(%w{bar/}, files).should == %w{bar/baz}
63
30
  end
64
31
  end
65
32
 
66
- context "a pattern without a slash" do
67
- it "should return all glob matches" do
68
- Dir.expects(:glob).with{ |*args| args.first == "**/*rb"}.returns []
69
- globby.matches_for("*rb")
33
+ context "a pattern starting in a slash" do
34
+ it "should return only root glob matches" do
35
+ files = files(%w{foo/bar bar/foo})
36
+ Globby.select(%w{/foo}, files).should == %w{foo/bar}
70
37
  end
71
38
  end
72
39
 
73
- context "a pattern with a slash" do
74
- it "should return all glob matches" do
75
- Dir.expects(:glob).with{ |*args| args.first == "**/foo/bar"}.returns []
76
- globby.matches_for("foo/bar")
40
+ context "a pattern with a *" do
41
+ it "should return matching files" do
42
+ files = files(%w{foo/bar foo/baz})
43
+ Globby.select(%w{*z}, files).should == %w{foo/baz}
77
44
  end
78
- end
79
45
 
80
- context "a pattern starting in a slash" do
81
- it "should return all root glob matches" do
82
- Dir.expects(:glob).with{ |*args| args.first == "foo/bar"}.returns []
83
- globby.matches_for("/foo/bar")
46
+ it "should not glob slashes" do
47
+ files = files(%w{foo/bar foo/baz})
48
+ Globby.select(%w{foo*bar}, files).should == []
84
49
  end
85
50
  end
86
- end
87
-
88
- describe "#matches" do
89
- it "should match gitignore perfectly" do
90
- require 'tmpdir'
91
- require 'fileutils'
92
-
93
- files = <<-FILES.strip.split(/\s+/)
94
- .gitignore
95
- foo.rb
96
- foo.html
97
- bar
98
- baz/lol.txt
99
- foo/.hidden
100
- foo/bar.rb
101
- foo/bar.html
102
- foo/bar/baz.pdf
103
- foobar/.hidden
104
- foobar/baz.txt
105
- foobar/baz.rb
106
- foobar/baz/lol.wut
107
- FILES
108
-
109
- ignore = <<-IGNORE.gsub(/^ +/, '')
110
- # here we go...
111
-
112
- # some dotfiles
113
- .hidden
114
-
115
- # html, but just in the root
116
- /*.html
117
51
 
118
- # all rb files anywhere
119
- *.rb
120
-
121
- # except rb files immediately under foobar
122
- !foobar/*.rb
123
-
124
- # this will match foo/bar but not bar
125
- bar/
126
-
127
- # this will match nothing
128
- foo*bar/baz.pdf
129
-
130
- # this will match baz/ and foobar/baz/
131
- baz
132
- IGNORE
52
+ context "a pattern with a ?" do
53
+ it "should return matching files" do
54
+ files = files(%w{foo/bar foo/baz})
55
+ Globby.select(%w{b?z}, files).should == %w{foo/baz}
56
+ end
133
57
 
134
- Dir.mktmpdir do |dir|
135
- Dir.chdir(dir) do
136
- File.open('.gitignore', 'w'){ |f| f.write ignore }
137
- files.each do |file|
138
- FileUtils.mkdir_p File.dirname(file)
139
- FileUtils.touch file
140
- end
58
+ it "should not glob slashes" do
59
+ files = files(%w{foo/bar foo/baz})
60
+ Globby.select(%w{foo?bar}, files).should == []
61
+ end
62
+ end
141
63
 
142
- `git init .`
143
- untracked = `git status -uall`.gsub(/.*#\n|#\s+|^nothing.*/m, '').split(/\n/)
64
+ context "a pattern with a **" do
65
+ it "should match directories recursively" do
66
+ files = files(%w{foo/bar foo/baz foo/c/bar foo/c/c/bar})
67
+ Globby.select(%w{foo/**/bar}, files).should == %w{foo/bar foo/c/bar foo/c/c/bar}
68
+ end
69
+ end
144
70
 
145
- globby = Globby.new(ignore.split(/\n/))
146
- ignored = globby.matches
147
-
148
- all_files = Dir.glob('**/*', File::FNM_DOTMATCH | File::FNM_PATHNAME).
149
- reject{ |f| f =~ /^\.git\// }.
150
- select{ |f| File.symlink?(f) || File.file?(f) }
151
-
152
- all_files.sort.should == (untracked + ignored).sort
153
- end
71
+ context "a pattern with bracket expressions" do
72
+ it "should return matching files" do
73
+ files = files(%w{boo fob f0o foo/bar poo/baz})
74
+ Globby.select(%w{[e-g][0-9[:alpha:]][!b]}, files).should == %w{f0o foo/bar}
154
75
  end
155
76
  end
156
77
  end
78
+
79
+ def files(files)
80
+ files = Array(files)
81
+ files.sort!
82
+ dirs = files.grep(/\//).map { |file| file.sub(/[^\/]+\z/, '') }.uniq.sort
83
+ {:files => files, :dirs => dirs}
84
+ end
157
85
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: globby
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jon Jensen
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2013-01-16 00:00:00 Z
18
+ date: 2013-01-25 00:00:00 Z
19
19
  dependencies: []
20
20
 
21
21
  description: find files using .gitignore-style globs
@@ -30,7 +30,9 @@ files:
30
30
  - LICENSE.txt
31
31
  - Rakefile
32
32
  - README.md
33
+ - lib/globby/glob.rb
33
34
  - lib/globby.rb
35
+ - spec/gitignore_spec.rb
34
36
  - spec/globby_spec.rb
35
37
  homepage: http://github.com/jenseng/globby
36
38
  licenses: []