2# Sizes - Calculate and sort all filesizes for current folder
3# Includes directory sizes, colorized output
4# Brett Terpstra 2019 WTF License
9# Just including term-ansicolor by @flori and avoiding all the
10# rigamarole of requiring multiple files when it's not a gem... - Brett
12# ansicolor Copyright: Florian Frank
13# License: <https://github.com/flori/term-ansicolor/blob/master/COPYING>
14# Home: <https://github.com/flori/term-ansicolor>
17 # The ANSIColor module can be used for namespacing and mixed into your own
20 # require 'term/ansicolor/version'
24 [ :clear , 0 ], # String#clear is already used to empty string in Ruby 1.9
25 [ :reset , 0 ], # synonym for :clear
28 [ :italic , 3 ], # not widely implemented
30 [ :underscore , 4 ], # synonym for :underline
32 [ :rapid_blink , 6 ], # not widely implemented
33 [ :negative , 7 ], # no reverse because of String#reverse
35 [ :strikethrough , 9 ], # not widely implemented
52 [ :intense_black , 90 ], # High intensity, aixterm (works in OS X)
53 [ :intense_red , 91 ],
54 [ :intense_green , 92 ],
55 [ :intense_yellow , 93 ],
56 [ :intense_blue , 94 ],
57 [ :intense_magenta , 95 ],
58 [ :intense_cyan , 96 ],
59 [ :intense_white , 97 ],
60 [ :on_intense_black , 100 ], # High intensity background, aixterm (works in OS X)
61 [ :on_intense_red , 101 ],
62 [ :on_intense_green , 102 ],
63 [ :on_intense_yellow , 103 ],
64 [ :on_intense_blue , 104 ],
65 [ :on_intense_magenta , 105 ],
66 [ :on_intense_cyan , 106 ],
67 [ :on_intense_white , 107 ]
70 ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
73 # Returns true if Term::ANSIColor supports the +feature+.
75 # The feature :clear, that is mixing the clear color attribute into String,
76 # is only supported on ruby implementations, that do *not* already
77 # implement the String#clear method. It's better to use the reset color
82 !String.instance_methods(false).map(&:to_sym).include?(:clear)
85 # Returns true, if the coloring function of this module
86 # is switched on, false otherwise.
91 # Turns the coloring on or off globally, so you can easily do
93 # Term::ANSIColor::coloring = STDOUT.isatty
94 def self.coloring=(val)
99 ATTRIBUTES.each do |c, v|
101 def #{c}(string = nil)
103 result << "\e[#{v}m" if Term::ANSIColor.coloring?
106 elsif string.respond_to?(:to_str)
107 result << string.to_str
108 elsif respond_to?(:to_str)
111 return result #only switch on
113 result << "\e[0m" if Term::ANSIColor.coloring?
119 # Regular expression that is used to scan for ANSI-sequences while
120 # uncoloring strings.
121 COLORED_REGEXP = /\e\[(?:(?:[349]|10)[0-7]|[0-9])?m/
123 # Returns an uncolored version of the string, that is all
124 # ANSI-sequences are stripped from the string.
125 def uncolored(string = nil) # :yields:
127 yield.to_str.gsub(COLORED_REGEXP, '')
128 elsif string.respond_to?(:to_str)
129 string.to_str.gsub(COLORED_REGEXP, '')
130 elsif respond_to?(:to_str)
131 to_str.gsub(COLORED_REGEXP, '')
139 # Returns an array of all Term::ANSIColor attributes as symbols.
151 include Term::ANSIColor
153 # ensure trailing slash
158 # colorize a human readable size format by size
174 # colorize files by type (directories and hidden files)
175 def color_file(force_check=false)
177 if force_check && File.directory?(filename)
178 filename.sub!(/\/?$/,'/')
191 # Replace $HOME in path with ~
194 self.sub(/#{home}/, '~')
197 # Convert a line like `120414 filename` to a colorized string with
198 # human readable size
200 parts = self.split(/\t/)
201 if parts[0] =~ /NO ACCESS/
202 " ERROR".red + " " + parts[1].color_file
204 size = to_human(parts[0].to_i).color_fmt
205 size.pad_escaped(7) + " " + parts[1].color_file
209 # Pad a line containing ansi escape codes to a given length, ignoring
213 str.gsub!(/\e\[\d+m/,'')
215 while prefix.length + str.length < len
222# Convert a number (assumed bytes) to a human readable format (12.5K)
223def to_human(n,fmt=false)
225 formats = %w(B K M G T P E Z Y)
227 while (fmt || n >= 1024) && count < 8
230 break if fmt && formats[count][0].upcase =~ /#{fmt[0].upcase}/
233 format("%.2f%s",n,formats[count])
236# Use `du` to size a single directory and all of its contents. This
237# number is returned in blocks (512B), so the human readable result may
238# be slightly different than you'd get from `ls` or a GUI file manager
239def du_size_single(dir)
240 res = %x{du -s #{Shellwords.escape(dir)} 2>/dev/null}.strip
242 parts = res.split(/\t/)
243 {:size => parts[0].to_i * 512, :file => parts[1].strip}
245 {:size => nil, :file => dir}
252 # Use `ls` to list all files in the target with long info
253 files = %x{ls -lSrAF #{dir.slashit} 2>/dev/null}
255 $stdout.puts "Error getting file listing".red
258 files = files.strip.split(/\n/)
260 files.delete_if {|line|
261 line.strip =~ /^total \d+/
264 # trim file list to just size and filename
265 matches = files.map {|line|
266 # Examples of output I'm trying to match:
268 # -rw-r--r-- 1 alexwlchan staff 1053 1 Jul 2018 LICENSE
269 # -rw-r--r-- 1 alexwlchan staff 306 8 Jan 20:36 Makefile
271 m = line.match(/^\S{10,11} +\d+ +\S+ +\w+ +(?<size>\d+) +\d+ +\w{3} +[\d:]+ +(?<file>.*?)$/)
272 {:size => m[:size].to_i, :file => m[:file]}
276 # if a line is a path to a directory, use `du` to update its size with
277 # the total filesize of the directory contents.
278 matches.map! {|match|
279 if File.directory?(match[:file])
280 du_size_single(match[:file])
286 # Sort by size (after updating directory sizes)
293 # Output each line with human-readable size and colorization
294 matches.each {|match|
295 entry = "#{match[:size]}\t#{match[:file]}"
296 $stdout.puts entry.line_to_human
298 # Include a total for the directory
299 $stdout.puts "-------".black.bold
300 total = du_size_single(dir)
301 entry = "#{total[:size]}\t#{total[:file]}"
302 $stdout.puts(entry.short_dir.line_to_human)
306 app = File.basename(__FILE__)
308#{app.bold.white} #{VERSION.green} by Brett Terpstra
309 Display a human-readable list of sizes for all files and directories.
311 $ #{app.bold.white} [directory]
312Leaving directory blank operates in the current working directory.
318# Assume operating on current directory...
321# ...unless an argument is provided
323 # Add some help. Why not?
324 if ARGV[0] =~ /^-?-h(elp)?/
326 elsif ARGV[0] =~ /^-?-v(ersion)?/
327 $stdout.puts File.basename(__FILE__) + " v" + VERSION
330 argdir = File.expand_path(ARGV[0])
331 if File.directory?(argdir)