# Copyright (c) 2010 Michael Dvorkin # # Awesome Print is freely distributable under the terms of MIT license. # See LICENSE file or http://www.opensource.org/licenses/mit-license.php #------------------------------------------------------------------------------ require "shellwords" class AwesomePrint AP = :__awesome_print__ CORE = [ :array, :hash, :class, :file, :dir, :bigdecimal, :rational, :struct, :method, :unboundmethod ] def initialize(options = {}) @options = { :multiline => true, :plain => false, :indent => 4, :index => true, :color => { :array => :white, :bigdecimal => :blue, :class => :yellow, :date => :greenish, :falseclass => :red, :fixnum => :blue, :float => :blue, :hash => :pale, :struct => :pale, :nilclass => :red, :string => :yellowish, :symbol => :cyanish, :time => :greenish, :trueclass => :green, :method => :purple, :args => :pale } } # Merge custom defaults and let explicit options parameter override them. merge_custom_defaults! merge_options!(options) @indentation = @options[:indent].abs Thread.current[AP] ||= [] end private # Format an array. #------------------------------------------------------------------------------ def awesome_array(a) return "[]" if a == [] if a.instance_variable_defined?('@__awesome_methods__') methods_array(a) elsif @options[:multiline] width = (a.size - 1).to_s.size data = a.inject([]) do |arr, item| index = if @options[:index] colorize("#{indent}[#{arr.size.to_s.rjust(width)}] ", :array) else colorize(indent, :array) end indented do arr << (index << awesome(item)) end end "[\n" << data.join(",\n") << "\n#{outdent}]" else data = a.inject([]) { |arr, item| arr << awesome(item) } "[ #{data.join(', ')} ]" end end # Format a hash. If @options[:indent] if negative left align hash keys. #------------------------------------------------------------------------------ def awesome_hash(h) return "{}" if h == {} data = h.keys.inject([]) do |arr, key| plain_single_line do arr << [ awesome(key), h[key] ] end end width = data.map { |key, | key.size }.max || 0 width += @indentation if @options[:indent] > 0 data = data.inject([]) do |arr, (key, value)| if @options[:multiline] formatted_key = (@options[:indent] >= 0 ? key.rjust(width) : indent + key.ljust(width)) else formatted_key = key end indented do arr << (formatted_key << colorize(" => ", :hash) << awesome(value)) end end if @options[:multiline] "{\n" << data.join(",\n") << "\n#{outdent}}" else "{ #{data.join(', ')} }" end end # Format a Struct. If @options[:indent] if negative left align hash keys. #------------------------------------------------------------------------------ def awesome_struct(s) h = {} s.each_pair { |k,v| h[k] = v } awesome_hash(h) end # Format Class object. #------------------------------------------------------------------------------ def awesome_class(c) if superclass = c.superclass # <-- Assign and test if nil. awesome_self(c, :with => " < #{superclass}") else awesome_self(c) end end # Format File object. #------------------------------------------------------------------------------ def awesome_file(f) ls = File.directory?(f) ? `ls -adlF #{f.path.shellescape}` : `ls -alF #{f.path.shellescape}` awesome_self(f, :with => ls.empty? ? nil : "\n#{ls.chop}") end # Format Dir object. #------------------------------------------------------------------------------ def awesome_dir(d) ls = `ls -alF #{d.path.shellescape}` awesome_self(d, :with => ls.empty? ? nil : "\n#{ls.chop}") end # Format BigDecimal and Rational objects by convering them to Float. #------------------------------------------------------------------------------ def awesome_bigdecimal(n) awesome_self(n.to_f, :as => :bigdecimal) end alias :awesome_rational :awesome_bigdecimal # Format a method. #------------------------------------------------------------------------------ def awesome_method(m) name, args, owner = method_tuple(m) "#{colorize(owner, :class)}##{colorize(name, :method)}#{colorize(args, :args)}" end alias :awesome_unboundmethod :awesome_method # Catch all method to format an arbitrary object. #------------------------------------------------------------------------------ def awesome_self(object, appear = {}) colorize(object.inspect.to_s << appear[:with].to_s, appear[:as] || declassify(object)) end # Dispatcher that detects data nesting and invokes object-aware formatter. #------------------------------------------------------------------------------ def awesome(object) if Thread.current[AP].include?(object.object_id) nested(object) else begin Thread.current[AP] << object.object_id send(:"awesome_#{printable(object)}", object) ensure Thread.current[AP].pop end end end # Format object.methods array. #------------------------------------------------------------------------------ def methods_array(a) object = a.instance_variable_get('@__awesome_methods__') tuples = a.map do |name| if object.respond_to?(name, true) # Regular method? method_tuple(object.method(name)) elsif object.respond_to?(:instance_method) # Unbound method? method_tuple(object.instance_method(name)) else # WTF method. [ name.to_s, '(?)', '' ] end end width = (tuples.size - 1).to_s.size name_width = tuples.map { |item| item[0].size }.max || 0 args_width = tuples.map { |item| item[1].size }.max || 0 data = tuples.inject([]) do |arr, item| index = if @options[:index] "#{indent}[#{arr.size.to_s.rjust(width)}]" else indent end indented do arr << "#{index} #{colorize(item[0].rjust(name_width), :method)}#{colorize(item[1].ljust(args_width), :args)} #{colorize(item[2], :class)}" end end "[\n" << data.join("\n") << "\n#{outdent}]" end # Format nested data, for example: # arr = [1, 2]; arr << arr # => [1,2, [...]] # hsh = { :a => 1 }; hsh[:b] = hsh # => { :a => 1, :b => {...} } #------------------------------------------------------------------------------ def nested(object) case printable(object) when :array then colorize("[...]", :array) when :hash then colorize("{...}", :hash) when :struct then colorize("{...}", :struct) else colorize("...#{object.class}...", :class) end end # Return one of the "core" types that have a formatter of :self otherwise. #------------------------------------------------------------------------------ def printable(object) CORE.grep(declassify(object))[0] || :self end # Turn class name into symbol, ex: Hello::World => :hello_world. #------------------------------------------------------------------------------ def declassify(object) if object.is_a?(Struct) :struct else object.class.to_s.gsub(/:+/, "_").downcase.to_sym end end # Pick the color and apply it to the given string as necessary. #------------------------------------------------------------------------------ def colorize(s, type) if @options[:plain] || @options[:color][type].nil? s else s.send(@options[:color][type]) end end # Return [ name, arguments, owner ] tuple for a given method. #------------------------------------------------------------------------------ def method_tuple(method) if method.respond_to?(:parameters) # Ruby 1.9.2+ # See http://ruby.runpaint.org/methods#method-objects-parameters args = method.parameters.inject([]) do |arr, (type, name)| name ||= (type == :block ? 'block' : "arg#{arr.size + 1}") arr << case type when :req then name.to_s when :opt, :rest then "*#{name}" when :block then "&#{name}" else '?' end end else # See http://ruby-doc.org/core/classes/Method.html#M001902 args = method.arity.abs.times.map { |i| "arg#{i+1}" } args[-1] = "*#{args[-1]}" if method.arity < 0 end if method.to_s =~ /(Unbound)*Method: (.*?)[#\.]/ owner = "#{$2}#{$1 ? '(unbound)' : ''}".gsub('(', ' (') end [ method.name.to_s, "(#{args.join(', ')})", owner.to_s ] end # Format hash keys as plain string regardless of underlying data type. #------------------------------------------------------------------------------ def plain_single_line plain, multiline = @options[:plain], @options[:multiline] @options[:plain], @options[:multiline] = true, false yield ensure @options[:plain], @options[:multiline] = plain, multiline end #------------------------------------------------------------------------------ def indented @indentation += @options[:indent].abs yield ensure @indentation -= @options[:indent].abs end def indent @indent = ' ' * @indentation end def outdent @outdent = ' ' * (@indentation - @options[:indent].abs) end # Update @options by first merging the :color hash and then the remaining keys. #------------------------------------------------------------------------------ def merge_options!(options = {}) @options[:color].merge!(options.delete(:color) || {}) @options.merge!(options) end # Load ~/.aprc file with custom defaults that override default options. #------------------------------------------------------------------------------ def merge_custom_defaults! dotfile = File.join(ENV["HOME"], ".aprc") if File.readable?(dotfile) load dotfile merge_options!(self.class.defaults) end rescue => e $stderr.puts "Could not load #{dotfile}: #{e}" end # Class accessors for custom defaults. #------------------------------------------------------------------------------ def self.defaults @@defaults ||= {} end def self.defaults=(args = {}) @@defaults = args end end