Update to Ruby 4, update gems, s/rubocop/standard/

This commit is contained in:
Sami Samhuri 2026-01-02 12:40:38 -08:00
parent 71a7c7d6ed
commit f72ddf7f32
No known key found for this signature in database
14 changed files with 140 additions and 130 deletions

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

View file

@ -1,47 +1,72 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
ast (2.4.2) ast (2.4.3)
base64 (0.1.1) io-console (0.8.2)
json (2.6.3) json (2.18.0)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.5)
minitest (5.20.0) lint_roller (1.1.0)
parallel (1.23.0) minitest (6.0.1)
parser (3.2.2.4) prism (~> 1.5)
parallel (1.27.0)
parser (3.3.10.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
racc (1.7.1) prism (1.7.0)
racc (1.8.1)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.3.1)
regexp_parser (2.8.1) regexp_parser (2.11.3)
rexml (3.2.6) reline (0.6.3)
rubocop (1.56.4) io-console (~> 0.5)
base64 (~> 0.1.1) rubocop (1.81.7)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.3) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.47.1, < 2.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.29.0) rubocop-ast (1.49.0)
parser (>= 3.2.1.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) ruby-progressbar (1.13.0)
unicode-display_width (2.5.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) wordexp (0.2.2)
PLATFORMS PLATFORMS
arm64-darwin-21 arm64-darwin-21
arm64-darwin-22 arm64-darwin-22
arm64-darwin-23 arm64-darwin-23
arm64-darwin-25
DEPENDENCIES DEPENDENCIES
minitest (~> 5.20) minitest (~> 6.0)
parser (~> 3.3.10)
rake (~> 13.0) rake (~> 13.0)
rubocop (= 1.56.4) reline (~> 0.6)
standard (~> 1.52.0)
wordexp (~> 0.2) wordexp (~> 0.2)
BUNDLED WITH BUNDLED WITH

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| Rake::TestTask.new do |task|
task.pattern = 'test/*_test.rb' task.pattern = "test/*_test.rb"
end end
task :clean do task :clean do
FileUtils.rm_rf('test_bin') FileUtils.rm_rf("test_bin")
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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