From 9114f9e2f4f0df6d399c83c55956ab02c352530f Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Wed, 23 Oct 2019 16:32:00 -0400 Subject: [PATCH] Add Brett Terpstra's sizes command --- bin/sizes | 332 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100755 bin/sizes diff --git a/bin/sizes b/bin/sizes new file mode 100755 index 0000000..2b3cb08 --- /dev/null +++ b/bin/sizes @@ -0,0 +1,332 @@ +#!/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: +# Home: +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 =<