diff --git a/ruby/.rubocop.yml b/ruby/.rubocop.yml deleted file mode 100644 index c58f192..0000000 --- a/ruby/.rubocop.yml +++ /dev/null @@ -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 diff --git a/ruby/.ruby-version b/ruby/.ruby-version index be94e6f..fcdb2e1 100644 --- a/ruby/.ruby-version +++ b/ruby/.ruby-version @@ -1 +1 @@ -3.2.2 +4.0.0 diff --git a/ruby/.standard.yml b/ruby/.standard.yml new file mode 100644 index 0000000..e084b5e --- /dev/null +++ b/ruby/.standard.yml @@ -0,0 +1,4 @@ +fix: true +parallel: true +ignore: + - "test_bin/**/*" diff --git a/ruby/Gemfile b/ruby/Gemfile index bff8709..b1cdd0f 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -1,6 +1,8 @@ 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" diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 6a134fb..6c67645 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -1,47 +1,72 @@ 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) + 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 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) BUNDLED WITH diff --git a/ruby/Rakefile b/ruby/Rakefile index f16f3fa..a047cc4 100644 --- a/ruby/Rakefile +++ b/ruby/Rakefile @@ -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 diff --git a/ruby/a1 b/ruby/a1 index a87b29b..101dc04 100755 --- a/ruby/a1 +++ b/ruby/a1 @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -w -require 'English' -require_relative 'shell' +require "English" +require_relative "shell" Shell::CLI.new.run(args: ARGV) if $PROGRAM_NAME == __FILE__ diff --git a/ruby/shell.rb b/ruby/shell.rb index c113b43..bbf5f68 100644 --- a/ruby/shell.rb +++ b/ruby/shell.rb @@ -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 diff --git a/ruby/shell/builtins.rb b/ruby/shell/builtins.rb index 6c7749f..4a39cd9 100644 --- a/ruby/shell/builtins.rb +++ b/ruby/shell/builtins.rb @@ -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 ' + logger.warn "Usage: bgkill " 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 diff --git a/ruby/shell/cli.rb b/ruby/shell/cli.rb index ac549d0..dde9cf5 100644 --- a/ruby/shell/cli.rb +++ b/ruby/shell/cli.rb @@ -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 diff --git a/ruby/shell/job_control.rb b/ruby/shell/job_control.rb index 5c95cb1..f116895 100644 --- a/ruby/shell/job_control.rb +++ b/ruby/shell/job_control.rb @@ -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['/'] && \ - File.executable?(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) diff --git a/ruby/shell/logger.rb b/ruby/shell/logger.rb index a5b4954..5b38504 100644 --- a/ruby/shell/logger.rb +++ b/ruby/shell/logger.rb @@ -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) diff --git a/ruby/shell/repl.rb b/ruby/shell/repl.rb index 1a46776..1136ec3 100644 --- a/ruby/shell/repl.rb +++ b/ruby/shell/repl.rb @@ -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 diff --git a/ruby/test/shell_test.rb b/ruby/test/shell_test.rb index bbd31ea..1663bde 100644 --- a/ruby/test/shell_test.rb +++ b/ruby/test/shell_test.rb @@ -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,42 @@ 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 end 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 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 + assert_equal "/usr/bin/which", `#{A1_PATH} -c 'which -a which'`.chomp 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 +74,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