2010-04-03 00:43:46 -04:00
|
|
|
# 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
|
|
|
|
#------------------------------------------------------------------------------
|
2010-05-06 00:30:00 -04:00
|
|
|
require "shellwords"
|
|
|
|
|
2010-04-03 00:43:46 -04:00
|
|
|
class AwesomePrint
|
|
|
|
AP = :__awesome_print__
|
2010-05-10 17:44:13 -04:00
|
|
|
CORE = [ :array, :hash, :class, :file, :dir, :bigdecimal, :rational, :struct ]
|
2010-04-03 00:43:46 -04:00
|
|
|
|
|
|
|
def initialize(options = {})
|
|
|
|
@options = {
|
|
|
|
:multiline => true,
|
|
|
|
:plain => false,
|
|
|
|
:indent => 4,
|
2010-09-10 12:03:53 -04:00
|
|
|
:index => true,
|
2010-04-03 00:43:46 -04:00
|
|
|
:color => {
|
|
|
|
:array => :white,
|
2010-04-08 23:31:53 -04:00
|
|
|
:bigdecimal => :blue,
|
2010-04-03 00:43:46 -04:00
|
|
|
:class => :yellow,
|
|
|
|
:date => :greenish,
|
|
|
|
:falseclass => :red,
|
|
|
|
:fixnum => :blue,
|
|
|
|
:float => :blue,
|
2010-05-06 01:23:18 -04:00
|
|
|
:hash => :pale,
|
2010-05-10 17:44:13 -04:00
|
|
|
:struct => :pale,
|
2010-04-03 00:43:46 -04:00
|
|
|
:nilclass => :red,
|
|
|
|
:string => :yellowish,
|
|
|
|
:symbol => :cyanish,
|
|
|
|
:time => :greenish,
|
|
|
|
:trueclass => :green
|
2010-04-08 23:14:47 -04:00
|
|
|
}
|
|
|
|
}
|
2010-04-03 00:43:46 -04:00
|
|
|
|
2010-04-08 23:14:47 -04:00
|
|
|
# Merge custom defaults and let explicit options parameter override them.
|
|
|
|
merge_custom_defaults!
|
|
|
|
merge_options!(options)
|
2010-04-08 00:31:21 -04:00
|
|
|
|
2010-04-04 23:10:16 -04:00
|
|
|
@indentation = @options[:indent].abs
|
2010-04-03 00:43:46 -04:00
|
|
|
Thread.current[AP] ||= []
|
|
|
|
end
|
2010-04-22 00:41:40 -04:00
|
|
|
|
2010-04-03 00:43:46 -04:00
|
|
|
private
|
|
|
|
|
|
|
|
# Format an array.
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
def awesome_array(a)
|
2010-04-05 23:02:07 -04:00
|
|
|
return "[]" if a == []
|
|
|
|
|
2010-04-03 00:43:46 -04:00
|
|
|
if @options[:multiline]
|
|
|
|
width = (a.size - 1).to_s.size
|
|
|
|
data = a.inject([]) do |arr, item|
|
2010-09-10 12:03:53 -04:00
|
|
|
index = if @options[:index]
|
|
|
|
colorize("#{indent}[#{arr.size.to_s.rjust(width)}] ", :array)
|
|
|
|
else
|
|
|
|
colorize(indent, :array)
|
|
|
|
end
|
2010-04-03 00:43:46 -04:00
|
|
|
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
|
|
|
|
|
2010-04-04 23:10:16 -04:00
|
|
|
# Format a hash. If @options[:indent] if negative left align hash keys.
|
2010-04-03 00:43:46 -04:00
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
def awesome_hash(h)
|
2010-04-05 23:02:07 -04:00
|
|
|
return "{}" if h == {}
|
|
|
|
|
2010-04-03 00:43:46 -04:00
|
|
|
data = h.keys.inject([]) do |arr, key|
|
|
|
|
plain_single_line do
|
|
|
|
arr << [ awesome(key), h[key] ]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-04-05 14:37:22 -04:00
|
|
|
width = data.map { |key, | key.size }.max || 0
|
2010-04-04 23:10:16 -04:00
|
|
|
width += @indentation if @options[:indent] > 0
|
2010-04-03 00:43:46 -04:00
|
|
|
|
|
|
|
data = data.inject([]) do |arr, (key, value)|
|
2010-04-04 23:10:16 -04:00
|
|
|
if @options[:multiline]
|
2010-04-04 23:18:19 -04:00
|
|
|
formatted_key = (@options[:indent] >= 0 ? key.rjust(width) : indent + key.ljust(width))
|
2010-04-04 23:10:16 -04:00
|
|
|
else
|
|
|
|
formatted_key = key
|
|
|
|
end
|
2010-04-03 00:43:46 -04:00
|
|
|
indented do
|
2010-04-04 23:10:16 -04:00
|
|
|
arr << (formatted_key << colorize(" => ", :hash) << awesome(value))
|
2010-04-03 00:43:46 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
if @options[:multiline]
|
|
|
|
"{\n" << data.join(",\n") << "\n#{outdent}}"
|
|
|
|
else
|
|
|
|
"{ #{data.join(', ')} }"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-05-10 17:44:13 -04:00
|
|
|
# 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
|
|
|
|
|
2010-04-03 00:43:46 -04:00
|
|
|
# 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)
|
2010-05-06 00:30:00 -04:00
|
|
|
ls = File.directory?(f) ? `ls -adlF #{f.path.shellescape}` : `ls -alF #{f.path.shellescape}`
|
2010-04-03 00:43:46 -04:00
|
|
|
awesome_self(f, :with => ls.empty? ? nil : "\n#{ls.chop}")
|
|
|
|
end
|
|
|
|
|
|
|
|
# Format Dir object.
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
def awesome_dir(d)
|
2010-05-06 00:30:00 -04:00
|
|
|
ls = `ls -alF #{d.path.shellescape}`
|
2010-04-03 00:43:46 -04:00
|
|
|
awesome_self(d, :with => ls.empty? ? nil : "\n#{ls.chop}")
|
|
|
|
end
|
|
|
|
|
2010-04-08 23:31:53 -04:00
|
|
|
# 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
|
|
|
|
|
2010-04-03 00:43:46 -04:00
|
|
|
# Catch all method to format an arbitrary object.
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
def awesome_self(object, appear = {})
|
2010-10-13 01:52:53 -04:00
|
|
|
colorize(object.inspect.to_s << appear[:with].to_s, appear[:as] || declassify(object))
|
2010-04-03 00:43:46 -04:00
|
|
|
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 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)
|
2010-05-13 01:03:33 -04:00
|
|
|
when :array then colorize("[...]", :array)
|
|
|
|
when :hash then colorize("{...}", :hash)
|
2010-05-10 17:44:13 -04:00
|
|
|
when :struct then colorize("{...}", :struct)
|
2010-04-03 00:43:46 -04:00
|
|
|
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)
|
2010-05-13 01:03:33 -04:00
|
|
|
if object.is_a?(Struct)
|
|
|
|
:struct
|
2010-05-10 17:44:13 -04:00
|
|
|
else
|
2010-05-13 01:03:33 -04:00
|
|
|
object.class.to_s.gsub(/:+/, "_").downcase.to_sym
|
2010-05-10 17:44:13 -04:00
|
|
|
end
|
2010-04-03 00:43:46 -04:00
|
|
|
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
|
|
|
|
|
|
|
|
# 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
|
2010-04-04 23:10:16 -04:00
|
|
|
@indentation += @options[:indent].abs
|
2010-04-03 00:43:46 -04:00
|
|
|
yield
|
|
|
|
ensure
|
2010-04-04 23:10:16 -04:00
|
|
|
@indentation -= @options[:indent].abs
|
2010-04-03 00:43:46 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def indent
|
2010-04-04 00:37:13 -04:00
|
|
|
@indent = ' ' * @indentation
|
2010-04-03 00:43:46 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def outdent
|
2010-04-04 23:10:16 -04:00
|
|
|
@outdent = ' ' * (@indentation - @options[:indent].abs)
|
2010-04-03 00:43:46 -04:00
|
|
|
end
|
|
|
|
|
2010-04-08 23:14:47 -04:00
|
|
|
# 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.
|
2010-04-08 00:31:21 -04:00
|
|
|
#------------------------------------------------------------------------------
|
2010-04-08 23:14:47 -04:00
|
|
|
def merge_custom_defaults!
|
2010-04-08 00:31:21 -04:00
|
|
|
dotfile = File.join(ENV["HOME"], ".aprc")
|
|
|
|
if File.readable?(dotfile)
|
|
|
|
load dotfile
|
2010-04-08 23:14:47 -04:00
|
|
|
merge_options!(self.class.defaults)
|
2010-04-08 00:31:21 -04:00
|
|
|
end
|
|
|
|
rescue => e
|
|
|
|
$stderr.puts "Could not load #{dotfile}: #{e}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Class accessors for custom defaults.
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
def self.defaults
|
|
|
|
@@defaults ||= {}
|
|
|
|
end
|
|
|
|
|
2010-04-08 08:01:39 -04:00
|
|
|
def self.defaults=(args = {})
|
|
|
|
@@defaults = args
|
2010-04-08 00:31:21 -04:00
|
|
|
end
|
|
|
|
|
2010-04-03 00:43:46 -04:00
|
|
|
end
|