diff --git a/PART_1.md b/PART_1.md index 0cbeecb..499750a 100644 --- a/PART_1.md +++ b/PART_1.md @@ -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`. diff --git a/samples/part1/bin/ruby-ls b/samples/part1/bin/ruby-ls index f10c79a..a84a4dd 100755 --- a/samples/part1/bin/ruby-ls +++ b/samples/part1/bin/ruby-ls @@ -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 diff --git a/samples/part1/command_result.txt b/samples/part1/command_result.txt new file mode 100644 index 0000000..49959f3 --- /dev/null +++ b/samples/part1/command_result.txt @@ -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 ...] \ No newline at end of file diff --git a/samples/part1/lib/ruby_ls.rb b/samples/part1/lib/ruby_ls.rb new file mode 100644 index 0000000..46a1e3a --- /dev/null +++ b/samples/part1/lib/ruby_ls.rb @@ -0,0 +1,4 @@ +require 'optparse' +require_relative "ruby_ls/application" +require_relative "ruby_ls/display" +require_relative "ruby_ls/file_info" \ No newline at end of file diff --git a/samples/part1/lib/ruby_ls/application.rb b/samples/part1/lib/ruby_ls/application.rb new file mode 100644 index 0000000..fbe8929 --- /dev/null +++ b/samples/part1/lib/ruby_ls/application.rb @@ -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 \ No newline at end of file diff --git a/samples/part1/lib/ruby_ls/display.rb b/samples/part1/lib/ruby_ls/display.rb new file mode 100644 index 0000000..314f30e --- /dev/null +++ b/samples/part1/lib/ruby_ls/display.rb @@ -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 \ No newline at end of file diff --git a/samples/part1/lib/ruby_ls/file_info.rb b/samples/part1/lib/ruby_ls/file_info.rb new file mode 100644 index 0000000..ce10b84 --- /dev/null +++ b/samples/part1/lib/ruby_ls/file_info.rb @@ -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 \ No newline at end of file diff --git a/samples/part1/ls_tests.rb b/samples/part1/ls_tests.rb index 4371b25..4f2b6f8 100644 --- a/samples/part1/ls_tests.rb +++ b/samples/part1/ls_tests.rb @@ -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!" @@ -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}")