Harden parser error handling and builtin usage checks

This commit is contained in:
Sami Samhuri 2026-02-07 12:07:50 -08:00
parent c94e4c87e2
commit 058a2e991f
No known key found for this signature in database
5 changed files with 51 additions and 2 deletions

View file

@ -24,6 +24,11 @@ module Shell
#################
def builtin_bg(args)
if args.empty?
logger.warn "Usage: bg <command>"
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

View file

@ -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

View file

@ -70,7 +70,7 @@ module Shell
end
end
result
rescue Errno => e
rescue StandardError => e
warn "#{red("[ERROR]")} #{e.message}"
-1
end

View file

@ -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

View file

@ -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 <command>/, 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 ###
#################################