tree.rb 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/.gemtest +0 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +160 -0
  4. data/Rakefile +12 -0
  5. data/bin/tree.rb +9 -0
  6. data/examples/directory_walker/directory_without_subdirectory.rb +37 -0
  7. data/examples/directory_walker/find_files.rb +18 -0
  8. data/examples/directory_walker/print_files.rb +12 -0
  9. data/examples/protovis/directory_to_json_visitor.rb +15 -0
  10. data/examples/protovis/index.html +87 -0
  11. data/examples/protovis/protovis-r3.2.js +277 -0
  12. data/examples/protovis/treevisitor.js +33 -0
  13. data/examples/protovis/treevisitor.png +0 -0
  14. data/lib/tree_visitor.rb +2 -0
  15. data/lib/treevisitor/abs_node.rb +144 -0
  16. data/lib/treevisitor/basic_tree_node_visitor.rb +44 -0
  17. data/lib/treevisitor/cli/cli_tree.rb +121 -0
  18. data/lib/treevisitor/directory_walker.rb +300 -0
  19. data/lib/treevisitor/leaf_node.rb +33 -0
  20. data/lib/treevisitor/tree_node.rb +296 -0
  21. data/lib/treevisitor/tree_node_visitor.rb +120 -0
  22. data/lib/treevisitor/util/dir_processor.rb +46 -0
  23. data/lib/treevisitor/version.rb +4 -0
  24. data/lib/treevisitor/visitors/block_tree_node_visitor.rb +21 -0
  25. data/lib/treevisitor/visitors/build_dir_tree_visitor.rb +57 -0
  26. data/lib/treevisitor/visitors/callback_tree_node_visitor2.rb +48 -0
  27. data/lib/treevisitor/visitors/clone_tree_node_visitor.rb +39 -0
  28. data/lib/treevisitor/visitors/depth_tree_node_visitor.rb +27 -0
  29. data/lib/treevisitor/visitors/directory_to_hash_visitor.rb +33 -0
  30. data/lib/treevisitor/visitors/flat_print_tree_node_visitors.rb +20 -0
  31. data/lib/treevisitor/visitors/print_dir_tree_visitor.rb +21 -0
  32. data/lib/treevisitor/visitors/print_tree_node_visitor.rb +40 -0
  33. data/lib/treevisitor.rb +40 -0
  34. data/lib/treevisitor_cli.rb +7 -0
  35. data/spec/fixtures/test_dir_1/.dir_with_dot/dummy.txt +0 -0
  36. data/spec/fixtures/test_dir_1/dir.1/dir.1.2/file.1.2.1 +0 -0
  37. data/spec/fixtures/test_dir_1/dir.1/file.1.1 +0 -0
  38. data/spec/fixtures/test_dir_1/dir.2/file.2.1 +0 -0
  39. data/spec/fixtures/test_dir_2/[Dsube]/sub/.gitkeep +0 -0
  40. data/spec/spec_helper.rb +27 -0
  41. data/spec/treevisitor/cli/cli_tree_spec.rb +69 -0
  42. data/spec/treevisitor/directory_walker_spec.rb +99 -0
  43. data/spec/treevisitor/tree_dsl_spec.rb +57 -0
  44. data/spec/treevisitor/tree_dsl_with_derived_class1_spec.rb +53 -0
  45. data/spec/treevisitor/tree_dsl_with_derived_class_spec.rb +51 -0
  46. data/spec/treevisitor/tree_node_paths_spec.rb +70 -0
  47. data/spec/treevisitor/tree_node_spec.rb +135 -0
  48. data/spec/treevisitor/tree_node_visitor_delegate_spec.rb +35 -0
  49. data/spec/treevisitor/tree_node_visitor_dsl_spec.rb +66 -0
  50. data/spec/treevisitor/util/dir_processor_spec.rb +13 -0
  51. data/spec/treevisitor/visitors/block_tree_node_visitor_spec.rb +25 -0
  52. data/spec/treevisitor/visitors/callback_tree_node_visitor2_spec.rb +38 -0
  53. data/spec/treevisitor/visitors/depth_tree_node_visitor_spec.rb +28 -0
  54. data/spec/treevisitor/visitors/tree_node_visitors_spec.rb +27 -0
  55. data/tasks/rspec.rake +34 -0
  56. data/tasks/yard.rake +36 -0
  57. data/tree.rb.gemspec +70 -0
  58. metadata +231 -0
@@ -0,0 +1,144 @@
1
+ # -*- coding: utf-8 -*-
2
+ module TreeVisitor
3
+
4
+ #
5
+ # @abstract Subclass to implement a concrete Node (Leaf or Tree).
6
+ #
7
+ # Abstract Node
8
+ # Class hierarchy
9
+ #
10
+ # AbsNode has a name, a parent
11
+ # ^ and define a path i.e. concatenation ancestor names
12
+ # |
13
+ # |-- LeafNode
14
+ # |
15
+ # `-- TreeNode
16
+ #
17
+ # Object diagram
18
+ #
19
+ # TreeNode (parent: nil)
20
+ # |
21
+ # |--->[ LeafNode, LeafNode, LeafNode ]
22
+ # |
23
+ # |--->[ TreeNode, TreeNode ]
24
+ # |
25
+ # |--> [LeafNode]
26
+ # |
27
+ # `--> [TreeNode, TreeNode]
28
+ class AbsNode
29
+
30
+ class << self
31
+ attr_accessor :path_separator
32
+ end
33
+ self.path_separator = File::SEPARATOR
34
+
35
+ attr_reader :parent
36
+ attr_reader :content
37
+
38
+ attr_accessor :prev
39
+ attr_accessor :next
40
+
41
+ #
42
+ # Create a new AbsNode
43
+ #
44
+ # @param content of node
45
+ #
46
+ def initialize(content)
47
+ @parent = nil
48
+ @content = content
49
+ @prefix_path = nil
50
+ invalidate
51
+ end
52
+
53
+ #
54
+ # invalidate cached path info
55
+ #
56
+ def invalidate
57
+ @path = nil
58
+ @path_with_prefix = nil
59
+ @depth = nil
60
+ @root = nil
61
+ end
62
+
63
+ #
64
+ # Root node could have assigned a path
65
+ #
66
+ def prefix_path
67
+ raise "Not root!!" unless @parent.nil?
68
+ @prefix_path
69
+ end
70
+
71
+ def prefix_path=(prefix)
72
+ raise "Not root!!" unless @parent.nil?
73
+ if prefix != @prefix_path
74
+ @prefix_path = prefix
75
+ if prefix and prefix !~ /\/$/
76
+ @prefix_path += AbsNode::path_separator
77
+ end
78
+ invalidate
79
+ end
80
+ end
81
+
82
+ #
83
+ # Return the root of this node
84
+ #
85
+ # @return AbsNode
86
+ #
87
+ def root
88
+ return @root if @root
89
+ @root = parent.nil? ? self : parent.root
90
+ end
91
+
92
+ #
93
+ # @return [String] path to this node
94
+ #
95
+ def path
96
+ return @path unless @path.nil?
97
+ if @parent.nil?
98
+ @path = @content
99
+ else
100
+ @path = @parent.path + AbsNode::path_separator + @content
101
+ end
102
+ @path
103
+ end
104
+
105
+ #
106
+ # @return [String] path to this node with prefix
107
+ #
108
+ def path_with_prefix
109
+ return @path_with_prefix if @path_with_prefix
110
+ if root.prefix_path
111
+ @path_with_prefix = root.prefix_path + path
112
+ else
113
+ @path_with_prefix = path
114
+ end
115
+ end
116
+
117
+ #
118
+ # @return [FixNum] depth of this node
119
+ #
120
+ def depth
121
+ return @depth unless @depth.nil?
122
+ @depth = @parent.nil? ? 1 : @parent.depth + 1
123
+ @depth
124
+ end
125
+
126
+ #
127
+ # Accept a node visitor
128
+ #
129
+ def accept(visitor)
130
+ not_implemented
131
+ end
132
+
133
+ def to_s
134
+ @content.to_s
135
+ end
136
+
137
+ protected
138
+
139
+ def parent=(parent)
140
+ @parent = parent
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ module TreeVisitor
3
+ #
4
+ # Callback methods used to visit a tree
5
+ # Are empty so it is possible to define only a subset when deriving subclass
6
+ #
7
+ class BasicTreeNodeVisitor
8
+
9
+ #
10
+ # called on tree node at start of the visit i.e. we start to visit the subtree
11
+ #
12
+ def enter_node( tree_node )
13
+ end
14
+
15
+ #
16
+ # called when the tree node is not accessible or an exception is raise when the node is accessed
17
+ #
18
+ def cannot_enter_node( tree_node, error)
19
+ end
20
+
21
+ # alias :enter_tree_node :enter_node
22
+
23
+ #
24
+ # called on tree node at end of the visit i.e. oll subtree are visited
25
+ #
26
+ def exit_node( tree_node )
27
+ end
28
+
29
+ # alias :exit_tree_node :exit_node
30
+
31
+ #
32
+ # called when visit leaf node
33
+ #
34
+ def visit_leaf( leaf_node )
35
+ end
36
+
37
+ #
38
+ # called when the leaf node is not accessible or an exception is raise when the node is accessed
39
+ #
40
+ def cannot_visit_leaf( tree_node, error)
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,121 @@
1
+ # -*- coding: utf-8 -*-
2
+ module TreeVisitor
3
+ #
4
+ #
5
+ #
6
+ class CliTree
7
+
8
+ def self.run
9
+ self.new.parse_args(ARGV)
10
+ end
11
+
12
+ def parse_args(argv)
13
+
14
+ options = {:verbose => true, :force => false, :algo => 'build-dir'}
15
+
16
+ opts = OptionParser.new
17
+ opts.banner = "Usage: tree.rb [options] [directory]"
18
+ opts.separator "list contents of directories in a tree-like format"
19
+ opts.separator "this is a almost :-) a clone of tree unix command written in ruby"
20
+ opts.separator "Code https://github.com/tokiro/treevisitor. Feedback to tokiro.oyama@gmail.com"
21
+
22
+ opts.separator ""
23
+ opts.separator "options: "
24
+
25
+ opts.on("-h", "--help", "Show this message") do
26
+ puts opts
27
+ return 0
28
+ end
29
+
30
+ opts.on("--version", "Show the version") do
31
+ puts "tree.rb version #{TreeVisitor::VERSION}"
32
+ return 0
33
+ end
34
+
35
+ opts.on("-a", "All file are listed") do |v|
36
+ options[:all_files] = true
37
+ end
38
+
39
+ opts.on("-d", "List directories only") do |v|
40
+ options[:only_directories] = true
41
+ end
42
+
43
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
44
+ options[:verbose] = v
45
+ end
46
+
47
+ opts.on("-q", "--quiet", "quiet mode as --no-verbose") do |v|
48
+ options[:verbose] = false
49
+ end
50
+
51
+ algos = %w[build-dir print-dir json yaml]
52
+ algo_aliases = {"b" => "build-dir", "v" => "print-dir", "j" => "json", "y" => "yaml"}
53
+
54
+ algo_list = (algo_aliases.keys + algos).join(',')
55
+ opts.on("-f", "--format ALGO", algos, algo_aliases, "select an algo", " (#{algo_list})") do |algo|
56
+ options[:algo] = algo
57
+ end
58
+
59
+ begin
60
+ rest = opts.parse(argv)
61
+ rescue OptionParser::InvalidOption => e
62
+ $stderr.puts e.to_s
63
+ $stderr.puts "try -h for help"
64
+ return false
65
+ end
66
+
67
+ if rest.length < 1
68
+ dirname = Dir.pwd
69
+ else
70
+ dirname = rest[0]
71
+ end
72
+
73
+ dirname = File.expand_path(dirname)
74
+
75
+ dtw = DirTreeWalker.new(dirname)
76
+ unless options[:all_files]
77
+ dtw.ignore(/^\.[^.]+/) # ignore all file starting with "."
78
+ end
79
+
80
+ dtw.visit_file = !options[:only_directories]
81
+
82
+ case options[:algo]
83
+
84
+ when 'build-dir'
85
+ # TODO: capture CTRL^C
86
+ # http://ruby-doc.org/core-1.9.3/Kernel.html#method-i-trap
87
+ Kernel.trap('INT') { put "User interrupted exit"; exit; }
88
+
89
+ visitor = BuildDirTreeVisitor.new
90
+ dtw.run(visitor)
91
+
92
+ # colors also in windows
93
+ if $stdout.isatty
94
+ puts visitor.root.to_str('', true) #use color
95
+ else
96
+ puts visitor.root.to_str
97
+ end
98
+ puts
99
+ puts "#{visitor.nr_directories} directories, #{visitor.nr_files} files"
100
+
101
+ when 'print-dir'
102
+ visitor = PrintDirTreeVisitor.new
103
+ dtw.run(visitor)
104
+
105
+ when 'json'
106
+ root = dtw.run(DirectoryToHashVisitor.new(dirname)).root
107
+ puts JSON.pretty_generate(root)
108
+
109
+ when 'yaml'
110
+ root = dtw.run(DirectoryToHashVisitor.new(dirname)).root
111
+ puts root.to_yaml
112
+
113
+ else
114
+ puts "unknown algo #{options[:algo]} specified"
115
+ end
116
+
117
+ 0
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,300 @@
1
+ # -*- coding: utf-8 -*-
2
+ module TreeVisitor
3
+
4
+ #
5
+ # Visit a file system directory
6
+ #
7
+ class DirTreeWalker
8
+
9
+ # Build a tree walker.
10
+ # Visit a director starting from dirname
11
+ # if dirname is missing, must be supplied when invoking run
12
+ #
13
+ # @yield [a, b, c] TreeNodeVisitor
14
+ # @yieldparam [optional, types, ...] argname description
15
+ # @yieldreturn [optional, types, ...] description
16
+ #
17
+ # @overload initialize(dirname)
18
+ # @param [String] dirname the root of the tree (top level directory)
19
+ #
20
+ # @overload initialize(dirname, options)
21
+ # @param [Hash] options
22
+ # @option options [String,Regex, Array<String,Regexp>] :ignore list of ignore pattern
23
+ # @option options [String] :ignore_dir
24
+ # @option options [String] :ignore_file
25
+ # @option options [String] :match
26
+ #
27
+ #
28
+ # @example Print the contents of tmp directory
29
+ # DirTreeWalker.new("/tmp") do
30
+ # on_visit_leaf_node { |pathname| puts pathname }
31
+ # end.run
32
+ #
33
+ def initialize(dirname = nil, options = nil)
34
+ #
35
+ # arg detection
36
+ #
37
+ if dirname and dirname.respond_to?(:key?)
38
+ options = dirname
39
+ dirname = nil
40
+ end
41
+
42
+ if dirname
43
+ @dirname = dirname
44
+ unless File.directory?(dirname)
45
+ raise "#{dirname} is not a directory!"
46
+ end
47
+ end
48
+
49
+ @visitor = nil
50
+
51
+ #
52
+ # pattern
53
+ #
54
+ @ignore_dir_patterns = []
55
+ @ignore_file_patterns = []
56
+ @match_file_patterns = []
57
+
58
+ if options and options[:ignore]
59
+ unless options[:ignore].respond_to?(:at)
60
+ options[:ignore] = [options[:ignore]]
61
+ end
62
+ options[:ignore].each { |p| ignore(p) }
63
+ end
64
+
65
+ if options and options[:ignore_dir]
66
+ unless options[:ignore_dir].respond_to?(:at)
67
+ options[:ignore_dir] = [options[:ignore_dir]]
68
+ end
69
+ options[:ignore_dir].each { |p| ignore_dir(p) }
70
+ end
71
+
72
+ if options and options[:ignore_file]
73
+ unless options[:ignore_file].respond_to?(:at)
74
+ options[:ignore_file] = [options[:ignore_file]]
75
+ end
76
+ options[:ignore_file].each { |p| ignore_file(p) }
77
+ end
78
+
79
+ if options and options[:match]
80
+ unless options[:match].respond_to?(:at)
81
+ options[:match] = [options[:match]]
82
+ end
83
+ options[:match].each { |p| match(p) }
84
+ end
85
+
86
+ #
87
+ # options
88
+ #
89
+ @visit_file = true
90
+ end
91
+
92
+ ##########################################################################
93
+ # Pattern
94
+
95
+ #
96
+ # Ignore a node (leaf/Tree) matching pattern
97
+ # @param [RegEx] pattern
98
+ #
99
+ def ignore(pattern)
100
+ @ignore_dir_patterns << pattern
101
+ @ignore_file_patterns << pattern
102
+ self
103
+ end
104
+
105
+ #
106
+ # Ignore a directory (Tree) matching pattern
107
+ # @param [RegEx] pattern
108
+ #
109
+ def ignore_dir(pattern)
110
+ @ignore_dir_patterns << pattern
111
+ self
112
+ end
113
+
114
+ #
115
+ # Ignore a file (Leaf) matching pattern
116
+ # @param [RegEx] pattern
117
+ #
118
+ def ignore_file(pattern)
119
+ @ignore_file_patterns << pattern
120
+ self
121
+ end
122
+
123
+ #
124
+ # Just the opposite of ignore
125
+ # directory/file matching pattern will be visited
126
+ #
127
+ # @param [RegEx] pattern
128
+ #
129
+ def match(pattern)
130
+ @match_file_patterns << pattern
131
+ self
132
+ end
133
+
134
+ ##########################################################################
135
+ # Options
136
+
137
+ #
138
+ # it is true to visit file
139
+ #
140
+ attr_accessor :visit_file
141
+
142
+ ##########################################################################
143
+
144
+ #
145
+ # Test directory ignore pattern
146
+ #
147
+ # @param [String] directory name
148
+ # @return [boolean] if dirname match almost one pattern
149
+ #
150
+ def ignore_dir?(dirname)
151
+ _include?(@ignore_dir_patterns, File.basename(dirname))
152
+ end
153
+
154
+ #
155
+ # Test file ignore pattern
156
+ #
157
+ # @param [String] file name
158
+ # @return [boolean] if filename match almost one pattern
159
+ #
160
+ def ignore_file?(filename)
161
+ _include?(@ignore_file_patterns, File.basename(filename))
162
+ end
163
+
164
+ #
165
+ # Test common ignore pattern
166
+ #
167
+ # @param [String] file name
168
+ # @return [boolean] if filename match almost one pattern
169
+ #
170
+ def match?(filename)
171
+ return true if @match_file_patterns.empty?
172
+ _include?(@match_file_patterns, File.basename(filename))
173
+ end
174
+
175
+ #
176
+ # Run the visitor through the directory tree
177
+ #
178
+ # @overload run
179
+ #
180
+ # @overload run(dirname)
181
+ # @param [String] dirname
182
+ # @yield define TreeNodeVisitor
183
+ #
184
+ # @overload run(tree_node_visitor)
185
+ # @param [TreeNodeVisitor]
186
+ # @yield define TreeNodeVisitor
187
+ #
188
+ # @overload run(dirname, tree_node_visitor)
189
+ # @param [String] dirname
190
+ # @param [TreeNodeVisitor]
191
+ # @yield define TreeNodeVisitor
192
+ #
193
+ # @return [TreeNodeVisitor] the visitor
194
+ #
195
+ # @example Print the contents of tmp directory
196
+ # w = DirTreeWalker.new
197
+ # w.run("/tmp") do
198
+ # on_visit_leaf_node { |pathname| puts pathname }
199
+ # end.run
200
+ #
201
+ def run(dirname = nil, tree_node_visitor = nil, &block)
202
+
203
+ #
204
+ # args detection
205
+ #
206
+ if dirname and dirname.respond_to?(:enter_node)
207
+ tree_node_visitor = dirname
208
+ dirname = nil
209
+ end
210
+
211
+ #
212
+ # check dirname
213
+ #
214
+ if @dirname.nil? and dirname.nil?
215
+ raise "missing starting directory"
216
+ end
217
+ @dirname = dirname if dirname
218
+
219
+ #
220
+ # check visitor
221
+ #
222
+ if tree_node_visitor and block
223
+ raise "cannot use block and parameter together"
224
+ end
225
+
226
+ if tree_node_visitor
227
+ @visitor = tree_node_visitor
228
+ end
229
+
230
+ if block
231
+ @visitor = TreeNodeVisitor.new(&block)
232
+ end
233
+
234
+ unless @visitor
235
+ raise "missing visitor"
236
+ end
237
+
238
+ #
239
+ # finally starts to process
240
+ #
241
+ process_directory(File.expand_path(@dirname))
242
+ @visitor
243
+ end
244
+
245
+ private
246
+
247
+ def _include?(patterns, basename)
248
+ # return false if the patters.empty?
249
+ patterns.find { |pattern|
250
+ if pattern.kind_of? Regexp
251
+ pattern.match(basename)
252
+ else
253
+ basename == pattern
254
+ end
255
+ }
256
+ end
257
+
258
+ #
259
+ # recurse on other directories
260
+ #
261
+ def process_directory(dirname)
262
+ # return if ignore_dir?( dirname )
263
+
264
+ begin
265
+ entries = Dir.entries(dirname).sort
266
+ rescue Errno::EACCES => e
267
+ $stderr.puts e
268
+ @visitor.cannot_enter_node(dirname, e)
269
+ return
270
+ rescue Errno::EPERM => e
271
+ $stderr.puts e
272
+ @visitor.cannot_enter_node(dirname, e)
273
+ return
274
+ end
275
+
276
+
277
+ @visitor.enter_node(dirname)
278
+ entries.each { |basename|
279
+ begin
280
+ next if basename == "." or basename == ".." # ignore always "." and ".."
281
+ pathname = File.join(dirname, basename)
282
+
283
+ if File.directory?(pathname)
284
+ process_directory(pathname) unless ignore_dir?(basename)
285
+ else
286
+ if !!@visit_file && match?(basename) && !ignore_file?(basename)
287
+ @visitor.visit_leaf(pathname)
288
+ end
289
+ end
290
+ rescue Errno::EACCES => e
291
+ $stderr.puts e
292
+ rescue Errno::EPERM
293
+ $stderr.puts e
294
+ end
295
+ }
296
+
297
+ @visitor.exit_node(dirname)
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+ module TreeVisitor
3
+
4
+ #
5
+ # Represent a LeafNode
6
+ #
7
+ class LeafNode < AbsNode
8
+
9
+ #
10
+ # @param [Object] content of node
11
+ #
12
+ def initialize( content, parent = nil )
13
+ super( content )
14
+ parent.add_leaf(self) if parent
15
+ end
16
+
17
+ #
18
+ # @return false because a leaf_node cannot be a root
19
+ #
20
+ def root?
21
+ false
22
+ end
23
+
24
+ #
25
+ # @return [TreeNodeVisitor] the visitor
26
+ #
27
+ def accept( visitor )
28
+ visitor.visit_leaf( self )
29
+ visitor
30
+ end
31
+
32
+ end
33
+ end