Skip to content

Instantly share code, notes, and snippets.

@mbarnes
Created June 11, 2022 01:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbarnes/bdf8d4efc6cb9e210cd30af385995a11 to your computer and use it in GitHub Desktop.
Save mbarnes/bdf8d4efc6cb9e210cd30af385995a11 to your computer and use it in GitHub Desktop.
Convert "mame -listxml" output to JSON
#!/usr/bin/python3
#
# Convert "mame -listxml" output to JSON.
#
# MAME's XML output is huge, so this dumps each complete
# <machine> element to keep memory usage bounded.
#
# Output can be piped to 'jq' to query machines.
#
# e.g. Select coin-op machines with trackball controls:
#
# mame -listxml | mamexmltojson | \
# jq '.machine[] | select(
# (.runnable != "no") and \
# (.cloneof == null) and \
# (.input.coins != null) and \
# (.input.control | \
# (type == "array" and \
# any(.type | contains("trackball")))))'
#
import argparse
import json
import sys
from html.parser import HTMLParser
class MAMEParser(HTMLParser):
LIST_ELEMENTS = set([
# <machine> children
'biosset',
'rom',
'disk',
'device_ref',
'sample',
'chip',
'display',
'dipswitch',
'configuration',
'port',
'adjuster',
'feature',
'device',
'slot',
'softwarelist',
'ramoption',
# <input> children
'control',
# <dipswitch> children
'diplocation',
'dipvalue',
# <configuration> children
'conflocation',
'confsetting',
# <port> children
'analog',
# <device> children
'extension',
# <slot>
'slotoption',
])
def reset(self):
super().reset()
self.tags = []
self.stack = []
self.machine = None
self.indent = ' '
self.indentlvl = 0
def print(self, message):
print((self.indent * self.indentlvl) + message)
def dump_machine(self, sep=','):
machine_dump = json.dumps(self.machine, indent=self.indent) + sep
for line in machine_dump.split('\n'):
self.print(line)
def handle_starttag(self, tag, attrs):
if tag == 'mame':
self.print('{')
self.indentlvl += 1
for name,value in attrs:
self.print('"{}": "{}",'.format(name, value))
self.print('"machine": [')
self.indentlvl += 1
else:
attrs = {name: value for name, value in attrs}
if tag == 'machine':
if self.machine:
self.dump_machine()
self.machine = attrs
else:
parent = self.stack[-1]
if tag in self.LIST_ELEMENTS:
# Special case: if attrs has only a 'name' key, append the value
if tag != 'ramoption' and len(attrs) == 1 and 'name' in attrs:
parent.setdefault(tag, []).append(attrs['name'])
else:
parent.setdefault(tag, []).append(attrs)
elif attrs:
assert tag not in parent, 'unexpected duplicate <{}>'.format(tag)
parent[tag] = attrs
self.tags.append(tag)
self.stack.append(attrs)
def handle_endtag(self, tag):
if tag == 'mame':
if self.machine:
self.dump_machine(sep='')
self.print(']')
self.indentlvl -= 1
self.print('}')
self.indentlvl -= 1
else:
self.tags.pop()
self.stack.pop()
def handle_data(self, data):
data = data.strip()
# Ignore embedded DTD
if self.tags and data:
tag = self.tags[-1]
# <ramoption> has both attributes and data
if tag == 'ramoption':
parent = self.stack[-1]
parent['value'] = data
else:
parent = self.stack[-2]
assert tag not in parent, 'unexpected duplicate <{}>'.format(tag)
parent[tag] = data
if __name__ == '__main__':
arg_parser = argparse.ArgumentParser(description='Convert MAME XML to JSON')
arg_parser.add_argument(
'file', nargs='?',
type=argparse.FileType(),
default=sys.stdin,
help='File containing "mame -listxml" output')
args = arg_parser.parse_args()
parser = MAMEParser()
for line in args.file:
parser.feed(line)
parser.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment