mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-04-27 14:57:43 +00:00
Harden parser error handling and builtin usage checks
This commit is contained in:
parent
c94e4c87e2
commit
058a2e991f
5 changed files with 51 additions and 2 deletions
|
|
@ -24,6 +24,11 @@ module Shell
|
||||||
#################
|
#################
|
||||||
|
|
||||||
def builtin_bg(args)
|
def builtin_bg(args)
|
||||||
|
if args.empty?
|
||||||
|
logger.warn "Usage: bg <command>"
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
cmd = args.shift
|
cmd = args.shift
|
||||||
job_control.exec_command(cmd, args, background: true)
|
job_control.exec_command(cmd, args, background: true)
|
||||||
end
|
end
|
||||||
|
|
@ -67,10 +72,16 @@ module Shell
|
||||||
end
|
end
|
||||||
|
|
||||||
def builtin_export(args)
|
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
|
# 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"
|
||||||
|
return -1
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ module Shell
|
||||||
if options[:command]
|
if options[:command]
|
||||||
logger.verbose "Executing command: #{options[:command]}"
|
logger.verbose "Executing command: #{options[:command]}"
|
||||||
print_logs
|
print_logs
|
||||||
exit repl.process_command(options[:command])
|
status = repl.process_command(options[:command])
|
||||||
|
print_logs
|
||||||
|
exit status
|
||||||
elsif $stdin.isatty
|
elsif $stdin.isatty
|
||||||
repl.start(options: options)
|
repl.start(options: options)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ module Shell
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
result
|
result
|
||||||
rescue Errno => e
|
rescue StandardError => e
|
||||||
warn "#{red("[ERROR]")} #{e.message}"
|
warn "#{red("[ERROR]")} #{e.message}"
|
||||||
-1
|
-1
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ module Shell
|
||||||
next
|
next
|
||||||
when "&"
|
when "&"
|
||||||
if line[i + 1] == "&"
|
if line[i + 1] == "&"
|
||||||
|
if command.strip.empty?
|
||||||
|
raise ArgumentError, "syntax error near unexpected token `&&`"
|
||||||
|
end
|
||||||
commands << {command: command, op: next_op}
|
commands << {command: command, op: next_op}
|
||||||
command = +""
|
command = +""
|
||||||
next_op = :and
|
next_op = :and
|
||||||
|
|
@ -63,6 +66,10 @@ module Shell
|
||||||
i += 1
|
i += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if next_op == :and && command.strip.empty?
|
||||||
|
raise ArgumentError, "syntax error: expected command after `&&`"
|
||||||
|
end
|
||||||
|
|
||||||
commands << {command: command, op: next_op}
|
commands << {command: command, op: next_op}
|
||||||
commands
|
commands
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
require "minitest/autorun"
|
require "minitest/autorun"
|
||||||
require "etc"
|
require "etc"
|
||||||
|
require "open3"
|
||||||
require "timeout"
|
require "timeout"
|
||||||
$LOAD_PATH.unshift(File.expand_path("..", __dir__))
|
$LOAD_PATH.unshift(File.expand_path("..", __dir__))
|
||||||
require_relative "../shell/job_control"
|
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
|
assert_equal "`echo hi`", %x(#{A1_PATH} -c 'echo "\\`echo hi\\`"').chomp
|
||||||
end
|
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 ###
|
### Execution and job control ###
|
||||||
#################################
|
#################################
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue