require 'strscan'

COMMANDS = {
  "set" => [ :path, :string ],
  "rm" => [ :path ],
  "clear" => [ :path ],
  "insert" => [ :string, :string, :path ]
}

COMMANDS["ins"] = COMMANDS["insert"]
COMMANDS["remove"] = COMMANDS["rm"]

# In reality, the value of the context attribute
CONTEXT = "/context/"

def parse_commands(data)
  if data.is_a?(String)
    s = data
    data = []
    s.each_line { |line| data << line }
  end
  args = []
  data.each do |line|
    sc = StringScanner.new(line)
    cmd = sc.scan(/\w+/)
    formals = COMMANDS[cmd]
    raise Exception, "Unknown command #{cmd}" unless formals
    args << cmd
    narg = 0
    formals.each do |f|
      sc.skip(/\s+/)
      narg += 1
      if f == :path
        start = sc.pos
        nbracket = 0
        begin
          sc.skip(/[^\]\[\s]+/)
          ch = sc.getch
          nbracket += 1 if ch == "["
          nbracket -= 1 if ch == "]"
          raise Exception, "unmatched [" if nbracket < 0
        end until nbracket == 0 && (sc.eos? || ch =~ /\s/)
        len = sc.pos - start
        len -= 1 unless sc.eos?
        unless p = sc.string[start, len]
          raise Exception, "missing path argument #{narg} for #{cmd}"
        end
        if p[0,1] != "$" && p[0,1] != "/"
          args << CONTEXT + p
        else
          args << p
        end
      elsif f == :string
        delim = sc.peek(1)
        if delim == "'" || delim == "\""
          sc.getch
          args << sc.scan(/([^\\#{delim}]|(\\.))*/)
          sc.getch
        else
          args << sc.scan(/[^\s]+/)
        end
        unless args[-1]
          raise Exception, "missing string argument #{narg} for #{cmd}"
        end
      end
    end
  end
  return args
end

TESTS = [ ]
TESTS << [ "rm */*[module='pam_console.so']",
           ["rm", "/context/*/*[module='pam_console.so']"] ]
TESTS << [ "ins 42 before /files/etc/hosts/*/ipaddr[ . = '127.0.0.1' ]",
            ["ins", "42", "before",
             "/files/etc/hosts/*/ipaddr[ . = '127.0.0.1' ]"] ]
TESTS << [ "set /files/etc/*/*[ipaddr = '127.0.0.1'] \"foo bar\"",
           ["set", "/files/etc/*/*[ipaddr = '127.0.0.1']", "foo bar"] ]
TESTS << [ "clear pam.d/*/*[module = 'system-auth'][type = 'account']",
           ["clear",
            "/context/pam.d/*/*[module = 'system-auth'][type = 'account']"] ]
TESTS << [ "set /foo 'hello \"there\"'" , ["set", "/foo", "hello \"there\""] ]
TESTS << [ "set /foo \"''\\\"''\"", [ "set", "/foo", "''\\\"''" ] ]

TESTS.each do |t|
  exp = t[1]
  begin
    act = parse_commands(t[0])
    if exp != act
      puts "Error parsing #{t[0]}"
      puts "  expected: #{exp.inspect}"
      puts "  actual:   #{act.inspect}"
    end
  rescue Exception => e
    puts "Exception parsing #{t[0]}"
    puts "  #{e.message}"
  end
end
