/bin/tf_plugin
Ruby | 299 lines | 259 code | 24 blank | 16 comment | 3 complexity | 4b5ad0f47dcbdaacb505f6b71450b48c MD5 | raw file
Possible License(s): BSD-3-Clause
1#!/usr/bin/env ruby
2
3require 'rbconfig'
4require 'optparse'
5require 'ostruct'
6require 'erb'
7require 'etc'
8require 'fileutils'
9
10require 'thingfish'
11
12#
13# tf_plugin -- ThingFish plugin generator
14#
15# == Synopsis
16#
17# $ tf_plugin [OPTIONS] TYPE NAME
18#
19# === Options
20#
21#
22class PluginGenerator
23 include FileUtils
24
25 # ANSI escape codes
26 AnsiAttributes = {
27 'clear' => 0,
28 'reset' => 0,
29 'bold' => 1,
30 'dark' => 2,
31 'underline' => 4,
32 'underscore' => 4,
33 'blink' => 5,
34 'reverse' => 7,
35 'concealed' => 8,
36
37 'black' => 30, 'on_black' => 40,
38 'red' => 31, 'on_red' => 41,
39 'green' => 32, 'on_green' => 42,
40 'yellow' => 33, 'on_yellow' => 43,
41 'blue' => 34, 'on_blue' => 44,
42 'magenta' => 35, 'on_magenta' => 45,
43 'cyan' => 36, 'on_cyan' => 46,
44 'white' => 37, 'on_white' => 47
45 }
46
47
48 if defined?( :Gem ) && dir = Gem.datadir('thingfish')
49 datadir = Pathname( dir )
50 DEFAULT_PLUGINDIR = datadir + 'plugin_templates'
51 else
52 DEFAULT_PLUGINDIR = Pathname( Config::CONFIG['datadir'] ) + 'thingfish/plugin_templates'
53 end
54
55 TEMPLATE_LIBDIR = DEFAULT_PLUGINDIR + 'lib'
56 TEMPLATE_TFDIR = TEMPLATE_LIBDIR + 'thingfish'
57
58
59 ### Return a list of the types of plugins that can be created.
60 def self::find_valid_types
61 Pathname.glob( TEMPLATE_TFDIR + '*' ).collect {|pn| pn.basename.to_s }
62 end
63
64
65 ### Return a struct which contains the default config values, appropriate
66 ### for passing as the first argument to #main
67 def self::default_config
68 options = OpenStruct.new
69
70 options.debugging = false
71 options.noaction = false
72 options.author = Etc.getpwuid( Process.euid ).gecos rescue "J Random Hacker"
73 options.templatedir = DEFAULT_PLUGINDIR
74
75 return options
76 end
77
78
79 ### Make an OptionParser
80 def self::parse_arguments( args )
81 program = Pathname.new( $0 ).expand_path
82 options = self.default_config
83
84 oparser = ARGV.options do |opts|
85
86 opts.banner = "Usage: #{program.basename} [OPTIONS] PLUGINNAME PLUGINTYPE"
87 opts.separator ""
88 opts.separator "PLUGINTYPE should be one of: "
89 opts.separator " " +
90 PluginGenerator.find_valid_types.collect {|type| %Q{"%s"} % type.to_s }.join(", ")
91
92 opts.separator "Generator options"
93
94 opts.on( '--author AUTHORNAME', '-a AUTHORNAME', String,
95 "Use AUTHORNAME in the plugin files instead of '#{options.author}'") do |str|
96 options.author = str
97 end
98
99 opts.on( '--template-dir DIRECTORY', '-t DIRECTORY', String,
100 "Use DIRECTORY as the template for the new plugin directory instead of ",
101 "'#{options.templatedir}'") do |dir|
102 options.templatedir = Pathname( dir )
103 end
104 opts.separator ""
105
106 opts.separator "Runtime options"
107 opts.on( '--debug', '-D', TrueClass, "Turn debugging on." ) do
108 options.debugging = $DEBUG = true
109 end
110
111 opts.on( '--verbose', '-v', TrueClass, "Turn verbose output on." ) do
112 options.verbose = $VERBOSE = true
113 end
114
115 opts.on( '--no-action', '-n', FalseClass, "Don't really do anything, " +
116 "just output what would happen." ) do
117 options.noaction = true
118 end
119
120 opts.on( '--help', '-h', TrueClass, "Display this text." ) do
121 $stderr.puts( opts )
122 exit!( 0 )
123 end
124
125 end
126
127 name, type = oparser.parse!
128 unless args.length >= 2
129 $stderr.puts( oparser )
130 exit( 1 )
131 end
132
133 return options, name, type
134 end
135
136
137 ### Create a new PluginGenerator object, configured with the given +options+
138 ### struct.
139 def initialize( args )
140 @options, @name, @type = self.class.parse_arguments( args )
141 @name = @name.sub( /#@type$/, '' ) # Trim the type off the plain name
142
143 self.extend FileUtils::DryRun if @options.noaction
144 end
145
146
147 ######
148 public
149 ######
150
151 ### Run the generator
152 def run
153 valid_types = self.class.find_valid_types
154 unless valid_types.include?( @type )
155 raise ArgumentError, "Unknown plugin type %p. Expected one of: %p" %
156 [ @type, valid_types ]
157 end
158
159 targetdir = self.make_project_name( @name, @type )
160 message "Project name is: %s\n" % [ targetdir ]
161 raise "#{targetdir}: already exists" if targetdir.exist?
162 verbose_msg " setting up new project..."
163 self.setup_new_project( targetdir )
164 verbose_msg " rendering project templates..."
165 self.render_project_templates( targetdir )
166 message "done.\n"
167
168 rescue => err
169 error_msg "ERROR: #{err.message}"
170 debug_msg " " + err.backtrace.join("\n ")
171 end
172
173
174 ### Search the target directory for files with the extension '.erb', render each
175 ### one using ERB to a file with the same name but with the '.erb' removed, then
176 ### remove the .erb version.
177 def render_project_templates( targetdir )
178 Pathname.glob( targetdir + '**/*.erb' ) do |pathname|
179 outputname = pathname.to_s.sub(/\.erb$/, '').gsub( /TEMPLATE/, @name )
180 outputfile = Pathname.new( outputname ).
181 relative_path_from( Pathname.pwd )
182 template = ERB.new( pathname.read )
183
184 verbose_msg "Rendering %s as %s" % [ pathname, outputfile ]
185 begin
186 name = @name
187 type = @type
188 unless @options.noaction
189 outputfile.open( File::CREAT|File::EXCL|File::WRONLY, 0644 ) do |fh|
190 fh.write( template.result(binding()) )
191 end
192 end
193 rescue => err
194 raise "%s while rendering %s\n from %s:\n %s" %
195 [ err.class.name, outputfile, pathname, err.message ]
196 end
197
198 rm_r( pathname )
199 end
200 end
201
202
203 ### Set up a new project directory by cloning the template directory and removing any
204 ### unnecessary files.
205 def setup_new_project( targetdir )
206 templatedir = @options.templatedir
207 debug_msg "Cloning %s to %s" % [ templatedir, targetdir ]
208 cp_r( templatedir, targetdir, :verbose => @options.verbose )
209
210 unused_types = []
211 self.class.find_valid_types.each do |utype|
212 next if @type == utype
213 debug_msg " Adding libdir #{utype} to the list of stuff to remove"
214 unused_types << targetdir + 'lib/thingfish' + utype
215 unused_types << targetdir + 'spec/thingfish' + utype
216 end
217 unused_types += Pathname.glob( targetdir + '**/.svn' )
218
219 debug_msg "Trimming unused paths (%p)" % [ unused_types ]
220 unused_types.each {|path| rm_rf( path ) }
221 end
222
223
224 ### Make a normalized name for the new project directory and return it as a Pathname object.
225 def make_project_name( name, type )
226 debug_msg "Making project name for a %p project named %p" % [ type, name ]
227 name = "thingfish-#{type}-#{name.downcase}" unless name =~ /^thingfish-/
228 message "Creating thingfish plugin project in %p\n" % [ name ]
229 return Pathname.pwd + name
230 end
231
232
233 #######
234 private
235 #######
236
237 ### Output <tt>msg</tt> to STDERR and flush it.
238 def message( *msgs )
239 $stderr.print( msgs.join("\n") )
240 $stderr.flush
241 end
242
243
244 ### Output +msg+ to STDERR and flush it if $VERBOSE is true.
245 def verbose_msg( msg )
246 msg.chomp!
247 message( msg + "\n" ) if $VERBOSE
248 end
249
250
251 ### Output the specified <tt>msg</tt> as an ANSI-colored error message
252 ### (white on red).
253 def error_msg( msg )
254 message ansi_code( 'bold', 'white', 'on_red' ) + msg + ansi_code( 'reset' )
255 end
256 alias :error_message :error_msg
257
258
259 ### Output the specified <tt>msg</tt> as an ANSI-colored debugging message
260 ### (yellow on blue).
261 def debug_msg( msg )
262 return unless $DEBUG
263 msg.chomp!
264 $stderr.puts ansi_code( 'yellow' ) + ">>> #{msg}" + ansi_code( 'reset' )
265 $stderr.flush
266 end
267
268
269 ### Create a string that contains the ANSI codes specified and return it
270 def ansi_code( *attributes )
271 attributes.flatten!
272 return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
273 attributes = AnsiAttributes.values_at( *attributes ).compact.join(';')
274
275 if attributes.empty?
276 return ''
277 else
278 return "\e[%sm" % attributes
279 end
280 end
281
282
283 ### Colorize the given +string+ with the specified +attributes+ and return it, handling line-endings, etc.
284 def colorize( string, *attributes )
285 ending = string[/(\s)$/] || ''
286 string = string.rstrip
287 return ansi_code( attributes.flatten ) + string + ansi_code( 'reset' ) + ending
288 end
289
290end
291
292
293### If running directly, handle parsing command-line arguments, etc.
294if __FILE__ == $0
295 $stderr.sync = $stdout.sync = true
296
297 PluginGenerator.new( ARGV ).run
298end
299