dumb_down_viewer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 168cf3ff44970f98481dbfa56a9bad5ad019c67b
4
+ data.tar.gz: 75373160ed335cf58bea04caab40f91d3f36d79b
5
+ SHA512:
6
+ metadata.gz: 5fa0f8ba7af6c70b32e10f99d8ec48cf287058b1efde59d2cf4c9742c3513d7cbcaa376275518023e613e01d290715e22b325ffe7f16a53fa4a4a35eacd16d42
7
+ data.tar.gz: 86a23ddc37f8de3b5dea366aa495b18b3257987d24571aceb60fc2377396b4aa766b6462363f4e9cd819efc54773fb2d78eb30877f6f67f983190a015aa9997a
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *~
11
+ spec/data/mammalia/cannot_fly/elephant.txt
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.4
5
+ before_install: gem install bundler -v 1.13.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dumb_down_viewer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 HASHIMOTO, Naoki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # DumbDownViewer
2
+
3
+ DumbDownViewer (ddv) is a recursive directory listing command with limited functionality.
4
+
5
+ In some case, you can use ddv like "[tree](http://mama.indstate.edu/users/ice/tree/)" command, even though these commands are not compatible:
6
+
7
+ Several options of "tree" are missing in ddv, but there are also some options that are available only in ddv (such as --copy-to).
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'dumb_down_viewer'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install dumb_down_viewer
24
+
25
+ ## Usage
26
+
27
+ Suppose you have a directory `spec/data`
28
+
29
+ $ find spec/data
30
+ spec/data
31
+ spec/data/README
32
+ spec/data/index.html
33
+ spec/data/mammalia
34
+ spec/data/mammalia/can_fly
35
+ spec/data/mammalia/can_fly/bat.txt
36
+ spec/data/mammalia/index.html
37
+ spec/data/mammalia/cannot_fly
38
+ spec/data/mammalia/cannot_fly/elephant.txt
39
+ spec/data/aves
40
+ spec/data/aves/can_fly
41
+ spec/data/aves/can_fly/sparrow.txt
42
+ spec/data/aves/index.html
43
+ spec/data/aves/cannot_fly
44
+ spec/data/aves/cannot_fly/penguin.jpg
45
+ spec/data/aves/cannot_fly/penguin.txt
46
+ spec/data/aves/cannot_fly/ostrich.txt
47
+ spec/data/aves/cannot_fly/ostrich.jpg
48
+
49
+
50
+ Then type at the command line (or replace `spec/data` with a directory name which is on your system):
51
+
52
+ $ ddv spec/data
53
+
54
+ And you will get something like the following
55
+
56
+ [spec/data]
57
+ ├─ README
58
+ ├─ index.html
59
+ ├─ [aves]
60
+ │ ├─ index.html
61
+ │ ├─ [can_fly]
62
+ │ │ └─ sparrow.txt
63
+ │ └─ [cannot_fly]
64
+ │ ├─ ostrich.jpg
65
+ │ ├─ ostrich.txt
66
+ │ ├─ penguin.jpg
67
+ │ └─ penguin.txt
68
+ └─ [mammalia]
69
+ ├─ index.html
70
+ ├─ [can_fly]
71
+ │ └─ bat.txt
72
+ └─ [cannot_fly]
73
+ └─ elephant.txt
74
+
75
+ ### Options
76
+
77
+ The following is the list of available options:
78
+
79
+ |Short |Long |Description |
80
+ |------|-----|------------|
81
+ |-s [style_name] |--style [=style_name] |Choose the style of output other than default from ascii_art, list, zenkaku or tree |
82
+ |-f [format_name] |--format [=format_name] |Choose the output format other than default from csv, tsv or tree_csv |
83
+ |-d |--directories-only |List directories only |
84
+ |-L [level] |- |Descend only level directories deep |
85
+ |-P [pattern] |- |List only those files that match the given pattern |
86
+ |-I [pattern] |- |Do not list files that match the given pattern |
87
+ |-a |--all |Show all files including dot files |
88
+ |- |--ignore-case |Ignore case when pattern matching |
89
+ |-o [output_file] |--output [=output_file] |Output to file instead of stdout |
90
+ |- |--filelimit [=number_of_files] |Do not descend dirs with more than the specified number of files in them |
91
+ |- |--summary [=summary_type] |Add summary information about directories. Available summary_type: default |
92
+ |-J |- |Print out a JSON representation |
93
+ |-X |- |Print out an XML representation |
94
+ |- |--report-total-count |Display file/directory count at the end of directory listing |
95
+ |- |--copy-to [=dest_dir] |Copy the directory tree to dest_dir |
96
+
97
+
98
+ ## Development
99
+
100
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/nico-hn/DumbDownViewer.
107
+
108
+
109
+ ## License
110
+
111
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
112
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dumb_down_viewer"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dumb_down_viewer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dumb_down_viewer"
8
+ spec.version = DumbDownViewer::VERSION
9
+ spec.authors = ["HASHIMOTO, Naoki"]
10
+ spec.email = ["hashimoto.naoki@gmail.com"]
11
+
12
+ spec.summary = %q{DumbDownViewer (ddv) is a recursive directory listing command with limited functionality.}
13
+ spec.description = <<-DESCRIPTION
14
+ DumbDownViewer (ddv) is a recursive directory listing command with limited functionality.
15
+ In some case, you can use ddv like "tree" command (http://mama.indstate.edu/users/ice/tree/),
16
+ even though these commands are not compatible: Several options of "tree" are missing in ddv,
17
+ but there are also some options that are available only in ddv (such as --copy-to).
18
+ DESCRIPTION
19
+ spec.homepage = "https://github.com/nico-hn/DumbDownViewer"
20
+ spec.license = "MIT"
21
+
22
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
23
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
24
+ if spec.respond_to?(:metadata)
25
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
26
+ else
27
+ raise "RubyGems 2.0 or newer is required to protect against " \
28
+ "public gem pushes."
29
+ end
30
+
31
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
32
+ f.match(%r{^(test|spec|features)/})
33
+ end
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+ spec.add_runtime_dependency "nokogiri", "~> 1.6"
38
+ spec.add_runtime_dependency "optparse_plus"
39
+
40
+ spec.add_development_dependency "bundler", "~> 1.13"
41
+ spec.add_development_dependency "rake", "~> 10.0"
42
+ spec.add_development_dependency "rspec", "~> 3.0"
43
+ end
data/exe/ddv ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "dumb_down_viewer"
4
+ require "dumb_down_viewer/cli"
5
+
6
+ DumbDownViewer::Cli.execute
@@ -0,0 +1,206 @@
1
+ require 'dumb_down_viewer'
2
+ require 'dumb_down_viewer/tree_view_builder'
3
+ require 'optparse_plus'
4
+
5
+ module DumbDownViewer
6
+ module Cli
7
+ FORMATTER = {
8
+ default: DumbDownViewer::TreeViewBuilder::PlainTextFormat,
9
+ csv: DumbDownViewer::TreeViewBuilder::CSVFormat,
10
+ tsv: DumbDownViewer::TreeViewBuilder::CSVFormat,
11
+ tree_csv: DumbDownViewer::TreeViewBuilder::TreeCSVFormat
12
+ }
13
+
14
+ OPTIONS = <<YAML
15
+ banner: "USAGE: #{File.basename($0)} [OPTION]... DIRECTORY"
16
+ style:
17
+ short: "-s [style_name]"
18
+ long: "--style [=style_name]"
19
+ description: "Choose the style of output other than default from ascii_art, list, zenkaku or tree"
20
+ format:
21
+ short: "-f [format_name]"
22
+ long: "--format [=format_name]"
23
+ description: Choose the output format other than default from csv, tsv, tree_csv
24
+ directories:
25
+ short: "-d"
26
+ long: "--directories-only"
27
+ description: "List directories only"
28
+ level:
29
+ short: "-L [level]"
30
+ description: "Descend only level directories deep"
31
+ match:
32
+ short: "-P [pattern]"
33
+ description: "List only those files that match the pattern given"
34
+ ignore_match:
35
+ short: "-I [pattern]"
36
+ description: "Do not list files that match the given pattern"
37
+ show_all:
38
+ short: "-a"
39
+ long: "--all"
40
+ description: "Show all files including dot files"
41
+ ignore_case:
42
+ long: "--ignore-case"
43
+ description: "Ignore case when pattern matching"
44
+ output:
45
+ short: "-o [output_file]"
46
+ long: "--output [=output_file]"
47
+ description: "Output to file instead of stdout"
48
+ file_limit:
49
+ long: "--filelimit [=number_of_files]"
50
+ description: "Do not descend dirs with more than the specified number of files in them"
51
+ summary:
52
+ long: "--summary [=summary_type]"
53
+ description: "Add summary information about directories. Available summary_type: default"
54
+ json:
55
+ short: "-J"
56
+ description: "Print out a JSON representation"
57
+ xml:
58
+ short: "-X"
59
+ description: "Print out an XML representation"
60
+ total_count:
61
+ long: "--report-total-count"
62
+ description: "Display file/directory count at the end of directory listing"
63
+ copy_to:
64
+ long: "--copy-to [=dest_dir]"
65
+ description: "Copy the directory tree to dest_dir"
66
+ YAML
67
+
68
+ def self.parse_command_line_options
69
+ options = { style: :default, format: :default }
70
+ OptionParser.new_with_yaml(OPTIONS) do |opt|
71
+ opt.version = DumbDownViewer::VERSION
72
+ opt.inherit_ruby_options("E") # -E --encoding
73
+
74
+ opt.on(:style) {|style| options[:style] = style.to_sym }
75
+ opt.on(:format) {|format| options[:format] = format.to_sym }
76
+ opt.on(:directories) {|directories| options[:directories] = true }
77
+ opt.on(:level) {|level| options[:level] = level.to_i }
78
+ opt.on(:match) {|pattern| options[:match] = pattern }
79
+ opt.on(:ignore_match) {|pattern| options[:ignore_match] = pattern }
80
+ opt.on(:show_all) { options[:show_all] = true }
81
+ opt.on(:ignore_case) { options[:ignore_case] = true }
82
+ opt.on(:output) {|output_file| options[:output] = output_file }
83
+ opt.on(:file_limit) {|number_of_files| options[:file_limit] = number_of_files.to_i }
84
+ opt.on(:summary) {|summary_type| options[:summary] = summary_type.to_sym }
85
+ opt.on(:json) { options[:json] = true }
86
+ opt.on(:xml) { options[:xml] = true }
87
+ opt.on(:total_count) { options[:total_count] = true }
88
+ opt.on(:copy_to) {|dest_dir| options[:copy_to] = dest_dir }
89
+ opt.parse!
90
+ end
91
+ options
92
+ end
93
+
94
+ def self.execute
95
+ options = parse_command_line_options
96
+ tree = DumbDownViewer.build_node_tree(ARGV[0])
97
+ prune_dot_files(tree) unless options[:show_all]
98
+ prune_dirs_with_more_than(tree, options[:file_limit]) if options[:file_limit]
99
+ prune_level(tree, options[:level]) if options[:level]
100
+ select_match(tree, options) if options[:match]
101
+ ignore_match(tree, options) if options[:ignore_match]
102
+ prune_files(tree) if options[:directories]
103
+ copy_to(tree, options) if options[:copy_to]
104
+ open_output(options[:output]) do |out|
105
+ out.print format_tree(tree, options)
106
+ out.puts total_count(tree) if options[:total_count]
107
+ end
108
+ end
109
+
110
+ def self.prune_dot_files(tree)
111
+ pruner = DumbDownViewer::TreePruner.create(false) {|node| node.name.start_with? '.' }
112
+ pruner.visit(tree, nil)
113
+ end
114
+
115
+ def self.prune_files(tree)
116
+ pruner = DumbDownViewer::TreePruner.create {|node| node.directory? }
117
+ pruner.visit(tree, nil)
118
+ end
119
+
120
+ def self.prune_level(tree, level)
121
+ pruner = DumbDownViewer::TreePruner.create {|node| node.depth <= level }
122
+ pruner.visit(tree, nil)
123
+ end
124
+
125
+ def self.select_match(tree, options)
126
+ pat = Regexp.compile(options[:match], options[:ignore_case])
127
+ pruner = DumbDownViewer::TreePruner.create {|node| node.directory? or pat =~ node.name }
128
+ pruner.visit(tree, nil)
129
+ end
130
+
131
+ def self.ignore_match(tree, options)
132
+ pat = Regexp.compile(options[:ignore_match], options[:ignore_case])
133
+ pruner = DumbDownViewer::TreePruner.create(false) {|node| not node.directory? and pat =~ node.name }
134
+ pruner.visit(tree, nil)
135
+ end
136
+
137
+ def self.prune_dirs_with_more_than(tree, number_of_files)
138
+ pruner = DumbDownViewer::TreePruner.create(false) do |node|
139
+ node.directory? and node.sub_nodes.size > number_of_files
140
+ end
141
+ if tree.sub_nodes.size > number_of_files
142
+ tree.directories.clear
143
+ tree.files.clear
144
+ end
145
+ pruner.visit(tree, nil)
146
+ end
147
+
148
+ def self.format_json(tree, options)
149
+ json = DumbDownViewer::JSONConverter.dump(tree)
150
+ json + $/
151
+ end
152
+
153
+ def self.format_xml(tree, options)
154
+ DumbDownViewer::XMLConverter.dump(tree)
155
+ end
156
+
157
+ def self.add_summary(tree, options)
158
+ if summary_type = options[:summary] and summary_type != :default
159
+ STDERR.puts "Unknown option #{summary_type} for --summary: default is used instead."
160
+ end
161
+ visitor = DumbDownViewer::FileCountSummary.new
162
+ tree.accept(visitor, nil)
163
+ DumbDownViewer::FileCountSummary::NodeFormat.new
164
+ end
165
+
166
+ def self.format_tree(tree, options)
167
+ if options[:file_limit] and tree.sub_nodes.empty?
168
+ ''
169
+ elsif options[:json]
170
+ format_json(tree, options)
171
+ elsif options[:xml]
172
+ format_xml(tree, options)
173
+ else
174
+ format_tree_with_builder(tree, options)
175
+ end
176
+ end
177
+
178
+ def self.format_tree_with_builder(tree, options)
179
+ style = options[:style]
180
+ col_sep = options[:format] == :tsv ? "\t" : ','
181
+ node_format = options[:summary] ? add_summary(tree, options) : nil
182
+ builder = TreeViewBuilder.create(tree)
183
+ formatter = FORMATTER[options[:format]].new(style, col_sep, node_format)
184
+ formatter.format_table(builder.tree_table)
185
+ end
186
+
187
+ def self.total_count(tree)
188
+ count = DumbDownViewer::TotalNodeCount.count(tree)
189
+ "#{$/}#{count[:directories]} directories, #{count[:files]} files"
190
+ end
191
+
192
+ def self.copy_to(tree, options)
193
+ DumbDownViewer::TreeDuplicator.duplicate(tree, options[:copy_to])
194
+ end
195
+
196
+ def self.open_output(filename)
197
+ if filename
198
+ open(File.expand_path(filename), "wb") do |out|
199
+ yield out
200
+ end
201
+ else
202
+ yield STDOUT
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,180 @@
1
+ require 'dumb_down_viewer'
2
+ require 'dumb_down_viewer/visitor'
3
+ require 'csv'
4
+ require 'yaml'
5
+
6
+ module DumbDownViewer
7
+ class TreeViewBuilder < Visitor
8
+ attr_reader :tree_table
9
+
10
+ class PlainTextFormat
11
+ attr_accessor :line
12
+
13
+ LINE_PATTERNS = YAML.load(<<YAML_DATA)
14
+ :default:
15
+ :spacer: ' '
16
+ :h_line: '── '
17
+ :v_line: '│ '
18
+ :branch: '├─ '
19
+ :corner: '└─ '
20
+ :ascii_art:
21
+ :spacer: ' '
22
+ :h_line: '--- '
23
+ :v_line: '| '
24
+ :branch: '|-- '
25
+ :corner: '`-- '
26
+ :list:
27
+ :spacer: ' '
28
+ :h_line: ' '
29
+ :v_line: ' '
30
+ :branch: ' * '
31
+ :corner: ' * '
32
+ :zenkaku:
33
+ :spacer: '   '
34
+ :h_line: '── '
35
+ :v_line: '│  '
36
+ :branch: '├─ '
37
+ :corner: '└─ '
38
+ :tree:
39
+ :spacer: ' '
40
+ :h_line: '─── '
41
+ :v_line: '│   '
42
+ :branch: '├── '
43
+ :corner: '└── '
44
+
45
+ YAML_DATA
46
+
47
+ def initialize(line_pattern=:default, col_sep=nil, node_format=nil)
48
+ # col_sep is just for having common interface
49
+ @line = LINE_PATTERNS[line_pattern]
50
+ @node_format = node_format || NodeFormat.new
51
+ end
52
+
53
+ def format_table(tree_table)
54
+ t = tree_table.transpose
55
+ t.each_cons(2) do |fr, sr|
56
+ fr.each_with_index do |f, i|
57
+ next unless f.kind_of? Node
58
+ draw_lines(fr, sr, f, i)
59
+ end
60
+ end
61
+
62
+ draw_last_line(t[-1])
63
+ update_root_directory_name(tree_table[0][0], t)
64
+ table_to_output_format(t.transpose)
65
+ end
66
+
67
+ def draw_lines(fr, sr, f_node, i)
68
+ sub_count = f_node.sub_nodes.size
69
+ j = i
70
+ while sub_count > 0
71
+ j += 1
72
+ s_node = sr[j]
73
+ if s_node
74
+ fr[j] = sub_count == 1 ? @line[:corner] : @line[:branch]
75
+ sub_count -= 1
76
+ else
77
+ fr[j] = @line[:v_line]
78
+ end
79
+ end
80
+ fr[i] = @node_format[f_node]
81
+ end
82
+
83
+ def draw_last_line(line)
84
+ line.each_index do |i|
85
+ node = line[i]
86
+ line[i] = @node_format[node] if node.kind_of?(DirNode)
87
+ end
88
+ end
89
+
90
+ def fill_spaces(table)
91
+ table.map do |row|
92
+ (row.size - 1).downto(0) do |i|
93
+ row[i] = @line[:spacer] if row[i + 1] and row[i].nil?
94
+ end
95
+ row
96
+ end
97
+ end
98
+
99
+ def update_root_directory_name(root, table)
100
+ if root.directory and not root.directory.empty?
101
+ table[0][0] = "[#{File.join(root.directory, root.name)}]"
102
+ end
103
+ end
104
+
105
+ def table_to_output_format(table)
106
+ fill_spaces(table).map {|r| r.join }.join($/) + $/
107
+ end
108
+ end
109
+
110
+ class TreeCSVFormat < PlainTextFormat
111
+ def initialize(line_pattern=:default, col_sep=',', node_format=nil)
112
+ @line = LINE_PATTERNS[line_pattern]
113
+ @col_sep = col_sep
114
+ @node_format = node_format || NodeFormat.new
115
+ end
116
+
117
+ def table_to_output_format(table)
118
+ CSV.generate('', col_sep: @col_sep) do |csv|
119
+ table.each {|row| csv << row }
120
+ end
121
+ end
122
+ end
123
+
124
+ class CSVFormat
125
+ def initialize(line_pattern=nil, col_sep=',', node_format=nil)
126
+ @line_pattern = line_pattern
127
+ @col_sep = col_sep
128
+ @node_format = node_format || NodeFormat.new
129
+ end
130
+
131
+ def format_table(tree_table)
132
+ root = tree_table[0][0]
133
+ tree_table[0][0] = "#{File.join(root.directory, root.name)}"
134
+ CSV.generate('', col_sep: @col_sep) do |csv|
135
+ tree_table.each {|row| csv << row }
136
+ end
137
+ end
138
+ end
139
+
140
+ def format(formatter=PlainTextFormat.new)
141
+ formatter.format_table(@tree_table)
142
+ end
143
+
144
+ def setup(tree)
145
+ @tree_table = []
146
+ determine_depth(tree)
147
+ tree.accept(self, nil)
148
+ end
149
+
150
+ def determine_depth(tree) # update of test necessary
151
+ @tree_depth = 0
152
+ depth_checker = Visitor.new do |node, memo|
153
+ @tree_depth = node.depth > @tree_depth ? node.depth : @tree_depth
154
+ end
155
+ tree.accept(depth_checker, 0)
156
+ end
157
+
158
+ def new_table_row
159
+ Array.new(@tree_depth + 1)
160
+ end
161
+
162
+ def visit_dir_node(node, memo)
163
+ add_current_node_row(node)
164
+
165
+ [node.files, node.directories].each do |nodes|
166
+ nodes.sort_by {|n| n.name }.each {|n| n.accept(self, memo) }
167
+ end
168
+ end
169
+
170
+ def visit_file_node(node, memo)
171
+ add_current_node_row(node)
172
+ end
173
+
174
+ def add_current_node_row(node)
175
+ row = new_table_row
176
+ row[node.depth] = node
177
+ @tree_table.push row
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,3 @@
1
+ module DumbDownViewer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,246 @@
1
+ require 'dumb_down_viewer'
2
+ require 'json'
3
+
4
+ module DumbDownViewer
5
+ class Visitor
6
+ attr_accessor
7
+
8
+ def self.create(*args, &memo_update)
9
+ new(&memo_update).tap {|visitor| visitor.setup(*args) }
10
+ end
11
+
12
+ def initialize(&memo_update)
13
+ @memo_update = memo_update
14
+ end
15
+
16
+ def setup(*args)
17
+ end
18
+
19
+ def visit(node, memo)
20
+ memo = @memo_update.call(node, memo) if @memo_update
21
+
22
+ if node.kind_of? DirNode
23
+ visit_dir_node(node, memo)
24
+ else
25
+ visit_file_node(node, memo)
26
+ end
27
+ end
28
+
29
+ def visit_dir_node(node, memo)
30
+ visit_sub_nodes(node, memo)
31
+ end
32
+
33
+ def visit_file_node(node, memo)
34
+ end
35
+
36
+ def visit_sub_nodes(node, memo)
37
+ node.sub_nodes.each do |node|
38
+ node.accept(self, memo)
39
+ end
40
+ end
41
+ end
42
+
43
+ class NodeFormat
44
+ def [](node)
45
+ case node
46
+ when DumbDownViewer::DirNode
47
+ format_dir(node)
48
+ when DumbDownViewer::FileNode
49
+ format_file(node)
50
+ end
51
+ end
52
+
53
+ def format_dir(node)
54
+ "[#{node.name}]"
55
+ end
56
+
57
+ def format_file(node)
58
+ node.name
59
+ end
60
+ end
61
+
62
+ class TreePruner < Visitor
63
+ def setup(keep=true)
64
+ criteria = @memo_update
65
+ delete_method = keep ? :keep_if : :delete_if
66
+ @memo_update = proc do |node, memo|
67
+ unless node.kind_of? FileNode
68
+ [node.directories, node.files].each do |nodes|
69
+ nodes.send(delete_method) {|n| criteria.call(n) }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ class FileCountSummary < Visitor
77
+ class NodeFormat < DumbDownViewer::NodeFormat
78
+ def format_dir(node)
79
+ report = "[#{node.name}]"
80
+ data = node.summary
81
+ counts = data.keys.map {|ext| file_count(data, ext) }.join(', ')
82
+ report << " => #{counts}" unless counts.empty?
83
+ report
84
+ end
85
+
86
+ def file_count(data, ext)
87
+ count = data[ext].size
88
+ unit = count == 1 ? 'file'.freeze : 'files'.freeze
89
+ ext = '(misc)'.freeze if ext.empty?
90
+ "#{ext}: #{count} #{unit}"
91
+ end
92
+ end
93
+
94
+ def visit_dir_node(node, memo)
95
+ node.summary = node.files.group_by {|file| file.extention }
96
+ visit_sub_nodes(node, memo)
97
+ end
98
+ end
99
+
100
+ TotalNodeCount = Visitor.create do |node, memo|
101
+ memo[node.class] += 1
102
+ memo
103
+ end
104
+
105
+ def TotalNodeCount.count(tree)
106
+ counter = Hash.new(0)
107
+ tree.accept(self, counter)
108
+ { directories: counter[DirNode] - 1, files: counter[FileNode] }
109
+ end
110
+
111
+ class JSONConverter
112
+ def self.dump(tree, with_path=false)
113
+ JSON.dump(new.visit(tree, with_path))
114
+ end
115
+
116
+ def visit(node, with_path=false)
117
+ case node
118
+ when DirNode
119
+ {
120
+ type: 'directory', name: name_value(node, with_path),
121
+ contents: node.sub_nodes.map {|n| n.accept(self, with_path) }
122
+ }
123
+ when FileNode
124
+ { type: 'file', name: name_value(node, with_path) }
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def name_value(node, with_path)
131
+ if with_path
132
+ File.join(node.directory, node.name)
133
+ else
134
+ node.name
135
+ end
136
+ end
137
+ end
138
+
139
+ class XMLConverter
140
+ attr_reader :doc, :tree_root
141
+ XML_TEMPLATE = '<?xml version="1.0" encoding="UTF-8"?>'
142
+
143
+ def self.dump(tree, with_path=false)
144
+ visitor = new.tap do |v|
145
+ v.create_doc
146
+ v.visit(tree, with_path)
147
+ end
148
+
149
+ visitor.dump(tree, with_path)
150
+ end
151
+
152
+ def visit(node, with_path)
153
+ case node
154
+ when DirNode
155
+ create_dir_element(node, with_path).tap do |elm|
156
+ node.sub_nodes.each do |n|
157
+ n.accept(self, with_path).parent = elm
158
+ end
159
+ end
160
+ when FileNode
161
+ create_file_element(node, with_path)
162
+ end
163
+ end
164
+
165
+ def create_doc
166
+ @doc = Nokogiri::XML(XML_TEMPLATE).tap do |doc|
167
+ @tree_root = Nokogiri::XML::Node.new('tree'.freeze, doc)
168
+ @tree_root.parent = doc
169
+ end
170
+ end
171
+
172
+ def dump(tree, with_path)
173
+ file_regexp = /#{Regexp.escape("> <\/file>")}/
174
+ visit(tree, with_path).parent = @tree_root
175
+ @doc.to_xml.gsub(file_regexp, '></file>')
176
+ end
177
+
178
+ private
179
+
180
+ def create_dir_element(node, with_path)
181
+ Nokogiri::XML::Node.new('directory'.freeze, @doc).tap do |elm|
182
+ elm['name'.freeze] = name_value(node, with_path)
183
+ end
184
+ end
185
+
186
+ def create_file_element(node, with_path)
187
+ Nokogiri::XML::Node.new('file'.freeze, @doc).tap do |elm|
188
+ elm['name'.freeze] = name_value(node, with_path)
189
+ elm.content = ' '.freeze
190
+ end
191
+ end
192
+
193
+ def name_value(node, with_path)
194
+ if with_path
195
+ File.join(node.directory, node.name)
196
+ else
197
+ node.name
198
+ end
199
+ end
200
+ end
201
+
202
+ class TreeDuplicator < Visitor
203
+ class DuplicationError < StandardError; end
204
+
205
+ def self.duplicate(tree, dest_root)
206
+ duplicator = new
207
+ duplicator.setup(tree, dest_root)
208
+ tree.accept(duplicator, nil)
209
+ end
210
+
211
+ def setup(tree, dest_root)
212
+ orig_root = File.join(tree.directory, tree.name)
213
+ @dest_root, @orig_root = [dest_root, orig_root].map do |root|
214
+ root.sub(/\/\Z/, '')
215
+ end
216
+ @orig_root_re = /\A#{Regexp.escape(@orig_root)}/
217
+ end
218
+
219
+ def visit_dir_node(node, memo)
220
+ dest_dir = replace_root(File.join(node.directory, node.name))
221
+ check_entry_existence(dest_dir)
222
+ FileUtils.mkdir(File.expand_path(dest_dir))
223
+ visit_sub_nodes(node, memo)
224
+ end
225
+
226
+ def visit_file_node(node, memo)
227
+ orig_file = File.join(node.directory, node.name)
228
+ dest_file = replace_root(orig_file)
229
+ check_entry_existence(dest_file)
230
+ FileUtils.cp(File.expand_path(orig_file),
231
+ File.expand_path(dest_file))
232
+ end
233
+
234
+ private
235
+
236
+ def replace_root(path)
237
+ path.sub(@orig_root_re, @dest_root)
238
+ end
239
+
240
+ def check_entry_existence(entry)
241
+ if File.exist?(File.expand_path(entry))
242
+ raise DuplicationError, "#{entry} already exists: #{@orig_root} is not copied."
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,86 @@
1
+ require 'dumb_down_viewer/version'
2
+ require 'fileutils'
3
+ require 'find'
4
+ require 'nokogiri'
5
+
6
+ module DumbDownViewer
7
+ def self.collect_directories_and_files(path)
8
+ entries = Dir.entries(path) - ['.', '..']
9
+ entries.partition do |entry|
10
+ entry_path = File.expand_path(File.join(path, entry))
11
+ File.directory? entry_path
12
+ end
13
+ end
14
+
15
+ def self.build_node_tree(dir)
16
+ dirname, filename = File.split(dir)
17
+ DirNode.new(dirname, filename, 0).tap {|dir| dir.collect_entries }
18
+ end
19
+
20
+ class Node
21
+ attr_reader :sub_nodes, :directory, :name, :depth
22
+ attr_accessor :summary
23
+
24
+ def initialize(pwd, name, depth)
25
+ @directory = pwd
26
+ @name = name
27
+ @depth = depth
28
+ @name_with_path = pwd.empty? ? @name : File.join(pwd, name)
29
+ setup
30
+ end
31
+
32
+ def setup
33
+ end
34
+
35
+ def accept(visitor, memo)
36
+ visitor.visit(self, memo)
37
+ end
38
+
39
+ def to_s
40
+ @name
41
+ end
42
+
43
+ def directory?
44
+ kind_of? DirNode
45
+ end
46
+
47
+ def file?
48
+ kind_of? FileNode
49
+ end
50
+ end
51
+
52
+ class DirNode < Node
53
+ attr_reader :directories, :files
54
+
55
+ def sub_nodes
56
+ (@files + @directories).freeze
57
+ end
58
+
59
+ def collect_entries
60
+ dirs, files = DumbDownViewer.collect_directories_and_files(@name_with_path)
61
+ depth = @depth + 1
62
+ @directories = entry_nodes(dirs, DirNode, depth)
63
+ @directories.each {|dir| dir.collect_entries }
64
+ @files = entry_nodes(files, FileNode, depth)
65
+ end
66
+
67
+ def entry_nodes(nodes, node_class, depth)
68
+ nodes.map {|node| node_class.new(@name_with_path, node, depth) }
69
+ end
70
+ end
71
+
72
+ class FileNode < Node
73
+ attr_reader :extention
74
+ def setup
75
+ extract_extention
76
+ @sub_nodes = [].freeze
77
+ end
78
+
79
+ private
80
+
81
+ def extract_extention
82
+ m = /\.([^.]+)\Z/.match(@name)
83
+ @extention = m ? m[1] : ''
84
+ end
85
+ end
86
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dumb_down_viewer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - HASHIMOTO, Naoki
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: optparse_plus
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: |
84
+ DumbDownViewer (ddv) is a recursive directory listing command with limited functionality.
85
+ In some case, you can use ddv like "tree" command (http://mama.indstate.edu/users/ice/tree/),
86
+ even though these commands are not compatible: Several options of "tree" are missing in ddv,
87
+ but there are also some options that are available only in ddv (such as --copy-to).
88
+ email:
89
+ - hashimoto.naoki@gmail.com
90
+ executables:
91
+ - ddv
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - ".gitignore"
96
+ - ".rspec"
97
+ - ".travis.yml"
98
+ - Gemfile
99
+ - LICENSE.txt
100
+ - README.md
101
+ - Rakefile
102
+ - bin/console
103
+ - bin/setup
104
+ - dumb_down_viewer.gemspec
105
+ - exe/ddv
106
+ - lib/dumb_down_viewer.rb
107
+ - lib/dumb_down_viewer/cli.rb
108
+ - lib/dumb_down_viewer/tree_view_builder.rb
109
+ - lib/dumb_down_viewer/version.rb
110
+ - lib/dumb_down_viewer/visitor.rb
111
+ homepage: https://github.com/nico-hn/DumbDownViewer
112
+ licenses:
113
+ - MIT
114
+ metadata:
115
+ allowed_push_host: https://rubygems.org
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.4.5.1
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: DumbDownViewer (ddv) is a recursive directory listing command with limited
136
+ functionality.
137
+ test_files: []