mirror of
https://github.com/danbee/dotfiles-local
synced 2025-03-04 08:49:07 +00:00
333 lines
8.8 KiB
Ruby
Executable File
333 lines
8.8 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# Sizes - Calculate and sort all filesizes for current folder
|
|
# Includes directory sizes, colorized output
|
|
# Brett Terpstra 2019 WTF License
|
|
VERSION = "1.0.0"
|
|
|
|
require 'shellwords'
|
|
|
|
# Just including term-ansicolor by @flori and avoiding all the
|
|
# rigamarole of requiring multiple files when it's not a gem... - Brett
|
|
#
|
|
# ansicolor Copyright: Florian Frank
|
|
# License: <https://github.com/flori/term-ansicolor/blob/master/COPYING>
|
|
# Home: <https://github.com/flori/term-ansicolor>
|
|
module Term
|
|
|
|
# The ANSIColor module can be used for namespacing and mixed into your own
|
|
# classes.
|
|
module ANSIColor
|
|
# require 'term/ansicolor/version'
|
|
|
|
# :stopdoc:
|
|
ATTRIBUTES = [
|
|
[ :clear , 0 ], # String#clear is already used to empty string in Ruby 1.9
|
|
[ :reset , 0 ], # synonym for :clear
|
|
[ :bold , 1 ],
|
|
[ :dark , 2 ],
|
|
[ :italic , 3 ], # not widely implemented
|
|
[ :underline , 4 ],
|
|
[ :underscore , 4 ], # synonym for :underline
|
|
[ :blink , 5 ],
|
|
[ :rapid_blink , 6 ], # not widely implemented
|
|
[ :negative , 7 ], # no reverse because of String#reverse
|
|
[ :concealed , 8 ],
|
|
[ :strikethrough , 9 ], # not widely implemented
|
|
[ :black , 30 ],
|
|
[ :red , 31 ],
|
|
[ :green , 32 ],
|
|
[ :yellow , 33 ],
|
|
[ :blue , 34 ],
|
|
[ :magenta , 35 ],
|
|
[ :cyan , 36 ],
|
|
[ :white , 37 ],
|
|
[ :on_black , 40 ],
|
|
[ :on_red , 41 ],
|
|
[ :on_green , 42 ],
|
|
[ :on_yellow , 43 ],
|
|
[ :on_blue , 44 ],
|
|
[ :on_magenta , 45 ],
|
|
[ :on_cyan , 46 ],
|
|
[ :on_white , 47 ],
|
|
[ :intense_black , 90 ], # High intensity, aixterm (works in OS X)
|
|
[ :intense_red , 91 ],
|
|
[ :intense_green , 92 ],
|
|
[ :intense_yellow , 93 ],
|
|
[ :intense_blue , 94 ],
|
|
[ :intense_magenta , 95 ],
|
|
[ :intense_cyan , 96 ],
|
|
[ :intense_white , 97 ],
|
|
[ :on_intense_black , 100 ], # High intensity background, aixterm (works in OS X)
|
|
[ :on_intense_red , 101 ],
|
|
[ :on_intense_green , 102 ],
|
|
[ :on_intense_yellow , 103 ],
|
|
[ :on_intense_blue , 104 ],
|
|
[ :on_intense_magenta , 105 ],
|
|
[ :on_intense_cyan , 106 ],
|
|
[ :on_intense_white , 107 ]
|
|
]
|
|
|
|
ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
|
|
# :startdoc:
|
|
|
|
# Returns true if Term::ANSIColor supports the +feature+.
|
|
#
|
|
# The feature :clear, that is mixing the clear color attribute into String,
|
|
# is only supported on ruby implementations, that do *not* already
|
|
# implement the String#clear method. It's better to use the reset color
|
|
# attribute instead.
|
|
def support?(feature)
|
|
case feature
|
|
when :clear
|
|
!String.instance_methods(false).map(&:to_sym).include?(:clear)
|
|
end
|
|
end
|
|
# Returns true, if the coloring function of this module
|
|
# is switched on, false otherwise.
|
|
def self.coloring?
|
|
@coloring
|
|
end
|
|
|
|
# Turns the coloring on or off globally, so you can easily do
|
|
# this for example:
|
|
# Term::ANSIColor::coloring = STDOUT.isatty
|
|
def self.coloring=(val)
|
|
@coloring = val
|
|
end
|
|
self.coloring = true
|
|
|
|
ATTRIBUTES.each do |c, v|
|
|
eval <<-EOT
|
|
def #{c}(string = nil)
|
|
result = ''
|
|
result << "\e[#{v}m" if Term::ANSIColor.coloring?
|
|
if block_given?
|
|
result << yield
|
|
elsif string.respond_to?(:to_str)
|
|
result << string.to_str
|
|
elsif respond_to?(:to_str)
|
|
result << to_str
|
|
else
|
|
return result #only switch on
|
|
end
|
|
result << "\e[0m" if Term::ANSIColor.coloring?
|
|
result
|
|
end
|
|
EOT
|
|
end
|
|
|
|
# Regular expression that is used to scan for ANSI-sequences while
|
|
# uncoloring strings.
|
|
COLORED_REGEXP = /\e\[(?:(?:[349]|10)[0-7]|[0-9])?m/
|
|
|
|
# Returns an uncolored version of the string, that is all
|
|
# ANSI-sequences are stripped from the string.
|
|
def uncolored(string = nil) # :yields:
|
|
if block_given?
|
|
yield.to_str.gsub(COLORED_REGEXP, '')
|
|
elsif string.respond_to?(:to_str)
|
|
string.to_str.gsub(COLORED_REGEXP, '')
|
|
elsif respond_to?(:to_str)
|
|
to_str.gsub(COLORED_REGEXP, '')
|
|
else
|
|
''
|
|
end
|
|
end
|
|
|
|
module_function
|
|
|
|
# Returns an array of all Term::ANSIColor attributes as symbols.
|
|
def attributes
|
|
ATTRIBUTE_NAMES
|
|
end
|
|
extend self
|
|
end
|
|
end
|
|
|
|
# Begin sizes
|
|
|
|
class String
|
|
|
|
include Term::ANSIColor
|
|
|
|
# ensure trailing slash
|
|
def slashit
|
|
self.sub(/\/?$/,'/')
|
|
end
|
|
|
|
# colorize a human readable size format by size
|
|
def color_fmt
|
|
case self
|
|
when /\dB?$/
|
|
self.blue
|
|
when /\dKB?$/
|
|
self.green
|
|
when /\dMB?$/
|
|
self.yellow
|
|
when /\dGB?$/
|
|
self.red
|
|
else
|
|
self.bold.red
|
|
end
|
|
end
|
|
|
|
# colorize files by type (directories and hidden files)
|
|
def color_file(force_check=false)
|
|
filename = self.dup
|
|
if force_check && File.directory?(filename)
|
|
filename.sub!(/\/?$/,'/')
|
|
end
|
|
|
|
case filename
|
|
when /\/$/
|
|
filename.green
|
|
when /^\./
|
|
filename.white
|
|
else
|
|
filename.bold.white
|
|
end
|
|
end
|
|
|
|
# Replace $HOME in path with ~
|
|
def short_dir
|
|
home = ENV['HOME']
|
|
self.sub(/#{home}/, '~')
|
|
end
|
|
|
|
# Convert a line like `120414 filename` to a colorized string with
|
|
# human readable size
|
|
def line_to_human
|
|
parts = self.split(/\t/)
|
|
if parts[0] =~ /NO ACCESS/
|
|
" ERROR".red + " " + parts[1].color_file
|
|
else
|
|
size = to_human(parts[0].to_i).color_fmt
|
|
size.pad_escaped(7) + " " + parts[1].color_file
|
|
end
|
|
end
|
|
|
|
# Pad a line containing ansi escape codes to a given length, ignoring
|
|
# the escape codes
|
|
def pad_escaped(len)
|
|
str = self.dup
|
|
str.gsub!(/\e\[\d+m/,'')
|
|
prefix = ""
|
|
while prefix.length + str.length < len
|
|
prefix += " "
|
|
end
|
|
prefix + self
|
|
end
|
|
end
|
|
|
|
# Convert a number (assumed bytes) to a human readable format (12.5K)
|
|
def to_human(n,fmt=false)
|
|
count = 0
|
|
formats = %w(B K M G T P E Z Y)
|
|
|
|
while (fmt || n >= 1024) && count < 8
|
|
n /= 1024.0
|
|
count += 1
|
|
break if fmt && formats[count][0].upcase =~ /#{fmt[0].upcase}/
|
|
end
|
|
|
|
format("%.2f%s",n,formats[count])
|
|
end
|
|
|
|
# Use `du` to size a single directory and all of its contents. This
|
|
# number is returned in blocks (512B), so the human readable result may
|
|
# be slightly different than you'd get from `ls` or a GUI file manager
|
|
def du_size_single(dir)
|
|
res = %x{du -s #{Shellwords.escape(dir)} 2>/dev/null}.strip
|
|
if $?.success?
|
|
parts = res.split(/\t/)
|
|
(parts[0].to_i * 512).to_s + "\t" + parts[1].strip
|
|
else
|
|
"NO ACCESS\t#{dir}"
|
|
end
|
|
end
|
|
|
|
# main function
|
|
def all_sizes(dir)
|
|
# Use `ls` to list all files in the target with long info
|
|
files = %x{ls -lSrAF #{dir.slashit} 2>/dev/null}
|
|
unless $?.success?
|
|
$stdout.puts "Error getting file listing".red
|
|
Process.exit 1
|
|
end
|
|
files = files.strip.split(/\n/)
|
|
|
|
files.delete_if {|line|
|
|
line.strip =~ /^total \d+/
|
|
}
|
|
|
|
# trim file list to just size and filename
|
|
files.map! {|line|
|
|
line.sub(/\S{10,11} +\d+ +\S+ +\w+ +(\d+) +\w{3} +\d+ +[\d:]+ +(.*?)$/, "\\1\t\\2")
|
|
}
|
|
|
|
|
|
# if a line is a path to a directory, use `du` to update its size with
|
|
# the total filesize of the directory contents.
|
|
files.map! {|entry|
|
|
file = entry.split(/\t/)[1]
|
|
if File.directory?(file)
|
|
du_size_single(file)
|
|
else
|
|
entry
|
|
end
|
|
}
|
|
|
|
# Sort by size (after updating directory sizes)
|
|
files.sort! {|a,b|
|
|
size1 = a.split(/\t/)[0].to_i
|
|
size2 = b.split(/\t/)[0].to_i
|
|
size1 <=> size2
|
|
}
|
|
|
|
# Output each line with human-readable size and colorization
|
|
files.each {|entry|
|
|
$stdout.puts entry.line_to_human
|
|
}
|
|
# Include a total for the directory
|
|
$stdout.puts "-------".black.bold
|
|
$stdout.puts(du_size_single(dir).short_dir.line_to_human)
|
|
end
|
|
|
|
def help
|
|
app = File.basename(__FILE__)
|
|
help =<<EOHELP
|
|
|
|
#{app.bold.white} #{VERSION.green} by Brett Terpstra
|
|
Display a human-readable list of sizes for all files and directories.
|
|
|
|
usage:
|
|
$ #{app.bold.white} [directory]
|
|
|
|
Leaving directory blank operates in the current working directory.
|
|
|
|
EOHELP
|
|
puts help
|
|
Process.exit 0
|
|
end
|
|
|
|
# Assume operating on current directory...
|
|
dir = ENV['PWD']
|
|
|
|
# ...unless an argument is provided
|
|
if ARGV[0]
|
|
# Add some help. Why not?
|
|
if ARGV[0] =~ /^-?-h(elp)?/
|
|
help
|
|
elsif ARGV[0] =~ /^-?-v(ersion)?/
|
|
$stdout.puts File.basename(__FILE__) + " v" + VERSION
|
|
Process.exit 0
|
|
else
|
|
argdir = File.expand_path(ARGV[0])
|
|
if File.directory?(argdir)
|
|
dir = argdir
|
|
end
|
|
end
|
|
end
|
|
|
|
all_sizes(dir)
|