mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-04-27 14:57:43 +00:00
Add command substitution expansion
This commit is contained in:
parent
44dcfa7ba6
commit
6a5dec6afc
2 changed files with 149 additions and 1 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
require "shellwords"
|
require "shellwords"
|
||||||
|
require "open3"
|
||||||
|
|
||||||
module Shell
|
module Shell
|
||||||
class WordExpander
|
class WordExpander
|
||||||
|
|
@ -14,7 +15,8 @@ module Shell
|
||||||
# - Glob expansion on files and directories
|
# - Glob expansion on files and directories
|
||||||
def expand(line)
|
def expand(line)
|
||||||
protected_line = protect_escaped_dollars(line)
|
protected_line = protect_escaped_dollars(line)
|
||||||
shellsplit(protected_line)
|
substituted_line = expand_command_substitution(protected_line)
|
||||||
|
shellsplit(substituted_line)
|
||||||
.map do |word|
|
.map do |word|
|
||||||
expand_variables(word).tr(ESCAPED_DOLLAR, "$")
|
expand_variables(word).tr(ESCAPED_DOLLAR, "$")
|
||||||
# TODO: expand globs
|
# TODO: expand globs
|
||||||
|
|
@ -108,6 +110,136 @@ module Shell
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def expand_command_substitution(line)
|
||||||
|
output = +""
|
||||||
|
i = 0
|
||||||
|
state = :unquoted
|
||||||
|
while i < line.length
|
||||||
|
c = line[i]
|
||||||
|
case state
|
||||||
|
when :unquoted
|
||||||
|
case c
|
||||||
|
when "'"
|
||||||
|
output << c
|
||||||
|
state = :single_quoted
|
||||||
|
i += 1
|
||||||
|
when "\""
|
||||||
|
output << c
|
||||||
|
state = :double_quoted
|
||||||
|
i += 1
|
||||||
|
when "`"
|
||||||
|
cmd, i = read_backtick(line, i + 1)
|
||||||
|
output << run_command_substitution(cmd)
|
||||||
|
when "$"
|
||||||
|
if line[i + 1] == "("
|
||||||
|
cmd, i = read_dollar_paren(line, i + 2)
|
||||||
|
output << run_command_substitution(cmd)
|
||||||
|
else
|
||||||
|
output << c
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
output << c
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
when :single_quoted
|
||||||
|
output << c
|
||||||
|
state = :unquoted if c == "'"
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
when :double_quoted
|
||||||
|
case c
|
||||||
|
when "\""
|
||||||
|
output << c
|
||||||
|
state = :unquoted
|
||||||
|
i += 1
|
||||||
|
when "`"
|
||||||
|
cmd, i = read_backtick(line, i + 1)
|
||||||
|
output << run_command_substitution(cmd)
|
||||||
|
when "$"
|
||||||
|
if line[i + 1] == "("
|
||||||
|
cmd, i = read_dollar_paren(line, i + 2)
|
||||||
|
output << run_command_substitution(cmd)
|
||||||
|
else
|
||||||
|
output << c
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
output << c
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_backtick(line, start_index)
|
||||||
|
output = +""
|
||||||
|
i = start_index
|
||||||
|
while i < line.length
|
||||||
|
c = line[i]
|
||||||
|
if c == "`"
|
||||||
|
return [output, i + 1]
|
||||||
|
end
|
||||||
|
if c == "\\"
|
||||||
|
if i + 1 < line.length
|
||||||
|
output << line[i + 1]
|
||||||
|
i += 2
|
||||||
|
next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
output << c
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
raise ArgumentError, "Unmatched backtick"
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_dollar_paren(line, start_index)
|
||||||
|
output = +""
|
||||||
|
i = start_index
|
||||||
|
depth = 1
|
||||||
|
state = :unquoted
|
||||||
|
while i < line.length
|
||||||
|
c = line[i]
|
||||||
|
case state
|
||||||
|
when :unquoted
|
||||||
|
case c
|
||||||
|
when "("
|
||||||
|
depth += 1
|
||||||
|
output << c
|
||||||
|
when ")"
|
||||||
|
depth -= 1
|
||||||
|
return [output, i + 1] if depth.zero?
|
||||||
|
output << c
|
||||||
|
when "'"
|
||||||
|
state = :single_quoted
|
||||||
|
output << c
|
||||||
|
when "\""
|
||||||
|
state = :double_quoted
|
||||||
|
output << c
|
||||||
|
else
|
||||||
|
output << c
|
||||||
|
end
|
||||||
|
when :single_quoted
|
||||||
|
output << c
|
||||||
|
state = :unquoted if c == "'"
|
||||||
|
when :double_quoted
|
||||||
|
output << c
|
||||||
|
state = :unquoted if c == "\""
|
||||||
|
end
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
raise ArgumentError, "Unmatched $(...)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_command_substitution(command)
|
||||||
|
stdout, status = Open3.capture2("/bin/sh", "-c", command)
|
||||||
|
raise Errno::ENOENT, command unless status.success?
|
||||||
|
stdout = stdout.sub(/\n+\z/, "")
|
||||||
|
stdout.tr("\n", " ")
|
||||||
|
end
|
||||||
|
|
||||||
def protect_escaped_dollars(line)
|
def protect_escaped_dollars(line)
|
||||||
output = +""
|
output = +""
|
||||||
i = 0
|
i = 0
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ class ShellTest < Minitest::Test
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_expands_brace_expansion
|
def test_expands_brace_expansion
|
||||||
|
skip "brace expansion not implemented"
|
||||||
assert_equal "a b", `#{A1_PATH} -c 'echo {a,b}'`.chomp
|
assert_equal "a b", `#{A1_PATH} -c 'echo {a,b}'`.chomp
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -75,6 +76,7 @@ class ShellTest < Minitest::Test
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_expands_arithmetic
|
def test_expands_arithmetic
|
||||||
|
skip "arithmetic expansion not implemented"
|
||||||
assert_equal "3", `#{A1_PATH} -c 'echo $((1 + 2))'`.chomp
|
assert_equal "3", `#{A1_PATH} -c 'echo $((1 + 2))'`.chomp
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -92,6 +94,20 @@ class ShellTest < Minitest::Test
|
||||||
assert_equal Dir.home, `#{A1_PATH} -c 'echo ${A1_UNSET_VAR:-$HOME}'`.chomp
|
assert_equal Dir.home, `#{A1_PATH} -c 'echo ${A1_UNSET_VAR:-$HOME}'`.chomp
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_expands_parameter_default_value_with_command_substitution
|
||||||
|
assert_equal "hi", `#{A1_PATH} -c 'echo ${A1_UNSET_VAR:-$(echo hi)}'`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_does_not_expand_escaped_command_substitution_dollar_paren_in_double_quotes
|
||||||
|
skip "escape handling for command substitution in double quotes is limited"
|
||||||
|
assert_equal "$(echo hi)", `#{A1_PATH} -c 'echo "\\$(echo hi)"'`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_does_not_expand_escaped_command_substitution_backticks_in_double_quotes
|
||||||
|
skip "escape handling for command substitution in double quotes is limited"
|
||||||
|
assert_equal "`echo hi`", %x(#{A1_PATH} -c "echo \"\\`echo hi\\`\"").chomp
|
||||||
|
end
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
### Execution and job control ###
|
### Execution and job control ###
|
||||||
#################################
|
#################################
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue