mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-04-27 14:57:43 +00:00
Share quote parsing across shell parsers
This commit is contained in:
parent
f5aa086aec
commit
650e38328c
3 changed files with 94 additions and 106 deletions
64
ruby/shell/quote_cursor.rb
Normal file
64
ruby/shell/quote_cursor.rb
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
module Shell
|
||||||
|
# Shared quote/escape state machine for parsers that walk shell-like strings.
|
||||||
|
class QuoteCursor
|
||||||
|
attr_reader :state
|
||||||
|
|
||||||
|
def initialize(state: :unquoted)
|
||||||
|
@state = state
|
||||||
|
end
|
||||||
|
|
||||||
|
def unquoted?
|
||||||
|
state == :unquoted
|
||||||
|
end
|
||||||
|
|
||||||
|
# Consumes one logical unit from line[index], which may be one character
|
||||||
|
# or an escape pair (e.g., \" or \\$), and updates internal quote state.
|
||||||
|
# Returns [segment, next_index].
|
||||||
|
def consume(line, index)
|
||||||
|
c = line[index]
|
||||||
|
|
||||||
|
case state
|
||||||
|
when :unquoted
|
||||||
|
consume_unquoted(line, index, c)
|
||||||
|
when :single_quoted
|
||||||
|
consume_single_quoted(index, c)
|
||||||
|
when :double_quoted
|
||||||
|
consume_double_quoted(line, index, c)
|
||||||
|
else
|
||||||
|
raise "Unknown state #{state}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def consume_unquoted(line, index, c)
|
||||||
|
case c
|
||||||
|
when "'"
|
||||||
|
@state = :single_quoted
|
||||||
|
when "\""
|
||||||
|
@state = :double_quoted
|
||||||
|
when "\\"
|
||||||
|
if index + 1 < line.length
|
||||||
|
return [line[index, 2], index + 2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
[c, index + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def consume_single_quoted(index, c)
|
||||||
|
@state = :unquoted if c == "'"
|
||||||
|
[c, index + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def consume_double_quoted(line, index, c)
|
||||||
|
if c == "\\"
|
||||||
|
if index + 1 < line.length
|
||||||
|
return [line[index, 2], index + 2]
|
||||||
|
end
|
||||||
|
elsif c == "\""
|
||||||
|
@state = :unquoted
|
||||||
|
end
|
||||||
|
[c, index + 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
|
require "shell/quote_cursor"
|
||||||
|
|
||||||
module Shell
|
module Shell
|
||||||
class StringParser
|
class StringParser
|
||||||
class << self
|
class << self
|
||||||
def split_commands(line)
|
def split_commands(line)
|
||||||
commands = []
|
commands = []
|
||||||
command = +""
|
command = +""
|
||||||
state = :unquoted
|
cursor = QuoteCursor.new
|
||||||
next_op = :always
|
next_op = :always
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
while i < line.length
|
while i < line.length
|
||||||
c = line[i]
|
c = line[i]
|
||||||
case state
|
if cursor.unquoted?
|
||||||
when :unquoted
|
|
||||||
case c
|
case c
|
||||||
when ";"
|
when ";"
|
||||||
commands << {command: command, op: next_op}
|
commands << {command: command, op: next_op}
|
||||||
|
|
@ -30,40 +31,11 @@ module Shell
|
||||||
i += 2
|
i += 2
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
when "'"
|
|
||||||
state = :single_quoted
|
|
||||||
when "\""
|
|
||||||
state = :double_quoted
|
|
||||||
when "\\"
|
|
||||||
state = :escaped
|
|
||||||
end
|
end
|
||||||
command << c
|
|
||||||
|
|
||||||
when :single_quoted
|
|
||||||
command << c
|
|
||||||
state = :unquoted if c == "'"
|
|
||||||
|
|
||||||
when :double_quoted
|
|
||||||
command << c
|
|
||||||
if c == "\\"
|
|
||||||
state = :double_quoted_escape
|
|
||||||
elsif c == "\""
|
|
||||||
state = :unquoted
|
|
||||||
end
|
|
||||||
|
|
||||||
when :double_quoted_escape
|
|
||||||
command << c
|
|
||||||
state = :double_quoted
|
|
||||||
|
|
||||||
when :escaped
|
|
||||||
command << c
|
|
||||||
state = :unquoted
|
|
||||||
|
|
||||||
else
|
|
||||||
raise "Unknown state #{state}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
i += 1
|
segment, i = cursor.consume(line, i)
|
||||||
|
command << segment
|
||||||
end
|
end
|
||||||
|
|
||||||
if next_op == :and && command.strip.empty?
|
if next_op == :and && command.strip.empty?
|
||||||
|
|
@ -78,58 +50,23 @@ module Shell
|
||||||
output = +""
|
output = +""
|
||||||
i = start_index
|
i = start_index
|
||||||
depth = 1
|
depth = 1
|
||||||
state = :unquoted
|
cursor = QuoteCursor.new
|
||||||
|
|
||||||
while i < line.length
|
while i < line.length
|
||||||
c = line[i]
|
c = line[i]
|
||||||
|
|
||||||
case state
|
if cursor.unquoted?
|
||||||
when :unquoted
|
|
||||||
case c
|
case c
|
||||||
when "("
|
when "("
|
||||||
depth += 1
|
depth += 1
|
||||||
output << c
|
|
||||||
when ")"
|
when ")"
|
||||||
depth -= 1
|
depth -= 1
|
||||||
return [output, i + 1] if depth.zero?
|
return [output, i + 1] if depth.zero?
|
||||||
output << c
|
|
||||||
when "'"
|
|
||||||
output << c
|
|
||||||
state = :single_quoted
|
|
||||||
when "\""
|
|
||||||
output << c
|
|
||||||
state = :double_quoted
|
|
||||||
when "\\"
|
|
||||||
output << c
|
|
||||||
if i + 1 < line.length
|
|
||||||
output << line[i + 1]
|
|
||||||
i += 1
|
|
||||||
end
|
|
||||||
else
|
|
||||||
output << c
|
|
||||||
end
|
end
|
||||||
|
|
||||||
when :single_quoted
|
|
||||||
output << c
|
|
||||||
state = :unquoted if c == "'"
|
|
||||||
|
|
||||||
when :double_quoted
|
|
||||||
if c == "\\"
|
|
||||||
output << c
|
|
||||||
if i + 1 < line.length
|
|
||||||
output << line[i + 1]
|
|
||||||
i += 1
|
|
||||||
end
|
|
||||||
else
|
|
||||||
output << c
|
|
||||||
state = :unquoted if c == "\""
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
|
||||||
raise "Unknown state #{state}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
i += 1
|
segment, i = cursor.consume(line, i)
|
||||||
|
output << segment
|
||||||
end
|
end
|
||||||
|
|
||||||
raise ArgumentError, "Unmatched $(...)"
|
raise ArgumentError, "Unmatched $(...)"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
require "shellwords"
|
require "shellwords"
|
||||||
require "open3"
|
require "open3"
|
||||||
|
require "shell/quote_cursor"
|
||||||
require "shell/string_parser"
|
require "shell/string_parser"
|
||||||
|
|
||||||
module Shell
|
module Shell
|
||||||
|
|
@ -116,20 +117,11 @@ module Shell
|
||||||
def expand_command_substitution(line)
|
def expand_command_substitution(line)
|
||||||
output = +""
|
output = +""
|
||||||
i = 0
|
i = 0
|
||||||
state = :unquoted
|
cursor = QuoteCursor.new
|
||||||
while i < line.length
|
while i < line.length
|
||||||
c = line[i]
|
c = line[i]
|
||||||
case state
|
if cursor.unquoted?
|
||||||
when :unquoted
|
|
||||||
case c
|
case c
|
||||||
when "'"
|
|
||||||
output << c
|
|
||||||
state = :single_quoted
|
|
||||||
i += 1
|
|
||||||
when "\""
|
|
||||||
output << c
|
|
||||||
state = :double_quoted
|
|
||||||
i += 1
|
|
||||||
when "`"
|
when "`"
|
||||||
cmd, i = read_backtick(line, i + 1)
|
cmd, i = read_backtick(line, i + 1)
|
||||||
output << escape_substitution_output(run_command_substitution(cmd), :unquoted)
|
output << escape_substitution_output(run_command_substitution(cmd), :unquoted)
|
||||||
|
|
@ -156,29 +148,20 @@ module Shell
|
||||||
output << ESCAPED_BACKTICK
|
output << ESCAPED_BACKTICK
|
||||||
i += 2
|
i += 2
|
||||||
else
|
else
|
||||||
output << c
|
segment, i = cursor.consume(line, i)
|
||||||
i += 1
|
output << segment
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
output << c
|
segment, i = cursor.consume(line, i)
|
||||||
i += 1
|
output << segment
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
output << c
|
segment, i = cursor.consume(line, i)
|
||||||
i += 1
|
output << segment
|
||||||
end
|
end
|
||||||
|
|
||||||
when :single_quoted
|
elsif cursor.state == :double_quoted
|
||||||
output << c
|
|
||||||
state = :unquoted if c == "'"
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
when :double_quoted
|
|
||||||
case c
|
case c
|
||||||
when "\""
|
|
||||||
output << c
|
|
||||||
state = :unquoted
|
|
||||||
i += 1
|
|
||||||
when "\\"
|
when "\\"
|
||||||
if i + 1 < line.length
|
if i + 1 < line.length
|
||||||
escaped = line[i + 1]
|
escaped = line[i + 1]
|
||||||
|
|
@ -190,8 +173,8 @@ module Shell
|
||||||
end
|
end
|
||||||
i += 2
|
i += 2
|
||||||
else
|
else
|
||||||
output << c
|
segment, i = cursor.consume(line, i)
|
||||||
i += 1
|
output << segment
|
||||||
end
|
end
|
||||||
when "`"
|
when "`"
|
||||||
cmd, i = read_backtick(line, i + 1)
|
cmd, i = read_backtick(line, i + 1)
|
||||||
|
|
@ -206,13 +189,17 @@ module Shell
|
||||||
output << escape_substitution_output(run_command_substitution(cmd), :double_quoted)
|
output << escape_substitution_output(run_command_substitution(cmd), :double_quoted)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
output << c
|
segment, i = cursor.consume(line, i)
|
||||||
i += 1
|
output << segment
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
output << c
|
segment, i = cursor.consume(line, i)
|
||||||
i += 1
|
output << segment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
segment, i = cursor.consume(line, i)
|
||||||
|
output << segment
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
output
|
output
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue