diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index f10c79a..c90f1e4 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -1,4 +1,26 @@ #!/usr/bin/env ruby -## FIXME: Replace this code with a pure Ruby clone of the ls utility -system("ls", *ARGV) +require 'optparse' + +require_relative '../lib/ruby_ls' + +options = {} +parser = OptionParser.new do |p| + p.on('-l') { options[:long_format] = true } + p.on('-a') { options[:include_hidden] = true } +end + +begin + paths = parser.parse(ARGV) +rescue OptionParser::InvalidOption => err + msg = err.message.sub('invalid', 'illegal').sub(': -', ' -- ') + usage = 'usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]' + abort "ls: #{msg}\n#{usage}" +end +paths << Dir.pwd if paths.empty? + +begin + RubyLS::Application.new(options, paths).run +rescue Errno::ENOENT => err + abort "ls: #{err.message}" +end diff --git a/samples/part1/lib/ruby_ls.rb b/samples/part1/lib/ruby_ls.rb new file mode 100644 index 0000000..71211d7 --- /dev/null +++ b/samples/part1/lib/ruby_ls.rb @@ -0,0 +1,4 @@ +require_relative 'ruby_ls/application' +require_relative 'ruby_ls/display' +require_relative 'ruby_ls/file_details' +require_relative 'ruby_ls/permissions' diff --git a/samples/part1/lib/ruby_ls/application.rb b/samples/part1/lib/ruby_ls/application.rb new file mode 100644 index 0000000..2b6b413 --- /dev/null +++ b/samples/part1/lib/ruby_ls/application.rb @@ -0,0 +1,43 @@ +module RubyLS + class Application + def initialize(options, paths) + @options = options + @paths = paths + @display = RubyLS::Display.new(display_options) + end + + def run + @paths.each { |p| @display.render(p) } + end + + private + + def display_options + @options.merge(max_byte_count: byte_counts.max) + end + + def byte_counts + @paths.flat_map do |p| + Dir.exist?(p) ? directory_byte_counts(p) : byte_count(p) + end + end + + def directory_byte_counts(path) + directory_subpaths(path).map { |p| byte_count(p) } + end + + def directory_subpaths(path) + non_dot_char = /[^\.]/ + + Dir.foreach(path) + .select { |e| e =~ non_dot_char } + .map { |e| File.join(path, e) } + end + + def byte_count(path) + File.stat(path).size + rescue Errno::ENOENT => e + raise(e, "#{path}: No such file or directory") + end + end +end diff --git a/samples/part1/lib/ruby_ls/display.rb b/samples/part1/lib/ruby_ls/display.rb new file mode 100644 index 0000000..1b8d960 --- /dev/null +++ b/samples/part1/lib/ruby_ls/display.rb @@ -0,0 +1,65 @@ +module RubyLS + class Display + def initialize(options) + @long_format = options[:long_format] + @include_hidden = options[:include_hidden] + @max_byte_count = options[:max_byte_count] + end + + def render(path) + if Dir.exist?(path) + render_directory(path) + else + render_file(path) + end + end + + private + + def render_directory(dirname) + list_total_blocks(dirname) if @long_format + Dir.foreach(dirname) { |e| render_file(e) } + end + + def list_total_blocks(dirname) + puts "total #{total_blocks(dirname)}" + end + + def total_blocks(dirname) + entries(dirname).reduce(0) do |sum, e| + path = File.join(dirname, e) + sum + blocks_allocated(path) + end + end + + def entries(dirname) + Dir.entries(dirname).select { |e| show?(e) } + end + + def blocks_allocated(path) + File.stat(path).blocks + end + + def render_file(filename) + if show?(filename) + puts formatted(filename) + end + end + + def show?(filename) + @include_hidden || !hidden?(filename) + end + + def hidden?(filename) + filename[0] == '.' + end + + def formatted(filename) + if @long_format + File.open(filename) { |f| RubyLS::FileDetails.new(f, @max_byte_count) } + else + filename + end + end + end +end diff --git a/samples/part1/lib/ruby_ls/file_details.rb b/samples/part1/lib/ruby_ls/file_details.rb new file mode 100644 index 0000000..7776dc1 --- /dev/null +++ b/samples/part1/lib/ruby_ls/file_details.rb @@ -0,0 +1,69 @@ +require 'etc' + +module RubyLS + class FileDetails + FTYPES = { + 'blockSpecial' => 'b', + 'characterSpecial' => 'c', + 'directory' => 'd', + 'link' => 'l', + 'socket' => 's', + 'fifo' => 'P', + 'file' => '-' + } + + def initialize(file, max_byte_count = nil) + @file = file + @stat = File.stat(@file) + @permissions = RubyLS::Permissions.new(@stat) + @max_byte_count = max_byte_count + end + + def to_s + "#{mode} #{links.to_s.rjust(2)} #{owner} #{group} #{bytes} #{mtime} #{path}" + end + + def [](key) + send(key) + end + + private + + attr_reader :permissions + + def mode + ftype = FTYPES[@stat.ftype] + "#{ftype}#{permissions}" + end + + def links + @stat.nlink + end + + def owner + Etc.getpwuid(@stat.uid).name + end + + def group + Etc.getgrgid(@stat.gid).name + end + + def bytes + col_width = (@max_byte_count || size).to_s.length + 1 + size.to_s.rjust(col_width) + end + + def size + @stat.size + end + + def mtime + time = @stat.mtime + time.strftime('%b %e %H:%M') + end + + def path + @file.path + end + end +end diff --git a/samples/part1/lib/ruby_ls/permissions.rb b/samples/part1/lib/ruby_ls/permissions.rb new file mode 100644 index 0000000..099dc12 --- /dev/null +++ b/samples/part1/lib/ruby_ls/permissions.rb @@ -0,0 +1,36 @@ +module RubyLS + class Permissions + ACTORS = [:owner, :group, :other] + OPERATIONS = [:r, :w, :x] + + def initialize(file_stat) + @fmode = file_stat.mode + end + + def to_s + symbols.join + end + + private + + def symbols + bit_masks.map do |mask, operation| + enabled?(@fmode & mask) ? operation : :- + end + end + + def bit_masks + msb_masks = lsb_masks.reverse + msb_masks.zip(OPERATIONS.cycle) + end + + def lsb_masks + bits = (ACTORS.count * OPERATIONS.count) + bits.times.map { |offset| 0b1 << offset } + end + + def enabled?(bits) + bits > 0 + end + end +end diff --git a/samples/part1/ls_tests.rb b/samples/part1/ls_tests.rb index 4371b25..04f0979 100644 --- a/samples/part1/ls_tests.rb +++ b/samples/part1/ls_tests.rb @@ -7,21 +7,21 @@ # TODO: Uncomment each test below and get it to pass. -# check("Dir listing", "foo") +check("Dir listing", "foo") -# check("File glob", "foo/*.txt") +check("File glob", "foo/*.txt") -# check("Detailed output", "-l") +check("Detailed output", "-l") -# check("Hidden files", "-a") +check("Hidden files", "-a") -# check("Hidden files with detailed output", "-a -l") +check("Hidden files with detailed output", "-a -l") -# check("File glob with detailed output", "-l foo/*.txt") +check("File glob with detailed output", "-l foo/*.txt") -# check("Invalid directory", "missingdir") +check("Invalid directory", "missingdir") -# check("Invalid flag", "-Z") +check("Invalid flag", "-Z") puts "You passed the tests, yay!"