csc360-a1-shell/ruby/shell/quote_cursor.rb
Sami Samhuri 4f4e97475b
[ruby] Modernize Ruby shell parsing and expansion, add C compat test mode (#4)
Replace Ruby's old wordexp-like command splitting with a tokenizer and
parser that understands ; and && while honoring quotes and nesting.

Implement richer expansions for command substitution, arithmetic,
parameter defaults (${var:-...}), brace expansion, and escaped
dollar/backtick behavior via shared quote-state handling.

Expand the test suite with parser/expansion edge cases, escaping
parity checks, builtin usage validation, and job-control refresh tests.

Keep C green by adding a compat test profile for c/Makefile test and
by returning nonzero on builtin failures in -c mode, including clearer
`bg` usage output.
2026-02-07 15:18:41 -08:00

64 lines
1.4 KiB
Ruby

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