Merge pull request #2 from samsonjs/ruby-4

Update to Ruby 4 and add GitHub workflow
This commit is contained in:
Sami Samhuri 2026-01-02 13:14:51 -08:00 committed by GitHub
commit 0e19e57734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 185 additions and 144 deletions

33
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Test
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ruby
steps:
- uses: actions/checkout@v6
- name: Set up Ruby
uses: ruby/setup-ruby@v1
env:
BUNDLE_GEMFILE: ruby/Gemfile
with:
ruby-version: 4.0.0
bundler-cache: true
- name: Run tests
run: bundle exec rake test
- name: Run standard
run: bundle exec rake standard
- name: Install C dependencies
working-directory: .
run: sudo apt-get update && sudo apt-get install -y libreadline-dev
- name: Run C tests
working-directory: .
run: make c

View file

@ -1,9 +1,7 @@
default: all
all: c ruby
default: c ruby
bootstrap:
cd ruby && bundle
cd ruby && bundle install --jobs 8
c:
cd c && make test
@ -15,4 +13,4 @@ clean:
cd c && make clean
cd ruby && bundle exec rake clean
.PHONY: c ruby clean
.PHONY: c ruby clean bootstrap

View file

@ -1,26 +0,0 @@
AllCops:
NewCops: enable
Layout/EmptyLineAfterGuardClause:
Enabled: false
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
Metrics/AbcSize:
Max: 25
Metrics/MethodLength:
Max: 30
Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/HashSyntax:
EnforcedShorthandSyntax: never
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: consistent_comma

View file

@ -1 +1 @@
3.2.2
4.0.0

4
ruby/.standard.yml Normal file
View file

@ -0,0 +1,4 @@
fix: true
parallel: true
ignore:
- "test_bin/**/*"

View file

@ -1,7 +1,8 @@
source 'https://rubygems.org'
ruby '3.2.2'
source "https://rubygems.org"
gem 'minitest', '~> 5.20'
gem 'rake', '~> 13.0'
gem 'rubocop', '1.56.4'
gem 'wordexp', '~> 0.2'
gem "minitest", "~> 6.0"
gem "parser", "~> 3.3.10"
gem "rake", "~> 13.0"
gem "reline", "~> 0.6"
gem "standard", "~> 1.52.0", require: false
gem "wordexp", "~> 0.2"

View file

@ -1,51 +1,74 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
base64 (0.1.1)
json (2.6.3)
language_server-protocol (3.17.0.3)
minitest (5.20.0)
parallel (1.23.0)
parser (3.2.2.4)
ast (2.4.3)
io-console (0.8.2)
json (2.18.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
minitest (6.0.1)
prism (~> 1.5)
parallel (1.27.0)
parser (3.3.10.0)
ast (~> 2.4.1)
racc
racc (1.7.1)
prism (1.7.0)
racc (1.8.1)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
rexml (3.2.6)
rubocop (1.56.4)
base64 (~> 0.1.1)
rake (13.3.1)
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
rubocop (1.81.7)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.2.2.3)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.1, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (1.13.0)
unicode-display_width (2.5.0)
wordexp (0.2.0)
standard (1.52.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.81.7)
standard-custom (~> 1.0.0)
standard-performance (~> 1.8)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.9.0)
lint_roller (~> 1.1)
rubocop-performance (~> 1.26.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
wordexp (0.2.2)
PLATFORMS
arm64-darwin-21
arm64-darwin-22
arm64-darwin-23
arm64-darwin-25
x86_64-linux
DEPENDENCIES
minitest (~> 5.20)
minitest (~> 6.0)
parser (~> 3.3.10)
rake (~> 13.0)
rubocop (= 1.56.4)
reline (~> 0.6)
standard (~> 1.52.0)
wordexp (~> 0.2)
RUBY VERSION
ruby 3.2.2p53
BUNDLED WITH
2.4.20
4.0.3

View file

@ -1,11 +1,12 @@
require 'rake/testtask'
require "rake/testtask"
require "standard/rake"
task default: 'test'
task default: %i[test standard]
Rake::TestTask.new do |task|
task.pattern = 'test/*_test.rb'
task.pattern = "test/*_test.rb"
end
task :clean do
FileUtils.rm_rf('test_bin')
FileUtils.rm_rf("test_bin")
end

View file

@ -1,6 +1,6 @@
#!/usr/bin/env ruby -w
#!/usr/bin/env -S ruby -w
require 'English'
require_relative 'shell'
require "English"
require_relative "shell"
Shell::CLI.new.run(args: ARGV) if $PROGRAM_NAME == __FILE__

View file

@ -1,7 +1,7 @@
$LOAD_PATH << File.expand_path(__dir__)
require 'shell/cli'
require 'shell/repl'
require "shell/cli"
require "shell/repl"
module Shell
end

View file

@ -1,5 +1,5 @@
require 'shell/job_control'
require 'shell/logger'
require "shell/job_control"
require "shell/logger"
module Shell
class Builtins
@ -34,14 +34,14 @@ module Shell
jobs.each do |job|
puts job_control.format_job(job)
end
plural = jobs.count == 1 ? '' : 's'
plural = (jobs.count == 1) ? "" : "s"
puts "#{jobs.count} background job#{plural}"
0
end
def builtin_bgkill(args)
if args.count != 1
logger.warn 'Usage: bgkill <job>'
logger.warn "Usage: bgkill <job>"
return -1
end
@ -57,11 +57,11 @@ module Shell
def builtin_export(args)
# only supports one variable and doesn't support quoting
name, *value_parts = args.first.strip.split('=')
name, *value_parts = args.first.strip.split("=")
if name.nil? || name.empty?
logger.warn "#{red('[ERROR]')} Invalid export command"
logger.warn "#{red("[ERROR]")} Invalid export command"
else
ENV[name] = value_parts.join('=').gsub(/\$\w+/) { |m| ENV[m[1..]] || '' }
ENV[name] = value_parts.join("=").gsub(/\$\w+/) { |m| ENV[m[1..]] || "" }
end
0
end

View file

@ -1,6 +1,6 @@
require 'shell/colours'
require 'shell/logger'
require 'shell/repl'
require "shell/colours"
require "shell/logger"
require "shell/repl"
module Shell
class CLI
@ -29,20 +29,20 @@ module Shell
def parse_options(args)
options = {
verbose: false,
verbose: false
}
while (arg = args.shift)
case arg
when '-c'
when "-c"
options[:command] = args.shift
if options[:command].nil?
warn 'ERROR: expected string after -c'
warn "ERROR: expected string after -c"
exit 1
end
when '-v', '--verbose'
when "-v", "--verbose"
options[:verbose] = true
else
logger.warn "#{red('[ERROR]')} Unknown argument: #{arg}"
logger.warn "#{red("[ERROR]")} Unknown argument: #{arg}"
exit 1
end
end

View file

@ -1,8 +1,8 @@
require 'English'
require "English"
require 'shell/colours'
require 'shell/job'
require 'shell/logger'
require "shell/colours"
require "shell/job"
require "shell/logger"
module Shell
class JobControl
@ -17,7 +17,7 @@ module Shell
def exec_command(cmd, args, background: false)
unless (path = resolve_executable(cmd))
warn "#{red('[ERROR]')} command not found: #{cmd}"
warn "#{red("[ERROR]")} command not found: #{cmd}"
return -2
end
@ -25,7 +25,7 @@ module Shell
if background
job = Job.new(next_job_id, pid, cmd, args)
@jobs_by_pid[pid] = job
puts white('Running job ') + yellow(job.id) + white(" (pid #{pid}) in background")
puts white("Running job ") + yellow(job.id) + white(" (pid #{pid}) in background")
Process.waitpid(pid, Process::WNOHANG)
0
else
@ -38,8 +38,8 @@ module Shell
0
end
end
rescue StandardError => e
warn "#{red('[ERROR]')} #{e.message} #{e.inspect}"
rescue => e
warn "#{red("[ERROR]")} #{e.message} #{e.inspect}"
-5
end
@ -50,7 +50,7 @@ module Shell
return
end
Process.kill('TERM', job.pid)
Process.kill("TERM", job.pid)
rescue Errno::ESRCH
logger.warn "No such proccess: #{job.pid}"
end
@ -60,24 +60,24 @@ module Shell
end
def format_job(job)
args = job.args.join(' ')
"#{yellow(job.id)}: #{white('(pid ', job.pid, ')')} #{green(job.cmd)} #{args}"
args = job.args.join(" ")
"#{yellow(job.id)}: #{white("(pid ", job.pid, ")")} #{green(job.cmd)} #{args}"
end
def trap_sigchld
# handler for SIGCHLD when a child's state changes
Signal.trap('CHLD') do |_signo|
Signal.trap("CHLD") do |_signo|
pid = Process.waitpid(-1, Process::WNOHANG)
if pid.nil?
# no-op
elsif (job = @jobs_by_pid[pid])
@jobs_by_pid.delete(pid)
time = Time.now.strftime('%T')
job_text = yellow('job ', job.id, ' exited')
args = job.args.join(' ')
puts "\n[#{time}] #{job_text} #{white('(pid ', job.pid, ')')}: #{green(job.cmd)} #{args}"
time = Time.now.strftime("%T")
job_text = yellow("job ", job.id, " exited")
args = job.args.join(" ")
puts "\n[#{time}] #{job_text} #{white("(pid ", job.pid, ")")}: #{green(job.cmd)} #{args}"
else
warn "\n#{yellow('[WARN]')} No job found for child with PID #{pid}"
warn "\n#{yellow("[WARN]")} No job found for child with PID #{pid}"
end
Readline.refresh_line
end
@ -88,11 +88,11 @@ module Shell
# Returns nil when no such command was found.
def resolve_executable(path_or_filename)
# process absolute and relative paths directly
return path_or_filename if path_or_filename['/'] && \
return path_or_filename if path_or_filename["/"] &&
File.executable?(path_or_filename)
filename = path_or_filename
ENV['PATH'].split(':').each do |dir|
ENV["PATH"].split(":").each do |dir|
path = File.join(dir, filename)
next unless File.exist?(path)
return path if File.executable?(path)

View file

@ -1,4 +1,4 @@
require 'shell/colours'
require "shell/colours"
module Shell
# Queues up messages to be printed out when readline is waiting for input, to prevent
@ -19,16 +19,16 @@ module Shell
end
def log(message)
@logs << Log.new(:info, "#{white('[INFO]')} #{message}")
@logs << Log.new(:info, "#{white("[INFO]")} #{message}")
end
alias info log
alias_method :info, :log
def warn(message)
@logs << Log.new(:warning, "#{yellow('[WARN]')} #{message}")
@logs << Log.new(:warning, "#{yellow("[WARN]")} #{message}")
end
def error(message)
@logs << Log.new(:error, "#{red('[ERROR]')} #{message}")
@logs << Log.new(:error, "#{red("[ERROR]")} #{message}")
end
def verbose(message)

View file

@ -1,10 +1,14 @@
require 'readline'
require 'wordexp'
begin
require "readline"
rescue LoadError
require "reline"
end
require "wordexp"
require 'shell/builtins'
require 'shell/colours'
require 'shell/job_control'
require 'shell/logger'
require "shell/builtins"
require "shell/colours"
require "shell/job_control"
require "shell/logger"
module Shell
class REPL
@ -55,13 +59,13 @@ module Shell
job_control.exec_command(cmd, args)
end
rescue Errno => e
warn "#{red('[ERROR]')} #{e.message}"
warn "#{red("[ERROR]")} #{e.message}"
-1
end
# Looks like this: /path/to/somewhere%
def prompt(pwd)
"#{blue(pwd)}#{white('%')} #{CLEAR}"
"#{blue(pwd)}#{white("%")} #{CLEAR}"
end
end
end

View file

@ -1,16 +1,16 @@
require 'minitest/autorun'
require "minitest/autorun"
class ShellTest < Minitest::Test
TRIVIAL_SHELL_SCRIPT = "#!/bin/sh\ntrue".freeze
A1_PATH = ENV.fetch('A1_PATH', './a1').freeze
A1_PATH = ENV.fetch("A1_PATH", "./a1").freeze
def setup
FileUtils.mkdir_p('test_bin')
FileUtils.mkdir_p("test_bin")
end
def teardown
FileUtils.rm_rf('test_bin')
FileUtils.rm_rf("test_bin")
end
def unique_shell_script(code)
@ -31,42 +31,45 @@ class ShellTest < Minitest::Test
#################################
def test_background_job
output = `#{A1_PATH} -c 'bg echo hello'`.gsub(/\e\[([;\d]+)?m/, '')
output = `#{A1_PATH} -c 'bg echo hello'`.gsub(/\e\[([;\d]+)?m/, "")
pid = /\(pid (\d+)\)/.match(output)[1]
assert_equal "Running job 1 (pid #{pid}) in background\nhello\n", output
lines = output.split("\n").map(&:chomp)
assert_equal ["Running job 1 (pid #{pid}) in background", "hello"], lines.sort
end
def test_resolves_executables_with_absolute_paths
assert_equal '/usr/bin/which', `#{A1_PATH} -c '/usr/bin/which -a which'`.chomp
output = `#{A1_PATH} -c '/usr/bin/which -a which'`.lines.map(&:chomp)
assert_includes output, "/usr/bin/which"
end
def test_resolves_executables_with_relative_paths
File.write('test_bin/something', TRIVIAL_SHELL_SCRIPT)
File.chmod(0o755, 'test_bin/something')
File.write("test_bin/something", TRIVIAL_SHELL_SCRIPT)
File.chmod(0o755, "test_bin/something")
assert system("#{A1_PATH} -c ./test_bin/something")
end
def test_resolves_executables_in_absolute_paths
assert_equal '/usr/bin/which', `#{A1_PATH} -c 'which -a which'`.chomp
output = `#{A1_PATH} -c 'which -a which'`.lines.map(&:chomp)
assert_includes output, "/usr/bin/which"
end
def test_resolves_executables_in_relative_paths
code = rand(1_000_000).to_s
File.write('test_bin/definitely_executable', unique_shell_script(code))
File.chmod(0o755, 'test_bin/definitely_executable')
File.write("test_bin/definitely_executable", unique_shell_script(code))
File.chmod(0o755, "test_bin/definitely_executable")
actual = `PATH="./test_bin:$PATH" #{A1_PATH} -c definitely_executable`.chomp
assert_equal code, actual
end
def test_does_not_resolve_non_executable_files_in_path
File.write('test_bin/definitely_not_executable', TRIVIAL_SHELL_SCRIPT)
File.chmod(0o644, 'test_bin/definitely_not_executable')
File.write("test_bin/definitely_not_executable", TRIVIAL_SHELL_SCRIPT)
File.chmod(0o644, "test_bin/definitely_not_executable")
actual = system("PATH=\"./test_bin:$PATH\" #{A1_PATH} -c definitely_not_executable 2>/dev/null")
assert_equal false, actual
end
def test_refreshes_readline_after_bg_execution
skip 'unimplemented'
skip "unimplemented"
end
#########################
@ -74,25 +77,25 @@ class ShellTest < Minitest::Test
#########################
def test_builtin_cd_no_args
skip 'cannot easily implement without sequencing with ; or &&'
skip "cannot easily implement without sequencing with ; or &&"
end
def test_builtin_cd
skip 'cannot easily implement without sequencing with ; or &&'
skip "cannot easily implement without sequencing with ; or &&"
end
def test_builtin_cd_dash
skip 'cannot easily implement without sequencing with ; or &&'
skip "cannot easily implement without sequencing with ; or &&"
end
def test_builtin_cd_parent
skip 'cannot easily implement without sequencing with ; or &&'
skip "cannot easily implement without sequencing with ; or &&"
end
def test_builtin_pwd
assert_equal Dir.pwd, `#{A1_PATH} -c pwd`.chomp
shell_path = File.expand_path(A1_PATH, Dir.pwd)
assert_equal '/usr/bin', `cd /usr/bin && '#{shell_path}' -c pwd`.chomp
assert_equal "/usr/bin", `cd /usr/bin && '#{shell_path}' -c pwd`.chomp
end
end