|
|
@@ -0,0 +1,329 @@ |
|
|
#!/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)/ |
|
|
help |
|
|
else |
|
|
argdir = File.expand_path(ARGV[0]) |
|
|
if File.directory?(argdir) |
|
|
dir = argdir |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
all_sizes(dir) |