globby 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []