Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RubyLS solution #13

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
26 changes: 24 additions & 2 deletions samples/part1/bin/ruby-ls
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions samples/part1/lib/ruby_ls.rb
Original file line number Diff line number Diff line change
@@ -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'
43 changes: 43 additions & 0 deletions samples/part1/lib/ruby_ls/application.rb
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions samples/part1/lib/ruby_ls/display.rb
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions samples/part1/lib/ruby_ls/file_details.rb
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions samples/part1/lib/ruby_ls/permissions.rb
Original file line number Diff line number Diff line change
@@ -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
16 changes: 8 additions & 8 deletions samples/part1/ls_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!"

Expand Down