diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index 9da48af55..7bebd18bb 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,199 +1,216 @@ require 'puppet/application' require 'puppet/face' require 'optparse' require 'pp' class Puppet::Application::FaceBase < Puppet::Application should_parse_config run_mode :agent option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end option("--verbose", "-v") do Puppet::Util::Log.level = :info end option("--render-as FORMAT") do |arg| @render_as = arg.to_sym end option("--mode RUNMODE", "-r") do |arg| raise "Invalid run mode #{arg}; supported modes are user, agent, master" unless %w{user agent master}.include?(arg) self.class.run_mode(arg.to_sym) set_run_mode self.class.run_mode end attr_accessor :face, :action, :type, :arguments, :render_as attr_writer :exit_code # This allows you to set the exit code if you don't want to just exit # immediately but you need to indicate a failure. def exit_code @exit_code || 0 end def render(result) format = render_as || action.render_as || :for_humans # Invoke the rendering hook supplied by the user, if appropriate. if hook = action.when_rendering(format) then result = hook.call(result) end if format == :for_humans then render_for_humans(result) else render_method = Puppet::Network::FormatHandler.format(format).render_method if render_method == "to_pson" PSON::pretty_generate(result, :allow_nan => true, :max_nesting => false) else result.send(render_method) end end end def render_for_humans(result) # String to String return result if result.is_a? String return result if result.is_a? Numeric # Simple hash to table if result.is_a? Hash and result.keys.all? { |x| x.is_a? String or x.is_a? Numeric } output = '' column_a = result.map do |k,v| k.to_s.length end.max + 2 column_b = 79 - column_a result.sort_by { |k,v| k.to_s } .each do |key, value| output << key.to_s.ljust(column_a) output << PP.pp(value, '', column_b). chomp.gsub(/\n */) { |x| x + (' ' * column_a) } output << "\n" end return output end # ...or pretty-print the inspect outcome. return result.pretty_inspect end def preinit super Signal.trap(:INT) do $stderr.puts "Cancelling Face" exit(0) end end def parse_options # We need to parse enough of the command line out early, to identify what # the action is, so that we can obtain the full set of options to parse. # REVISIT: These should be configurable versions, through a global # '--version' option, but we don't implement that yet... --daniel 2011-03-29 - @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym - @face = Puppet::Face[@type, :current] + @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym + @face = Puppet::Face[@type, :current] # Now, walk the command line and identify the action. We skip over # arguments based on introspecting the action and all, and find the first # non-option word to use as the action. action = nil index = -1 until @action or (index += 1) >= command_line.args.length do item = command_line.args[index] if item =~ /^-/ then option = @face.options.find do |name| item =~ /^-+#{name.to_s.gsub(/[-_]/, '[-_]')}(?:[ =].*)?$/ end if option then option = @face.get_option(option) # If we have an inline argument, just carry on. We don't need to # care about optional vs mandatory in that case because we do a real # parse later, and that will totally take care of raising the error # when we get there. --daniel 2011-04-04 if option.takes_argument? and !item.index('=') then index += 1 unless (option.optional_argument? and command_line.args[index + 1] =~ /^-/) end elsif option = find_global_settings_argument(item) then unless Puppet.settings.boolean? option.name then # As far as I can tell, we treat non-bool options as always having # a mandatory argument. --daniel 2011-04-05 index += 1 # ...so skip the argument. end + elsif option = find_application_argument(item) then + index += 1 if (option[:argument] and option[:optional]) else raise OptionParser::InvalidOption.new(item.sub(/=.*$/, '')) end else @action = @face.get_action(item.to_sym) end end if @action.nil? @action = @face.get_default_action() @is_default_action = true end # Now we can interact with the default option code to build behaviour # around the full set of options we now know we support. @action.options.each do |option| option = @action.get_option(option) # make it the object. self.class.option(*option.optparse) # ...and make the CLI parse it. end if @action # ...and invoke our parent to parse all the command line options. super end def find_global_settings_argument(item) Puppet.settings.each do |name, object| object.optparse_args.each do |arg| next unless arg =~ /^-/ # sadly, we have to emulate some of optparse here... pattern = /^#{arg.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ pattern.match item and return object end end return nil # nothing found. end + def find_application_argument(item) + self.class.option_parser_commands.each do |options, function| + options.each do |option| + next unless option =~ /^-/ + pattern = /^#{option.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ + next unless pattern.match(item) + return { + :argument => option =~ /[ =]/, + :optional => option =~ /[ =]\[/ + } + end + end + return nil # not found + end + def setup Puppet::Util::Log.newdestination :console @arguments = command_line.args # Note: because of our definition of where the action is set, we end up # with it *always* being the first word of the remaining set of command # line arguments. So, strip that off when we construct the arguments to # pass down to the face action. --daniel 2011-04-04 # Of course, now that we have default actions, we should leave the # "action" name on if we didn't actually consume it when we found our # action. @arguments.delete_at(0) unless @is_default_action # We copy all of the app options to the end of the call; This allows each # action to read in the options. This replaces the older model where we # would invoke the action with options set as global state in the # interface object. --daniel 2011-03-28 @arguments << options end def main # Call the method associated with the provided action (e.g., 'find'). if @action result = @face.send(@action.name, *arguments) puts render(result) unless result.nil? else if arguments.first.is_a? Hash puts "#{@face} does not have a default action" else puts "#{@face} does not respond to action #{arguments.first}" end puts Puppet::Face[:help, :current].help(@face.name, *arguments) end exit(exit_code) end end diff --git a/lib/puppet/faces/help/action.erb b/lib/puppet/faces/help/action.erb deleted file mode 100644 index eaf131464..000000000 --- a/lib/puppet/faces/help/action.erb +++ /dev/null @@ -1,3 +0,0 @@ -Use: puppet <%= face.name %> [options] <%= action.name %> [options] - -Summary: <%= action.summary %> diff --git a/lib/puppet/faces/help/face.erb b/lib/puppet/faces/help/face.erb deleted file mode 100644 index efe5fd809..000000000 --- a/lib/puppet/faces/help/face.erb +++ /dev/null @@ -1,7 +0,0 @@ -Use: puppet <%= face.name %> [options] [options] - -Available actions: -% face.actions.each do |actionname| -% action = face.get_action(actionname) - <%= action.name.to_s.ljust(16) %> <%= action.summary %> -% end diff --git a/lib/puppet/faces/help/global.erb b/lib/puppet/faces/help/global.erb deleted file mode 100644 index e123367a2..000000000 --- a/lib/puppet/faces/help/global.erb +++ /dev/null @@ -1,20 +0,0 @@ -puppet [options] [options] - -Available subcommands, from Puppet Faces: -% Puppet::Faces.faces.sort.each do |name| -% face = Puppet::Faces[name, :current] - <%= face.name.to_s.ljust(16) %> <%= face.summary %> -% end - -% unless legacy_applications.empty? then # great victory when this is true! -Available applications, soon to be ported to Faces: -% legacy_applications.each do |appname| -% summary = horribly_extract_summary_from appname - <%= appname.to_s.ljust(16) %> <%= summary %> -% end -% end - -See 'puppet help ' for help on a specific subcommand action. -See 'puppet help ' for help on a specific subcommand. -See 'puppet man ' for the full man page. -Puppet v<%= Puppet::PUPPETVERSION %> diff --git a/lib/puppet/interface.rb b/lib/puppet/interface.rb index 5c8ade749..ced00863d 100644 --- a/lib/puppet/interface.rb +++ b/lib/puppet/interface.rb @@ -1,160 +1,159 @@ require 'puppet' require 'puppet/util/autoload' class Puppet::Interface require 'puppet/interface/face_collection' require 'puppet/interface/action_manager' include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager require 'puppet/interface/option_manager' include Puppet::Interface::OptionManager extend Puppet::Interface::OptionManager include Puppet::Util class << self # This is just so we can search for actions. We only use its # list of directories to search. # Can't we utilize an external autoloader, or simply use the $LOAD_PATH? -pvb def autoloader @autoloader ||= Puppet::Util::Autoload.new(:application, "puppet/face") end def faces Puppet::Interface::FaceCollection.faces end - def face?(name, version) - Puppet::Interface::FaceCollection.face?(name, version) - end - def register(instance) Puppet::Interface::FaceCollection.register(instance) end def define(name, version, &block) - if face?(name, version) - face = Puppet::Interface::FaceCollection[name, version] - else + face = Puppet::Interface::FaceCollection[name, version] + if face.nil? then face = self.new(name, version) Puppet::Interface::FaceCollection.register(face) # REVISIT: Shouldn't this be delayed until *after* we evaluate the # current block, not done before? --daniel 2011-04-07 face.load_actions end face.instance_eval(&block) if block_given? return face end + def face?(name, version) + Puppet::Interface::FaceCollection[name, version] + end + def [](name, version) unless face = Puppet::Interface::FaceCollection[name, version] if current = Puppet::Interface::FaceCollection[name, :current] - raise Puppet::Error, "Could not find version #{version} of #{current}" + raise Puppet::Error, "Could not find version #{version} of #{name}" else raise Puppet::Error, "Could not find Puppet Face #{name.inspect}" end end face end end def set_default_format(format) Puppet.warning("set_default_format is deprecated (and ineffective); use render_as on your actions instead.") end ######################################################################## # Documentation. We currently have to rewrite both getters because we share # the same instance between build-time and the runtime instance. When that # splits out this should merge into a module that both the action and face # include. --daniel 2011-04-17 attr_accessor :summary, :description def summary(value = nil) self.summary = value unless value.nil? @summary end def summary=(value) value = value.to_s value =~ /\n/ and raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." @summary = value end def description(value = nil) self.description = value unless value.nil? @description end ######################################################################## attr_reader :name, :version def initialize(name, version, &block) unless Puppet::Interface::FaceCollection.validate_version(version) raise ArgumentError, "Cannot create face #{name.inspect} with invalid version number '#{version}'!" end @name = Puppet::Interface::FaceCollection.underscorize(name) @version = version instance_eval(&block) if block_given? end # Try to find actions defined in other files. def load_actions Puppet::Interface.autoloader.search_directories.each do |dir| Dir.glob(File.join(dir, "puppet/face/#{name}", "*.rb")).each do |file| action = file.sub(dir, '').sub(/^[\\\/]/, '').sub(/\.rb/, '') Puppet.debug "Loading action '#{action}' for '#{name}' from '#{dir}/#{action}.rb'" require(action) end end end def to_s "Puppet::Face[#{name.inspect}, #{version.inspect}]" end ######################################################################## # Action decoration, whee! You are not expected to care about this code, # which exists to support face building and construction. I marked these # private because the implementation is crude and ugly, and I don't yet know # enough to work out how to make it clean. # # Once we have established that these methods will likely change radically, # to be unrecognizable in the final outcome. At which point we will throw # all this away, replace it with something nice, and work out if we should # be making this visible to the outside world... --daniel 2011-04-14 private def __invoke_decorations(type, action, passed_args = [], passed_options = {}) [:before, :after].member?(type) or fail "unknown decoration type #{type}" # Collect the decoration methods matching our pass. methods = action.options.select do |name| passed_options.has_key? name end.map do |name| action.get_option(name).__decoration_name(type) end methods.each do |hook| begin respond_to? hook and self.__send__(hook, action, passed_args, passed_options) rescue => e Puppet.warning("invoking #{action} #{type} hook: #{e}") end end end def __add_method(name, proc) meta_def(name, &proc) method(name).unbind end def self.__add_method(name, proc) define_method(name, proc) instance_method(name) end end diff --git a/lib/puppet/interface/action.rb b/lib/puppet/interface/action.rb index 23366b407..08bc0a345 100644 --- a/lib/puppet/interface/action.rb +++ b/lib/puppet/interface/action.rb @@ -1,231 +1,252 @@ # -*- coding: utf-8 -*- require 'puppet/interface' require 'puppet/interface/option' class Puppet::Interface::Action def initialize(face, name, attrs = {}) raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/ @face = face @name = name.to_sym attrs.each do |k, v| send("#{k}=", v) end @options = {} @when_rendering = {} end # This is not nice, but it is the easiest way to make us behave like the # Ruby Method object rather than UnboundMethod. Duplication is vaguely # annoying, but at least we are a shallow clone. --daniel 2011-04-12 def __dup_and_rebind_to(to) bound_version = self.dup bound_version.instance_variable_set(:@face, to) return bound_version end def to_s() "#{@face}##{@name}" end attr_reader :name attr_accessor :default def default? !!@default end attr_accessor :summary ######################################################################## # Support for rendering formats and all. def when_rendering(type) unless type.is_a? Symbol raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" end return unless @when_rendering.has_key? type return @when_rendering[type].bind(@face) end def set_rendering_method_for(type, proc) unless proc.is_a? Proc msg = "The second argument to set_rendering_method_for must be a Proc" msg += ", not #{proc.class.name}" unless proc.nil? raise ArgumentError, msg end if proc.arity != 1 then msg = "when_rendering methods take one argument, the result, not " if proc.arity < 0 then msg += "a variable number" else msg += proc.arity.to_s end raise ArgumentError, msg end unless type.is_a? Symbol raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" end if @when_rendering.has_key? type then raise ArgumentError, "You can't define a rendering method for #{type} twice" end # Now, the ugly bit. We add the method to our interface object, and # retrieve it, to rotate through the dance of getting a suitable method # object out of the whole process. --daniel 2011-04-18 @when_rendering[type] = @face.__send__( :__add_method, __render_method_name_for(type), proc) end def __render_method_name_for(type) :"#{name}_when_rendering_#{type}" end private :__render_method_name_for attr_accessor :render_as def render_as=(value) @render_as = value.to_sym end ######################################################################## # Documentation stuff, whee! attr_accessor :summary, :description def summary=(value) value = value.to_s value =~ /\n/ and raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." @summary = value end ######################################################################## # Initially, this was defined to allow the @action.invoke pattern, which is # a very natural way to invoke behaviour given our introspection # capabilities. Heck, our initial plan was to have the faces delegate to # the action object for invocation and all. # # It turns out that we have a binding problem to solve: @face was bound to # the parent class, not the subclass instance, and we don't pass the # appropriate context or change the binding enough to make this work. # # We could hack around it, by either mandating that you pass the context in # to invoke, or try to get the binding right, but that has probably got # subtleties that we don't instantly think of – especially around threads. # # So, we are pulling this method for now, and will return it to life when we # have the time to resolve the problem. For now, you should replace... # # @action = @face.get_action(name) # @action.invoke(arg1, arg2, arg3) # # ...with... # # @action = @face.get_action(name) # @face.send(@action.name, arg1, arg2, arg3) # # I understand that is somewhat cumbersome, but it functions as desired. # --daniel 2011-03-31 # # PS: This code is left present, but commented, to support this chunk of # documentation, for the benefit of the reader. # # def invoke(*args, &block) # @face.send(name, *args, &block) # end + + # We need to build an instance method as a wrapper, using normal code, to be + # able to expose argument defaulting between the caller and definer in the + # Ruby API. An extra method is, sadly, required for Ruby 1.8 to work since + # it doesn't expose bind on a block. + # + # Hopefully we can improve this when we finally shuffle off the last of Ruby + # 1.8 support, but that looks to be a few "enterprise" release eras away, so + # we are pretty stuck with this for now. + # + # Patches to make this work more nicely with Ruby 1.9 using runtime version + # checking and all are welcome, provided that they don't change anything + # outside this little ol' bit of code and all. + # + # Incidentally, we though about vendoring evil-ruby and actually adjusting + # the internal C structure implementation details under the hood to make + # this stuff work, because it would have been cleaner. Which gives you an + # idea how motivated we were to make this cleaner. Sorry. + # --daniel 2011-03-31 def when_invoked=(block) - # We need to build an instance method as a wrapper, using normal code, to - # be able to expose argument defaulting between the caller and definer in - # the Ruby API. An extra method is, sadly, required for Ruby 1.8 to work. - # - # In future this also gives us a place to hook in additional behaviour - # such as calling out to the action instance to validate and coerce - # parameters, which avoids any exciting context switching and all. - # - # Hopefully we can improve this when we finally shuffle off the last of - # Ruby 1.8 support, but that looks to be a few "enterprise" release eras - # away, so we are pretty stuck with this for now. - # - # Patches to make this work more nicely with Ruby 1.9 using runtime - # version checking and all are welcome, but they can't actually help if - # the results are not totally hidden away in here. - # - # Incidentally, we though about vendoring evil-ruby and actually adjusting - # the internal C structure implementation details under the hood to make - # this stuff work, because it would have been cleaner. Which gives you an - # idea how motivated we were to make this cleaner. Sorry. --daniel 2011-03-31 internal_name = "#{@name} implementation, required on Ruby 1.8".to_sym - file = __FILE__ + "+eval" - line = __LINE__ + 1 + + arity = block.arity + if arity == 0 then + # This will never fire on 1.8.7, which treats no arguments as "*args", + # but will on 1.9.2, which treats it as "no arguments". Which bites, + # because this just begs for us to wind up in the horrible situation + # where a 1.8 vs 1.9 error bites our end users. --daniel 2011-04-19 + raise ArgumentError, "action when_invoked requires at least one argument (options)" + elsif arity > 0 then + range = Range.new(1, arity - 1) + decl = range.map { |x| "arg#{x}" } << "options = {}" + optn = "" + args = "[" + (range.map { |x| "arg#{x}" } << "options").join(", ") + "]" + else + range = Range.new(1, arity.abs - 1) + decl = range.map { |x| "arg#{x}" } << "*rest" + optn = "rest << {} unless rest.last.is_a?(Hash)" + if arity == -1 then + args = "rest" + else + args = "[" + range.map { |x| "arg#{x}" }.join(", ") + "] + rest" + end + end + + file = __FILE__ + "+eval[wrapper]" + line = __LINE__ + 2 # <== points to the same line as 'def' in the wrapper. wrapper = <