mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-03-25 08:45:52 +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
|
||||
class StringParser
|
||||
class << self
|
||||
def split_commands(line)
|
||||
commands = []
|
||||
command = +""
|
||||
state = :unquoted
|
||||
cursor = QuoteCursor.new
|
||||
next_op = :always
|
||||
i = 0
|
||||
|
||||
while i < line.length
|
||||
c = line[i]
|
||||
case state
|
||||
when :unquoted
|
||||
if cursor.unquoted?
|
||||
case c
|
||||
when ";"
|
||||
commands << {command: command, op: next_op}
|
||||
|
|
@ -30,40 +31,11 @@ module Shell
|
|||
i += 2
|
||||
next
|
||||
end
|
||||
when "'"
|
||||
state = :single_quoted
|
||||
when "\""
|
||||
state = :double_quoted
|
||||
when "\\"
|
||||
state = :escaped
|
||||
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
|
||||
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
command << segment
|
||||
end
|
||||
|
||||
if next_op == :and && command.strip.empty?
|
||||
|
|
@ -78,58 +50,23 @@ module Shell
|
|||
output = +""
|
||||
i = start_index
|
||||
depth = 1
|
||||
state = :unquoted
|
||||
cursor = QuoteCursor.new
|
||||
|
||||
while i < line.length
|
||||
c = line[i]
|
||||
|
||||
case state
|
||||
when :unquoted
|
||||
if cursor.unquoted?
|
||||
case c
|
||||
when "("
|
||||
depth += 1
|
||||
output << c
|
||||
when ")"
|
||||
depth -= 1
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
|
||||
raise ArgumentError, "Unmatched $(...)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
require "shellwords"
|
||||
require "open3"
|
||||
require "shell/quote_cursor"
|
||||
require "shell/string_parser"
|
||||
|
||||
module Shell
|
||||
|
|
@ -116,20 +117,11 @@ module Shell
|
|||
def expand_command_substitution(line)
|
||||
output = +""
|
||||
i = 0
|
||||
state = :unquoted
|
||||
cursor = QuoteCursor.new
|
||||
while i < line.length
|
||||
c = line[i]
|
||||
case state
|
||||
when :unquoted
|
||||
if cursor.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 << escape_substitution_output(run_command_substitution(cmd), :unquoted)
|
||||
|
|
@ -156,29 +148,20 @@ module Shell
|
|||
output << ESCAPED_BACKTICK
|
||||
i += 2
|
||||
else
|
||||
output << c
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
else
|
||||
output << c
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
else
|
||||
output << c
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
|
||||
when :single_quoted
|
||||
output << c
|
||||
state = :unquoted if c == "'"
|
||||
i += 1
|
||||
|
||||
when :double_quoted
|
||||
elsif cursor.state == :double_quoted
|
||||
case c
|
||||
when "\""
|
||||
output << c
|
||||
state = :unquoted
|
||||
i += 1
|
||||
when "\\"
|
||||
if i + 1 < line.length
|
||||
escaped = line[i + 1]
|
||||
|
|
@ -190,8 +173,8 @@ module Shell
|
|||
end
|
||||
i += 2
|
||||
else
|
||||
output << c
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
when "`"
|
||||
cmd, i = read_backtick(line, i + 1)
|
||||
|
|
@ -206,13 +189,17 @@ module Shell
|
|||
output << escape_substitution_output(run_command_substitution(cmd), :double_quoted)
|
||||
end
|
||||
else
|
||||
output << c
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
else
|
||||
output << c
|
||||
i += 1
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
|
||||
else
|
||||
segment, i = cursor.consume(line, i)
|
||||
output << segment
|
||||
end
|
||||
end
|
||||
output
|
||||
|
|
|
|||
Loading…
Reference in a new issue