# # Author:: Adam Jacob () # Author:: Christopher Brown () # Author:: Daniel DeLeo () # Copyright:: Copyright 2009-2016, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require "forwardable" require "chef/platform/query_helpers" require "chef/knife/core/generic_presenter" require "tempfile" class Chef class Knife #==Chef::Knife::UI # The User Interaction class used by knife. class UI extend Forwardable attr_reader :stdout attr_reader :stderr attr_reader :stdin attr_reader :config attr_reader :presenter def_delegator :@presenter, :format_list_for_display def_delegator :@presenter, :format_for_display def_delegator :@presenter, :format_cookbook_list_for_display def initialize(stdout, stderr, stdin, config) @stdout, @stderr, @stdin, @config = stdout, stderr, stdin, config @presenter = Chef::Knife::Core::GenericPresenter.new(self, config) end # Creates a new +presenter_class+ object and uses it to format structured # data for display. By default, a Chef::Knife::Core::GenericPresenter # object is used. def use_presenter(presenter_class) @presenter = presenter_class.new(self, config) end def highline @highline ||= begin require "highline" HighLine.new end end # Prints a message to stdout. Aliased as +info+ for compatibility with # the logger API. def msg(message) stdout.puts message rescue Errno::EPIPE => e raise e if @config[:verbosity] >= 2 exit 0 end # Prints a msg to stderr. Used for info, warn, error, and fatal. def log(message) stderr.puts message rescue Errno::EPIPE => e raise e if @config[:verbosity] >= 2 exit 0 end alias :info :log alias :err :log # Print a warning message def warn(message) log("#{color('WARNING:', :yellow, :bold)} #{message}") end # Print an error message def error(message) log("#{color('ERROR:', :red, :bold)} #{message}") end # Print a message describing a fatal error. def fatal(message) log("#{color('FATAL:', :red, :bold)} #{message}") end def color(string, *colors) if color? highline.color(string, *colors) else string end end # Should colored output be used? For output to a terminal, this is # determined by the value of `config[:color]`. When output is not to a # terminal, colored output is never used def color? Chef::Config[:color] && stdout.tty? end def ask(*args, &block) highline.ask(*args, &block) end def list(*args) highline.list(*args) end # Formats +data+ using the configured presenter and outputs the result # via +msg+. Formatting can be customized by configuring a different # presenter. See +use_presenter+ def output(data) msg @presenter.format(data) end # Determines if the output format is a data interchange format, i.e., # JSON or YAML def interchange? @presenter.interchange? end def ask_question(question, opts = {}) question += "[#{opts[:default]}] " if opts[:default] if opts[:default] && config[:defaults] opts[:default] else stdout.print question a = stdin.readline.strip if opts[:default] a.empty? ? opts[:default] : a else a end end end def pretty_print(data) stdout.puts data rescue Errno::EPIPE => e raise e if @config[:verbosity] >= 2 exit 0 end # Hash -> Hash # Works the same as edit_data but # returns a hash rather than a JSON string/Fully inflated object def edit_hash(hash) raw = edit_data(hash, false) Chef::JSONCompat.parse(raw) end def edit_data(data, parse_output = true, object_class: nil) output = Chef::JSONCompat.to_json_pretty(data) if !config[:disable_editing] Tempfile.open([ "knife-edit-", ".json" ]) do |tf| tf.sync = true tf.puts output tf.close raise "Please set EDITOR environment variable. See https://docs.chef.io/knife_setup.html for details." unless system("#{config[:editor]} #{tf.path}") output = IO.read(tf.path) end end if parse_output if object_class.nil? raise ArgumentError, "Please pass in the object class to hydrate or use #edit_hash" else object_class.from_hash(Chef::JSONCompat.parse(output)) end else output end end def edit_object(klass, name) object = klass.load(name) output = edit_data(object, object_class: klass) # Only make the save if the user changed the object. # # Output JSON for the original (object) and edited (output), then parse # them without reconstituting the objects into real classes # (create_additions=false). Then, compare the resulting simple objects, # which will be Array/Hash/String/etc. # # We wouldn't have to do these shenanigans if all the editable objects # implemented to_hash, or if to_json against a hash returned a string # with stable key order. object_parsed_again = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(object)) output_parsed_again = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(output)) if object_parsed_again != output_parsed_again output.save msg("Saved #{output}") else msg("Object unchanged, not saving") end output(format_for_display(object)) if config[:print_after] end def confirmation_instructions(default_choice) case default_choice when true "? (Y/n) " when false "? (y/N) " else "? (Y/N) " end end # See confirm method for argument information def confirm_without_exit(question, append_instructions = true, default_choice = nil) return true if config[:yes] stdout.print question stdout.print confirmation_instructions(default_choice) if append_instructions answer = stdin.readline answer.chomp! case answer when "Y", "y" true when "N", "n" msg("You said no, so I'm done here.") false when "" unless default_choice.nil? default_choice else msg("I have no idea what to do with '#{answer}'") msg("Just say Y or N, please.") confirm_without_exit(question, append_instructions, default_choice) end else msg("I have no idea what to do with '#{answer}'") msg("Just say Y or N, please.") confirm_without_exit(question, append_instructions, default_choice) end end # # Not the ideal signature for a function but we need to stick with this # for now until we get a chance to break our API in Chef 12. # # question => Question to print before asking for confirmation # append_instructions => Should print '? (Y/N)' as instructions # default_choice => Set to true for 'Y', and false for 'N' as default answer # def confirm(question, append_instructions = true, default_choice = nil) unless confirm_without_exit(question, append_instructions, default_choice) exit 3 end true end end end end