From 189e4306f324ea2328281ee2f28642f7c3abc95f Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Wed, 20 Apr 2016 19:45:33 -0400 Subject: [PATCH 01/15] Ruby ls clone v0 Basic tests passing. Project structure taking form. Thinking of refactoring the permissions implementation. --- samples/part1/bin/ruby-ls | 5 ++- samples/part1/lib/rls.rb | 6 +++ samples/part1/lib/rls/application.rb | 35 ++++++++++++++++ samples/part1/lib/rls/display.rb | 61 ++++++++++++++++++++++++++++ samples/part1/lib/rls/long_format.rb | 59 +++++++++++++++++++++++++++ samples/part1/lib/rls/permissions.rb | 31 ++++++++++++++ samples/part1/ls_tests.rb | 6 +-- 7 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 samples/part1/lib/rls.rb create mode 100644 samples/part1/lib/rls/application.rb create mode 100644 samples/part1/lib/rls/display.rb create mode 100644 samples/part1/lib/rls/long_format.rb create mode 100644 samples/part1/lib/rls/permissions.rb diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index f10c79a..b2ea1cf 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -1,4 +1,5 @@ #!/usr/bin/env ruby -## FIXME: Replace this code with a pure Ruby clone of the ls utility -system("ls", *ARGV) +require_relative '../lib/rls' + +RLs::Application.new(ARGV).run diff --git a/samples/part1/lib/rls.rb b/samples/part1/lib/rls.rb new file mode 100644 index 0000000..11b456c --- /dev/null +++ b/samples/part1/lib/rls.rb @@ -0,0 +1,6 @@ +require 'optparse' + +require_relative 'rls/application' +require_relative 'rls/display' +require_relative 'rls/long_format' +require_relative 'rls/permissions' diff --git a/samples/part1/lib/rls/application.rb b/samples/part1/lib/rls/application.rb new file mode 100644 index 0000000..bfcc306 --- /dev/null +++ b/samples/part1/lib/rls/application.rb @@ -0,0 +1,35 @@ +module RLs + class Application + def initialize(argv) + @options, @paths = parse_options(argv) + @display = RLs::Display.new(@options) + end + + def run + paths.any? ? list_paths : list_current_directory + end + + private + + attr_reader :paths, :display + + def parse_options(argv) + options = {} + parser = OptionParser.new do |p| + p.on('-l') { options[:long_format] = true } + p.on('-a') { options[:include_hidden] = true } + end + paths = parser.parse(argv) + + [options, paths] + end + + def list_current_directory + display.render(Dir.pwd) + end + + def list_paths + paths.each { |path| display.render(path) } + end + end +end diff --git a/samples/part1/lib/rls/display.rb b/samples/part1/lib/rls/display.rb new file mode 100644 index 0000000..7092e5e --- /dev/null +++ b/samples/part1/lib/rls/display.rb @@ -0,0 +1,61 @@ +module RLs + class Display + def initialize(options) + @long_format = options[:long_format] + end + + def render(path) + if Dir.exist?(path) + render_directory(path) + else + list_if_visible(path) + end + end + + private + + attr_reader :long_format + + def render_directory(dirname) + list_total_blocks(dirname) if long_format + Dir.foreach(dirname) { |e| list_if_visible(e) } + end + + def list_total_blocks(dirname) + puts "total #{total_blocks(dirname)}" + end + + def total_blocks(dirname) + entries = Dir.entries(dirname).reject { |e| hidden?(e) } + + entries.reduce(0) do |sum, e| + path = File.join(dirname, e) + sum + blocks_allocated(path) + end + end + + def blocks_allocated(path) + File.stat(path).blocks + end + + def list_if_visible(filename) + list(filename) if ! hidden?(filename) + end + + def hidden?(filename) + filename[0] == '.' + end + + def list(filename) + puts formatted(filename) + end + + def formatted(filename) + if long_format + File.open(filename) { |f| RLs::LongFormat.new(f) } + else + filename + end + end + end +end diff --git a/samples/part1/lib/rls/long_format.rb b/samples/part1/lib/rls/long_format.rb new file mode 100644 index 0000000..b40fa6c --- /dev/null +++ b/samples/part1/lib/rls/long_format.rb @@ -0,0 +1,59 @@ +module RLs + class LongFormat + FTYPES = { + 'blockSpecial' => 'b', + 'characterSpecial' => 'c', + 'directory' => 'd', + 'link' => 'l', + 'socket' => 's', + 'fifo' => 'P', + 'file' => '-' + } + + def initialize(file) + @file = file + @stat = File.stat(@file) + @permissions = RLs::Permissions.new(@stat) + end + + def to_s + "#{mode} #{links} #{owner} #{group} #{bytes} #{last_modified} #{path}" + end + + private + + attr_reader :file, :stat, :permissions + + def mode + ftype = FTYPES[stat.ftype] + "#{ftype}#{permissions}" + end + + def links + nlink = stat.nlink + nlink.to_s.rjust(2) + end + + def owner + `id -un #{stat.uid}`.chomp + end + + def group + `id -gn #{stat.uid}`.chomp + end + + def bytes + size = stat.size + size.to_s.rjust(4) + end + + def last_modified + time = stat.mtime + time.strftime('%b %e %H:%M') + end + + def path + file.path + end + end +end diff --git a/samples/part1/lib/rls/permissions.rb b/samples/part1/lib/rls/permissions.rb new file mode 100644 index 0000000..924ed48 --- /dev/null +++ b/samples/part1/lib/rls/permissions.rb @@ -0,0 +1,31 @@ +module RLs + class Permissions + def initialize(file_stat) + @fmode = file_stat.mode + end + + def to_s + fm = @fmode + + world = fm % 8 + fm /= 8 + group = fm % 8 + fm /= 8 + owner = fm % 8 + + [owner, group, world].map { |bits| read_write_execute(bits) }.join + end + + private + + def read_write_execute(bits) + x = (bits % 2) > 0 ? 'x' : '-' + bits /= 2 + w = (bits % 2) > 0 ? 'w' : '-' + bits /= 2 + r = (bits % 2) > 0 ? 'r' : '-' + + "#{r}#{w}#{x}" + end + end +end diff --git a/samples/part1/ls_tests.rb b/samples/part1/ls_tests.rb index 4371b25..9e4e1a8 100644 --- a/samples/part1/ls_tests.rb +++ b/samples/part1/ls_tests.rb @@ -7,11 +7,11 @@ # 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") From 40956ecc874b7387247f8adde361b159dc18b3e3 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Thu, 21 Apr 2016 01:00:29 -0400 Subject: [PATCH 02/15] it's alive interesting evolution of Permissions class. Next step is eliminating duplication between the #component_fields and #permitted_operations methods. --- samples/part1/lib/rls/permissions.rb | 48 +++++++++++++++++++--------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/samples/part1/lib/rls/permissions.rb b/samples/part1/lib/rls/permissions.rb index 924ed48..c3ee5dc 100644 --- a/samples/part1/lib/rls/permissions.rb +++ b/samples/part1/lib/rls/permissions.rb @@ -1,31 +1,49 @@ module RLs class Permissions + COMPONENTS = [:owner, :group, :world] + OPERATIONS = [:r, :w, :x] + DASH = '-'.freeze + def initialize(file_stat) @fmode = file_stat.mode end def to_s - fm = @fmode + rwx = component_fields.map do |field| + permitted_operations(field) + end + rwx.join + end + + private - world = fm % 8 - fm /= 8 - group = fm % 8 - fm /= 8 - owner = fm % 8 + def component_fields + max_component_index = COMPONENTS.count - 1 + bits_per_component = OPERATIONS.count + operation_combos = 2 ** bits_per_component - [owner, group, world].map { |bits| read_write_execute(bits) }.join + COMPONENTS.each_with_index.map do |component, i| + lesser_components = max_component_index - i + bit_offset = lesser_components * bits_per_component + + (@fmode >> bit_offset) % operation_combos + end end - private + def permitted_operations(field) + max_operation_index = OPERATIONS.count - 1 + bits_per_operation = 1 + cardinality = 2 ** bits_per_operation + + bits = OPERATIONS.each_with_index.map do |symbol, index| + lesser_operations = max_operation_index - index + bit_offset = lesser_operations * bits_per_operation - def read_write_execute(bits) - x = (bits % 2) > 0 ? 'x' : '-' - bits /= 2 - w = (bits % 2) > 0 ? 'w' : '-' - bits /= 2 - r = (bits % 2) > 0 ? 'r' : '-' + bit = (field >> bit_offset) % cardinality + bit > 0 ? symbol : DASH + end - "#{r}#{w}#{x}" + bits.join end end end From 731f1bdc3ca613ae71db8c6f2ffb0fe2786dec32 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Thu, 21 Apr 2016 01:44:31 -0400 Subject: [PATCH 03/15] kinda cool, but is it better or worse? --- samples/part1/lib/rls/permissions.rb | 48 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/samples/part1/lib/rls/permissions.rb b/samples/part1/lib/rls/permissions.rb index c3ee5dc..532978e 100644 --- a/samples/part1/lib/rls/permissions.rb +++ b/samples/part1/lib/rls/permissions.rb @@ -1,49 +1,49 @@ module RLs class Permissions - COMPONENTS = [:owner, :group, :world] + FIELDS = [:owner, :group, :world] OPERATIONS = [:r, :w, :x] - DASH = '-'.freeze + MINUS = '-'.freeze def initialize(file_stat) @fmode = file_stat.mode end def to_s - rwx = component_fields.map do |field| - permitted_operations(field) + rwx = field_components.map do |bit_field| + permitted_operations(bit_field) do |bit, operation| + enabled?(bit) ? operation : MINUS + end end + rwx.join end private - def component_fields - max_component_index = COMPONENTS.count - 1 - bits_per_component = OPERATIONS.count - operation_combos = 2 ** bits_per_component - - COMPONENTS.each_with_index.map do |component, i| - lesser_components = max_component_index - i - bit_offset = lesser_components * bits_per_component + def field_components + mode_components(@fmode, FIELDS, bits_per_component: OPERATIONS.count) + end - (@fmode >> bit_offset) % operation_combos - end + def permitted_operations(bit_field, &block) + mode_components(bit_field, OPERATIONS, bits_per_component: 1, &block) end - def permitted_operations(field) - max_operation_index = OPERATIONS.count - 1 - bits_per_operation = 1 - cardinality = 2 ** bits_per_operation + def mode_components(mode, components, bits_per_component:) + max_index = components.count - 1 + cardinality = 2 ** bits_per_component - bits = OPERATIONS.each_with_index.map do |symbol, index| - lesser_operations = max_operation_index - index - bit_offset = lesser_operations * bits_per_operation + components.each_with_index.map do |component, index| + lesser_components = max_index - index + bit_offset = lesser_components * bits_per_component - bit = (field >> bit_offset) % cardinality - bit > 0 ? symbol : DASH + val = (mode >> bit_offset) % cardinality + val = yield(val, component) if block_given? + val end + end - bits.join + def enabled?(bit) + bit > 0 end end end From dda38f34afa0095390a5eb7531a51c87a350ec87 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Fri, 6 May 2016 23:19:44 -0400 Subject: [PATCH 04/15] use bit masks for permissions --- samples/part1/lib/rls/permissions.rb | 41 ++++++++++------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/samples/part1/lib/rls/permissions.rb b/samples/part1/lib/rls/permissions.rb index 532978e..b58ca86 100644 --- a/samples/part1/lib/rls/permissions.rb +++ b/samples/part1/lib/rls/permissions.rb @@ -1,49 +1,36 @@ module RLs class Permissions - FIELDS = [:owner, :group, :world] + ACTORS = [:owner, :group, :other] OPERATIONS = [:r, :w, :x] - MINUS = '-'.freeze def initialize(file_stat) @fmode = file_stat.mode end def to_s - rwx = field_components.map do |bit_field| - permitted_operations(bit_field) do |bit, operation| - enabled?(bit) ? operation : MINUS - end - end - - rwx.join + symbols.join end private - def field_components - mode_components(@fmode, FIELDS, bits_per_component: OPERATIONS.count) + def symbols + bit_masks.map do |mask, operation| + enabled?(@fmode & mask) ? operation : :- + end end - def permitted_operations(bit_field, &block) - mode_components(bit_field, OPERATIONS, bits_per_component: 1, &block) + def bit_masks + msb_masks = lsb_masks.reverse + msb_masks.zip(OPERATIONS.cycle) end - def mode_components(mode, components, bits_per_component:) - max_index = components.count - 1 - cardinality = 2 ** bits_per_component - - components.each_with_index.map do |component, index| - lesser_components = max_index - index - bit_offset = lesser_components * bits_per_component - - val = (mode >> bit_offset) % cardinality - val = yield(val, component) if block_given? - val - end + def lsb_masks + bits = (ACTORS.count * OPERATIONS.count) + bits.times.map { |offset| 0b1 << offset } end - def enabled?(bit) - bit > 0 + def enabled?(bits) + bits > 0 end end end From a7fbe2f2acfe49b0ea906813c8abce729f074198 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Fri, 6 May 2016 23:58:01 -0400 Subject: [PATCH 05/15] allow display of hidden files --- samples/part1/lib/rls/display.rb | 17 ++++++++++++----- samples/part1/ls_tests.rb | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/samples/part1/lib/rls/display.rb b/samples/part1/lib/rls/display.rb index 7092e5e..de271e5 100644 --- a/samples/part1/lib/rls/display.rb +++ b/samples/part1/lib/rls/display.rb @@ -2,6 +2,7 @@ module RLs class Display def initialize(options) @long_format = options[:long_format] + @include_hidden = options[:include_hidden] end def render(path) @@ -14,7 +15,7 @@ def render(path) private - attr_reader :long_format + attr_reader :long_format, :include_hidden def render_directory(dirname) list_total_blocks(dirname) if long_format @@ -26,20 +27,26 @@ def list_total_blocks(dirname) end def total_blocks(dirname) - entries = Dir.entries(dirname).reject { |e| hidden?(e) } - - entries.reduce(0) do |sum, e| + 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 list_if_visible(filename) - list(filename) if ! hidden?(filename) + list(filename) if show?(filename) + end + + def show?(filename) + include_hidden || !hidden?(filename) end def hidden?(filename) diff --git a/samples/part1/ls_tests.rb b/samples/part1/ls_tests.rb index 9e4e1a8..4392382 100644 --- a/samples/part1/ls_tests.rb +++ b/samples/part1/ls_tests.rb @@ -13,9 +13,9 @@ 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") From ece3e99b984750366f5c750e98cc0958c535c826 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Sun, 8 May 2016 16:10:36 -0400 Subject: [PATCH 06/15] max-byte-counts, long-format, glob In order to make the column line up, each entry needs to share some knowledge related to the byte counts of other entries. I probably need to refactor this a bit and make clearer distinctions between code that parses trees (figures out what files/directories will be listed) and code that formats for display. --- samples/part1/lib/rls/application.rb | 38 +++++++++++++++++++--------- samples/part1/lib/rls/display.rb | 5 ++-- samples/part1/lib/rls/long_format.rb | 12 +++++---- samples/part1/ls_tests.rb | 2 +- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/samples/part1/lib/rls/application.rb b/samples/part1/lib/rls/application.rb index bfcc306..f2e8973 100644 --- a/samples/part1/lib/rls/application.rb +++ b/samples/part1/lib/rls/application.rb @@ -1,17 +1,38 @@ module RLs class Application def initialize(argv) - @options, @paths = parse_options(argv) - @display = RLs::Display.new(@options) + options, @paths = parse_options(argv) + options.merge!(max_byte_count: byte_counts.max) + @display = RLs::Display.new(options) end def run - paths.any? ? list_paths : list_current_directory + @paths.each { |p| @display.render(p) } end private - attr_reader :paths, :display + 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 + end def parse_options(argv) options = {} @@ -20,16 +41,9 @@ def parse_options(argv) p.on('-a') { options[:include_hidden] = true } end paths = parser.parse(argv) + paths << Dir.pwd if paths.empty? [options, paths] end - - def list_current_directory - display.render(Dir.pwd) - end - - def list_paths - paths.each { |path| display.render(path) } - end end end diff --git a/samples/part1/lib/rls/display.rb b/samples/part1/lib/rls/display.rb index de271e5..11150e7 100644 --- a/samples/part1/lib/rls/display.rb +++ b/samples/part1/lib/rls/display.rb @@ -1,8 +1,9 @@ module RLs class Display def initialize(options) - @long_format = options[:long_format] + @long_format = options[:long_format] @include_hidden = options[:include_hidden] + @max_byte_count = options[:max_byte_count] end def render(path) @@ -59,7 +60,7 @@ def list(filename) def formatted(filename) if long_format - File.open(filename) { |f| RLs::LongFormat.new(f) } + File.open(filename) { |f| RLs::LongFormat.new(f, @max_byte_count) } else filename end diff --git a/samples/part1/lib/rls/long_format.rb b/samples/part1/lib/rls/long_format.rb index b40fa6c..b279ad2 100644 --- a/samples/part1/lib/rls/long_format.rb +++ b/samples/part1/lib/rls/long_format.rb @@ -10,14 +10,15 @@ class LongFormat 'file' => '-' } - def initialize(file) - @file = file - @stat = File.stat(@file) + def initialize(file, max_byte_count) + @file = file + @stat = File.stat(@file) @permissions = RLs::Permissions.new(@stat) + @max_byte_count = max_byte_count end def to_s - "#{mode} #{links} #{owner} #{group} #{bytes} #{last_modified} #{path}" + "#{mode} #{links} #{owner} #{group}#{bytes} #{last_modified} #{path}" end private @@ -44,7 +45,8 @@ def group def bytes size = stat.size - size.to_s.rjust(4) + col_width = @max_byte_count.to_s.length + 2 + size.to_s.rjust(col_width) end def last_modified diff --git a/samples/part1/ls_tests.rb b/samples/part1/ls_tests.rb index 4392382..c47cbb6 100644 --- a/samples/part1/ls_tests.rb +++ b/samples/part1/ls_tests.rb @@ -17,7 +17,7 @@ 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") From c9374ac5ab0c34bac001e78b0cb6a80f2ff7e042 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Sun, 8 May 2016 17:58:49 -0400 Subject: [PATCH 07/15] gracefully handle non-existent files/directories --- samples/part1/bin/ruby-ls | 6 +++++- samples/part1/lib/rls/application.rb | 2 ++ samples/part1/ls_tests.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index b2ea1cf..00e16cf 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -2,4 +2,8 @@ require_relative '../lib/rls' -RLs::Application.new(ARGV).run +begin + RLs::Application.new(ARGV).run +rescue Errno::ENOENT => err + abort "ls: #{err.message}" +end diff --git a/samples/part1/lib/rls/application.rb b/samples/part1/lib/rls/application.rb index f2e8973..bf15c45 100644 --- a/samples/part1/lib/rls/application.rb +++ b/samples/part1/lib/rls/application.rb @@ -32,6 +32,8 @@ def directory_subpaths(path) def byte_count(path) File.stat(path).size + rescue Errno::ENOENT => e + raise(e, "#{path}: No such file or directory") end def parse_options(argv) diff --git a/samples/part1/ls_tests.rb b/samples/part1/ls_tests.rb index c47cbb6..12be17f 100644 --- a/samples/part1/ls_tests.rb +++ b/samples/part1/ls_tests.rb @@ -19,7 +19,7 @@ check("File glob with detailed output", "-l foo/*.txt") -# check("Invalid directory", "missingdir") +check("Invalid directory", "missingdir") # check("Invalid flag", "-Z") From b3951bdc35991587a52141ab3e8a26a13dffdf47 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Sun, 8 May 2016 18:14:04 -0400 Subject: [PATCH 08/15] handle invalid option flags --- samples/part1/bin/ruby-ls | 4 ++++ samples/part1/ls_tests.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index 00e16cf..650ca7e 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -6,4 +6,8 @@ begin RLs::Application.new(ARGV).run rescue Errno::ENOENT => err abort "ls: #{err.message}" +rescue OptionParser::InvalidOption => err + msg = err.message.sub('invalid', 'illegal').sub(': -', ' -- ') + usage = 'usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]' + abort "ls: #{msg}\n#{usage}" end diff --git a/samples/part1/ls_tests.rb b/samples/part1/ls_tests.rb index 12be17f..04f0979 100644 --- a/samples/part1/ls_tests.rb +++ b/samples/part1/ls_tests.rb @@ -21,7 +21,7 @@ check("Invalid directory", "missingdir") -# check("Invalid flag", "-Z") +check("Invalid flag", "-Z") puts "You passed the tests, yay!" From 790fdba103ddb158825e8284a24e815c87ce5d34 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Tue, 10 May 2016 08:06:07 -0400 Subject: [PATCH 09/15] move option parsing out of application --- samples/part1/bin/ruby-ls | 17 ++++++++++++++--- samples/part1/lib/rls/application.rb | 24 ++++++++---------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index 650ca7e..6287941 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -2,12 +2,23 @@ require_relative '../lib/rls' +options = {} +parser = OptionParser.new do |p| + p.on('-l') { options[:long_format] = true } + p.on('-a') { options[:include_hidden] = true } +end + begin - RLs::Application.new(ARGV).run -rescue Errno::ENOENT => err - abort "ls: #{err.message}" + 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 + RLs::Application.new(options, paths).run +rescue Errno::ENOENT => err + abort "ls: #{err.message}" +end diff --git a/samples/part1/lib/rls/application.rb b/samples/part1/lib/rls/application.rb index bf15c45..860f3e4 100644 --- a/samples/part1/lib/rls/application.rb +++ b/samples/part1/lib/rls/application.rb @@ -1,9 +1,9 @@ module RLs class Application - def initialize(argv) - options, @paths = parse_options(argv) - options.merge!(max_byte_count: byte_counts.max) - @display = RLs::Display.new(options) + def initialize(options, paths) + @options = options + @paths = paths + @display = RLs::Display.new(display_options) end def run @@ -12,6 +12,10 @@ def run 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) @@ -35,17 +39,5 @@ def byte_count(path) rescue Errno::ENOENT => e raise(e, "#{path}: No such file or directory") end - - def parse_options(argv) - options = {} - parser = OptionParser.new do |p| - p.on('-l') { options[:long_format] = true } - p.on('-a') { options[:include_hidden] = true } - end - paths = parser.parse(argv) - paths << Dir.pwd if paths.empty? - - [options, paths] - end end end From 5face41aaaa2df34f86799c137fdb3ff19a8ab7b Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Tue, 10 May 2016 08:06:36 -0400 Subject: [PATCH 10/15] directly use instance vars internally Defining private attr_readers does nothing but obscure what's going on. Using instance variables directly within the class gives a nice visual reminder of where these values came from. --- samples/part1/lib/rls/long_format.rb | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/samples/part1/lib/rls/long_format.rb b/samples/part1/lib/rls/long_format.rb index b279ad2..f7d300f 100644 --- a/samples/part1/lib/rls/long_format.rb +++ b/samples/part1/lib/rls/long_format.rb @@ -18,44 +18,42 @@ def initialize(file, max_byte_count) end def to_s - "#{mode} #{links} #{owner} #{group}#{bytes} #{last_modified} #{path}" + "#{mode} #{links} #{owner} #{group} #{bytes} #{last_modified} #{path}" end private - attr_reader :file, :stat, :permissions - def mode - ftype = FTYPES[stat.ftype] - "#{ftype}#{permissions}" + ftype = FTYPES[@stat.ftype] + "#{ftype}#{@permissions}" end def links - nlink = stat.nlink + nlink = @stat.nlink nlink.to_s.rjust(2) end def owner - `id -un #{stat.uid}`.chomp + `id -un #{@stat.uid}`.chomp end def group - `id -gn #{stat.uid}`.chomp + `id -gn #{@stat.uid}`.chomp end def bytes - size = stat.size - col_width = @max_byte_count.to_s.length + 2 + size = @stat.size + col_width = @max_byte_count.to_s.length + 1 size.to_s.rjust(col_width) end def last_modified - time = stat.mtime + time = @stat.mtime time.strftime('%b %e %H:%M') end def path - file.path + @file.path end end end From eaccc3561fd8387c37971f6ef58eac5ca8b16337 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Tue, 10 May 2016 08:13:40 -0400 Subject: [PATCH 11/15] rename RLs -> RubyLS --- samples/part1/bin/ruby-ls | 4 ++-- samples/part1/lib/rls.rb | 6 ------ samples/part1/lib/ruby_ls.rb | 6 ++++++ samples/part1/lib/{rls => ruby_ls}/application.rb | 4 ++-- samples/part1/lib/{rls => ruby_ls}/display.rb | 4 ++-- samples/part1/lib/{rls => ruby_ls}/long_format.rb | 4 ++-- samples/part1/lib/{rls => ruby_ls}/permissions.rb | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) delete mode 100644 samples/part1/lib/rls.rb create mode 100644 samples/part1/lib/ruby_ls.rb rename samples/part1/lib/{rls => ruby_ls}/application.rb (92%) rename samples/part1/lib/{rls => ruby_ls}/display.rb (93%) rename samples/part1/lib/{rls => ruby_ls}/long_format.rb (94%) rename samples/part1/lib/{rls => ruby_ls}/permissions.rb (97%) diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index 6287941..41fea38 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -require_relative '../lib/rls' +require_relative '../lib/ruby_ls' options = {} parser = OptionParser.new do |p| @@ -18,7 +18,7 @@ end paths << Dir.pwd if paths.empty? begin - RLs::Application.new(options, paths).run + RubyLS::Application.new(options, paths).run rescue Errno::ENOENT => err abort "ls: #{err.message}" end diff --git a/samples/part1/lib/rls.rb b/samples/part1/lib/rls.rb deleted file mode 100644 index 11b456c..0000000 --- a/samples/part1/lib/rls.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'optparse' - -require_relative 'rls/application' -require_relative 'rls/display' -require_relative 'rls/long_format' -require_relative 'rls/permissions' diff --git a/samples/part1/lib/ruby_ls.rb b/samples/part1/lib/ruby_ls.rb new file mode 100644 index 0000000..46ef54d --- /dev/null +++ b/samples/part1/lib/ruby_ls.rb @@ -0,0 +1,6 @@ +require 'optparse' + +require_relative 'ruby_ls/application' +require_relative 'ruby_ls/display' +require_relative 'ruby_ls/long_format' +require_relative 'ruby_ls/permissions' diff --git a/samples/part1/lib/rls/application.rb b/samples/part1/lib/ruby_ls/application.rb similarity index 92% rename from samples/part1/lib/rls/application.rb rename to samples/part1/lib/ruby_ls/application.rb index 860f3e4..2b6b413 100644 --- a/samples/part1/lib/rls/application.rb +++ b/samples/part1/lib/ruby_ls/application.rb @@ -1,9 +1,9 @@ -module RLs +module RubyLS class Application def initialize(options, paths) @options = options @paths = paths - @display = RLs::Display.new(display_options) + @display = RubyLS::Display.new(display_options) end def run diff --git a/samples/part1/lib/rls/display.rb b/samples/part1/lib/ruby_ls/display.rb similarity index 93% rename from samples/part1/lib/rls/display.rb rename to samples/part1/lib/ruby_ls/display.rb index 11150e7..8da1582 100644 --- a/samples/part1/lib/rls/display.rb +++ b/samples/part1/lib/ruby_ls/display.rb @@ -1,4 +1,4 @@ -module RLs +module RubyLS class Display def initialize(options) @long_format = options[:long_format] @@ -60,7 +60,7 @@ def list(filename) def formatted(filename) if long_format - File.open(filename) { |f| RLs::LongFormat.new(f, @max_byte_count) } + File.open(filename) { |f| RubyLS::LongFormat.new(f, @max_byte_count) } else filename end diff --git a/samples/part1/lib/rls/long_format.rb b/samples/part1/lib/ruby_ls/long_format.rb similarity index 94% rename from samples/part1/lib/rls/long_format.rb rename to samples/part1/lib/ruby_ls/long_format.rb index f7d300f..b6cbaad 100644 --- a/samples/part1/lib/rls/long_format.rb +++ b/samples/part1/lib/ruby_ls/long_format.rb @@ -1,4 +1,4 @@ -module RLs +module RubyLS class LongFormat FTYPES = { 'blockSpecial' => 'b', @@ -13,7 +13,7 @@ class LongFormat def initialize(file, max_byte_count) @file = file @stat = File.stat(@file) - @permissions = RLs::Permissions.new(@stat) + @permissions = RubyLS::Permissions.new(@stat) @max_byte_count = max_byte_count end diff --git a/samples/part1/lib/rls/permissions.rb b/samples/part1/lib/ruby_ls/permissions.rb similarity index 97% rename from samples/part1/lib/rls/permissions.rb rename to samples/part1/lib/ruby_ls/permissions.rb index b58ca86..099dc12 100644 --- a/samples/part1/lib/rls/permissions.rb +++ b/samples/part1/lib/ruby_ls/permissions.rb @@ -1,4 +1,4 @@ -module RLs +module RubyLS class Permissions ACTORS = [:owner, :group, :other] OPERATIONS = [:r, :w, :x] From 45834c1534da9936efff0b3c4b72fabc50b8c81d Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Tue, 10 May 2016 08:44:33 -0400 Subject: [PATCH 12/15] LongFormat -> FileDetails Rename and make some small adjustments to accomodate unit testing. --- samples/part1/lib/ruby_ls.rb | 2 +- samples/part1/lib/ruby_ls/display.rb | 10 ++++------ .../lib/ruby_ls/{long_format.rb => file_details.rb} | 12 +++++++----- 3 files changed, 12 insertions(+), 12 deletions(-) rename samples/part1/lib/ruby_ls/{long_format.rb => file_details.rb} (81%) diff --git a/samples/part1/lib/ruby_ls.rb b/samples/part1/lib/ruby_ls.rb index 46ef54d..f70c662 100644 --- a/samples/part1/lib/ruby_ls.rb +++ b/samples/part1/lib/ruby_ls.rb @@ -2,5 +2,5 @@ require_relative 'ruby_ls/application' require_relative 'ruby_ls/display' -require_relative 'ruby_ls/long_format' +require_relative 'ruby_ls/file_details' require_relative 'ruby_ls/permissions' diff --git a/samples/part1/lib/ruby_ls/display.rb b/samples/part1/lib/ruby_ls/display.rb index 8da1582..ef2b2b1 100644 --- a/samples/part1/lib/ruby_ls/display.rb +++ b/samples/part1/lib/ruby_ls/display.rb @@ -16,10 +16,8 @@ def render(path) private - attr_reader :long_format, :include_hidden - def render_directory(dirname) - list_total_blocks(dirname) if long_format + list_total_blocks(dirname) if @long_format Dir.foreach(dirname) { |e| list_if_visible(e) } end @@ -47,7 +45,7 @@ def list_if_visible(filename) end def show?(filename) - include_hidden || !hidden?(filename) + @include_hidden || !hidden?(filename) end def hidden?(filename) @@ -59,8 +57,8 @@ def list(filename) end def formatted(filename) - if long_format - File.open(filename) { |f| RubyLS::LongFormat.new(f, @max_byte_count) } + if @long_format + File.open(filename) { |f| RubyLS::FileDetails.new(f, @max_byte_count) } else filename end diff --git a/samples/part1/lib/ruby_ls/long_format.rb b/samples/part1/lib/ruby_ls/file_details.rb similarity index 81% rename from samples/part1/lib/ruby_ls/long_format.rb rename to samples/part1/lib/ruby_ls/file_details.rb index b6cbaad..460be86 100644 --- a/samples/part1/lib/ruby_ls/long_format.rb +++ b/samples/part1/lib/ruby_ls/file_details.rb @@ -1,5 +1,7 @@ +require 'etc' + module RubyLS - class LongFormat + class FileDetails FTYPES = { 'blockSpecial' => 'b', 'characterSpecial' => 'c', @@ -10,7 +12,7 @@ class LongFormat 'file' => '-' } - def initialize(file, max_byte_count) + def initialize(file, max_byte_count = nil) @file = file @stat = File.stat(@file) @permissions = RubyLS::Permissions.new(@stat) @@ -34,16 +36,16 @@ def links end def owner - `id -un #{@stat.uid}`.chomp + Etc.getpwuid(@stat.uid).name end def group - `id -gn #{@stat.uid}`.chomp + Etc.getgrgid(@stat.gid).name end def bytes size = @stat.size - col_width = @max_byte_count.to_s.length + 1 + col_width = (@max_byte_count || size).to_s.length + 1 size.to_s.rjust(col_width) end From 33c34047437ae285b5932856fd2ddb832259bec2 Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Tue, 10 May 2016 08:57:45 -0400 Subject: [PATCH 13/15] further accomodate unit tests --- samples/part1/lib/ruby_ls/file_details.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/samples/part1/lib/ruby_ls/file_details.rb b/samples/part1/lib/ruby_ls/file_details.rb index 460be86..7776dc1 100644 --- a/samples/part1/lib/ruby_ls/file_details.rb +++ b/samples/part1/lib/ruby_ls/file_details.rb @@ -20,19 +20,24 @@ def initialize(file, max_byte_count = nil) end def to_s - "#{mode} #{links} #{owner} #{group} #{bytes} #{last_modified} #{path}" + "#{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}" + "#{ftype}#{permissions}" end def links - nlink = @stat.nlink - nlink.to_s.rjust(2) + @stat.nlink end def owner @@ -44,12 +49,15 @@ def group end def bytes - size = @stat.size col_width = (@max_byte_count || size).to_s.length + 1 size.to_s.rjust(col_width) end - def last_modified + def size + @stat.size + end + + def mtime time = @stat.mtime time.strftime('%b %e %H:%M') end From 0c88db5891c660fe8c4a013ab209d48cfc028dee Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Tue, 10 May 2016 09:03:01 -0400 Subject: [PATCH 14/15] move require to executable --- samples/part1/bin/ruby-ls | 2 ++ samples/part1/lib/ruby_ls.rb | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index 41fea38..c90f1e4 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -1,5 +1,7 @@ #!/usr/bin/env ruby +require 'optparse' + require_relative '../lib/ruby_ls' options = {} diff --git a/samples/part1/lib/ruby_ls.rb b/samples/part1/lib/ruby_ls.rb index f70c662..71211d7 100644 --- a/samples/part1/lib/ruby_ls.rb +++ b/samples/part1/lib/ruby_ls.rb @@ -1,5 +1,3 @@ -require 'optparse' - require_relative 'ruby_ls/application' require_relative 'ruby_ls/display' require_relative 'ruby_ls/file_details' From 726475de03ec807efff71a199d0d2ac36937b49f Mon Sep 17 00:00:00 2001 From: ivanbrennan Date: Tue, 10 May 2016 09:08:48 -0400 Subject: [PATCH 15/15] clean up a couple methods --- samples/part1/lib/ruby_ls/display.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/samples/part1/lib/ruby_ls/display.rb b/samples/part1/lib/ruby_ls/display.rb index ef2b2b1..1b8d960 100644 --- a/samples/part1/lib/ruby_ls/display.rb +++ b/samples/part1/lib/ruby_ls/display.rb @@ -10,7 +10,7 @@ def render(path) if Dir.exist?(path) render_directory(path) else - list_if_visible(path) + render_file(path) end end @@ -18,7 +18,7 @@ def render(path) def render_directory(dirname) list_total_blocks(dirname) if @long_format - Dir.foreach(dirname) { |e| list_if_visible(e) } + Dir.foreach(dirname) { |e| render_file(e) } end def list_total_blocks(dirname) @@ -40,8 +40,10 @@ def blocks_allocated(path) File.stat(path).blocks end - def list_if_visible(filename) - list(filename) if show?(filename) + def render_file(filename) + if show?(filename) + puts formatted(filename) + end end def show?(filename) @@ -52,10 +54,6 @@ def hidden?(filename) filename[0] == '.' end - def list(filename) - puts formatted(filename) - end - def formatted(filename) if @long_format File.open(filename) { |f| RubyLS::FileDetails.new(f, @max_byte_count) }