PageRenderTime 56ms CodeModel.GetById 25ms app.highlight 11ms RepoModel.GetById 0ms app.codeStats 0ms

/bin/tf_plugin

https://bitbucket.org/laika/thingfish
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