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

Solution to part1 #11

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions PART_1.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,66 @@ and exercises.
**Q1:** What steps are involved in making a Ruby scripts runnable as a
command line utility? (i.e. directly runnable like `rake` or `gem`
rather than having to type `ruby my_script.rb`)
A shebang line needs to be placed above the ruby code in a script that points to the location of the ruby environment,
in order to execute the script. #! /usr/bin/env ruby

**Q2:** What is `ARGF` stream used for in Ruby?
Is used in a Ruby script to process files that are passes in as command line arguments. It works in conjunction with ARGV so when a file is read from ARGF it is removed from the ARGV array,

**Q3:** What is `$?` used for in Bash/Ruby?
It is a global variable in Ruby that returns information about a Process's status. It is an instance of the Process::Status class.


**Q4:** What does an exit status of zero indicate when a command line script
terminates? How about a non-zero exit status?
Zero indicates that there were no errors in the execution, while a non zero exit status indicates the error and I would think a well designed command line script would have some meaning to various non-zero values.

**Q5:** What is the difference between the `STDOUT` and `STDERR` output streams?
STDOUT is a stream to print non-error related messages to, for instance puts prints to standard out. STDERR is the
output stream for errors, so raise "Some message" would print to STDERR.

**Q6:** When executing shell commands from within a Ruby script, how can you capture
what gets written to `STDOUT`? How do you go about capturing both `STDOUT` and
`STDERR` streams?
The ruby standard libraru Open3 provides a simple way to access stdout and stderr without having to do any parsing of one's own.

**Q7:** How can you efficiently write the contents of an input file
to `STDOUT` with empty lines omitted? Being efficient in this context
means avoiding storing the full contents of the input file in memory
and processing the stream in a single pass.

File.open("samplefile.txt").readline do |line|
puts line unless line.chomp.empty?
end

**Q8:** How would you go about parsing command line arguments that contain a mixture
of flags and file arguments? (i.e. something like `ls -a -l foo/*.txt`)
I would use a library like OptionParser to first extract the flags that I'm looking for.

Ex.

params = {}
parser = OptionParser.new

parser.on("-a") { params[:all_files] ||= true }
parser.on("-l") { params[:show_permission] = true }

files = parser.parse(ARGV)

parser.parse extracts the -a and -l from ARGV and sets the rest equal to files.

**Q9:** What features are provided by Ruby's `String` class to help with fixed width
text layouts? (i.e. right aligning a column of numbers, or left aligning a
column of text with some whitespace after it to keep the total
column width uniform)
String provides methods such as rjust and ljust that take an integer in order to right or left justify the string.

**Q10:** Suppose your script encounters an error and has to terminate itself. What is
the idiomatic Unix-style way of reporting that the command did not run
successfully?

It would be to first have the application be aware of differing types of erros from incorrect input to failure during processing and then to show to the end user the command they called, an error message and when applicable a helpful tip. One immediate example that comes to mind is git where if I type a command like git commt it will be smart enough to suggest to me 'do you mean commit'.

## Exercises

> NOTE: The supporting materials for these exercises are in `samples/part1`.
Expand Down
9 changes: 6 additions & 3 deletions samples/part1/bin/ruby-ls
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/usr/bin/env ruby

## FIXME: Replace this code with a pure Ruby clone of the ls utility
system("ls", *ARGV)
require_relative "../lib/ruby_ls"
begin
RubyLs::Application.new(ARGV).run
rescue Errno::ENOENT => err
abort "ruby-ls: #{err.message}"
end
30 changes: 30 additions & 0 deletions samples/part1/command_result.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
foo hello.sh hello.txt

apple.md banana.md bar.txt baz.txt quux.txt

foo/bar.txt foo/baz.txt foo/quux.txt

total 16
drwxr-xr-x 7 Kavinder staff 238 May 24 19:40 foo
-rwxr-xr-x 1 Kavinder staff 39 May 24 19:40 hello.sh
-rw-r--r-- 1 Kavinder staff 13 May 24 19:40 hello.txt

. .secret hello.sh
.. foo hello.txt

total 24
drwxr-xr-x 6 Kavinder staff 204 May 24 19:40 .
drwxr-xr-x 6 Kavinder staff 204 Jun 9 11:00 ..
-rw-r--r-- 1 Kavinder staff 9 May 24 19:40 .secret
drwxr-xr-x 7 Kavinder staff 238 May 24 19:40 foo
-rwxr-xr-x 1 Kavinder staff 39 May 24 19:40 hello.sh
-rw-r--r-- 1 Kavinder staff 13 May 24 19:40 hello.txt

-rw-r--r-- 1 Kavinder staff 8 May 24 19:40 foo/bar.txt
-rw-r--r-- 1 Kavinder staff 8 May 24 19:40 foo/baz.txt
-rw-r--r-- 1 Kavinder staff 10 May 24 19:40 foo/quux.txt

ls: missingdir: No such file or directory

ls: illegal option -- Z
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
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 'optparse'
require_relative "ruby_ls/application"
require_relative "ruby_ls/display"
require_relative "ruby_ls/file_info"
49 changes: 49 additions & 0 deletions samples/part1/lib/ruby_ls/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module RubyLs
class Application

def initialize(argv)
@options, @args = parse_options(argv)
@dir = @args.first
@display = RubyLs::Display.new(@options)
end

def run
begin
@display.render(files, directory?)
rescue SystemCallError => e
abort("ls: #{@dir}: No such file or directory")
end
end

private

def parse_options(argv)
options = {}
parser = OptionParser.new
parser.on("-l") {options[:detail] = true}
parser.on("-a") {options[:hidden] = true}
begin
args = parser.parse(argv)
[options, args]
rescue OptionParser::InvalidOption => e
invalid_flag = e.message[/invalid option: -(.*)/, 1]
abort "ls: illegal option -- #{invalid_flag}\n"+
"usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]"
end
end

def files
if @dir =~/\./
@args
else
Dir.chdir("#{@dir}") if @dir
@options[:hidden] ? Dir.entries(".") : Dir.glob("*")
end
end

def directory?
@args.empty? || (@args.count == 1 && File.directory?(@args.first))
end

end
end
60 changes: 60 additions & 0 deletions samples/part1/lib/ruby_ls/display.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'etc'
module RubyLs
class Display

def initialize(params)
@details = params[:detail]
@hidden = params[:hidden]
@column_widths = Hash.new(0)
@total_blocks = 0
end

def render(data, directory=false)
if @details
details = get_details(data)
output, total_blocks = build_details_output(details)
print_total_blocks if directory
print_output(details)
else
puts data
end
end

private

def get_details(data)
data.inject([]) do |info, file|
info << FileInfo.new(file).details
info
end
end

def print_total_blocks
puts "total #{@total_blocks}"
end

def print_output(details)
details.each do |d|
output = [d[:permissions],
d[:link_count].to_s.rjust(@column_widths[:link_count] + 1, " "),
d[:owner] + " ",
d[:group],
d[:size].to_s.rjust(@column_widths[:size] + 1, " "),
d[:time],
d[:name]]
puts output.join(" ")
end
end

def build_details_output(details, total_blocks=0)
@output = details.each do |d|
d.keys.each do |k|
@column_widths[k] = [@column_widths[k], d[k].to_s.size].max
end
total_blocks += d[:blocks]
end
@total_blocks = total_blocks
end

end
end
44 changes: 44 additions & 0 deletions samples/part1/lib/ruby_ls/file_info.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module RubyLs
class FileInfo

attr_reader :data
MODES = { "0" => "---", "1" => "--x", "2" => "-w-", "3" => "-wx",
"4" => "r--", "5" => "r-x", "6" => "rw-", "7" => "rwx" }

attr_reader :file

def initialize(file)
@file = file
end

def details
file_info
end

def keys
@data.keys
end

private

def file_info
stats = File::Stat.new(file)
@data = {
permissions: permission_string(stats.mode),
link_count: stats.nlink,
owner: Etc.getpwuid(stats.uid).name,
group: Etc.getgrgid(stats.gid).name,
size: File.size(file),
time: stats.mtime.strftime("%b %e %H:%M"),
name: file,
blocks: stats.blocks
}
end

def permission_string(mode)
dir_flag = mode[15] == 0 ? "d" : "-"
ugw_codes = (mode & 0777).to_s(8).chars
dir_flag + ugw_codes.map { |n| MODES[n] }.join
end
end
end
19 changes: 9 additions & 10 deletions samples/part1/ls_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@

check("No arguments", "")

# 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 All @@ -38,6 +36,7 @@
require "open3"

def check(test_name, args)

ls_stdout, ls_stderr, ls_status = Open3.capture3("ls #{args}")
rb_stdout, rb_stderr, rb_status = Open3.capture3("ruby-ls #{args}")

Expand Down