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