directory_template 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,306 @@
1
+ # encoding: utf-8
2
+
3
+
4
+
5
+ require 'fileutils'
6
+ require 'directory_template/process_data'
7
+ require 'directory_template/processor'
8
+ require 'directory_template/version'
9
+
10
+
11
+
12
+ # @version 1.0.0
13
+ # @author Stefan Rusterholz <stefan.rusterholz@gmail.com>
14
+ #
15
+ # DirectoryTemplate
16
+ # Create directory structures from template directory structures or template data.
17
+ #
18
+ # Preregistered processors
19
+ # * :stop: Stops the preprocessing chain, it's advised to add that to all files for
20
+ # future-proofness.
21
+ class DirectoryTemplate
22
+
23
+ # All registered processors
24
+ Processors = []
25
+
26
+ # You can register custom processors for templates. They're triggered based on the
27
+ # pattern.
28
+ # A processor can change the ProcessData struct passed to it, which will be reflected
29
+ # when creating the file or directory
30
+ #
31
+ # @param [DirectoryTemplate::Processor] processor
32
+ # The processor to register
33
+ #
34
+ def self.register(processor)
35
+ Processors << processor
36
+ end
37
+ Processor.register_all
38
+
39
+ # The standard path processor, just replaces `%{sprintf_style_variables}` with their
40
+ # values.
41
+ StandardPathProcessor = Processor::Format
42
+
43
+ # The default options used by DirectoryTemplate
44
+ DefaultOptions = {
45
+ :verbose => false,
46
+ :silent => false,
47
+ :out => $stdout,
48
+ :processors => Processors,
49
+ :path_processor => StandardPathProcessor,
50
+ :source => '(unknown)',
51
+ :meta => {},
52
+ }
53
+
54
+ # Create a DirectoryTemplate from an existing directory structure.
55
+ def self.directory(template_path, options={})
56
+ data = Dir.chdir(template_path) {
57
+ paths = Dir['**/{*,.*}']
58
+ paths -= paths.grep(/(?:^|\/)\.\.?$/)
59
+ directories, files = paths.sort.partition { |path| File.directory?(path) }
60
+ filemap = Hash[files.map { |path| [path, File.read(path)] }]
61
+
62
+ {:directories => directories, :files => filemap}
63
+ }
64
+
65
+ new(data, options.merge(:source => template_path))
66
+ end
67
+
68
+ # @private
69
+ # Converts a recursive hash into a suitable data structure for DirectoryTemplate::new
70
+ def self.convert_recursive_structure(current, stack=[], dirs=[], files={})
71
+ current.each do |segment, content|
72
+ new_stack = stack+[segment]
73
+ path = new_stack.join('/')
74
+ case content
75
+ when String,nil
76
+ files[path] = content || ''
77
+ when Hash
78
+ dirs << path
79
+ convert_recursive_structure(content, new_stack, dirs, files)
80
+ else
81
+ raise "Invalid structure"
82
+ end
83
+ end
84
+
85
+ return dirs, files
86
+ end
87
+
88
+ # Create a DirectoryTemplate from a nested hash structure.
89
+ # The hash should just be a recursive hash of strings. Use an empty hash to indicate
90
+ # an empty directory. Leaf-strings are considered to be the content of a file. Use nil
91
+ # to indicate an empty file.
92
+ def self.from_hash(hash, options=nil)
93
+ dirs, files = convert_recursive_structure(hash)
94
+ data = {:directories => dirs, :files => files}
95
+
96
+ new(data, options)
97
+ end
98
+
99
+ # Create a DirectoryTemplate from a YAML file.
100
+ # The yaml should just be a recursive hash of strings. Use an empty hash to indicate
101
+ # an empty directory. Leaf-strings are considered to be the content of a file. Use nil
102
+ # to indicate an empty file.
103
+ def self.yaml_file(path, options={})
104
+ from_hash(YAML.load_file(path), options.merge(:source => template_path))
105
+ end
106
+
107
+ # Meta information can be used by processors. There's no requirements on them, except
108
+ # that the toplevel container is a hash.
109
+ attr_reader :meta
110
+
111
+ # @return [Array] All directories of the template
112
+ attr_reader :directories
113
+
114
+ # @return [Hash<String,String>] All files of the template and their content.
115
+ attr_reader :files
116
+
117
+ # @return [Array<DirectoryTemplate::Processor>] The content processors used by this template.
118
+ attr_reader :processors
119
+
120
+ # @return [#call] The path processor used by this template.
121
+ attr_reader :path_processor
122
+
123
+ # @return [IO, #puts] The object on which info and debug messages are printed
124
+ attr_reader :out
125
+
126
+ # @private
127
+ # @return [Boolean] Whether the current run is a dry-run or not.
128
+ attr_reader :dry_run
129
+
130
+ # When true, will not even output info messages
131
+ attr_accessor :silent
132
+
133
+ # When true, will additionally output debug messages
134
+ attr_accessor :verbose
135
+
136
+ # Create a new DirectoryTemplate
137
+ #
138
+ # @param [Hash] data
139
+ # A hash with the two keys :directories and :files, where :directories contains an
140
+ # Array of all directory names, and :files contains a Hash of all file names and their
141
+ # unprocessed content.
142
+ #
143
+ # @param [Hash, nil] options
144
+ # An options hash, @see DirectoryTemplate::DefaultOptions for a list of all available
145
+ # options.
146
+ #
147
+ # @see DirectoryTemplate::directory
148
+ # To create a DirectoryTemplate from an existing directory structure.
149
+ # @see DirectoryTemplate::from_yaml
150
+ # To create a DirectoryTemplate from a description in a YAML file.
151
+ def initialize(data, options=nil)
152
+ options = options ? DefaultOptions.merge(options) : DefaultOptions.dup
153
+ @directories = data[:directories] || []
154
+ @files = data[:files] || []
155
+ @source = options.delete(:source)
156
+ @meta = options.delete(:meta)
157
+ @verbose = options.delete(:verbose)
158
+ @silent = options.delete(:silent)
159
+ @out = options.delete(:out)
160
+ @processors = options.delete(:processors)
161
+ @path_processor = options.delete(:path_processor)
162
+ @dry_run = false
163
+ raise ArgumentError, "Unknown options: #{options.keys.join(', ')}" unless options.empty?
164
+ end
165
+
166
+ # Same as #materialize, but doesn't actually do anything, except print the debug and
167
+ # info messages. It additionally prints an info message, containing the file content
168
+ # of files that would be created.
169
+ def dry_run(in_path='.', env={}, &on_collision)
170
+ @dry_run = true
171
+ materialize(in_path, env, &on_collision)
172
+ ensure
173
+ @dry_run = false
174
+ end
175
+
176
+ # Creates all the directories and files from the template in the given path.
177
+ #
178
+ # @param [String] in_path
179
+ # The directory within which to generate the structure.
180
+ #
181
+ # @param [Hash] env
182
+ # A hash with various information used to generate the structure. Most important one
183
+ # being the :variables value.
184
+ # @option env [Hash] :variables
185
+ # A hash with variables available to both, path and content processing
186
+ # @option env [Hash] :path_variables
187
+ # A hash with variables available to path processing
188
+ # @option env [Hash] :content_variables
189
+ # A hash with variables available to content processing
190
+ #
191
+ # @see #dry_run For a way to see what would happen with materialize
192
+ def materialize(in_path='.', env={}, &on_collision)
193
+ in_path = File.expand_path(in_path)
194
+ create_directory(in_path) { "Creating root '#{in_path}'" }
195
+
196
+ Dir.chdir(in_path) do
197
+ if @directories.empty? then
198
+ info { "No directories to create" }
199
+ else
200
+ info { "Creating directories" }
201
+ @directories.each do |source_dir_path|
202
+ target_dir_path = process_path(source_dir_path, env)
203
+ create_directory(target_dir_path) { " #{target_dir_path}" }
204
+ end
205
+ end
206
+
207
+ if @files.empty? then
208
+ info { "No files to create" }
209
+ else
210
+ info { "Creating files" }
211
+ @files.each do |source_file_path, content|
212
+ target_file_path = process_path(source_file_path, env)
213
+ data = process_content(target_file_path, content, env)
214
+ if File.exist?(data.path) then
215
+ if block_given? && yield(data) then
216
+ create_file(data.path, data.content) { " #{data.path} (overwrite)" }
217
+ else
218
+ info { " #{data.path} (exists already)" }
219
+ end
220
+ else
221
+ create_file(data.path, data.content) { " #{data.path} (new)" }
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ self
228
+ end
229
+
230
+ # @private
231
+ # Preprocesses the given path
232
+ def process_path(path, env)
233
+ @path_processor.call(ProcessData.new(self, path, nil, env)).path
234
+ end
235
+
236
+ # @private
237
+ # Preprocesses the given content
238
+ def process_content(path, content, env)
239
+ data = ProcessData.new(self, path, content, env)
240
+ catch(:stop_processing) {
241
+ #p :process_content => path, :available => @processors.size, :processors => processors_for(data).tap { |x| x && x.size }
242
+ while processor = processor_for(data)
243
+ debug { " -> Applying #{processor.name}" }
244
+ processor.call(data)
245
+ end
246
+ }
247
+
248
+ data
249
+ end
250
+
251
+ # @private
252
+ # Create the given directory and emit an info message (unless in dry_run mode).
253
+ #
254
+ # @note The mode param is currently unused.
255
+ def create_directory(path, mode=0755, &message)
256
+ unless File.exist?(path) then
257
+ info(&message)
258
+ FileUtils.mkdir_p(path) unless @dry_run
259
+ end
260
+ end
261
+
262
+ # @private
263
+ # Create the given file and emit an info message (unless in dry_run mode).
264
+ #
265
+ # @note The mode param is currently unused.
266
+ def create_file(path, content="", mode=0644, &message)
267
+ info(&message)
268
+ if @dry_run then
269
+ info { " Content:\n#{content.gsub(/^/, ' ')}" }
270
+ else
271
+ File.open(path, 'wb:binary') do |fh|
272
+ fh.write(content)
273
+ end
274
+ end
275
+ end
276
+
277
+ # @private
278
+ # @param [ProcessData] data
279
+ # The data which the processor should apply to.
280
+ #
281
+ # @return [Processor, nil]
282
+ # Returns the processor or nil
283
+ def processor_for(data)
284
+ @processors.find { |processor| processor === data }
285
+ end
286
+
287
+ # @private
288
+ # Emit an info string (the return value of the block). Will not be emitted if
289
+ # DirectoryTemplate#silent is true
290
+ def info
291
+ @out.puts yield unless @silent
292
+ end
293
+
294
+ # @private
295
+ # Emit a debug string (the return value of the block). Will only be emitted if
296
+ # DirectoryTemplate#debug is true
297
+ def debug
298
+ @out.puts yield if @verbose
299
+ end
300
+
301
+ # @private
302
+ # See Object#inspect
303
+ def inspect
304
+ sprintf "#<%s:0x%x source=%p>", self.class, object_id<<1, @source
305
+ end
306
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: directory_template
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Stefan Rusterholz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-03 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! 'Create directories from templates.
15
+
16
+ Existing directory structures, yaml files and ruby datastructures can all serve
17
+ as
18
+
19
+ sources of a template.
20
+
21
+ Can preprocess pathnames and content.
22
+
23
+ Path- and ContentProcessors are exchangeable.'
24
+ email: stefan.rusterholz@gmail.com
25
+ executables: []
26
+ extensions: []
27
+ extra_rdoc_files: []
28
+ files:
29
+ - lib/directory_template/blank_slate.rb
30
+ - lib/directory_template/erb_template.rb
31
+ - lib/directory_template/process_data.rb
32
+ - lib/directory_template/processor/erb.rb
33
+ - lib/directory_template/processor/format.rb
34
+ - lib/directory_template/processor/markdown.rb
35
+ - lib/directory_template/processor/stop.rb
36
+ - lib/directory_template/processor.rb
37
+ - lib/directory_template/version.rb
38
+ - lib/directory_template.rb
39
+ - examples/dir_gem_template/%{gem_name}/%{gem_name}.gemspec.stop.erb
40
+ - examples/dir_gem_template/%{gem_name}/lib/%{require_name}/version.rb.stop.erb
41
+ - examples/dir_gem_template/%{gem_name}/lib/%{require_name}.rb.stop.erb
42
+ - examples/dir_gem_template/%{gem_name}/Rakefile.stop
43
+ - examples/dir_gem_template/%{gem_name}/README.markdown.stop.erb
44
+ - examples/dir_gem_template/%{gem_name}/README_DIRECTORIES.stop
45
+ - examples/yaml_gem_template.yaml
46
+ - documentation/ContentProcessors.markdown
47
+ - documentation/PathProcessors.markdown
48
+ - directory_template.gemspec
49
+ - Rakefile
50
+ - README.markdown
51
+ homepage: https://github.com/apeiros/directory_template
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>'
67
+ - !ruby/object:Gem::Version
68
+ version: 1.3.1
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 1.8.24
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Create directories from templates, optionally preprocessing paths and contents.
75
+ test_files: []
76
+ has_rdoc: