Last active
July 12, 2021 03:33
-
-
Save ttscoff/bc6ba3cf1fffcdb11e228c12e994fed7 to your computer and use it in GitHub Desktop.
sizes: Calculate and sort all filesizes for current folder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Could also just use
\S
to get all non-whitespace characters, just in case... Not sure offhand what all is valid in usernames, though.