Start working on a Ruby port

This commit is contained in:
Sami Samhuri 2022-01-16 14:08:01 -08:00
parent fe3e6cc099
commit 635f345017
8 changed files with 238 additions and 0 deletions

26
ruby/.rubocop.yml Normal file
View 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
View file

@ -0,0 +1,2 @@
source 'https://rubygems.org'
ruby '3.1.0'

14
ruby/Gemfile.lock Normal file
View 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
View 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
View 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
View file

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

120
ruby/main.rb Executable file
View 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
View 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