Skip to content

Instantly share code, notes, and snippets.

@webstandardcss
Forked from ttscoff/sizes.rb
Created April 18, 2019 15:32
Show Gist options
  • Select an option

  • Save webstandardcss/fb9219373ea70efd8378230b0a97ca47 to your computer and use it in GitHub Desktop.

Select an option

Save webstandardcss/fb9219373ea70efd8378230b0a97ca47 to your computer and use it in GitHub Desktop.

Revisions

  1. @ttscoff ttscoff revised this gist Apr 13, 2019. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion sizes.rb
    Original file line number Diff line number Diff line change
    @@ -316,8 +316,11 @@ def help
    # ...unless an argument is provided
    if ARGV[0]
    # Add some help. Why not?
    if ARGV[0] =~ /^(-?-h(elp)?|help)/
    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)
  2. @ttscoff ttscoff created this gist Apr 13, 2019.
    329 changes: 329 additions & 0 deletions sizes.rb
    Original file line number Diff line number Diff line change
    @@ -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)