/bin/tf_plugin

https://bitbucket.org/laika/thingfish · Ruby · 299 lines · 259 code · 24 blank · 16 comment · 3 complexity · 4b5ad0f47dcbdaacb505f6b71450b48c MD5 · raw file

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