mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-03-25 08:45:52 +00:00
137 lines
3.3 KiB
Ruby
137 lines
3.3 KiB
Ruby
begin
|
|
require "readline"
|
|
rescue LoadError
|
|
require "reline"
|
|
end
|
|
|
|
require "shell/builtins"
|
|
require "shell/colours"
|
|
require "shell/job_control"
|
|
require "shell/logger"
|
|
require "shell/word_expander"
|
|
|
|
module Shell
|
|
class REPL
|
|
include Colours
|
|
|
|
attr_reader :builtins, :job_control, :logger, :options, :word_expander
|
|
|
|
attr_accessor :precmd_hook
|
|
|
|
def initialize(builtins: nil, job_control: nil, logger: nil, word_expander: nil)
|
|
logger ||= Logger.instance
|
|
job_control ||= JobControl.new(logger: logger)
|
|
builtins ||= Builtins.new(job_control: job_control)
|
|
word_expander ||= WordExpander.new
|
|
|
|
@builtins = builtins
|
|
@job_control = job_control
|
|
@logger = logger
|
|
@options = {}
|
|
@word_expander = word_expander
|
|
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}"
|
|
commands = parse_line(line)
|
|
result = nil
|
|
commands.each do |command|
|
|
args = word_expander.expand(command)
|
|
program = args.shift
|
|
logger.verbose "Parsed command: #{program} #{args.inspect}"
|
|
if builtins.builtin?(program)
|
|
logger.verbose "Executing builtin #{program}"
|
|
result = builtins.exec(program, args)
|
|
else
|
|
logger.verbose "Shelling out for #{program}"
|
|
result = job_control.exec_command(program, args)
|
|
end
|
|
end
|
|
result
|
|
rescue Errno => e
|
|
warn "#{red("[ERROR]")} #{e.message}"
|
|
-1
|
|
end
|
|
|
|
# Looks like this: /path/to/somewhere%
|
|
def prompt(pwd)
|
|
"#{blue(pwd)}#{white("%")} #{CLEAR}"
|
|
end
|
|
|
|
def parse_line(line)
|
|
commands = []
|
|
command = "".dup
|
|
state = :unquoted
|
|
line.each_char do |c|
|
|
case state
|
|
when :unquoted
|
|
case c
|
|
when ";"
|
|
commands << command
|
|
command = "".dup
|
|
when "'"
|
|
command << c
|
|
state = :single_quoted
|
|
when "\""
|
|
command << c
|
|
state = :double_quoted
|
|
when "\\"
|
|
command << c
|
|
state = :escaped
|
|
else
|
|
command << c
|
|
end
|
|
|
|
when :single_quoted
|
|
command << c
|
|
state = :unquoted if c == "'"
|
|
|
|
when :double_quoted
|
|
case c
|
|
when "\\"
|
|
state = :double_quoted_escape
|
|
else
|
|
command << c
|
|
end
|
|
state = :unquoted if c == "\""
|
|
|
|
when :double_quoted_escape
|
|
case c
|
|
when "\"", "\\", "$", "`"
|
|
# no-op
|
|
else
|
|
command << "\\" # POSIX behaviour, backslash remains
|
|
end
|
|
command << c
|
|
state = :double_quoted
|
|
|
|
when :escaped
|
|
command << c
|
|
state = :unquoted
|
|
|
|
else
|
|
raise "Unknown state #{state}"
|
|
end
|
|
end
|
|
commands << command
|
|
commands
|
|
end
|
|
end
|
|
end
|