summaryrefslogtreecommitdiff
path: root/knife/lib/chef/knife/core/ui.rb
diff options
context:
space:
mode:
Diffstat (limited to 'knife/lib/chef/knife/core/ui.rb')
-rw-r--r--knife/lib/chef/knife/core/ui.rb338
1 files changed, 338 insertions, 0 deletions
diff --git a/knife/lib/chef/knife/core/ui.rb b/knife/lib/chef/knife/core/ui.rb
new file mode 100644
index 0000000000..782df1ca10
--- /dev/null
+++ b/knife/lib/chef/knife/core/ui.rb
@@ -0,0 +1,338 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Christopher Brown (<cb@chef.io>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) 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" unless defined?(Forwardable)
+require "chef/platform/query_helpers" # NOTE - this require doesn't defined any const we can check.
+require_relative "generic_presenter"
+require "tempfile" unless defined?(Tempfile)
+
+class Chef
+ class Knife
+
+ # 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
+
+ # Creates a new object of class TTY::Prompt
+ # with interrupt as exit so that it can be terminated with status code.
+ def prompt
+ @prompt ||= begin
+ require "tty-prompt"
+ TTY::Prompt.new(interrupt: :exit)
+ end
+ end
+
+ # pastel.decorate is a lightweight replacement for highline.color
+ def pastel
+ @pastel ||= begin
+ require "pastel" unless defined?(Pastel)
+ Pastel.new
+ end
+ end
+
+ # Prints a message to stdout. Aliased as +info+ for compatibility with
+ # the logger API.
+ #
+ # @param message [String] the text string
+ 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.
+ #
+ # @param message [String] the text string
+ def log(message)
+ lines = message.split("\n")
+ first_line = lines.shift
+ stderr.puts first_line
+ # If the message is multiple lines,
+ # indent subsequent lines to align with the
+ # log type prefix ("ERROR: ", etc)
+ unless lines.empty?
+ prefix, = first_line.split(":", 2)
+ return if prefix.nil?
+
+ prefix_len = prefix.length
+ prefix_len -= 9 if color? # prefix includes 9 bytes of color escape sequences
+ prefix_len += 2 # include room to align to the ": " following PREFIX
+ padding = " " * prefix_len
+ lines.each do |line|
+ stderr.puts "#{padding}#{line}"
+ end
+ end
+ rescue Errno::EPIPE => e
+ raise e if @config[:verbosity] >= 2
+
+ exit 0
+ end
+
+ alias :info :log
+ alias :err :log
+
+ # Print a Debug
+ #
+ # @param message [String] the text string
+ def debug(message)
+ log("#{color("DEBUG:", :blue, :bold)} #{message}")
+ end
+
+ # Print a warning message
+ #
+ # @param message [String] the text string
+ def warn(message)
+ log("#{color("WARNING:", :yellow, :bold)} #{message}")
+ end
+
+ # Print an error message
+ #
+ # @param message [String] the text string
+ def error(message)
+ log("#{color("ERROR:", :red, :bold)} #{message}")
+ end
+
+ # Print a message describing a fatal error.
+ #
+ # @param message [String] the text string
+ def fatal(message)
+ log("#{color("FATAL:", :red, :bold)} #{message}")
+ end
+
+ # Print a message describing a fatal error and exit 1
+ #
+ # @param message [String] the text string
+ def fatal!(message)
+ fatal(message)
+ exit 1
+ end
+
+ def color(string, *colors)
+ if color?
+ pastel.decorate(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, **options, &block)
+ prompt.ask(*args, **options, &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)
+ unless 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/ 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