diff --git a/lib/puppet/face/help.rb b/lib/puppet/face/help.rb index 2d8e4b663..f20df3a30 100644 --- a/lib/puppet/face/help.rb +++ b/lib/puppet/face/help.rb @@ -1,172 +1,195 @@ require 'puppet/face' require 'puppet/application/face_base' require 'puppet/util/command_line' require 'puppet/util/constant_inflector' require 'pathname' require 'erb' Puppet::Face.define(:help, '0.0.1') do copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" summary "Display Puppet help." action(:help) do summary "Display help about Puppet subcommands and their actions." arguments "[] []" returns "Short help text for the specified subcommand or action." examples <<-'EOT' Get help for an action: $ puppet help EOT option "--version VERSION" do summary "The version of the subcommand for which to show help." end default when_invoked do |*args| # Check our invocation, because we want varargs and can't do defaults # yet. REVISIT: when we do option defaults, and positional options, we # should rewrite this to use those. --daniel 2011-04-04 options = args.pop if options.nil? or args.length > 2 then if args.select { |x| x == 'help' }.length > 2 then c = "\n %'(),-./=ADEFHILORSTUXY\\_`gnv|".split('') i = <<-'EOT'.gsub(/\s*/, '').to_i(36) 3he6737w1aghshs6nwrivl8mz5mu9nywg9tbtlt081uv6fq5kvxse1td3tj1wvccmte806nb cy6de2ogw0fqjymbfwi6a304vd56vlq71atwmqsvz3gpu0hj42200otlycweufh0hylu79t3 gmrijm6pgn26ic575qkexyuoncbujv0vcscgzh5us2swklsp5cqnuanlrbnget7rt3956kam j8adhdrzqqt9bor0cv2fqgkloref0ygk3dekiwfj1zxrt13moyhn217yy6w4shwyywik7w0l xtuevmh0m7xp6eoswin70khm5nrggkui6z8vdjnrgdqeojq40fya5qexk97g4d8qgw0hvokr pli1biaz503grqf2ycy0ppkhz1hwhl6ifbpet7xd6jjepq4oe0ofl575lxdzjeg25217zyl4 nokn6tj5pq7gcdsjre75rqylydh7iia7s3yrko4f5ud9v8hdtqhu60stcitirvfj6zphppmx 7wfm7i9641d00bhs44n6vh6qvx39pg3urifgr6ihx3e0j1ychzypunyou7iplevitkyg6gbg wm08oy1rvogcjakkqc1f7y1awdfvlb4ego8wrtgu9vzw4vmj59utwifn2ejcs569dh1oaavi sc581n7jjg1dugzdu094fdobtx6rsvk3sfctvqnr36xctold EOT 353.times{i,x=i.divmod(1184);a,b=x.divmod(37);print(c[a]*b)} end raise ArgumentError, "Puppet help only takes two (optional) arguments: a subcommand and an action" end version = :current if options.has_key? :version then if options[:version].to_s !~ /^current$/i then version = options[:version] else if args.length == 0 then raise ArgumentError, "Version only makes sense when a Faces subcommand is given" end end end - # Name those parameters... - facename, actionname = args + return erb('global.erb').result(binding) if args.empty? - if facename then - if legacy_applications.include? facename then - actionname and raise ArgumentError, "Legacy subcommands don't take actions" - return Puppet::Application[facename].help - else - face = Puppet::Face[facename.to_sym, version] - actionname and action = face.get_action(actionname.to_sym) + facename, actionname = args + if legacy_applications.include? facename then + if actionname then + raise ArgumentError, "Legacy subcommands don't take actions" end + return render_application_help(facename) + else + return render_face_help(facename, actionname, version) end + end + end - case args.length - when 0 then - template = erb 'global.erb' - when 1 then - face or fail ArgumentError, "Unable to load face #{facename}" - template = erb 'face.erb' - when 2 then - face or fail ArgumentError, "Unable to load face #{facename}" - action or fail ArgumentError, "Unable to load action #{actionname} from #{face}" - template = erb 'action.erb' - else - fail ArgumentError, "Too many arguments to help action" + def render_application_help(applicationname) + return Puppet::Application[applicationname].help + end + + def render_face_help(facename, actionname, version) + face, action = load_face_help(facename, actionname, version) + return template_for(face, action).result(binding) + end + + def load_face_help(facename, actionname, version) + begin + face = Puppet::Face[facename.to_sym, version] + rescue Puppet::Error => detail + fail ArgumentError, <<-MSG +Could not load help for the face #{facename}. +Please check the error logs for more information. + +Detail: "#{detail.message}" + MSG + end + if actionname + action = face.get_action(actionname.to_sym) + if not action + fail ArgumentError, "Unable to load action #{actionname} from #{face}" end + end + + [face, action] + end - # Run the ERB template in our current binding, including all the local - # variables we established just above. --daniel 2011-04-11 - return template.result(binding) + def template_for(face, action) + if action.nil? + erb('face.erb') + else + erb('action.erb') end end def erb(name) template = (Pathname(__FILE__).dirname + "help" + name) erb = ERB.new(template.read, nil, '-') erb.filename = template.to_s return erb end # Return a list of applications that are not simply just stubs for Faces. def legacy_applications Puppet::Util::CommandLine.available_subcommands.reject do |appname| (is_face_app?(appname)) or (exclude_from_docs?(appname)) end.sort end # Return a list of all applications (both legacy and Face applications), along with a summary # of their functionality. # @returns [Array] An Array of Arrays. The outer array contains one entry per application; each # element in the outer array is a pair whose first element is a String containing the application # name, and whose second element is a String containing the summary for that application. def all_application_summaries() Puppet::Util::CommandLine.available_subcommands.sort.inject([]) do |result, appname| next result if exclude_from_docs?(appname) if (is_face_app?(appname)) - face = Puppet::Face[appname, :current] - result << [appname, face.summary] + begin + face = Puppet::Face[appname, :current] + result << [appname, face.summary] + rescue Puppet::Error => detail + result << [ "! #{appname}", "! Subcommand unavailable due to error. Check error logs." ] + end else result << [appname, horribly_extract_summary_from(appname)] end end end def horribly_extract_summary_from(appname) begin require "puppet/application/#{appname}" help = Puppet::Application[appname].help.split("\n") # Now we find the line with our summary, extract it, and return it. This # depends on the implementation coincidence of how our pages are # formatted. If we can't match the pattern we expect we return the empty # string to ensure we don't blow up in the summary. --daniel 2011-04-11 while line = help.shift do if md = /^puppet-#{appname}\([^\)]+\) -- (.*)$/.match(line) then return md[1] end end rescue Exception # Damn, but I hate this: we just ignore errors here, no matter what # class they are. Meh. end return '' end # This should absolutely be a private method, but for some reason it appears # that you can't use the 'private' keyword inside of a Face definition. # See #14205. #private :horribly_extract_summary_from def exclude_from_docs?(appname) %w{face_base indirection_base}.include? appname end # This should absolutely be a private method, but for some reason it appears # that you can't use the 'private' keyword inside of a Face definition. # See #14205. #private :exclude_from_docs? def is_face_app?(appname) clazz = Puppet::Application.find(appname) clazz.ancestors.include?(Puppet::Application::FaceBase) end # This should probably be a private method, but for some reason it appears # that you can't use the 'private' keyword inside of a Face definition. # See #14205. #private :is_face_app? end diff --git a/spec/unit/face/help_spec.rb b/spec/unit/face/help_spec.rb index 06b138a73..ceb0aee67 100755 --- a/spec/unit/face/help_spec.rb +++ b/spec/unit/face/help_spec.rb @@ -1,145 +1,145 @@ #! /usr/bin/env ruby -S rspec require 'spec_helper' require 'puppet/face' describe Puppet::Face[:help, '0.0.1'] do - it "should have a help action" do + it "has a help action" do subject.should be_action :help end - it "should have a default action of help" do + it "has a default action of help" do subject.get_action('help').should be_default end - it "should accept a call with no arguments" do + it "accepts a call with no arguments" do expect { subject.help() }.should_not raise_error end - it "should accept a face name" do + it "accepts a face name" do expect { subject.help(:help) }.should_not raise_error end - it "should accept a face and action name" do + it "accepts a face and action name" do expect { subject.help(:help, :help) }.should_not raise_error end - it "should fail if more than a face and action are given" do + it "fails if more than a face and action are given" do expect { subject.help(:help, :help, :for_the_love_of_god) }. should raise_error ArgumentError end - it "should treat :current and 'current' identically" do + it "treats :current and 'current' identically" do subject.help(:help, :version => :current).should == subject.help(:help, :version => 'current') end - it "should complain when the request version of a face is missing" do + it "raises an error when the face is unavailable" do expect { subject.help(:huzzah, :bar, :version => '17.0.0') }. - should raise_error Puppet::Error + should raise_error(ArgumentError, /Could not find version 17\.0\.0/) end - it "should find a face by version" do + it "finds a face by version" do face = Puppet::Face[:huzzah, :current] subject.help(:huzzah, :version => face.version). should == subject.help(:huzzah, :version => :current) end context "when listing subcommands" do subject { Puppet::Face[:help, :current].help } RSpec::Matchers.define :have_a_summary do match do |instance| instance.summary.is_a?(String) end end # Check a precondition for the next block; if this fails you have # something odd in your set of face, and we skip testing things that # matter. --daniel 2011-04-10 - it "should have at least one face with a summary" do + it "has at least one face with a summary" do Puppet::Face.faces.should be_any do |name| Puppet::Face[name, :current].summary end end - it "should list all faces which are runnable from the command line" do + it "lists all faces which are runnable from the command line" do help_face = Puppet::Face[:help, :current] # The main purpose of the help face is to provide documentation for # command line users. It shouldn't show documentation for faces # that can't be run from the command line, so, rather than iterating # over all available faces, we need to iterate over the subcommands # that are available from the command line. Puppet::Util::CommandLine.available_subcommands.each do |name| next unless help_face.is_face_app?(name) next if help_face.exclude_from_docs?(name) face = Puppet::Face[name, :current] summary = face.summary subject.should =~ %r{ #{name} } summary and subject.should =~ %r{ #{name} +#{summary}} end end context "face summaries" do # we need to set a bunk module path here, because without doing so, # the autoloader will try to use it before it is initialized. Puppet[:modulepath] = "/dev/null" Puppet::Face.faces.each do |name| - it "should have a summary for #{name}" do + it "has a summary for #{name}" do Puppet::Face[name, :current].should have_a_summary end end end - it "should list all legacy applications" do + it "lists all legacy applications" do Puppet::Face[:help, :current].legacy_applications.each do |appname| subject.should =~ %r{ #{appname} } summary = Puppet::Face[:help, :current].horribly_extract_summary_from(appname) summary and subject.should =~ %r{ #{summary}\b} end end end context "#legacy_applications" do subject { Puppet::Face[:help, :current].legacy_applications } # If we don't, these tests are ... less than useful, because they assume # it. When this breaks you should consider ditching the entire feature # and tests, but if not work out how to fake one. --daniel 2011-04-11 it { should have_at_least(1).item } # Meh. This is nasty, but we can't control the other list; the specific # bug that caused these to be listed is annoyingly subtle and has a nasty # fix, so better to have a "fail if you do something daft" trigger in # place here, I think. --daniel 2011-04-11 %w{face_base indirection_base}.each do |name| it { should_not include name } end end context "help for legacy applications" do subject { Puppet::Face[:help, :current] } let :appname do subject.legacy_applications.first end # This test is purposely generic, so that as we eliminate legacy commands # we don't get into a loop where we either test a face-based replacement # and fail to notice breakage, or where we have to constantly rewrite this # test and all. --daniel 2011-04-11 - it "should return the legacy help when given the subcommand" do + it "returns the legacy help when given the subcommand" do help = subject.help(appname) help.should =~ /puppet-#{appname}/ %w{SYNOPSIS USAGE DESCRIPTION OPTIONS COPYRIGHT}.each do |heading| help.should =~ /^#{heading}$/ end end - it "should fail when asked for an action on a legacy command" do + it "fails when asked for an action on a legacy command" do expect { subject.help(appname, :whatever) }. to raise_error ArgumentError, /Legacy subcommands don't take actions/ end end end