/bin/tf_plugin
https://bitbucket.org/laika/thingfish · Ruby · 299 lines · 259 code · 24 blank · 16 comment · 3 complexity · 4b5ad0f47dcbdaacb505f6b71450b48c MD5 · raw file
- #!/usr/bin/env ruby
- require 'rbconfig'
- require 'optparse'
- require 'ostruct'
- require 'erb'
- require 'etc'
- require 'fileutils'
- require 'thingfish'
- #
- # tf_plugin -- ThingFish plugin generator
- #
- # == Synopsis
- #
- # $ tf_plugin [OPTIONS] TYPE NAME
- #
- # === Options
- #
- #
- class PluginGenerator
- include FileUtils
- # ANSI escape codes
- AnsiAttributes = {
- 'clear' => 0,
- 'reset' => 0,
- 'bold' => 1,
- 'dark' => 2,
- 'underline' => 4,
- 'underscore' => 4,
- 'blink' => 5,
- 'reverse' => 7,
- 'concealed' => 8,
- 'black' => 30, 'on_black' => 40,
- 'red' => 31, 'on_red' => 41,
- 'green' => 32, 'on_green' => 42,
- 'yellow' => 33, 'on_yellow' => 43,
- 'blue' => 34, 'on_blue' => 44,
- 'magenta' => 35, 'on_magenta' => 45,
- 'cyan' => 36, 'on_cyan' => 46,
- 'white' => 37, 'on_white' => 47
- }
- if defined?( :Gem ) && dir = Gem.datadir('thingfish')
- datadir = Pathname( dir )
- DEFAULT_PLUGINDIR = datadir + 'plugin_templates'
- else
- DEFAULT_PLUGINDIR = Pathname( Config::CONFIG['datadir'] ) + 'thingfish/plugin_templates'
- end
- TEMPLATE_LIBDIR = DEFAULT_PLUGINDIR + 'lib'
- TEMPLATE_TFDIR = TEMPLATE_LIBDIR + 'thingfish'
- ### Return a list of the types of plugins that can be created.
- def self::find_valid_types
- Pathname.glob( TEMPLATE_TFDIR + '*' ).collect {|pn| pn.basename.to_s }
- end
- ### Return a struct which contains the default config values, appropriate
- ### for passing as the first argument to #main
- def self::default_config
- options = OpenStruct.new
- options.debugging = false
- options.noaction = false
- options.author = Etc.getpwuid( Process.euid ).gecos rescue "J Random Hacker"
- options.templatedir = DEFAULT_PLUGINDIR
- return options
- end
- ### Make an OptionParser
- def self::parse_arguments( args )
- program = Pathname.new( $0 ).expand_path
- options = self.default_config
- oparser = ARGV.options do |opts|
- opts.banner = "Usage: #{program.basename} [OPTIONS] PLUGINNAME PLUGINTYPE"
- opts.separator ""
- opts.separator "PLUGINTYPE should be one of: "
- opts.separator " " +
- PluginGenerator.find_valid_types.collect {|type| %Q{"%s"} % type.to_s }.join(", ")
- opts.separator "Generator options"
- opts.on( '--author AUTHORNAME', '-a AUTHORNAME', String,
- "Use AUTHORNAME in the plugin files instead of '#{options.author}'") do |str|
- options.author = str
- end
- opts.on( '--template-dir DIRECTORY', '-t DIRECTORY', String,
- "Use DIRECTORY as the template for the new plugin directory instead of ",
- "'#{options.templatedir}'") do |dir|
- options.templatedir = Pathname( dir )
- end
- opts.separator ""
- opts.separator "Runtime options"
- opts.on( '--debug', '-D', TrueClass, "Turn debugging on." ) do
- options.debugging = $DEBUG = true
- end
- opts.on( '--verbose', '-v', TrueClass, "Turn verbose output on." ) do
- options.verbose = $VERBOSE = true
- end
- opts.on( '--no-action', '-n', FalseClass, "Don't really do anything, " +
- "just output what would happen." ) do
- options.noaction = true
- end
- opts.on( '--help', '-h', TrueClass, "Display this text." ) do
- $stderr.puts( opts )
- exit!( 0 )
- end
- end
- name, type = oparser.parse!
- unless args.length >= 2
- $stderr.puts( oparser )
- exit( 1 )
- end
- return options, name, type
- end
- ### Create a new PluginGenerator object, configured with the given +options+
- ### struct.
- def initialize( args )
- @options, @name, @type = self.class.parse_arguments( args )
- @name = @name.sub( /#@type$/, '' ) # Trim the type off the plain name
- self.extend FileUtils::DryRun if @options.noaction
- end
- ######
- public
- ######
- ### Run the generator
- def run
- valid_types = self.class.find_valid_types
- unless valid_types.include?( @type )
- raise ArgumentError, "Unknown plugin type %p. Expected one of: %p" %
- [ @type, valid_types ]
- end
- targetdir = self.make_project_name( @name, @type )
- message "Project name is: %s\n" % [ targetdir ]
- raise "#{targetdir}: already exists" if targetdir.exist?
- verbose_msg " setting up new project..."
- self.setup_new_project( targetdir )
- verbose_msg " rendering project templates..."
- self.render_project_templates( targetdir )
- message "done.\n"
- rescue => err
- error_msg "ERROR: #{err.message}"
- debug_msg " " + err.backtrace.join("\n ")
- end
- ### Search the target directory for files with the extension '.erb', render each
- ### one using ERB to a file with the same name but with the '.erb' removed, then
- ### remove the .erb version.
- def render_project_templates( targetdir )
- Pathname.glob( targetdir + '**/*.erb' ) do |pathname|
- outputname = pathname.to_s.sub(/\.erb$/, '').gsub( /TEMPLATE/, @name )
- outputfile = Pathname.new( outputname ).
- relative_path_from( Pathname.pwd )
- template = ERB.new( pathname.read )
- verbose_msg "Rendering %s as %s" % [ pathname, outputfile ]
- begin
- name = @name
- type = @type
- unless @options.noaction
- outputfile.open( File::CREAT|File::EXCL|File::WRONLY, 0644 ) do |fh|
- fh.write( template.result(binding()) )
- end
- end
- rescue => err
- raise "%s while rendering %s\n from %s:\n %s" %
- [ err.class.name, outputfile, pathname, err.message ]
- end
- rm_r( pathname )
- end
- end
- ### Set up a new project directory by cloning the template directory and removing any
- ### unnecessary files.
- def setup_new_project( targetdir )
- templatedir = @options.templatedir
- debug_msg "Cloning %s to %s" % [ templatedir, targetdir ]
- cp_r( templatedir, targetdir, :verbose => @options.verbose )
- unused_types = []
- self.class.find_valid_types.each do |utype|
- next if @type == utype
- debug_msg " Adding libdir #{utype} to the list of stuff to remove"
- unused_types << targetdir + 'lib/thingfish' + utype
- unused_types << targetdir + 'spec/thingfish' + utype
- end
- unused_types += Pathname.glob( targetdir + '**/.svn' )
- debug_msg "Trimming unused paths (%p)" % [ unused_types ]
- unused_types.each {|path| rm_rf( path ) }
- end
- ### Make a normalized name for the new project directory and return it as a Pathname object.
- def make_project_name( name, type )
- debug_msg "Making project name for a %p project named %p" % [ type, name ]
- name = "thingfish-#{type}-#{name.downcase}" unless name =~ /^thingfish-/
- message "Creating thingfish plugin project in %p\n" % [ name ]
- return Pathname.pwd + name
- end
- #######
- private
- #######
- ### Output <tt>msg</tt> to STDERR and flush it.
- def message( *msgs )
- $stderr.print( msgs.join("\n") )
- $stderr.flush
- end
- ### Output +msg+ to STDERR and flush it if $VERBOSE is true.
- def verbose_msg( msg )
- msg.chomp!
- message( msg + "\n" ) if $VERBOSE
- end
- ### Output the specified <tt>msg</tt> as an ANSI-colored error message
- ### (white on red).
- def error_msg( msg )
- message ansi_code( 'bold', 'white', 'on_red' ) + msg + ansi_code( 'reset' )
- end
- alias :error_message :error_msg
- ### Output the specified <tt>msg</tt> as an ANSI-colored debugging message
- ### (yellow on blue).
- def debug_msg( msg )
- return unless $DEBUG
- msg.chomp!
- $stderr.puts ansi_code( 'yellow' ) + ">>> #{msg}" + ansi_code( 'reset' )
- $stderr.flush
- end
- ### Create a string that contains the ANSI codes specified and return it
- def ansi_code( *attributes )
- attributes.flatten!
- return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
- attributes = AnsiAttributes.values_at( *attributes ).compact.join(';')
- if attributes.empty?
- return ''
- else
- return "\e[%sm" % attributes
- end
- end
- ### Colorize the given +string+ with the specified +attributes+ and return it, handling line-endings, etc.
- def colorize( string, *attributes )
- ending = string[/(\s)$/] || ''
- string = string.rstrip
- return ansi_code( attributes.flatten ) + string + ansi_code( 'reset' ) + ending
- end
- end
- ### If running directly, handle parsing command-line arguments, etc.
- if __FILE__ == $0
- $stderr.sync = $stdout.sync = true
- PluginGenerator.new( ARGV ).run
- end