mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-03-25 08:45:52 +00:00
Start working on a Ruby port
This commit is contained in:
parent
fe3e6cc099
commit
635f345017
8 changed files with 238 additions and 0 deletions
26
ruby/.rubocop.yml
Normal file
26
ruby/.rubocop.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
AllCops:
|
||||
NewCops: enable
|
||||
|
||||
Layout/EmptyLineAfterGuardClause:
|
||||
Enabled: false
|
||||
|
||||
Layout/FirstHashElementIndentation:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 25
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
Style/HashSyntax:
|
||||
EnforcedShorthandSyntax: never
|
||||
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
EnforcedStyleForMultiline: consistent_comma
|
||||
2
ruby/Gemfile
Normal file
2
ruby/Gemfile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
source 'https://rubygems.org'
|
||||
ruby '3.1.0'
|
||||
14
ruby/Gemfile.lock
Normal file
14
ruby/Gemfile.lock
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-21
|
||||
|
||||
DEPENDENCIES
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.1.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.3
|
||||
31
ruby/builtins.rb
Normal file
31
ruby/builtins.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
class Shell
|
||||
def exec_builtin(name, args)
|
||||
send(:"builtin_#{name}", args)
|
||||
end
|
||||
|
||||
def builtin?(name)
|
||||
respond_to?(:"builtin_#{name}")
|
||||
end
|
||||
|
||||
#################
|
||||
### Built-ins ###
|
||||
#################
|
||||
|
||||
def builtin_cd(args)
|
||||
Dir.chdir args.first
|
||||
end
|
||||
|
||||
def builtin_export(args)
|
||||
# only supports one variable and doesn't support quoting
|
||||
name, *value_parts = args.first.strip.split('=')
|
||||
if name.nil? || name.empty?
|
||||
logger.warn "#{RED}[ERROR]#{CLEAR} Invalid export command"
|
||||
else
|
||||
ENV[name] = value_parts.join('=').gsub(/\$\w+/) { |m| ENV[m[1..]] || '' }
|
||||
end
|
||||
end
|
||||
|
||||
def bulitin_pwd(_args)
|
||||
puts Dir.pwd
|
||||
end
|
||||
end
|
||||
9
ruby/colours.rb
Normal file
9
ruby/colours.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class Shell
|
||||
# These colours should be safe on dark and light backgrounds.
|
||||
BLUE = "\033[1;34m".freeze
|
||||
GREEN = "\033[1;32m".freeze
|
||||
YELLOW = "\033[1;33m".freeze
|
||||
RED = "\033[1;31m".freeze
|
||||
WHITE = "\033[1;37m".freeze
|
||||
CLEAR = "\033[0;m".freeze
|
||||
end
|
||||
3
ruby/job.rb
Normal file
3
ruby/job.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Shell
|
||||
Job = Struct.new(:id, :cmd)
|
||||
end
|
||||
120
ruby/main.rb
Executable file
120
ruby/main.rb
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env ruby -w
|
||||
|
||||
require 'English'
|
||||
require 'open3'
|
||||
|
||||
require './builtins'
|
||||
require './colours'
|
||||
require './shell_logger'
|
||||
require './job'
|
||||
|
||||
class Shell
|
||||
attr_reader :logger, :options
|
||||
|
||||
def initialize(args)
|
||||
@logger = ShellLogger.new
|
||||
@options = parse_options(args)
|
||||
@jobs_by_pid = {}
|
||||
logger.verbose "options: #{options.inspect}"
|
||||
end
|
||||
|
||||
def main
|
||||
# this breaks Open3.capture3 so hold off until we fork + exec
|
||||
# trap_sigchld
|
||||
if options[:command]
|
||||
logger.verbose "Executing command: #{options[:command]}"
|
||||
print_logs
|
||||
exit exec_command(options[:command])
|
||||
elsif $stdin.isatty
|
||||
loop do
|
||||
# TODO: use readline instead
|
||||
print_logs
|
||||
print prompt(Dir.pwd)
|
||||
process_command(gets&.chomp)
|
||||
end
|
||||
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]#{CLEAR} Unknown argument: #{arg}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def trap_sigchld
|
||||
# handler for SIGCHLD when a child's state changes
|
||||
Signal.trap('CHLD') do |signo|
|
||||
logger.info "SIGCHLD #{signo}"
|
||||
pid = Process.waitpid(-1, Process::WNOHANG)
|
||||
if (job = @jobs_by_pid[pid])
|
||||
logger.info "#{YELLOW}#{job.id}#{CLEAR}: #{WHITE}(pid #{pid})#{CLEAR} #{job.cmd}"
|
||||
else
|
||||
logger.warn "No job found for child with PID #{pid}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def print_logs
|
||||
logger.logs.each do |log|
|
||||
message = "#{log.message}#{CLEAR}"
|
||||
case log.level
|
||||
when :verbose
|
||||
puts message if options[:verbose]
|
||||
when :warning
|
||||
warn message
|
||||
else
|
||||
puts message
|
||||
end
|
||||
end
|
||||
logger.clear
|
||||
end
|
||||
|
||||
def process_command(cmd)
|
||||
# TODO: proper word splitting, pass arrays to built-ins
|
||||
args = cmd&.split
|
||||
argv0 = args&.first
|
||||
case argv0
|
||||
when nil, 'exit'
|
||||
exit 0
|
||||
when ''
|
||||
# noop
|
||||
when builtin?(argv0)
|
||||
args.shift
|
||||
exec_builtin(argv0, args)
|
||||
else
|
||||
status = exec_command(cmd)
|
||||
print "#{RED}-#{status}-#{CLEAR} " unless status.zero?
|
||||
end
|
||||
# TODO: add to readline history
|
||||
end
|
||||
|
||||
def exec_command(cmd)
|
||||
# TODO: background execution using fork + exec, streaming output
|
||||
out, err, status = Open3.capture3(cmd)
|
||||
puts out.chomp unless out.empty?
|
||||
warn err.chomp unless err.empty?
|
||||
status.exitstatus
|
||||
rescue StandardError => e
|
||||
logger.warn "#{RED}[ERROR]#{CLEAR} #{e.message}"
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
Shell.new(ARGV).main if $PROGRAM_NAME == __FILE__
|
||||
33
ruby/shell_logger.rb
Normal file
33
ruby/shell_logger.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Queues up messages to be printed out when readline is waiting for input, to prevent
|
||||
# mixing shell output with command output.
|
||||
class ShellLogger
|
||||
Log = Struct.new(:level, :message)
|
||||
|
||||
attr_reader :logs
|
||||
|
||||
def self.instance
|
||||
@instance ||= new
|
||||
end
|
||||
|
||||
def initialize
|
||||
clear
|
||||
end
|
||||
|
||||
def log(message)
|
||||
@logs << Log.new(:info, message)
|
||||
end
|
||||
alias info log
|
||||
|
||||
def warn(message)
|
||||
@logs << Log.new(:warning, message)
|
||||
end
|
||||
|
||||
def verbose(message)
|
||||
@logs << Log.new(:verbose, message)
|
||||
end
|
||||
|
||||
def clear
|
||||
@logs = []
|
||||
nil
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue