-
-
Save ttscoff/bc6ba3cf1fffcdb11e228c12e994fed7 to your computer and use it in GitHub Desktop.
#!/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.1" | |
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) |
Line 264 seems to fail when there is a -
in a username. This makes sense since -
is not part of w+
in regex.
I changed it locally from
line.sub(/\S{10,11} +\d+ +\S+ +\w+ +(\d+) +\w{3} +\d+ +[\d:]+ +(.*?)$/, "\\1\t\\2")
to
line.sub(/\S{10,11} +\d+ +\S+ +[\w-]+ +(\d+) +\w{3} +\d+ +[\d:]+ +(.*?)$/, "\\1\t\\2")
I think that will also include dashes in the search for user/group now. I don't know if it breaks anything else though 😉
Line 264 seems to fail when there is a
-
in a username. This makes sense since-
is not part ofw+
in regex.I changed it locally from
line.sub(/\S{10,11} +\d+ +\S+ +\w+ +(\d+) +\w{3} +\d+ +[\d:]+ +(.*?)$/, "\\1\t\\2")
to
line.sub(/\S{10,11} +\d+ +\S+ +[\w-]+ +(\d+) +\w{3} +\d+ +[\d:]+ +(.*?)$/, "\\1\t\\2")
I think that will also include dashes in the search for user/group now. I don't know if it breaks anything else though 😉
Could also just use \S
to get all non-whitespace characters, just in case... Not sure offhand what all is valid in usernames, though.
@rleaver152 thanks, updated the gist.