Extract remaining code from main.rb

This commit is contained in:
Sami Samhuri 2022-01-17 20:11:56 -08:00
parent ec8baa2e1c
commit 9f0eb1378b
10 changed files with 163 additions and 125 deletions

10
ruby/a1
View file

@ -1,3 +1,9 @@
#!/bin/sh #!/usr/bin/env ruby -w
./main.rb "$@" require 'English'
$LOAD_PATH << File.expand_path(__dir__)
require 'shell'
Shell::CLI.new.run(args: ARGV) if $PROGRAM_NAME == __FILE__

View file

@ -1,108 +0,0 @@
#!/usr/bin/env ruby -w
require 'English'
require 'readline'
require 'wordexp'
require './builtins'
require './colours'
require './job_control'
require './logger'
# TODO: change to module after extracting all or most of the code
class Shell
include Colours
attr_reader :builtins, :job_control, :logger, :options
def initialize(args: ARGV, builtins: nil, job_control: nil, logger: nil)
logger ||= Logger.instance
job_control ||= JobControl.new
builtins ||= Builtins.new(job_control)
@builtins = builtins
@job_control = job_control
@logger = logger
@options = parse_options(args)
logger.verbose "Options: #{options.inspect}"
end
def main
if options[:command]
logger.verbose "Executing command: #{options[:command]}"
print_logs
exit process_command(options[:command])
end
repl if $stdin.isatty
end
def repl
@job_control.trap_sigchld
add_to_history = true
status = 0
loop do
print_logs
print "#{red(status)} " unless status.zero?
line = Readline.readline(prompt(Dir.pwd), add_to_history)
Readline::HISTORY.pop if line.nil? || line.strip.empty?
status = process_command(line)
end
end
# Looks like this: /path/to/somewhere%
def prompt(pwd)
"#{blue(pwd)}#{white('%')} #{CLEAR}"
end
def parse_options(args)
options = {
verbose: false,
}
while (arg = args.shift)
case arg
when '-c'
options[:command] = args.shift
when '-v', '--verbose'
options[:verbose] = true
else
logger.warn "#{red('[ERROR]')} Unknown argument: #{arg}"
exit 1
end
end
options
end
def print_logs
logger.logs.each do |log|
message = "#{log.message}#{CLEAR}"
case log.level
when :verbose
warn message if options[:verbose]
else
warn message
end
end
logger.clear
end
def process_command(line)
exit 0 if line.nil? # EOF, ctrl-d
return 0 if line.strip.empty? # no input, no-op
logger.verbose "Processing command: #{line.inspect}"
args = Wordexp.expand(line)
cmd = args.shift
logger.verbose "Parsed command: #{cmd} #{args.inspect}"
if @builtins.builtin?(cmd)
logger.verbose "Executing builtin #{cmd}"
@builtins.exec(cmd, args)
else
logger.verbose "Shelling out for #{cmd}"
@job_control.exec_command(cmd, args)
end
rescue Errno => e
warn "#{red('[ERROR]')} #{e.message}"
-1
end
end
Shell.new(args: ARGV).main if $PROGRAM_NAME == __FILE__

5
ruby/shell.rb Normal file
View file

@ -0,0 +1,5 @@
require 'shell/cli'
require 'shell/repl'
module Shell
end

View file

@ -1,9 +1,13 @@
class Shell require 'shell/job_control'
class Builtins require 'shell/logger'
attr_reader :logger
def initialize(job_control, logger = Logger.instance) module Shell
@job_control = job_control class Builtins
attr_reader :job_control, :logger
def initialize(job_control: nil, logger: nil)
logger ||= Logger.instance
@job_control = job_control || JobControl.new(logger: logger)
@logger = logger @logger = logger
end end
@ -21,7 +25,7 @@ class Shell
def builtin_bg(args) def builtin_bg(args)
cmd = args.shift cmd = args.shift
@job_control.exec_command(cmd, args, background: true) job_control.exec_command(cmd, args, background: true)
end end
def builtin_cd(args) def builtin_cd(args)

61
ruby/shell/cli.rb Normal file
View file

@ -0,0 +1,61 @@
require 'shell/colours'
require 'shell/logger'
require 'shell/repl'
module Shell
class CLI
include Colours
attr_reader :logger, :options, :repl
def initialize(logger: nil, repl: nil)
@logger = logger || Logger.instance
@options = {}
@repl = repl || REPL.new(logger: @logger)
@repl.precmd_hook = -> { print_logs }
end
def run(args: nil)
@options = parse_options(args || ARGV)
logger.verbose "Options: #{options.inspect}"
if options[:command]
logger.verbose "Executing command: #{options[:command]}"
print_logs
exit repl.process_command(options[:command])
elsif $stdin.isatty
repl.start(options: options)
end
end
def parse_options(args)
options = {
verbose: false,
}
while (arg = args.shift)
case arg
when '-c'
options[:command] = args.shift
when '-v', '--verbose'
options[:verbose] = true
else
logger.warn "#{red('[ERROR]')} Unknown argument: #{arg}"
exit 1
end
end
options
end
def print_logs
logger.logs.each do |log|
message = "#{log.message}#{CLEAR}"
case log.level
when :verbose
warn message if options[:verbose]
else
warn message
end
end
logger.clear
end
end
end

View file

@ -1,4 +1,4 @@
class Shell module Shell
module Colours module Colours
# These colours should be safe on dark and light backgrounds. # These colours should be safe on dark and light backgrounds.
BLUE = "\033[1;34m".freeze BLUE = "\033[1;34m".freeze

View file

@ -1,3 +1,3 @@
class Shell module Shell
Job = Struct.new(:id, :pid, :cmd, :args) Job = Struct.new(:id, :pid, :cmd, :args)
end end

View file

@ -1,15 +1,18 @@
require './colours' require 'English'
require './job'
class Shell require 'shell/colours'
require 'shell/job'
require 'shell/logger'
module Shell
class JobControl class JobControl
include Colours include Colours
attr_reader :logger attr_reader :logger
def initialize(logger = Logger.instance) def initialize(logger: nil)
@jobs_by_pid = {} @jobs_by_pid = {}
@logger = logger @logger = logger || Logger.instance
end end
def trap_sigchld def trap_sigchld

View file

@ -1,6 +1,6 @@
require './colours' require 'shell/colours'
class Shell module Shell
# Queues up messages to be printed out when readline is waiting for input, to prevent # Queues up messages to be printed out when readline is waiting for input, to prevent
# mixing shell output with command output. # mixing shell output with command output.
class Logger class Logger

67
ruby/shell/repl.rb Normal file
View file

@ -0,0 +1,67 @@
require 'readline'
require 'wordexp'
require 'shell/builtins'
require 'shell/colours'
require 'shell/job_control'
require 'shell/logger'
module Shell
class REPL
include Colours
attr_reader :builtins, :job_control, :logger, :options
attr_accessor :precmd_hook
def initialize(builtins: nil, job_control: nil, logger: nil)
logger ||= Logger.instance
job_control ||= JobControl.new(logger: logger)
builtins ||= Builtins.new(job_control: job_control)
@builtins = builtins
@job_control = job_control
@logger = logger
@options = {}
end
def start(options: nil)
@options = options || {}
job_control.trap_sigchld
add_to_history = true
status = 0
loop do
precmd_hook&.call
print "#{red(status)} " unless status.zero?
line = Readline.readline(prompt(Dir.pwd), add_to_history)
Readline::HISTORY.pop if line.nil? || line.strip.empty?
status = process_command(line)
end
end
def process_command(line)
exit 0 if line.nil? # EOF, ctrl-d
return 0 if line.strip.empty? # no input, no-op
logger.verbose "Processing command: #{line.inspect}"
args = Wordexp.expand(line)
cmd = args.shift
logger.verbose "Parsed command: #{cmd} #{args.inspect}"
if builtins.builtin?(cmd)
logger.verbose "Executing builtin #{cmd}"
builtins.exec(cmd, args)
else
logger.verbose "Shelling out for #{cmd}"
job_control.exec_command(cmd, args)
end
rescue Errno => e
warn "#{red('[ERROR]')} #{e.message}"
-1
end
# Looks like this: /path/to/somewhere%
def prompt(pwd)
"#{blue(pwd)}#{white('%')} #{CLEAR}"
end
end
end