From 058a2e991feb64eafff62ce8186cfe49c27c5228 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sat, 7 Feb 2026 12:07:50 -0800 Subject: [PATCH] Harden parser error handling and builtin usage checks --- ruby/shell/builtins.rb | 11 +++++++++++ ruby/shell/cli.rb | 4 +++- ruby/shell/repl.rb | 2 +- ruby/shell/string_parser.rb | 7 +++++++ ruby/test/shell_test.rb | 29 +++++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/ruby/shell/builtins.rb b/ruby/shell/builtins.rb index 2feb094..dc58b36 100644 --- a/ruby/shell/builtins.rb +++ b/ruby/shell/builtins.rb @@ -24,6 +24,11 @@ module Shell ################# def builtin_bg(args) + if args.empty? + logger.warn "Usage: bg " + return -1 + end + cmd = args.shift job_control.exec_command(cmd, args, background: true) end @@ -67,10 +72,16 @@ module Shell end def builtin_export(args) + if args.count != 1 || args.first.nil? || !args.first.include?("=") + logger.warn "Usage: export NAME=value" + return -1 + end + # 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]")} Invalid export command" + return -1 else ENV[name] = value_parts.join("=").gsub(/\$\w+/) { |m| ENV[m[1..]] || "" } end diff --git a/ruby/shell/cli.rb b/ruby/shell/cli.rb index dde9cf5..e9deebc 100644 --- a/ruby/shell/cli.rb +++ b/ruby/shell/cli.rb @@ -21,7 +21,9 @@ module Shell if options[:command] logger.verbose "Executing command: #{options[:command]}" print_logs - exit repl.process_command(options[:command]) + status = repl.process_command(options[:command]) + print_logs + exit status elsif $stdin.isatty repl.start(options: options) end diff --git a/ruby/shell/repl.rb b/ruby/shell/repl.rb index 7b08da6..940ec13 100644 --- a/ruby/shell/repl.rb +++ b/ruby/shell/repl.rb @@ -70,7 +70,7 @@ module Shell end end result - rescue Errno => e + rescue StandardError => e warn "#{red("[ERROR]")} #{e.message}" -1 end diff --git a/ruby/shell/string_parser.rb b/ruby/shell/string_parser.rb index 9593b22..d2fce2d 100644 --- a/ruby/shell/string_parser.rb +++ b/ruby/shell/string_parser.rb @@ -21,6 +21,9 @@ module Shell next when "&" if line[i + 1] == "&" + if command.strip.empty? + raise ArgumentError, "syntax error near unexpected token `&&`" + end commands << {command: command, op: next_op} command = +"" next_op = :and @@ -63,6 +66,10 @@ module Shell i += 1 end + if next_op == :and && command.strip.empty? + raise ArgumentError, "syntax error: expected command after `&&`" + end + commands << {command: command, op: next_op} commands end diff --git a/ruby/test/shell_test.rb b/ruby/test/shell_test.rb index 17e01df..dee998b 100644 --- a/ruby/test/shell_test.rb +++ b/ruby/test/shell_test.rb @@ -1,5 +1,6 @@ require "minitest/autorun" require "etc" +require "open3" require "timeout" $LOAD_PATH.unshift(File.expand_path("..", __dir__)) require_relative "../shell/job_control" @@ -130,6 +131,34 @@ class ShellTest < Minitest::Test assert_equal "`echo hi`", %x(#{A1_PATH} -c 'echo "\\`echo hi\\`"').chomp end + def test_reports_parse_errors_without_ruby_backtrace + _stdout, stderr, status = Open3.capture3(A1_PATH, "-c", "echo \"unterminated") + refute status.success? + refute_match(/\.rb:\d+:in /, stderr) + end + + def test_export_without_args_does_not_raise_nomethoderror + _stdout, stderr, status = Open3.capture3(A1_PATH, "-c", "export") + refute status.success? + refute_match(/NoMethodError|undefined method/, stderr) + end + + def test_bg_without_command_reports_usage_error + _stdout, stderr, status = Open3.capture3(A1_PATH, "-c", "bg") + refute status.success? + assert_match(/Usage: bg /, stderr) + end + + def test_rejects_empty_command_around_and_operator + _stdout1, stderr1, status1 = Open3.capture3(A1_PATH, "-c", "&& echo hi") + refute status1.success? + assert_match(/syntax/i, stderr1) + + _stdout2, stderr2, status2 = Open3.capture3(A1_PATH, "-c", "echo hi &&") + refute status2.success? + assert_match(/syntax/i, stderr2) + end + ################################# ### Execution and job control ### #################################